Files
MYSOPHAL/inc/impact.class.php
2025-08-07 13:15:31 +01:00

1843 lines
58 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
/**
* @since 9.5.0
*/
class Impact extends CommonGLPI {
// Constants used to express the direction or "flow" of a graph
// Theses constants can also be used to express if an edge is reachable
// when exploring the graph forward, backward or both (0b11)
const DIRECTION_FORWARD = 0b01;
const DIRECTION_BACKWARD = 0b10;
// Default colors used for the edges of the graph according to their flow
const DEFAULT_COLOR = 'black'; // The edge is not accessible from the starting point of the graph
const IMPACT_COLOR = '#ff3418'; // Forward
const DEPENDS_COLOR = '#1c76ff'; // Backward
const IMPACT_AND_DEPENDS_COLOR = '#ca29ff'; // Forward and backward
const NODE_ID_DELIMITER = "::";
const EDGE_ID_DELIMITER = "->";
// Consts for depth values
const DEFAULT_DEPTH = 5;
const MAX_DEPTH = 10;
const NO_DEPTH_LIMIT = 10000;
// Config values
const CONF_ENABLED = 'impact_enabled_itemtypes';
public static function getTypeName($nb = 0) {
return __('Impact analysis');
}
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) {
global $DB;
// Class of the current item
$class = get_class($item);
// Only enabled for CommonDBTM
if (!is_a($item, "CommonDBTM", true)) {
throw new InvalidArgumentException(
"Argument \$item ($class) must be a CommonDBTM."
);
}
$is_enabled_asset = self::isEnabled($class);
$is_itil_object = is_a($item, "CommonITILObject", true);
// Check if itemtype is valid
if (!$is_enabled_asset && !$is_itil_object) {
throw new InvalidArgumentException(
"Argument \$item ($class) is not a valid target for impact analysis."
);
}
if (!$_SESSION['glpishow_count_on_tabs']
|| !isset($item->fields['id'])
|| $is_itil_object
) {
// Count is disabled in config OR no item loaded OR ITIL object -> no count
$total = 0;
} else if ($is_enabled_asset) {
// If on an asset, get the number of its direct dependencies
$total = count($DB->request([
'FROM' => ImpactRelation::getTable(),
'WHERE' => [
'OR' => [
[
// Source item is our item
'itemtype_source' => get_class($item),
'items_id_source' => $item->fields['id'],
],
[
// Impacted item is our item AND source item is enabled
'itemtype_impacted' => get_class($item),
'items_id_impacted' => $item->fields['id'],
'itemtype_source' => self::getEnabledItemtypes()
]
]
]
]));
}
return self::createTabEntry(__("Impact analysis"), $total);
}
public static function displayTabContentForItem(
CommonGLPI $item,
$tabnum = 1,
$withtemplate = 0
) {
// Impact analysis should not be available outside of central
if (Session::getCurrentInterface() !== "central") {
return false;
}
$class = get_class($item);
// Only enabled for CommonDBTM
if (!is_a($item, "CommonDBTM")) {
throw new InvalidArgumentException(
"Argument \$item ($class) must be a CommonDBTM)."
);
}
$ID = $item->fields['id'];
// Don't show the impact analysis on new object
if ($item->isNewID($ID)) {
return false;
}
// Check READ rights
$itemtype = $item->getType();
if (!$itemtype::canView()) {
return false;
}
// For an ITIL object, load the first linked element by default
if (is_a($item, "CommonITILObject")) {
$linked_items = $item->getLinkedItems();
// Search for a valid linked item of this ITILObject
$found = false;
foreach ($linked_items as $linked_item) {
$class = $linked_item['itemtype'];
if (self::isEnabled($class)) {
$item = new $class;
$found = $item->getFromDB($linked_item['items_id']);
break;
}
}
// No valid linked item were found, tab shouldn't be visible
if (!$found) {
return false;
}
self::printAssetSelectionForm($linked_items);
}
// Check is the impact analysis is enabled for $class
if (!self::isEnabled($class)) {
return false;
}
// Build graph and params
$graph = self::buildGraph($item);
$params = self::prepareParams($item);
$readonly = !$item->can($item->fields['id'], UPDATE);
// Print header
self::printHeader(self::makeDataForCytoscape($graph), $params, $readonly);
// Displays views
self::displayGraphView($item);
self::displayListView($item, $graph, true);
// Select view
echo Html::scriptBlock("
// Select default view
$(document).ready(function() {
if (location.hash == '#list') {
showListView();
} else {
showGraphView();
}
});
");
return true;
}
/**
* Display the impact analysis as an interactive graph
*
* @param CommonDBTM $item starting point of the graph
*/
public static function displayGraphView(
CommonDBTM $item
) {
self::loadLibs();
echo '<div id="impact_graph_view">';
self::prepareImpactNetwork($item);
echo '</div>';
}
/**
* Display the impact analysis as a list
*
* @param CommonDBTM $item starting point of the graph
* @param string $graph array containing the graph nodes and egdes
*/
public static function displayListView(
CommonDBTM $item,
array $graph,
bool $scripts = false
) {
global $CFG_GLPI;
$impact_item = ImpactItem::findForItem($item);
$impact_context = ImpactContext::findForImpactItem($impact_item);
if (!$impact_context) {
$max_depth = self::DEFAULT_DEPTH;
} else {
$max_depth = $impact_context->fields['max_depth'];
}
echo '<div id="impact_list_view">';
echo '<div class="impact-list-container">';
// One table will be printed for each direction
$lists = [
__("Impact") => self::DIRECTION_FORWARD,
__("Impacted by") => self::DIRECTION_BACKWARD,
];
$has_impact = false;
foreach ($lists as $label => $direction) {
$start_node_id = self::getNodeID($item);
$data = self::buildListData($graph, $direction, $item, $max_depth);
if (!count($data)) {
continue;
}
$has_impact = true;
echo '<table class="tab_cadre_fixehov impact-list-group">';
// Header
echo '<thead>';
echo '<tr class="noHover">';
echo '<th class="impact-list-header" colspan="6" width="90%"><h3>' . $label . '';
echo '<i class="fas fa-2x fa-caret-down impact-toggle-subitems-master impact-pointer"></i></h3></th>';
echo '</tr>';
echo '<tr class="noHover">';
echo '<th>'._n('Item', 'Items', 1).'</th>';
echo '<th>'.__('Relation').'</th>';
echo '<th>'.Ticket::getTypeName(Session::getPluralNumber()).'</th>';
echo '<th>'.Problem::getTypeName(Session::getPluralNumber()).'</th>';
echo '<th>'.Change::getTypeName(Session::getPluralNumber()).'</th>';
echo '<th width="50px"></th>';
echo '</tr>';
echo '</thead>';
foreach ($data as $itemtype => $items) {
echo '<tbody>';
// Subheader
echo '<tr class="tab_bg_1">';
echo '<td class="left subheader impact-left" colspan="6">';
$total = count($items);
echo '<a>' . $itemtype::getTypeName() . '</a>' . ' (' . $total . ')';
echo '<i class="fas fa-2x fa-caret-down impact-toggle-subitems impact-pointer"></i>';
echo '</td>';
echo '</tr>';
foreach ($items as $itemtype_item) {
// Content: one row per item
echo '<tr class=tab_bg_1><div></div>';
echo '<td class="impact-left" width="15%">';
echo '<div><a target="_blank" href="' .
$itemtype_item['stored']->getLinkURL() . '">' .
$itemtype_item['stored']->getFriendlyName() . '</a></div>';
echo '</td>';
echo '<td width="40%"><div>';
$path = [];
foreach ($itemtype_item['node']['path'] as $node) {
if ($node['id'] == $start_node_id) {
$path[] = '<b>' . $node['label'] . '</b>';
} else {
$path[] = $node['label'];
}
}
$separator = '<i class="fas fa-angle-right"></i>';
echo implode(" $separator ", $path);
echo '</div></td>';
self::displayListNumber(
$itemtype_item['node']['ITILObjects']['incidents'],
Ticket::class,
$itemtype_item['node']['id']
);
self::displayListNumber(
$itemtype_item['node']['ITILObjects']['problems'],
Problem::class,
$itemtype_item['node']['id']
);
self::displayListNumber(
$itemtype_item['node']['ITILObjects']['changes'],
Change::class,
$itemtype_item['node']['id']
);
echo '<td class="center"><div></div></td>';
echo '</tr>';
}
echo '</tbody>';
}
echo '</table>';
}
if (!$has_impact) {
echo '<p>' . __("This asset doesn't have any dependencies.") . '</p>';
}
echo '</div>';
$can_update = $item->can($item->fields['id'], UPDATE);
// Toolbar
echo '<div class="impact-list-toolbar">';
if ($has_impact) {
echo '<a target="_blank" href="'.$CFG_GLPI['root_doc'].'/front/impactcsv.php?itemtype=' . $impact_item->fields['itemtype'] . '&items_id=' . $impact_item->fields['items_id'] .'">';
echo '<i class="fas fa-download impact-pointer impact-list-tools" title="' . __('Export to csv') .'"></i>';
echo '</a>';
}
if ($can_update && $impact_context) {
echo '<i id="impact-list-settings" class="fas fa-cog impact-pointer impact-list-tools" title="' . __('Settings') .'"></i>';
}
echo '</div>';
// Settings dialog
if ($can_update && $impact_context) {
$rand = mt_rand();
echo '<div id="list_depth_dialog" class="impact-dialog" title=' . __("Settings") . '>';
echo '<form action="'.$CFG_GLPI['root_doc'].'/front/impactitem.form.php" method="POST">';
echo '<table class="tab_cadre_fixe">';
echo '<tr>';
echo '<td><label for="impact_max_depth_' . $rand . '">' . __("Max depth") . '</label></td>';
echo '<td>' . Html::input("max_depth", [
'id' => "impact_max_depth_$rand",
'value' => $max_depth >= self::MAX_DEPTH ? '' : $max_depth,
]) . '</td>';
echo '</tr>';
echo '<tr>';
echo '<td><label for="check_no_limit_' . $rand . '">' . __("No limit") . '</label></td>';
echo '<td>' . Html::getCheckbox([
'name' => 'no_limit',
'id' => "check_no_limit_$rand",
'checked' => $max_depth >= self::MAX_DEPTH,
]) . '</td>';
echo '</tr>';
echo '</table>';
echo Html::input('id', [
'type' => "hidden",
'value' => $impact_context->fields['id'],
]);
echo Html::input('update', [
'type' => "hidden",
'value' => "1",
]);
Html::closeForm();
echo '</div>';
}
echo '</div>';
// Stop here if we do not need to generate scripts
if (!$scripts) {
return;
}
// Hide / show handler
echo Html::scriptBlock('
// jQuery doesn\'t allow slide animation on table elements, we need
// to apply the animation to each cells content and then remove the
// padding to get the desired "slide" animation
function impactListUp(target) {
target.removeClass("fa-caret-down");
target.addClass("fa-caret-up");
target.closest("tbody").find(\'tr:gt(0) td\').animate({padding: \'0px\'}, {duration: 400});
target.closest("tbody").find(\'tr:gt(0) div\').slideUp("400");
}
function impactListDown(target) {
target.addClass("fa-caret-down");
target.removeClass("fa-caret-up");
target.closest("tbody").find(\'tr:gt(0) td\').animate({padding: \'8px 5px\'}, {duration: 400});
target.closest("tbody").find(\'tr:gt(0) div\').slideDown("400");
}
$(document).on("click", ".impact-toggle-subitems", function(e) {
if ($(e.target).hasClass("fa-caret-up")) {
impactListDown($(e.target));
} else {
impactListUp($(e.target));
}
});
$(document).on("click", ".impact-toggle-subitems-master", function(e) {
$(e.target).closest("table").find(".impact-toggle-subitems").each(function(i, elem) {
if ($(e.target).hasClass("fa-caret-up")) {
impactListDown($(elem));
} else {
impactListUp($(elem));
}
});
$(e.target).toggleClass("fa-caret-up");
$(e.target).toggleClass("fa-caret-down");
});
$(document).on("impactUpdated", function() {
$.ajax({
type: "GET",
url: "' . $CFG_GLPI['root_doc'] . '/ajax/impact.php",
data: {
itemtype: "' . get_class($item) . '",
items_id: "' . $item->fields['id'] . '",
action : "load",
view : "list",
},
success: function(data){
$("#impact_list_view").replaceWith(data);
showGraphView();
},
});
});
');
if ($can_update) {
// Handle settings actions
echo Html::scriptBlock('
$("#impact-list-settings").click(function() {
$("#list_depth_dialog").dialog({
modal: true,
buttons: {
' . __("Save") . ': function() {
if ($("input[name=\'no_limit\']:checked").length > 0) {
$("input[name=\'max_depth\']").val(' . self::NO_DEPTH_LIMIT . ');
}
$(this).find("form").submit();
},
' . __("Cancel") . ': function() {
$(this).dialog( "close" );
}
},
});
});
');
}
}
/**
* Display "number" cell in list view
* The cell is empty if no itilobjets are found, else it contains the
* number of iitilobjets found, use the highest priority as it's background
* color and is a link to matching search result
*
* @param array $itil_objects
* @param string $type
* @param string $node_id
*/
private static function displayListNumber($itil_objects, $type, $node_id) {
$user = new User();
$user->getFromDB(Session::getLoginUserID());
$user->computePreferences();
$count = count($itil_objects) ?: "";
$extra = "";
$node_details = explode(self::NODE_ID_DELIMITER, $node_id);
if ($count) {
$priority = 1;
$id = "impact_list_itilcount_" . mt_rand();
$link = "";
switch ($type) {
case Ticket::class:
$link = Ticket::getSearchURL();
$link .= "?is_deleted=0&as_map=0&search=Search&itemtype=Ticket";
$link .= "&criteria[0][link]=AND&criteria[0][field]=13&criteria[0][searchtype]=contains&criteria[0][value]=" . $node_details[1];
$link .= "&criteria[1][link]=AND&criteria[1][field]=131&criteria[1][searchtype]=equals&criteria[1][value]=" . $node_details[0];
$link .= "&criteria[2][link]=AND&criteria[2][field]=14&criteria[2][searchtype]=equals&criteria[2][value]=1";
$link .= "&criteria[3][link]=AND&criteria[3][field]=12&criteria[3][searchtype]=equals&criteria[3][value]=notold";
break;
case Problem::class:
$link = Problem::getSearchURL();
$link .= "?is_deleted=0&as_map=0&search=Search&itemtype=Problem";
$link .= "&criteria[0][link]=AND&criteria[0][field]=13&criteria[0][searchtype]=contains&criteria[0][value]=" . $node_details[1];
$link .= "&criteria[1][link]=AND&criteria[1][field]=131&criteria[1][searchtype]=equals&criteria[1][value]=" . $node_details[0];
$link .= "&criteria[3][link]=AND&criteria[3][field]=12&criteria[3][searchtype]=equals&criteria[3][value]=notold";
break;
case Change::class:
$link = Change::getSearchURL();
$link .= "?is_deleted=0&as_map=0&search=Search&itemtype=Change";
$link .= "&criteria[0][link]=AND&criteria[0][field]=13&criteria[0][searchtype]=contains&criteria[0][value]=" . $node_details[1];
$link .= "&criteria[1][link]=AND&criteria[1][field]=131&criteria[1][searchtype]=equals&criteria[1][value]=" . $node_details[0];
$link .= "&criteria[3][link]=AND&criteria[3][field]=12&criteria[3][searchtype]=equals&criteria[3][value]=notold";
break;
}
// Compute max priority
foreach ($itil_objects as $itil_object) {
if ($priority < $itil_object['priority']) {
$priority = $itil_object['priority'];
}
}
$extra = 'id="' . $id . '" style="background-color:' . $user->fields["priority_$priority"] .'; cursor:pointer;"';
echo Html::scriptBlock('
$(document).on("click", "#' . $id . '", function(e) {
window.open("' . $link . '");
});
');
}
echo '<td class="center" ' . $extra . '><div>' . $count . '</div></td>';
}
/**
* Build the data used to represent the impact graph as a semi-flat list
*
* @param array $graph array containing the graph nodes and egdes
* @param int $direction should the list be build for item that are
* impacted by $item or that impact $item ?
* @param CommonDBTM $item starting point of the graph
* @param int $max_depth max depth from context
*
* @return array
*/
public static function buildListData(
array $graph,
int $direction,
CommonDBTM $item,
int $max_depth
) {
$data = [];
// Filter tree
$sub_graph = self::filterGraph($graph, $direction);
// Empty graph, no need to go further
if (!count($sub_graph['nodes'])) {
return $data;
}
// Evaluate path to each assets from the starting node
$start_node_id = self::getNodeID($item);
$start_node = $sub_graph['nodes'][$start_node_id];
foreach ($sub_graph['nodes'] as $key => $vertex) {
if ($key !== $start_node_id) {
// Set path for target node using BFS
$path = self::bfs(
$sub_graph,
$start_node,
$vertex,
$direction
);
// Add if path is not longer than the allowed value
if (count($path) - 1 <= $max_depth) {
$sub_graph['nodes'][$key]['path'] = $path;
}
}
}
// Split the items by type
foreach ($sub_graph['nodes'] as $node) {
$details = explode(self::NODE_ID_DELIMITER, $node['id']);
$itemtype = $details[0];
$items_id = $details[1];
// Skip start node or empty path
if ($node['id'] == $start_node_id || !isset($node['path'])) {
continue;
}
// Init itemtype if empty
if (!isset($data[$itemtype])) {
$data[$itemtype] = [];
}
// Add to itemtype
$itemtype_item = new $itemtype;
$itemtype_item->getFromDB($items_id);
$data[$itemtype][] = [
'stored' => $itemtype_item,
'node' => $node,
];
}
return $data;
}
/**
* Return a subgraph matching the given direction
*
* @param array $graph array containing the graph nodes and egdes
* @param int $direction direction to match
*
* @return array
*/
public static function filterGraph(array $graph, int $direction) {
$new_graph = [
'edges' => [],
'nodes' => [],
];
// For each edge in the graph
foreach ($graph['edges'] as $edge) {
// Filter on direction
if ($edge['flag'] & $direction) {
// Add the edge and its two connected nodes
$source = $edge['source'];
$target = $edge['target'];
$new_graph['edges'][] = $edge;
$new_graph['nodes'][$source] = $graph['nodes'][$source];
$new_graph['nodes'][$target] = $graph['nodes'][$target];
}
}
return $new_graph;
}
/**
* Evaluate the path from one node to another using BFS algorithm
*
* @param array $graph array containing the graph nodes and egdes
* @param array $a a node of the graph
* @param array $b a node of the graph
* @param int $direction direction used to travel the graph
*/
public static function bfs(array $graph, array $a, array $b, int $direction) {
switch ($direction) {
case self::DIRECTION_FORWARD:
$start = $a;
$target = $b;
break;
case self::DIRECTION_BACKWARD:
$start = $b;
$target = $a;
break;
default:
throw new \InvalidArgumentException("Invalid direction : $direction");
}
// Insert start node in the queue
$queue = [];
$queue[] = $start;
$discovered = [$start['id'] => true];
// Label start as discovered
$start['discovered'] = true;
// For each other nodes
while (count($queue) > 0) {
$node = array_shift($queue);
if ($node['id'] == $target['id']) {
// target found, build path to node
$path = [$target];
while (isset($node['dfs_parent'])) {
$node = $node['dfs_parent'];
array_unshift($path, $node);
}
return $path;
}
foreach ($graph['edges'] as $edge) {
// Skip edge if not connected to the current node
if ($edge['source'] !== $node['id']) {
continue;
}
$nextNode = $graph['nodes'][$edge['target']];
// Skip already discovered node
if (isset($discovered[$nextNode['id']])) {
continue;
}
$nextNode['dfs_parent'] = $node;
$discovered[$nextNode['id']] = true;
$queue[] = $nextNode;
}
}
}
/**
* Print the title and view switch
*
* @param string $graph The network graph (json)
* @param string $params Params of the graph (json)
* @param bool $readonly Is the graph editable ?
*/
public static function printHeader(
string $graph,
string $params,
bool $readonly
) {
echo '<div class="impact-header">';
echo "<h2>" . __("Impact analysis") . "</h2>";
echo "<div id='switchview'>";
echo "<a id='sviewlist' href='#list'><i class='pointer fa fa-list-alt' title='".__('View as list')."'></i></a>";
echo "<a id='sviewgraph' href='#graph'><i class='pointer fa fa-bezier-curve' title='".__('View graphical representation')."'></i></a>";
echo "</div>";
echo "</div>";
// View selection
echo Html::scriptBlock("
function showGraphView() {
$('#impact_list_view').hide();
$('#impact_graph_view').show();
$('#sviewlist i').removeClass('selected');
$('#sviewgraph i').addClass('selected');
if (window.GLPIImpact !== undefined && GLPIImpact.cy === null) {
GLPIImpact.buildNetwork($graph, $params, $readonly);
}
}
function showListView() {
$('#impact_graph_view').hide();
$('#impact_list_view').show();
$('#sviewgraph i').removeClass('selected');
$('#sviewlist i').addClass('selected');
$('#save_impact').removeClass('clean');
}
$('#sviewgraph').click(function() {
showGraphView();
});
$('#sviewlist').click(function() {
showListView();
});
");
}
/**
* Load the cytoscape library
*
* @since 9.5
*/
public static function loadLibs() {
echo Html::css('public/lib/cytoscape.css');
echo Html::script("public/lib/cytoscape.js");
}
/**
* Print the asset selection form used in the impact tab of ITIL objects
*
* @param array $items
*
* @since 9.5
*/
public static function printAssetSelectionForm(array $items) {
global $CFG_GLPI;
// Dropdown values
$values = [];
// Add a value in the dropdown for each items, grouped by type
foreach ($items as $item) {
if (self::isEnabled($item['itemtype'])) {
// Add itemtype group if it doesn't exist in the dropdown yet
$itemtype_label = $item['itemtype']::getTypeName();
if (!isset($values[$itemtype_label])) {
$values[$itemtype_label] = [];
}
$key = $item['itemtype'] . "::" . $item['items_id'];
$values[$itemtype_label][$key] = $item['name'];
}
}
Dropdown::showFromArray("impact_assets_selection_dropdown", $values);
echo '<div class="impact-mb-2"></div>';
// Form interaction: load a new graph on value change
echo Html::scriptBlock('
$(function() {
var selector = "select[name=impact_assets_selection_dropdown]";
$(selector).change(function(){
var values = $(selector + " option:selected").val().split("::");
$.ajax({
type: "GET",
url: "'. $CFG_GLPI['root_doc'] . '/ajax/impact.php",
data: {
itemtype: values[0],
items_id: values[1],
action : "load",
},
success: function(data, textStatus, jqXHR) {
GLPIImpact.buildNetwork(
JSON.parse(data.graph),
JSON.parse(data.params),
data.readonly
);
}
});
});
});
');
}
/**
* Search asset by itemtype and name
*
* @param string $itemtype type
* @param array $used ids to exlude from the search
* @param string $filter filter on name
* @param int $page page offset
*/
public static function searchAsset(
string $itemtype,
array $used,
string $filter,
int $page = 0
) {
global $DB;
// Check if this type is enabled in config
if (!self::isEnabled($itemtype)) {
throw new \InvalidArgumentException(
"itemtype ($itemtype) must be enabled in config"
);
}
// Check class exist and is a child of CommonDBTM
if (!is_subclass_of($itemtype, "CommonDBTM", true)) {
throw new \InvalidArgumentException(
"itemtype ($itemtype) must be a valid child of CommonDBTM"
);
}
// Return empty result if the user doesn't have READ rights
if (!Session::haveRight($itemtype::$rightname, READ)) {
return [
"items" => [],
"total" => 0
];
}
// This array can't be empty since we will use it in the NOT IN part of the reqeust
if (!count($used)) {
$used[] = -1;
}
// Search for items
$table = $itemtype::getTable();
$base_request = [
'FROM' => $table,
'WHERE' => [
'NOT' => [
"$table.id" => $used
],
],
];
// Add friendly name search criteria
$base_request['WHERE'] = array_merge(
$base_request['WHERE'],
$itemtype::getFriendlyNameSearchCriteria($filter)
);
if (is_subclass_of($itemtype, "ExtraVisibilityCriteria", true)) {
$base_request = array_merge_recursive(
$base_request,
$itemtype::getVisibilityCriteria()
);
}
$item = new $itemtype();
if ($item->isEntityAssign()) {
$base_request['WHERE'] = array_merge_recursive(
$base_request['WHERE'],
getEntitiesRestrictCriteria($itemtype::getTable())
);
}
if ($item->mayBeDeleted()) {
$base_request['WHERE']["$table.is_deleted"] = 0;
}
if ($item->mayBeTemplate()) {
$base_request['WHERE']["$table.is_template"] = 0;
}
$select = [
'SELECT' => ["$table.id", $itemtype::getFriendlyNameFields()],
];
$limit = [
'START' => $page * 20,
'LIMIT' => "20",
];
$count = [
'COUNT' => "total",
];
// Get items
$rows = $DB->request($base_request + $select + $limit);
// Get total
$total = $DB->request($base_request + $count);
return [
"items" => iterator_to_array($rows, false),
"total" => iterator_to_array($total, false)[0]['total'],
];
}
/**
* Load the impact network container
*
* @since 9.5
*/
public static function printImpactNetworkContainer() {
global $CFG_GLPI;
$action = $CFG_GLPI['root_doc'] . '/ajax/impact.php';
$formName = "form_impact_network";
echo "<form name=\"$formName\" action=\"$action\" method=\"post\" class='no-track'>";
echo "<table class='tab_cadre_fixe network-table'>";
echo '<tr><td class="network-parent">';
echo '<span id="help_text"></span>';
echo '<div id="network_container"></div>';
echo '<img class="impact-drop-preview">';
echo '<div class="impact-side">';
echo '<div class="impact-side-panel">';
echo '<div class="impact-side-add-node">';
echo '<h3>' . __('Add assets') . '</h3>';
echo '<div class="impact-side-select-itemtype">';
echo Html::input("impact-side-filter-itemtypes", [
'id' => 'impact-side-filter-itemtypes',
'placeholder' => __('Filter itemtypes...'),
]);
echo '<div class="impact-side-filter-itemtypes-items">';
$itemtypes = $CFG_GLPI["impact_asset_types"];
// Sort by translated itemtypes
uksort($itemtypes, function($a, $b) {
return strcasecmp($a::getTypeName(), $b::getTypeName());
});
foreach ($itemtypes as $itemtype => $icon) {
// Do not display this itemtype if the user doesn't have READ rights
if (!Session::haveRight($itemtype::$rightname, READ)) {
continue;
}
// Skip if not enabled
if (!self::isEnabled($itemtype)) {
continue;
}
$icon = self::checkIcon($icon);
echo '<div class="impact-side-filter-itemtypes-item">';
echo '<h4><img class="impact-side-icon" src="' . $CFG_GLPI['root_doc'] . '/' . $icon . '" title="' . $itemtype::getTypeName() . '" data-itemtype="' . $itemtype . '">';
echo "<span>" . $itemtype::getTypeName() . "</span></h4>";
echo '</div>'; // impact-side-filter-itemtypes-item
}
echo '</div>'; // impact-side-filter-itemtypes-items
echo '</div>'; // <div class="impact-side-select-itemtype">
echo '<div class="impact-side-search">';
echo '<h4><i class="fas fa-chevron-left"></i><img><span></span></h4>';
echo Html::input("impact-side-filter-assets", [
'id' => 'impact-side-filter-assets',
'placeholder' => __('Filter assets...'),
]);
echo '<div class="impact-side-search-panel">';
echo '<div class="impact-side-search-results"></div>';
echo '<div class="impact-side-search-more">';
echo '<h4><i class="fas fa-chevron-down"></i>' . __("More...") . '</h4>';
echo '</div>'; // <div class="impact-side-search-more">
echo '<div class="impact-side-search-no-results">';
echo '<p>'. __("No results") . '</p>';
echo '</div>'; // <div class="impact-side-search-no-results">
echo '<div class="impact-side-search-spinner">';
echo '<i class="fas fa-spinner fa-2x fa-spin"></i>';
echo '</div>'; // <div class="impact-side-search-spinner">
echo '</div>'; // <div class="impact-side-search-panel">
echo '</div>'; // <div class="impact-side-search">
echo '</div>'; // div class="impact-side-add-node">
echo '<div class="impact-side-settings">';
echo '<h3>' . __('Settings') . '</h3>';
echo '<h4>' . __('Visibility') . '</h4>';
echo '<div class="impact-side-settings-item">';
echo Html::getCheckbox([
'id' => "toggle_impact",
'name' => "toggle_impact",
'checked' => "true",
]);
echo '<span class="impact-checkbox-label">' . __("Show impact") . '</span>';
echo '</div>';
echo '<div class="impact-side-settings-item">';
echo Html::getCheckbox([
'id' => "toggle_depends",
'name' => "toggle_depends",
'checked' => "true",
]);
echo '<span class="impact-checkbox-label">' . __("Show depends") . '</span>';
echo '</div>';
echo '<h4>' . __('Colors') . '</h4>';
echo '<div class="impact-side-settings-item">';
Html::showColorField("depends_color", []);
echo '<span class="impact-checkbox-label">' . __("Depends") . '</span>';
echo '</div>';
echo '<div class="impact-side-settings-item">';
Html::showColorField("impact_color", []);
echo '<span class="impact-checkbox-label">' . __("Impact") . '</span>';
echo '</div>';
echo '<div class="impact-side-settings-item">';
Html::showColorField("impact_and_depends_color", []);
echo '<span class="impact-checkbox-label">' . __("Impact and depends") . '</span>';
echo '</div>';
echo '<h4>' . __('Max depth') . '</h4>';
echo '<div class="impact-side-settings-item">';
echo '<input id="max_depth" type="range" class="impact-range" min="1" max ="10" step="1" value="5"><span id="max_depth_view" class="impact-checkbox-label"></span>';
echo '</div>';
echo '</div>'; // div class="impact-side-settings">
echo '<div class="impact-side-search-footer"></div>';
echo '</div>'; // div class="impact-side-panel">
echo '<ul>';
echo '<li id="save_impact" title="' . __("Save") .'"><i class="fas fa-fw fa-save"></i></li>';
echo '<li id="impact_undo" class="impact-disabled" title="' . __("Undo") .'"><i class="fas fa-fw fa-undo"></i></li>';
echo '<li id="impact_redo" class="impact-disabled" title="' . __("Redo") .'"><i class="fas fa-fw fa-redo"></i></li>';
echo '<li class="impact-separator"></li>';
echo '<li id="add_node" title="' . __("Add asset") .'"><i class="fas fa-fw fa-plus"></i></li>';
echo '<li id="add_edge" title="' . __("Add relation") .'"><i class="fas fa-fw fa-slash"></i></li>';
echo '<li id="add_compound" title="' . __("Add group") .'"><i class="far fa-fw fa-object-group"></i></li>';
echo '<li id="delete_element" title="' . __("Delete element") .'"><i class="fas fa-fw fa-trash"></i></li>';
echo '<li class="impact-separator"></li>';
echo '<li id="export_graph" title="' . __("Download") .'"><i class="fas fa-fw fa-download"></i></li>';
echo '<li id="toggle_fullscreen" title="' . __("Fullscreen") .'"><i class="fas fa-fw fa-expand"></i></li>';
echo '<li id="impact_settings" title="' . __("Settings") .'"><i class="fas fa-fw fa-cog"></i></li>';
echo '</ul>';
echo '<span class="impact-side-toggle"><i class="fas fa-2x fa-chevron-left"></i></span>';
echo '</div>'; // <div class="impact-side impact-side-expanded">
echo "</td></tr>";
echo "</table>";
Html::closeForm();
}
/**
* Build the impact graph starting from a node
*
* @since 9.5
*
* @param CommonDBTM $item Current item
*
* @return array Array containing edges and nodes
*/
public static function buildGraph(CommonDBTM $item) {
$nodes = [];
$edges = [];
// Explore the graph forward
self::buildGraphFromNode($nodes, $edges, $item, self::DIRECTION_FORWARD);
// Explore the graph backward
self::buildGraphFromNode($nodes, $edges, $item, self::DIRECTION_BACKWARD);
// Add current node to the graph if no impact relations were found
if (count($nodes) == 0) {
self::addNode($nodes, $item);
}
// Add special flag to start node
$nodes[self::getNodeID($item)]['start'] = 1;
return [
'nodes' => $nodes,
'edges' => $edges
];
}
/**
* Explore dependencies of the current item, subfunction of buildGraph()
*
* @since 9.5
*
* @param array $edges Edges of the graph
* @param array $nodes Nodes of the graph
* @param CommonDBTM $node Current node
* @param int $direction The direction in which the graph
* is being explored : DIRECTION_FORWARD
* or DIRECTION_BACKWARD
* @param array $explored_nodes List of nodes that have already been
* explored
*
* @throws InvalidArgumentException
*/
private static function buildGraphFromNode(
array &$nodes,
array &$edges,
CommonDBTM $node,
int $direction,
array $explored_nodes = []
) {
global $DB;
// Source and target are determined by the direction in which we are
// exploring the graph
switch ($direction) {
case self::DIRECTION_BACKWARD:
$source = "source";
$target = "impacted";
break;
case self::DIRECTION_FORWARD:
$source = "impacted";
$target = "source";
break;
default:
throw new InvalidArgumentException(
"Invalid value for argument \$direction ($direction)."
);
}
// Get relations of the current node
$relations = $DB->request([
'FROM' => ImpactRelation::getTable(),
'WHERE' => [
'itemtype_' . $target => get_class($node),
'items_id_' . $target => $node->fields['id']
]
]);
// Add current code to the graph if we found at least one impact relation
if (count($relations)) {
self::addNode($nodes, $node);
}
// Iterate on each relations found
foreach ($relations as $related_item) {
// Do not explore disabled itemtypes
if (!self::isEnabled($related_item['itemtype_' . $source])) {
continue;
}
// Add the related node
if (!($related_node = getItemForItemtype($related_item['itemtype_' . $source]))) {
continue;
}
$related_node->getFromDB($related_item['items_id_' . $source]);
self::addNode($nodes, $related_node);
// Add or update the relation on the graph
$edgeID = self::getEdgeID($node, $related_node, $direction);
self::addEdge($edges, $edgeID, $node, $related_node, $direction);
// Keep exploring from this node unless we already went through it
$related_node_id = self::getNodeID($related_node);
if (!isset($explored_nodes[$related_node_id])) {
$explored_nodes[$related_node_id] = true;
self::buildGraphFromNode(
$nodes,
$edges,
$related_node,
$direction,
$explored_nodes
);
}
}
}
/**
* Check if the icon path is valid, if not return a fallback path
*
* @param string $icon_path
* @return string
*/
private static function checkIcon(string $icon_path): string {
// Special case for images returned dynamicly
if (strpos($icon_path, ".php") !== false) {
return $icon_path;
}
// Check if icon exist on the filesystem
$file_path = GLPI_ROOT . "/$icon_path";
if (file_exists($file_path) && is_file($file_path)) {
return $icon_path;
}
// Fallback "default" icon
return "pics/impact/default.png";
}
/**
* Add a node to the node list if missing
*
* @param array $nodes Nodes of the graph
* @param CommonDBTM $item Node to add
*
* @since 9.5
*
* @return bool true if the node was missing, else false
*/
private static function addNode(array &$nodes, CommonDBTM $item) {
global $CFG_GLPI;
// Check if the node already exist
$key = self::getNodeID($item);
if (isset($nodes[$key])) {
return false;
}
// Get web path to the image matching the itemtype from config
$image_name = $CFG_GLPI["impact_asset_types"][get_class($item)] ?? "";
$image_name = self::checkIcon($image_name);
// Define basic data of the new node
$new_node = [
'id' => $key,
'label' => $item->getFriendlyName(),
'image' => $CFG_GLPI['root_doc'] . "/$image_name",
'ITILObjects' => $item->getITILTickets(true),
];
// Only set GOTO link if the user have READ rights
if ($item::canView()) {
$new_node['link'] = $item->getLinkURL();
}
// Set incident badge if needed
if (count($new_node['ITILObjects']['incidents'])) {
$priority = 0;
foreach ($new_node['ITILObjects']['incidents'] as $incident) {
if ($priority < $incident['priority']) {
$priority = $incident['priority'];
}
}
$user = new User();
$user->getFromDB(Session::getLoginUserID());
$user->computePreferences();
$new_node['badge'] = [
'color' => $user->fields["priority_$priority"],
'count' => count($new_node['ITILObjects']['incidents']),
'target' => Ticket::getSearchURL(),
];
}
// Alter the label if we found some linked ITILObjects
$itil_tickets_count = $new_node['ITILObjects']['count'];
if ($itil_tickets_count > 0) {
$new_node['label'] .= " ($itil_tickets_count)";
$new_node['hasITILObjects'] = 1;
}
// Load or create a new ImpactItem object
$impact_item = ImpactItem::findForItem($item);
// Load node position and parent
$new_node['impactitem_id'] = $impact_item->fields['id'];
$new_node['parent'] = $impact_item->fields['parent_id'];
// If the node has a parent, add it to the node list aswell
if (!empty($new_node['parent'])) {
$compound = new ImpactCompound();
$compound->getFromDB($new_node['parent']);
if (!isset($nodes[$new_node['parent']])) {
$nodes[$new_node['parent']] = [
'id' => $compound->fields['id'],
'label' => $compound->fields['name'],
'color' => $compound->fields['color'],
];
}
}
// Insert the node
$nodes[$key] = $new_node;
return true;
}
/**
* Add an edge to the edge list if missing, else update it's direction
*
* @param array $edges Edges of the graph
* @param string $key ID of the new edge
* @param CommonDBTM $itemA One of the node connected to this edge
* @param CommonDBTM $itemB The other node connected to this edge
* @param int $direction Direction of the edge : A to B or B to A ?
*
* @since 9.5
*
* @return bool true if the node was missing, else false
*
* @throws InvalidArgumentException
*/
private static function addEdge(
array &$edges,
string $key,
CommonDBTM $itemA,
CommonDBTM $itemB,
int $direction
) {
// Just update the flag if the edge already exist
if (isset($edges[$key])) {
$edges[$key]['flag'] = $edges[$key]['flag'] | $direction;
return;
}
// Assign 'from' and 'to' according to the direction
switch ($direction) {
case self::DIRECTION_FORWARD:
$from = self::getNodeID($itemA);
$to = self::getNodeID($itemB);
break;
case self::DIRECTION_BACKWARD:
$from = self::getNodeID($itemB);
$to = self::getNodeID($itemA);
break;
default:
throw new InvalidArgumentException(
"Invalid value for argument \$direction ($direction)."
);
}
// Add the new edge
$edges[$key] = [
'id' => $key,
'source' => $from,
'target' => $to,
'flag' => $direction
];
}
/**
* Build the graph and the cytoscape object
*
* @since 9.5
*
* @param string $graph The network graph (json)
* @param string $params Params of the graph (json)
* @param bool $readonly Is the graph editable ?
*/
public static function buildNetwork(
string $graph,
string $params,
bool $readonly
) {
echo Html::scriptBlock("
$(function() {
GLPIImpact.buildNetwork($graph, $params, $readonly);
});
");
}
/**
* Get saved graph params for the current item
*
* @param CommonDBTM $item
*
* @return string $item
*/
public static function prepareParams(CommonDBTM $item) {
$impact_item = ImpactItem::findForItem($item);
$params = array_intersect_key($impact_item->fields, [
'parent_id' => 1,
'impactcontexts_id' => 1,
'is_slave' => 1,
]);
// Load context if exist
if ($params['impactcontexts_id']) {
$impact_context = ImpactContext::findForImpactItem($impact_item);
if ($impact_context) {
$params = $params + array_intersect_key(
$impact_context->fields,
[
'positions' => 1,
'zoom' => 1,
'pan_x' => 1,
'pan_y' => 1,
'impact_color' => 1,
'depends_color' => 1,
'impact_and_depends_color' => 1,
'show_depends' => 1,
'show_impact' => 1,
'max_depth' => 1,
]
);
}
}
return json_encode($params);
}
/**
* Convert the php array reprensenting the graph into the format required by
* the Cytoscape library
*
* @param array $graph
*
* @return string json data
*/
public static function makeDataForCytoscape(array $graph) {
$data = [];
foreach ($graph['nodes'] as $node) {
$data[] = [
'group' => 'nodes',
'data' => $node,
];
}
foreach ($graph['edges'] as $edge) {
$data[] = [
'group' => 'edges',
'data' => $edge,
];
}
return json_encode($data);
}
/**
* Load the "show ongoing tickets" dialog
*
* @since 9.5
*/
public static function printShowOngoingDialog() {
// This dialog will be built dynamically by the front end
echo '<div id="ongoing_dialog"></div>';
}
/**
* Load the "edit compound" dialog
*
* @since 9.5
*/
public static function printEditCompoundDialog() {
echo '<div id="edit_compound_dialog" class="impact-dialog">';
echo "<table class='tab_cadre_fixe'>";
// First row: name field
echo "<tr>";
echo "<td>";
echo "<label>&nbsp;" . __("Name") . "</label>";
echo "</td>";
echo "<td>";
echo Html::input("compound_name", []);
echo "</td>";
echo "</tr>";
// Second row: color field
echo "<tr>";
echo "<td>";
echo "<label>&nbsp;" . __("Color") . "</label>";
echo "</td>";
echo "<td>";
Html::showColorField("compound_color", [
'value' => '#d2d2d2'
]);
echo "</td>";
echo "</tr>";
echo "</table>";
echo "</div>";
}
/**
* Prepare the impact network
*
* @since 9.5
*
* @param CommonDBTM $item The specified item
*/
public static function prepareImpactNetwork(CommonDBTM $item) {
// Load requirements
self::printImpactNetworkContainer();
self::printShowOngoingDialog();
self::printEditCompoundDialog();
echo Html::script("js/impact.js");
// Load backend values
$default = self::DEFAULT_COLOR;
$forward = self::IMPACT_COLOR;
$backward = self::DEPENDS_COLOR;
$both = self::IMPACT_AND_DEPENDS_COLOR;
$start_node = self::getNodeID($item);
// Bind the backend values to the client and start the network
echo Html::scriptBlock("
$(function() {
GLPIImpact.prepareNetwork(
$(\"#network_container\"),
{
default : '$default',
forward : '$forward',
backward: '$backward',
both : '$both',
},
'$start_node'
)
});
");
}
/**
* Check that a given asset exist in the DB
*
* @param string $itemtype Class of the asset
* @param string $items_id id of the asset
*/
public static function assetExist(string $itemtype, string $items_id) {
try {
// Check this asset type is enabled
if (!self::isEnabled($itemtype)) {
return false;
}
// Try to create an object matching the given item type
$reflection_class = new ReflectionClass($itemtype);
if (!$reflection_class->isInstantiable()) {
return false;
}
// Look for a matching asset in the DB
$asset = new $itemtype();
return $asset->getFromDB($items_id);
} catch (ReflectionException $e) {
// Class does not exist
return false;
}
}
/**
* Create an ID for a node (itemtype::items_id)
*
* @param CommonDBTM $item Name of the node
*
* @return string
*/
public static function getNodeID(CommonDBTM $item) {
return get_class($item) . self::NODE_ID_DELIMITER . $item->fields['id'];
}
/**
* Create an ID for an edge (NodeID->NodeID)
*
* @param CommonDBTM $itemA First node of the edge
* @param CommonDBTM $itemB Second node of the edge
* @param int $direction Direction of the edge : A to B or B to A ?
*
* @return string|null
*
* @throws InvalidArgumentException
*/
public static function getEdgeID(
CommonDBTM $itemA,
CommonDBTM $itemB,
int $direction
) {
switch ($direction) {
case self::DIRECTION_FORWARD:
return self::getNodeID($itemA) . self::EDGE_ID_DELIMITER . self::getNodeID($itemB);
case self::DIRECTION_BACKWARD:
return self::getNodeID($itemB) . self::EDGE_ID_DELIMITER . self::getNodeID($itemA);
default:
throw new InvalidArgumentException(
"Invalid value for argument \$direction ($direction)."
);
}
}
/**
* Print the form for the global impact page
*/
public static function printImpactForm() {
global $CFG_GLPI;
$rand = mt_rand();
echo "<form name=\"item\" action=\"{$_SERVER['PHP_SELF']}\" method=\"GET\">";
echo '<table class="tab_cadre_fixe" style="width:30%">';
// First row: header
echo "<tr>";
echo "<th colspan=\"2\">" . self::getTypeName() . "</th>";
echo "</tr>";
// Second row: itemtype field
echo "<tr>";
echo "<td width=\"40%\"> <label>" . __('Item type') . "</label> </td>";
echo "<td>";
Dropdown::showItemTypes(
'type',
self::getEnabledItemtypes(),
[
'value' => null,
'width' => '100%',
'emptylabel' => Dropdown::EMPTY_VALUE,
'rand' => $rand
]
);
echo "</td>";
echo "</tr>";
// Third row: items_id field
echo "<tr>";
echo "<td> <label>" . _n('Item', 'Items', 1) . "</label> </td>";
echo "<td>";
Ajax::updateItemOnSelectEvent("dropdown_type$rand", "form_results",
$CFG_GLPI["root_doc"] . "/ajax/dropdownTrackingDeviceType.php",
[
'itemtype' => '__VALUE__',
'entity_restrict' => 0,
'multiple' => 1,
'admin' => 1,
'rand' => $rand,
'myname' => "id",
]
);
echo "<span id='form_results'>\n";
echo "</span>\n";
echo "</td>";
echo "</tr>";
// Fourth row: submit
echo "<tr><td colspan=\"2\" style=\"text-align:center\">";
echo Html::submit(__("Show impact analysis"));
echo "</td></tr>";
echo "</table>";
echo "<br><br>";
Html::closeForm();
}
/**
* Clean impact records for a given item that has been purged form the db
*
* @param CommonDBTM $item The item being purged
*/
public static function clean(\CommonDBTM $item) {
global $DB, $CFG_GLPI;
// Skip if not a valid impact type
if (!self::isEnabled($item::getType())) {
return;
}
// Remove each relations
$DB->delete(\ImpactRelation::getTable(), [
'OR' => [
[
'itemtype_source' => get_class($item),
'items_id_source' => $item->fields['id']
],
[
'itemtype_impacted' => get_class($item),
'items_id_impacted' => $item->fields['id']
],
]
]);
// Remove associated ImpactItem
$impact_item = ImpactItem::findForItem($item, false);
if (!$impact_item) {
// Stop here if no impactitem, nothing more to delete
return;
}
$impact_item->delete($impact_item->fields);
// Remove impact context if defined and not a slave, update others
// contexts if they are slave to us
if ($impact_item->fields['impactcontexts_id'] != 0
&& $impact_item->fields['is_slave'] != 0) {
$DB->update(ImpactItem::getTable(), [
'impactcontexts_id' => 0,
], [
'impactcontexts_id' => $impact_item->fields['impactcontexts_id'],
]
);
$DB->delete(ImpactContext::getTable(), [
'id' => $impact_item->fields['impactcontexts_id']
]);
}
// Delete group if less than two children remaining
if ($impact_item->fields['parent_id'] != 0) {
$count = countElementsInTable(ImpactItem::getTable(), [
'parent_id' => $impact_item->fields['parent_id']
]);
if ($count < 2) {
$DB->update(ImpactItem::getTable(), [
'parent_id' => 0,
], [
'parent_id' => $impact_item->fields['parent_id']
]
);
$DB->delete(ImpactCompound::getTable(), [
'id' => $impact_item->fields['parent_id']
]);
}
}
}
/**
* Check if the given itemtype is enabled in impact config
*
* @param string $itemtype
* @return bool
*/
public static function isEnabled(string $itemtype): bool {
return in_array($itemtype, self::getEnabledItemtypes());
}
/**
* Return enabled itemtypes
*
* @return array
*/
public static function getEnabledItemtypes(): array {
// Get configured values
$conf = Config::getConfigurationValues('core');
if (!isset($conf[self::CONF_ENABLED])) {
return [];
}
$enabled = importArrayFromDB($conf[self::CONF_ENABLED]);
// Remove any forbidden values
return array_filter($enabled, function($itemtype) {
global $CFG_GLPI;
return isset($CFG_GLPI['impact_asset_types'][$itemtype]);
});
}
/**
* Return default itemtypes
*
* @return array
*/
public static function getDefaultItemtypes() {
global $CFG_GLPI;
$values = $CFG_GLPI["default_impact_asset_types"];
return array_keys($values);
}
/**
* Print the impact config tab
*/
public static function showConfigForm() {
global $CFG_GLPI;
// Form head
$action = Toolbox::getItemTypeFormURL(Config::getType());
echo "<form name='form' action='$action' method='post'>";
// Table head
echo '<table class="tab_cadre_fixe">';
echo '<tr><th colspan="2">' . __('Impact analysis configuration') . '</th></tr>';
// First row: enabled itemtypes
$input_name = self::CONF_ENABLED;
$values = $CFG_GLPI["impact_asset_types"];
foreach ($values as $itemtype => $icon) {
$values[$itemtype]= $itemtype::getTypeName();
}
echo '<tr class="tab_bg_2">';
echo '<td width="40%">';
echo "<label for='$input_name'>";
echo __('Enabled itemtypes');
echo '</label>';
echo '</td>';
$core_config = Config::getConfigurationValues("core");
$db_values = importArrayFromDB($core_config[self::CONF_ENABLED]);
echo '<td>';
Dropdown::showFromArray($input_name, $values, [
'multiple' => true,
'values' => $db_values
]);
echo "</td>";
echo "</tr>";
echo '</table>';
// Submit button
echo '<div style="text-align:center">';
echo Html::submit(__('Save'), ['name' => 'update']);
echo '</div>';
Html::closeForm();
}
}