. * --------------------------------------------------------------------- */ namespace Glpi\Dashboard; use Ramsey\Uuid\Uuid; use Config; use CommonGLPI; use Dropdown; use DBConnection; use Entity; use Group; use Html; use Plugin; use Profile; use Session; use Telemetry; use Toolbox; use User; if (!defined('GLPI_ROOT')) { die("Sorry. You can't access this file directly"); } class Grid extends CommonGLPI { protected $cell_margin = 6; protected $grid_cols = 26; protected $grid_rows = 24; protected $current = ""; protected $dashboard = null; protected $items = []; static $embed = false; static $context = ''; static $all_dashboards = []; public function __construct( string $dashboard_key = "central", int $grid_cols = 26, int $grid_rows = 24, string $context = "core" ) { $this->current = $dashboard_key; $this->grid_cols = $grid_cols; $this->grid_rows = $grid_rows; $this->dashboard = new Dashboard($dashboard_key); self::$context = $context; } /** * Return the instance of current dasbhoard * * @return Dashboard */ public function getDashboard() { return $this->dashboard; } /** * load all existing dashboards from DB into a static property for caching data * * @param bool $force, if false, don't use cache * * @return bool */ static function loadAllDashboards(bool $force = true): bool { if (!is_array(self::$all_dashboards) || count(self::$all_dashboards) === 0 || $force) { self::$all_dashboards = Dashboard::getAll($force, !self::$embed, self::$context); } return is_array(self::$all_dashboards); } /** * Init dashboards cards * A define.php constant (GLPI_AJAX_DASHBOARD) exists to control how the cards should be loaded * - if true: only the parent block will be initialized and the content will be load by ajax * pros: if a widget fails, only this one will crash * - else: load all html * pros: better perfs * * @return void */ public function getCards() { self::loadAllDashboards(); if (!isset(self::$all_dashboards[$this->current]) || !isset(self::$all_dashboards[$this->current]['items'])) { self::$all_dashboards[$this->current] = [ 'items' => [] ]; } foreach (self::$all_dashboards[$this->current]['items'] as $specs) { $card_id = $specs['card_id'] ?? $specs['gridstack_id'] ?? $specs['id']; $gridstack_id = $specs['gridstack_id'] ?? $specs['id']; $card_options = ($specs['card_options'] ?? []) + [ 'card_id' => $card_id ]; if (GLPI_AJAX_DASHBOARD) { $card_html = << HTML; } else { $card_html = $this->getCardHtml($card_id, ['args' => $card_options]); } $this->addGridItem( $card_html, $gridstack_id, $specs['x'] ?? -1, $specs['y'] ?? -1, $specs['width'] ?? 2, $specs['height'] ?? 2, $card_options ); } } /** * Do we have the right to view at least one dashboard int the current collection * * @return bool */ public function canViewCurrent(): bool { // check global (admin) right if (Dashboard::canView()) { return true; } return $this->dashboard->canViewCurrent(); } /** * Do we have the right to view at least one dashboard in the current collection * * @return bool */ static function canViewOneDashboard(): bool { // check global (admin) right if (Dashboard::canView()) { return true; } self::loadAllDashboards(); return (count(self::$all_dashboards) > 0); } /** * Do we have the right to view the specified dashboard int the current collection * * @param string $key the dashboard to check * * @return bool */ static function canViewSpecificicDashboard($key): bool { // check global (admin) right if (Dashboard::canView()) { return true; } self::loadAllDashboards(); return isset(self::$all_dashboards[$key]); } /** * Display grid for the current dashboard * * @return void display html of the grid */ public function show(bool $mini = false) { $rand = mt_rand(); if (!self::$embed && !$this->dashboard->canViewCurrent()) { return; } self::loadAllDashboards(); $this->restoreLastDashboard(); if ($mini) { $this->cell_margin = 3; } $embed_str = self::$embed ? "true" : "false"; $embed_class = self::$embed ? "embed" : ""; $mini_class = $mini ? "mini" : ""; $nb_dashboards = count(self::$all_dashboards); $can_view_all = Session::haveRight('dashboard', READ) || self::$embed; $can_create = Session::haveRight('dashboard', CREATE); $can_edit = Session::haveRight('dashboard', UPDATE) && $nb_dashboards; $can_purge = Session::haveRight('dashboard', PURGE) && $nb_dashboards; $can_clone = $can_create && $nb_dashboards; // prepare html for add controls $add_controls = ""; for ($y = 0; $y < $this->grid_rows; $y++) { for ($x = 0; $x < $this->grid_cols; $x++) { $add_controls.= "
 
"; } } // prepare all available cards $cards = $this->getAllDasboardCards(); $cards_json = json_encode($cards); // prepare all available widgets $all_widgets = Widget::getAllTypes(); $all_widgets_json = json_encode($all_widgets); // prepare labels $embed_label = __("Share or embed this dashboard"); $delete_label = __("Delete this dashboard"); $history_label = __("Toggle auto-refresh"); $night_label = __("Toggle night mode"); $fs_label = __("Toggle fullscreen"); $clone_label = __("Clone this dashboard"); $edit_label = __("Toggle edit mode"); $add_filter_lbl = __("Add filter"); $add_dash_label = __("Add a new dashboard"); $save_label = _x('button', "Save"); $gridstack_items = $this->getGridItemsHtml(); $dropdown_dashboards = ""; if ($nb_dashboards) { $dropdown_dashboards = self::dropdownDashboard("", [ 'value' => $this->current, 'display' => false, 'class' => 'dashboard_select', 'can_view_all' => $can_view_all, 'noselect2' => true, ]); } $dashboard_title = $this->dashboard->getTitle(); $l_tb_icons = ""; $r_tb_icons = ""; $rename = ""; $left_toolbar = ""; $grid_guide = ""; if (!self::$embed) { if (!$mini && $can_create) { $l_tb_icons.= ""; } if (!$mini && $can_clone) { $r_tb_icons.= ""; } if (!$mini && $can_edit) { $r_tb_icons.= ""; $rename = "
"; } if (!$mini && $can_purge) { $r_tb_icons.= ""; } if ($can_edit) { $r_tb_icons.= ""; } if (!$mini) { $r_tb_icons.= ""; } if (!$mini) { $left_toolbar = <<
$dropdown_dashboards $l_tb_icons
$rename HTML; } $grid_guide = << $add_controls HTML; } $toolbars = << $r_tb_icons HTML; $filters = ""; if (!$mini) { $filters = << {$add_filter_lbl} HTML; } $embed_watermark = ""; if (self::$embed) { $embed_watermark = ""; } // display the grid $html = << $embed_watermark $toolbars $filters $grid_guide
$gridstack_items
HTML; $ajax_cards = GLPI_AJAX_DASHBOARD; $context = self::$context; $cache_key = sha1($_SESSION['glpiactiveentities_string '] ?? ""); $js = <<current}', cols: {$this->grid_cols}, rows: {$this->grid_rows}, cell_margin: {$this->cell_margin}, rand: '{$rand}', embed: {$embed_str}, ajax_cards: {$ajax_cards}, all_cards: {$cards_json}, all_widgets: {$all_widgets_json}, context: "{$context}", cache_key: "{$cache_key}", }) }); JAVASCRIPT; $js = Html::scriptBlock($js); echo $html.$js; } public function showDefault() { echo "
"; $this->show(); echo "
"; } /** * Show an embeded dashboard. * We must check token validity to avoid displaying dashboard to invalid users * * @param array $params contains theses keys: * - dashboard: the dashboard system name * - entities_id: entity to init in session * - is_recursive: do we need to display sub entities * - token: the token to check * * @return void (display) */ public function embed(array $params = []) { $defaults = [ 'dashboard' => '', 'entities_id' => 0, 'is_recursive' => 0, 'token' => '' ]; $params = array_merge($defaults, $params); if (!self::checkToken($params)) { Html::displayRightError(); exit; } self::$embed = true; // load minimal session $_SESSION["glpiactive_entity"] = $params['entities_id']; $_SESSION["glpiactive_entity_recursive"] = $params['is_recursive']; $_SESSION["glpiname"] = 'embed_dashboard'; $_SESSION["glpigroups"] = []; if ($params['is_recursive']) { $entities = getSonsOf("glpi_entities", $params['entities_id']); } else { $entities = [$params['entities_id']]; } $_SESSION['glpiactiveentities'] = $entities; $_SESSION['glpiactiveentities_string'] = "'".implode("', '", $entities)."'"; // show embeded dashboard $this->show(true); } static function getToken(string $dasboard = "", int $entities_id = 0, int $is_recursive = 0): string { $seed = $dasboard.$entities_id.$is_recursive.Telemetry::getInstanceUuid(); $uuid = Uuid::uuid5(Uuid::NAMESPACE_OID, $seed); $token = $uuid->toString(); return $token; } /** * Check token variables (compare it to `dashboard`, `entities_id` and `is_recursive` paramater) * * @param array $params contains theses keys: * - dashboard: the dashboard system name * - entities_id: entity to init in session * - is_recursive: do we need to display sub entities * - token: the token to check * * @return bool */ static function checkToken(array $params = []):bool { $defaults = [ 'dashboard' => '', 'entities_id' => 0, 'is_recursive' => 0, 'token' => '' ]; $params = array_merge($defaults, $params); $token = self::getToken( $params['dashboard'], $params['entities_id'], $params['is_recursive'] ); if ($token !== $params['token']) { return false; Html::displayRightError(); exit; } return true; } /** * Return the html for all items for the current dashboard * * @param bool $with_lock if true, return also a locked bottom item (to fix grid height) * * @return string html of the grid items */ public function getGridItemsHtml(bool $with_lock = true, bool $embed = false): string { if ($embed) { self::$embed = true; } $this->getCards(); if ($with_lock) { $this->items[] = << HTML; } // append all elements to insert them in html return implode("", $this->items); } /** * Add a new grid item * * @param string $html content of the card * @param string $gridstack_id unique id identifying the card (used in gridstack) * @param int $x position in the grid * @param int $y position in the grid * @param int $width size in the grid * @param int $height size in the grid * @param array $data_option aditional options passed to the widget, contains at least thses keys: * - string 'color' * @return void */ public function addGridItem( string $html = "", string $gridstack_id = "", int $x = -1, int $y = -1, int $width = 2, int $height = 2, array $data_option = []) { // let grid-stack to autoposition item $autoposition = 'data-gs-auto-position="true"'; $coordinates = ''; if ((int) $x >= 0 && (int) $y >= 0) { $autoposition = ""; $coordinates = "data-gs-x='$x' data-gs-y='$y'"; } $color = $data_option['color'] ?? "#FFFFFF"; $fg_color = Toolbox::getFgColor($color, 100); // add card options in data attribute $data_option_attr = ""; if (count($data_option)) { $data_option_attr = "data-card-options='".json_encode($data_option, JSON_HEX_APOS)."'"; } $refresh_label = __("Refresh this card"); $edit_label = __("Edit this card"); $delete_label = __("Delete this card"); $this->items[] = <<
{$html}
HTML; } /** * Display a mini form fo adding a new dashboard * * @return void (display) */ public function displayAddDashboardForm() { $rand = mt_rand(); echo "
"; echo "
"; echo ""; echo "
"; echo Html::input('title', ['id' => "title_$rand"]); echo "
"; echo "
"; // .field echo Html::submit(_x('button', "Add"), [ 'class' => 'submit vsubmit submit-new-dashboard' ]); echo "
"; // .card.display-widget-form } /** * Display mini configuration form to add or edit a widget * * @param array $params with these keys: * - int 'gridstack_id': unique identifier of the card * - int 'x': position in the grid * - int 'y: position in the grid * - int 'width': size in the grid * - int 'height': size in the grid * - string 'rand': unique identifier for the dom * - string 'action': [display_add_widget|display_edit_widget] current action for the form * - array 'card_options': aditionnal options for the card, contains at least: * - string 'card_id': identifier return by @see self::getAllDasboardCards * - string 'color' * * @return void */ public function displayWidgetForm(array $params = []) { $gridstack_id = $params['gridstack_id'] ?? ""; $old_id = $gridstack_id; $x = (int) ($params['x'] ?? 0); $y = (int) ($params['y'] ?? 0); $width = (int) ($params['width'] ?? 0); $height = (int) ($params['height'] ?? 0); $cardopt = $params['card_options'] ?? ['color' => "#FAFAFA"]; $card_id = $cardopt['card_id'] ?? ""; $widgettypes = Widget::getAllTypes(); $widgettype = $cardopt['widgettype'] ?? ""; $widget_def = $widgettypes[$widgettype] ?? []; $use_gradient = $cardopt['use_gradient'] ?? 0; $point_labels = $cardopt['point_labels'] ?? 0; $limit = $cardopt['limit'] ?? 7; $color = $cardopt['color']; $edit = $params['action'] === "display_edit_widget"; $rand = $params['rand'] ?? mt_rand(); $cards = $this->getAllDasboardCards(); $card = $cards[$card_id] ?? []; // append card id to options if (!isset($cardopt['card_id'] )) { $cardopt['card_id'] = $card_id; } $list_cards = []; array_walk($cards, function($data, $index) use (&$list_cards) { $group = $data['group'] ?? __("others"); $list_cards[$group][$index] = $data['label'] ?? $data['itemtype']::getTypeName(); }); echo "
"; echo "
"; echo ""; echo "
"; Html::showColorField('color', [ 'rand' => $rand, 'value' => $color, ]); echo "
"; echo "
"; // .field echo "
"; echo ""; echo "
"; Dropdown::showFromArray('card_id', $list_cards, [ 'display_emptychoice' => true, 'rand' => $rand, 'value' => $card_id ]); echo "
"; echo "
"; // .field // display widget list $displayed = ""; if (!$edit) { $displayed = "style='display: none'"; } echo "
"; echo ""; echo "
"; foreach (Widget::getAllTypes() as $key => $current) { $selected = ''; if ($key === $widgettype) { $selected = 'checked'; } $w_diplayed = ""; if ($edit && isset($card['widgettype']) && in_array($key, $card['widgettype'])) { $w_diplayed = "style='display: inline-block;'"; } echo " "; } echo "
"; echo "
"; // .field // display checkbox to use gradient palette or not $gradient_displayed = ""; if (!$edit || !isset($widget_def['gradient']) || !$widget_def['gradient']) { $gradient_displayed = "style='display: none'"; } echo "
"; echo ""; echo "
"; Html::showCheckbox([ 'label' => " ", 'name' => "use_gradient", 'id' => "check_gradient_$rand", 'checked' => $use_gradient, ]); echo "
"; echo "
"; // .field // display checkbox to use point label or not $point_labels_displayed = ""; if (!$edit || !isset($widget_def['pointlbl']) || !$widget_def['pointlbl']) { $point_labels_displayed = "style='display: none'"; } echo "
"; echo ""; echo "
"; Html::showCheckbox([ 'label' => " ", 'name' => "point_labels", 'id' => "check_point_labels_$rand", 'checked' => $point_labels, ]); echo "
"; echo "
"; // .field // show limit dropdown $limit_displayed = ""; if (!$edit || !isset($widget_def['limit']) || !$widget_def['limit']) { $limit_displayed = "style='display: none'"; } echo "
"; echo ""; echo "
"; Dropdown::showNumber('limit', [ 'value' => $limit, 'rand' => $rand, ]); echo "
"; echo "
"; // .field $class_submit = "add-widget"; $label_submit = " "._x('button', "Add"); if ($edit) { $class_submit = "edit-widget"; $label_submit = " "._x('button', "Update"); } // manage autoescaping if (isset($cardopt['markdown_content'])) { $cardopt['markdown_content'] = Html::cleanPostForTextArea($cardopt['markdown_content']); } echo Html::submit($label_submit, [ 'class' => 'submit vsubmit '.$class_submit ]); echo Html::hidden('gridstack_id', ['value' => $gridstack_id]); echo Html::hidden('old_id', ['value' => $old_id]); echo Html::hidden('x', ['value' => $x]); echo Html::hidden('y', ['value' => $y]); echo Html::hidden('width', ['value' => $width]); echo Html::hidden('height', ['value' => $height]); echo Html::hidden('card_options', ['value' => json_encode($cardopt, JSON_HEX_APOS | JSON_HEX_QUOT)]); echo "
"; // .card.display-widget-form } /** * Display mini form to add filter to the current dashboard * * @param array $params default values for * - 'used' already used filters * * @return void */ public function displayFilterForm(array $params = []) { $default_params = [ 'used' => [], ]; $params = array_merge($default_params, $params); $used = array_flip($params['used']); $list_filters = array_diff_key(Filter::getAll(), $used); $rand = mt_rand(); echo "
"; echo "
"; echo ""; echo "
"; Dropdown::showFromArray('filter_id', $list_filters, [ 'display_emptychoice' => true, 'rand' => $rand, ]); echo "
"; echo "
"; // .field echo Html::submit(" "._x('button', "Add"), [ 'class' => 'submit vsubmit' ]); echo "
"; // form.card.display-filter-form } /** * Display a mini form for embedding current dashboard in another application. * Also, display a select for sharing current dashboard to another users/groups/entities/profiles * * @return void */ public function displayEmbedForm() { global $CFG_GLPI; $entities_id = $_SESSION['glpiactive_entity']; $is_recursive = $_SESSION['glpiactive_entity_recursive']; $token = self::getToken($this->current, $entities_id, $is_recursive); $embed_url = $CFG_GLPI['url_base']. "/front/central.php?embed&dashboard=".$this->current. "&entities_id=$entities_id". "&is_recursive=$is_recursive". "&token=$token"; echo "
"; echo "
"; echo __("Direct link"); echo "
"; echo Html::input('direct_link', [ 'value' => $embed_url, 'style' => 'width: calc(100% - 38px)' ]); echo "

"; $iframe = ""; echo __("Iframe"); echo "
"; echo Html::input('iframe_code', [ 'value' => $iframe, 'style' => 'width: calc(100% - 38px)' ]); echo "
"; echo "

"; $this->displayEditRightsForm(); } /** * Display a mini form for sharing current dashboard to another users/groups/entities/profiles. * * @return void */ public function displayEditRightsForm() { self::loadAllDashboards(); $rand = mt_rand(); $values = []; echo "
"; $profiles_value = self::$all_dashboards[$this->current]['rights']['profiles_id'] ?? []; $profile_item = new Profile; $profiles = $profile_item->find(); $profiles_items = []; foreach ($profiles as $profile) { $new_key = 'profiles_id-'.$profile['id']; $profiles_items[$new_key] = $profile['name']; if (in_array($profile['id'], $profiles_value)) { $values[$new_key] = $new_key; } } $entities_value = self::$all_dashboards[$this->current]['rights']['entities_id'] ?? []; $entity_item = new Entity; $entities = $entity_item->find(getEntitiesRestrictCriteria(Entity::getTable())); $entities_items = []; foreach ($entities as $entity) { $new_key = 'entities_id-'.$entity['id']; $entities_items[$new_key] = $entity['completename']; if (in_array($entity['id'], $entities_value)) { $values[$new_key] = $new_key; } } $users_value = self::$all_dashboards[$this->current]['rights']['users_id'] ?? []; $users = iterator_to_array(User::getSqlSearchResult(false)); $users_items = []; foreach ($users as $user) { $new_key = 'users_id-'.$user['id']; $users_items[$new_key] = $user['name']; if (in_array($user['id'], $users_value)) { $values[$new_key] = $new_key; } } $groups_value = self::$all_dashboards[$this->current]['rights']['groups_id'] ?? []; $group_item = new Group; $groups = $group_item->find(getEntitiesRestrictCriteria(Group::getTable())); $groups_items = []; foreach ($groups as $group) { $new_key = 'groups_id-'.$group['id']; $groups_items[$new_key] = $group['name']; if (in_array($group['id'], $groups_value)) { $values[$new_key] = $new_key; } } $possible_rights = [ _n("Profile", "Profiles", 1) => $profiles_items, _n("Entity", "Entities", 1) => $entities_items, _n("User", "Users", 1) => $users_items, _n("Group", "Groups", 1) => $groups_items, ]; echo "
"; Dropdown::showFromArray('rights_id', $possible_rights, [ 'values' => $values, 'multiple' => true, 'rand' => $rand, 'width' => '100%' ]); echo "

"; echo "".__("Save").""; Html::closeForm(true); } /** * Return the html for the given card_id * * @param string $card_id identifier return by @see self::getAllDasboardCards * @param array $card_options contains these keys: * - array 'args': * - string 'gridstack_id' unique identifier of the card in the grid, used to return html by cache * - bool 'force' if true, cache will be bypassed * - bool 'embed' is the dashboard emebeded or not * * @return string html of the card */ public function getCardHtml(string $card_id = "", array $card_options = []): string { global $GLPI_CACHE; $gridstack_id = $card_options['args']['gridstack_id'] ?? $card_id; $dashboard = $card_options['dashboard'] ?? ""; $force = ($card_options['args']['force'] ?? $card_options['force'] ?? false); // retrieve card $notfound_html = "
". __('empty card !')."
"; $cards = $this->getAllDasboardCards($force); if (!isset($cards[$card_id])) { return $notfound_html; } $card = $cards[$card_id]; // manage cache $options_footprint = sha1(serialize($card_options). $_SESSION['glpiactiveentities_string'] ?? ""); $use_cache = !$force && $_SESSION['glpi_use_mode'] != Session::DEBUG_MODE && (!isset($card['cache']) || $card['cache'] == true); $cache_key = "dashboard_card_{$dashboard}_{$options_footprint}"; $cache_age = 40; if ($use_cache) { // browser cache if (GLPI_AJAX_DASHBOARD) { header_remove('Pragma'); header('Cache-Control: public'); header('Cache-Control: max-age=' . $cache_age); header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + $cache_age)); } // server cache if ($GLPI_CACHE->has($cache_key)) { $dashboard_cards = $GLPI_CACHE->get($cache_key); if (isset($dashboard_cards[$gridstack_id])) { return (string) $dashboard_cards[$gridstack_id]; } } } $html = ""; $start = microtime(true); // call provider to retrieve data if (isset($card['provider'])) { $provider_args = ($card['args'] ?? []) + [ 'params' => [ 'label' => $card['label'] ?? "" ] ]; if (isset($card_options['args']['apply_filters'])) { $provider_args['params']['apply_filters'] = $card_options['args']['apply_filters']; } $widget_args = call_user_func_array($card['provider'], $provider_args); } $widget_args = array_merge($widget_args ?? [], $card_options['args'] ?? []); // call widget function to construct html $all_widgets = Widget::getAllTypes(); $widgettype = $card_options['args']['widgettype'] ?? ""; $widgetfct = $all_widgets[$widgettype]['function'] ?? ""; if (strlen($widgetfct)) { // clean urls in embed mode if (isset($card_options['embed']) && $card_options['embed']) { unset($widget_args['url']); if (isset($widget_args['data'])) { $unset_url = function (&$array) use(&$unset_url) { unset($array['url']); foreach ($array as &$value) { if (is_array($value)) { $unset_url($value, 'url'); } } }; $unset_url($widget_args['data']); } } if (isset($card['filters'])) { $widget_args['filters'] = $card['filters']; } // call widget function $html = call_user_func($widgetfct, $widget_args); } // display a warning for empty card if (strlen($html) === 0) { return $notfound_html; } $execution_time = round(microtime(true) - $start, 3); // store server cache if (strlen($dashboard)) { $dashboard_cards = $GLPI_CACHE->get($cache_key) ?? []; $dashboard_cards[$gridstack_id] = $html; $GLPI_CACHE->set($cache_key, $dashboard_cards, new \DateInterval("PT".$cache_age."S")); } if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) { $html.= << {$execution_time}s HTML; } return $html; } /** * Return Html for a provided set of filters * @param array $filter_names * * @return string the html */ public function getFiltersSetHtml(array $filters = []): string { $html = ""; foreach ($filters as $filter_id => $filter_values) { $html.= $this->getFilterHtml($filter_id, $filter_values); } return $html; } /** * Return Html for a provided filter name * * @param string $filter_id the system name of a filter (ex dates) * @param string|array $filter_values init the input with these values, * will be a string if empty values * * @return string the html */ public function getFilterHtml(string $filter_id = "", $filter_values = ""): string { if (method_exists("Glpi\Dashboard\Filter", $filter_id)) { return call_user_func("Glpi\Dashboard\Filter::$filter_id", $filter_values); } return ""; } /** * Return all itemtypes possible for constructing cards. * User in @see self::getAllDasboardCards * * @return array [itemtype1, itemtype2] */ protected function getMenuItemtypes(): array { $menu_itemtypes = []; $exclude = [ 'Config', ]; $menu = Html::getMenuInfos(); array_walk($menu, function($firstlvl) use (&$menu_itemtypes) { $key = $firstlvl['title']; if (isset($firstlvl['types'])) { $menu_itemtypes[$key] = array_merge($menu_itemtypes[$key] ?? [], $firstlvl['types']); } }); foreach ($menu_itemtypes as &$firstlvl) { $firstlvl = array_filter($firstlvl, function($itemtype) use ($exclude) { if (in_array($itemtype, $exclude) || !is_subclass_of($itemtype, 'CommonDBTM')) { return false; } $testClass = new \ReflectionClass($itemtype); return !$testClass->isAbstract(); }); } return $menu_itemtypes; } /** * Construct catalog of all possible cards addable in a dashboard. * * @param bool $force if true, don't use cache * * @return array */ public function getAllDasboardCards(bool $force = false): array { global $GLPI_CACHE, $CFG_GLPI; if (!$force && $GLPI_CACHE->has("dashboards_cards") && $_SESSION['glpi_use_mode'] != Session::DEBUG_MODE) { return $GLPI_CACHE->get("dashboards_cards"); } // anonymous fct for adding relevant filters to cards $add_filters_fct = function($itemtable) { $DB = DBConnection::getReadConnection(); $add_filters = []; if ($DB->fieldExists($itemtable, "ititlcategories_id")) { $add_filters[] = "itilcategory"; } if ($DB->fieldExists($itemtable, "requesttypes_id")) { $add_filters[] = "requesttype"; } if ($DB->fieldExists($itemtable, "locations_id")) { $add_filters[] = "location"; } if ($DB->fieldExists($itemtable, "manufacturers_id")) { $add_filters[] = "manufacturer"; } return $add_filters; }; $cards = []; $menu_itemtypes = $this->getMenuItemtypes(); foreach ($menu_itemtypes as $firstlvl => $itemtypes) { foreach ($itemtypes as $itemtype) { $clean_itemtype = str_replace('\\', '_', $itemtype); $cards["bn_count_$clean_itemtype"] = [ 'widgettype' => ["bigNumber"], 'group' => $firstlvl, 'itemtype' => "\\$itemtype", 'label' => sprintf(__("Number of %s"), $itemtype::getTypeName()), 'provider' => "Glpi\\Dashboard\\Provider::bigNumber$itemtype", 'filters' => array_merge([ 'dates', 'dates_mod', ], $add_filters_fct($itemtype::getTable())) ]; } } // add multiple width for Assets itemtypes grouped by their foreign keys $assets = array_merge($CFG_GLPI['asset_types'], ['Software']); foreach ($assets as $itemtype) { $fk_itemtypes = [ 'State', 'Entity', 'Manufacturer', 'Location', ]; if (class_exists($itemtype.'Type')) { $fk_itemtypes[] = $itemtype.'Type'; } if (class_exists($itemtype.'Model')) { $fk_itemtypes[] = $itemtype.'Model'; } foreach ($fk_itemtypes as $fk_itemtype) { $label = sprintf( __("%s by %s"), $itemtype::getTypeName(Session::getPluralNumber()), $fk_itemtype::getFieldLabel() ); $cards["count_".$itemtype."_".$fk_itemtype] = [ 'widgettype' => ['summaryNumbers', 'multipleNumber', 'pie', 'donut', 'halfpie', 'halfdonut', 'bar', 'hbar'], 'itemtype' => "\\Computer", 'group' => __('Assets'), 'label' => $label, 'provider' => "Glpi\\Dashboard\\Provider::multipleNumber".$itemtype."By".$fk_itemtype, 'filters' => array_merge([ 'dates', 'dates_mod', ], $add_filters_fct($itemtype::getTable())) ]; } } $tickets_cases = [ 'late' => __("Late tickets"), 'waiting_validation' => __("Ticket waiting for your approval"), 'notold' => __('Not solved tickets'), 'incoming' => __("New tickets"), 'waiting' => __('Pending tickets'), 'assigned' => __('Assigned tickets'), 'planned' => __('Planned tickets'), 'solved' => __('Solved tickets'), 'closed' => __('Closed tickets'), ]; foreach ($tickets_cases as $case => $label) { $cards["bn_count_tickets_$case"] = [ 'widgettype' => ["bigNumber"], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => sprintf(__("Number of %s"), $label), 'provider' => "Glpi\\Dashboard\\Provider::nbTicketsGeneric", 'args' => [ 'case' => $case, ], 'cache' => false, 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; $cards["table_count_tickets_$case"] = [ 'widgettype' => ["searchShowList"], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => sprintf(__("List of %s"), $label), 'provider' => "Glpi\\Dashboard\\Provider::nbTicketsGeneric", 'args' => [ 'case' => $case, ], 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; } // add specific ticket's cases $cards["nb_opened_ticket"] = [ 'widgettype' => ['line', 'area', 'bar'], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => __("Number of tickets by month"), 'provider' => "Glpi\\Dashboard\\Provider::ticketsOpened", 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; $cards["ticket_evolution"] = [ 'widgettype' => ['lines', 'areas', 'bars', 'stackedbars'], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => __("Evolution of ticket in the past year"), 'provider' => "Glpi\\Dashboard\\Provider::getTicketsEvolution", 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; $cards["ticket_status"] = [ 'widgettype' => ['lines', 'areas', 'bars', 'stackedbars'], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => __("Tickets status by month"), 'provider' => "Glpi\\Dashboard\\Provider::getTicketsStatus", 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; $cards["ticket_times"] = [ 'widgettype' => ['lines', 'areas', 'bars', 'stackedbars'], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => __("Tickets times (in hours)"), 'provider' => "Glpi\\Dashboard\\Provider::averageTicketTimes", 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; $cards["tickets_summary"] = [ 'widgettype' => ['summaryNumbers', 'multipleNumber', 'bar', 'hbar'], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => __("Tickets summary"), 'provider' => "Glpi\\Dashboard\\Provider::getTicketSummary", 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; foreach ([ 'ITILCategory' => __("Top ticket's categories"), 'Entity' => __("Top ticket's entities"), 'RequestType' => __("Top ticket's request types"), 'Location' => __("Top ticket's locations"), ] as $itemtype => $label) { $cards["top_ticket_$itemtype"] = [ 'widgettype' => ['summaryNumbers', 'pie', 'donut', 'halfpie', 'halfdonut', 'multipleNumber', 'bar', 'hbar'], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => $label, 'provider' => "Glpi\\Dashboard\\Provider::multipleNumberTicketBy$itemtype", 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; } foreach ([ 'user_requester' => __("Top ticket's requesters"), 'group_requester' => __("Top ticket's requester groups"), 'user_observer' => __("Top ticket's observers"), 'group_observer' => __("Top ticket's observer groups"), 'user_assign' => __("Top ticket's assignees"), 'group_assign' => __("Top ticket's assignee groups"), ] as $type => $label) { $cards["top_ticket_$type"] = [ 'widgettype' => ['pie', 'donut', 'halfpie', 'halfdonut', 'summaryNumbers', 'multipleNumber', 'bar', 'hbar'], 'itemtype' => "\\Ticket", 'group' => __('Assistance'), 'label' => $label, 'provider' => "Glpi\\Dashboard\\Provider::nbTicketsActor", 'args' => [ 'case' => $type, ], 'filters' => ['dates', 'dates_mod', 'itilcategory', 'requesttype', 'location'] ]; } $cards["RemindersList"] = [ 'widgettype' => ["articleList"], 'label' => __("List of reminders"), 'group' => __('Tools'), 'provider' => "Glpi\\Dashboard\\Provider::getArticleListReminder", 'filters' => ['dates', 'dates_mod'] ]; $cards["markdown_editable"] = [ 'widgettype' => ["markdown"], 'label' => __("Editable markdown card"), 'group' => __('Others'), 'card_options' => [ 'content' => __("Toggle edit mode to edit content"), ] ]; $more_cards = Plugin::doHookFunction("dashboard_cards"); if (is_array($more_cards)) { $cards = array_merge($cards, $more_cards); } $GLPI_CACHE->set("dashboards_cards", $cards, new \DateInterval("PT1H")); return $cards; } function getRights($interface = 'central') { return [ READ => __('Read'), UPDATE => __('Update'), CREATE => __('Create'), PURGE => [ 'short' => __('Purge'), 'long' => _x('button', 'Delete permanently') ] ]; } /** * Save last dashboard viewed * * @param string $page current page * @param string $dashboard current dashboard * * @return void */ function setLastDashboard(string $page = "", string $dashboard = "") { $_SESSION['last_dashboards'][$page] = $dashboard; } /** * Restore last viewed dashboard * * @return string the dashboard key */ function restoreLastDashboard():string { global $CFG_GLPI; $new_key = ""; $target = $_REQUEST['_target'] ?? $_SERVER['REQUEST_URI'] ?? ""; if (isset($_SESSION['last_dashboards']) && strlen($target) > 0) { $target = str_replace($CFG_GLPI['root_doc'], '', $target); if (!isset($_SESSION['last_dashboards'][$target])) { return ""; } $new_key = $_SESSION['last_dashboards'][$target]; $dashboard = new Dashboard($new_key); if (!$dashboard->canViewCurrent()) { return ""; } $this->current = $new_key; } return $new_key; } /** * Retrieve the default dashboard for a specific menu entry * First try from session * then on config * And Fallback on the first dashboard found * * @param string $menu * @param bool $strict if true, do not provide a fallback * * @return string the dashboard key */ static function getDefaultDashboardForMenu(string $menu = "", bool $strict = false): string { $grid = new self; if (!$strict) { $restored = $grid->restoreLastDashboard(); if (strlen($restored) > 0) { return $restored; } } $config_key = 'default_dashboard_'.$menu; $default = $_SESSION["glpi$config_key"] ?? ""; if (strlen($default)) { $dasboard = new Dashboard($default); if ($dasboard->load()) { return $default; } } // if default not found, return first dashboards if (!$strict) { self::loadAllDashboards(); $first_dashboard = array_shift(self::$all_dashboards); if (isset($first_dashboard['key'])) { return $first_dashboard['key']; } } return ""; } static function dropdownDashboard(string $name = "", array $params = []): string { self::loadAllDashboards(); $can_view_all = $params['can_view_all'] ?? false; $options_dashboards = []; foreach (self::$all_dashboards as $key => $dashboard) { if ($can_view_all || self::canViewSpecificicDashboard($key)) { $options_dashboards[$key] = $dashboard['name'] ?? $key;; } } return Dropdown::showFromArray($name, $options_dashboards, $params); } }