You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

511 lines
19 KiB

<?php
/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/
/**
* Template dashboards API implementation.
*/
class CTemplateDashboard extends CDashboardGeneral {
public const ACCESS_RULES = [
'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN],
'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN]
];
/**
* @param array $options
*
* @throws APIException if the input is invalid.
*
* @return array|int
*/
public function get(array $options = []) {
$api_input_rules = ['type' => API_OBJECT, 'fields' => [
// filter
'dashboardids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
'templateids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null],
'filter' => ['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['uuid', 'dashboardid', 'name', 'templateid', 'display_period', 'auto_start']],
'search' => ['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['name']],
'searchByAny' => ['type' => API_BOOLEAN, 'default' => false],
'startSearch' => ['type' => API_FLAG, 'default' => false],
'excludeSearch' => ['type' => API_FLAG, 'default' => false],
'searchWildcardsEnabled' => ['type' => API_BOOLEAN, 'default' => false],
// output
'output' => ['type' => API_OUTPUT, 'in' => implode(',', ['dashboardid', 'name', 'templateid', 'display_period', 'auto_start', 'uuid']), 'default' => API_OUTPUT_EXTEND],
'selectPages' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', ['dashboard_pageid', 'name', 'display_period', 'widgets']), 'default' => null],
'countOutput' => ['type' => API_FLAG, 'default' => false],
'groupCount' => ['type' => API_FLAG, 'default' => false],
// sort and limit
'sortfield' => ['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []],
'sortorder' => ['type' => API_SORTORDER, 'default' => []],
'limit' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null],
// flags
'editable' => ['type' => API_BOOLEAN, 'default' => false],
'preservekeys' => ['type' => API_BOOLEAN, 'default' => false]
]];
if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
}
$sql_parts = [
'select' => ['dashboard' => 'd.dashboardid'],
'from' => ['dashboard' => 'dashboard d'],
'where' => [],
'order' => [],
'group' => []
];
if (!$options['countOutput'] && $options['output'] === API_OUTPUT_EXTEND) {
$options['output'] = $this->getTableSchema()['fields'];
unset($options['output']['userid'], $options['output']['private']);
$options['output'] = array_keys($options['output']);
}
$options['groupCount'] = ($options['groupCount'] && $options['countOutput']);
// permissions
if (in_array(self::$userData['type'], [USER_TYPE_ZABBIX_USER, USER_TYPE_ZABBIX_ADMIN])) {
if ($options['editable']) {
if ($options['templateids'] !== null) {
$options['templateids'] = array_keys(API::Template()->get([
'output' => [],
'templateids' => $options['templateids'],
'editable' => true,
'preservekeys' => true
]));
}
else {
$user_groups = getUserGroupsByUserId(self::$userData['userid']);
$sql_parts['where'][] = 'EXISTS ('.
'SELECT NULL'.
' FROM hosts_groups hgg'.
' JOIN rights r'.
' ON r.id=hgg.groupid'.
' AND '.dbConditionInt('r.groupid', $user_groups).
' WHERE d.templateid=hgg.hostid'.
' GROUP BY hgg.hostid'.
' HAVING MIN(r.permission)>'.PERM_DENY.
' AND MAX(r.permission)>='.PERM_READ_WRITE.
')';
}
}
else {
$user_groups = getUserGroupsByUserId(self::$userData['userid']);
// Select direct templates of all hosts accessible to the current user.
$db_host_templates = DBselect(
'SELECT DISTINCT ht.templateid FROM hosts_templates ht'.
' WHERE ht.hostid IN ('.
'SELECT h.hostid FROM hosts h'.
' WHERE h.flags IN ('.ZBX_FLAG_DISCOVERY_NORMAL.','.ZBX_FLAG_DISCOVERY_CREATED.')'.
' AND h.status IN ('.HOST_STATUS_MONITORED.','.HOST_STATUS_NOT_MONITORED.')'.
' AND EXISTS ('.
'SELECT NULL'.
' FROM hosts_groups hgg'.
' JOIN rights r'.
' ON r.id=hgg.groupid'.
' AND '.dbConditionId('r.groupid', $user_groups).
' WHERE h.hostid=hgg.hostid'.
' GROUP BY hgg.hostid'.
' HAVING MIN(r.permission)>'.PERM_DENY.
' AND MAX(r.permission)>='.PERM_READ.
')'.
')'
);
$templateids = [];
while ($db_host_template = DBfetch($db_host_templates)) {
$templateids[$db_host_template['templateid']] = true;
}
$all_templateids = [];
while ($templateids) {
$all_templateids += $templateids;
$db_parent_templates = DBselect(
'SELECT ht.templateid'.
' FROM hosts_templates ht'.
' WHERE '.dbConditionId('ht.hostid', array_keys($templateids))
);
$templateids = [];
while ($db_parent_template = DBfetch($db_parent_templates)) {
$templateids[$db_parent_template['templateid']] = true;
}
}
$options['templateids'] = $options['templateids'] !== null
? array_intersect($options['templateids'], array_keys($all_templateids))
: array_keys($all_templateids);
}
}
// dashboardids
if ($options['dashboardids'] !== null) {
$sql_parts['where'][] = dbConditionInt('d.dashboardid', $options['dashboardids']);
}
// dashboardids
$sql_parts['where'][] = ($options['templateids'] !== null)
? dbConditionInt('d.templateid', $options['templateids'])
: 'd.templateid IS NOT NULL';
// filter
if ($options['filter'] !== null) {
$this->dbFilter('dashboard d', $options, $sql_parts);
}
// search
if ($options['search'] !== null) {
zbx_db_search('dashboard d', $options, $sql_parts);
}
if ($options['groupCount']) {
$sql_parts['group']['templateid'] = 'd.templateid';
}
$sql_parts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
$sql_parts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
$result = DBselect(self::createSelectQueryFromParts($sql_parts), $options['limit']);
$db_dashboards = [];
while ($row = DBfetch($result)) {
if ($options['countOutput']) {
if ($options['groupCount']) {
$db_dashboards[] = $row;
}
else {
return $row['rowscount'];
}
}
else {
$db_dashboards[$row['dashboardid']] = $row;
}
}
if ($db_dashboards && !$options['groupCount']) {
$db_dashboards = $this->addRelatedObjects($options, $db_dashboards);
$db_dashboards = $this->unsetExtraFields($db_dashboards, ['dashboardid'], $options['output']);
if (!$options['preservekeys']) {
$db_dashboards = array_values($db_dashboards);
}
}
return $db_dashboards;
}
/**
* @param array $dashboards
*
* @return array
*/
public function create(array $dashboards): array {
$this->validateCreate($dashboards);
$ins_dashboards = [];
foreach ($dashboards as $dashboard) {
unset($dashboard['pages']);
$ins_dashboards[] = $dashboard;
}
$dashboardids = DB::insert('dashboard', $ins_dashboards);
foreach ($dashboards as $index => &$dashboard) {
$dashboard['dashboardid'] = $dashboardids[$index];
}
unset($dashboard);
$this->updatePages($dashboards);
self::addAuditLog(CAudit::ACTION_ADD, CAudit::RESOURCE_TEMPLATE_DASHBOARD, $dashboards);
return ['dashboardids' => $dashboardids];
}
/**
* @param array $dashboards
*
* @return array
*/
public function update(array $dashboards): array {
$this->validateUpdate($dashboards, $db_dashboards);
$upd_dashboards = [];
foreach ($dashboards as $dashboard) {
$upd_dashboard = DB::getUpdatedValues('dashboard', $dashboard, $db_dashboards[$dashboard['dashboardid']]);
if ($upd_dashboard) {
$upd_dashboards[] = [
'values' => $upd_dashboard,
'where' => ['dashboardid' => $dashboard['dashboardid']]
];
}
}
if ($upd_dashboards) {
DB::update('dashboard', $upd_dashboards);
}
$this->updatePages($dashboards, $db_dashboards);
self::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_TEMPLATE_DASHBOARD, $dashboards, $db_dashboards);
return ['dashboardids' => array_column($dashboards, 'dashboardid')];
}
/**
* @param array $dashboards
*
* @throws APIException if the input is invalid.
*/
protected function validateCreate(array &$dashboards): void {
$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['uuid'], ['templateid', 'name']], 'fields' => [
'uuid' => ['type' => API_UUID],
'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('dashboard', 'name')],
'templateid' => ['type' => API_ID, 'flags' => API_REQUIRED],
'display_period' => ['type' => API_INT32, 'in' => implode(',', DASHBOARD_DISPLAY_PERIODS)],
'auto_start' => ['type' => API_INT32, 'in' => '0,1'],
'pages' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DASHBOARD_MAX_PAGES, 'fields' => [
'name' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('dashboard_page', 'name')],
'display_period' => ['type' => API_INT32, 'in' => implode(',', array_merge([0], DASHBOARD_DISPLAY_PERIODS))],
'widgets' => ['type' => API_OBJECTS, 'fields' => [
'type' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('widget', 'type')],
'name' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('widget', 'name')],
'view_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_WIDGET_VIEW_MODE_NORMAL, ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER])],
'x' => ['type' => API_INT32, 'in' => '0:'.(DASHBOARD_MAX_COLUMNS - 1)],
'y' => ['type' => API_INT32, 'in' => '0:'.(DASHBOARD_MAX_ROWS - 2)],
'width' => ['type' => API_INT32, 'in' => '1:'.DASHBOARD_MAX_COLUMNS],
'height' => ['type' => API_INT32, 'in' => '2:'.DASHBOARD_WIDGET_MAX_ROWS],
'fields' => ['type' => API_OBJECTS, 'fields' => [
'type' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', array_keys(self::WIDGET_FIELD_TYPE_COLUMNS))],
'name' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('widget_field', 'name'), 'default' => DB::getDefault('widget_field', 'name')],
'value' => ['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [
['if' => ['field' => 'type', 'in' => implode(',', [ZBX_WIDGET_FIELD_TYPE_INT32])], 'type' => API_INT32],
['if' => ['field' => 'type', 'in' => implode(',', [ZBX_WIDGET_FIELD_TYPE_STR])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('widget_field', 'value_str')],
['if' => ['field' => 'type', 'in' => implode(',', array_keys(self::WIDGET_FIELD_TYPE_COLUMNS_FK))], 'type' => API_ID]
]]
]]
]]
]]
]];
if (!CApiInputValidator::validate($api_input_rules, $dashboards, '/', $error)) {
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
}
$templateids = array_column($dashboards, 'templateid', 'templateid');
$db_templates_count = API::Template()->get([
'countOutput' => true,
'templateids' => $templateids
]);
if ($db_templates_count != count($templateids)) {
self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
}
self::addUuid($dashboards);
self::checkUuidDuplicates($dashboards);
$this->checkDuplicates($dashboards);
$this->checkWidgets($dashboards);
$this->checkWidgetFields($dashboards);
}
/**
* Add the UUID to those of the given template dashboards that don't have the 'uuid' parameter set.
*
* @param array $dashboards
*/
private static function addUuid(array &$dashboards): void {
foreach ($dashboards as &$dashboard) {
if (!array_key_exists('uuid', $dashboard)) {
$dashboard['uuid'] = generateUuidV4();
}
}
unset($dashboard);
}
/**
* Verify template dashboard UUIDs are not repeated.
*
* @param array $dashboards
* @param array|null $db_dashboards
*
* @throws APIException
*/
private static function checkUuidDuplicates(array $dashboards, array $db_dashboards = null): void {
$dashboard_indexes = [];
foreach ($dashboards as $i => $dashboard) {
if (!array_key_exists('uuid', $dashboard)) {
continue;
}
if ($db_dashboards === null || $dashboard['uuid'] !== $db_dashboards[$dashboard['dashboardid']]['uuid']) {
$dashboard_indexes[$dashboard['uuid']] = $i;
}
}
if (!$dashboard_indexes) {
return;
}
$duplicates = DB::select('dashboard', [
'output' => ['uuid'],
'filter' => [
'uuid' => array_keys($dashboard_indexes)
],
'limit' => 1
]);
if ($duplicates) {
self::exception(ZBX_API_ERROR_PARAMETERS,
_s('Invalid parameter "%1$s": %2$s.', '/'.($dashboard_indexes[$duplicates[0]['uuid']] + 1),
_('template dashboard with the same UUID already exists')
)
);
}
}
/**
* @param array $dashboards
* @param array|null $db_dashboards
*
* @throws APIException if the input is invalid.
*/
protected function validateUpdate(array &$dashboards, array &$db_dashboards = null): void {
$api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['uuid'], ['dashboardid']], 'fields' => [
'uuid' => ['type' => API_UUID],
'dashboardid' => ['type' => API_ID, 'flags' => API_REQUIRED],
'name' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('dashboard', 'name')],
'display_period' => ['type' => API_INT32, 'in' => implode(',', DASHBOARD_DISPLAY_PERIODS)],
'auto_start' => ['type' => API_INT32, 'in' => '0,1'],
'pages' => ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'uniq' => [['dashboard_pageid']], 'length' => DASHBOARD_MAX_PAGES, 'fields' => [
'dashboard_pageid' => ['type' => API_ID],
'name' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('dashboard_page', 'name')],
'display_period' => ['type' => API_INT32, 'in' => implode(',', array_merge([0], DASHBOARD_DISPLAY_PERIODS))],
'widgets' => ['type' => API_OBJECTS, 'uniq' => [['widgetid']], 'fields' => [
'widgetid' => ['type' => API_ID],
'type' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('widget', 'type')],
'name' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('widget', 'name')],
'view_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_WIDGET_VIEW_MODE_NORMAL, ZBX_WIDGET_VIEW_MODE_HIDDEN_HEADER])],
'x' => ['type' => API_INT32, 'in' => '0:'.(DASHBOARD_MAX_COLUMNS - 1)],
'y' => ['type' => API_INT32, 'in' => '0:'.(DASHBOARD_MAX_ROWS - 2)],
'width' => ['type' => API_INT32, 'in' => '1:'.DASHBOARD_MAX_COLUMNS],
'height' => ['type' => API_INT32, 'in' => '2:'.DASHBOARD_WIDGET_MAX_ROWS],
'fields' => ['type' => API_OBJECTS, 'fields' => [
'type' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', array_keys(self::WIDGET_FIELD_TYPE_COLUMNS))],
'name' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('widget_field', 'name'), 'default' => DB::getDefault('widget_field', 'name')],
'value' => ['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [
['if' => ['field' => 'type', 'in' => implode(',', [ZBX_WIDGET_FIELD_TYPE_INT32])], 'type' => API_INT32],
['if' => ['field' => 'type', 'in' => implode(',', [ZBX_WIDGET_FIELD_TYPE_STR])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('widget_field', 'value_str')],
['if' => ['field' => 'type', 'in' => implode(',', array_keys(self::WIDGET_FIELD_TYPE_COLUMNS_FK))], 'type' => API_ID]
]]
]]
]]
]]
]];
if (!CApiInputValidator::validate($api_input_rules, $dashboards, '/', $error)) {
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
}
$db_dashboards = $this->get([
'output' => ['uuid', 'dashboardid', 'name', 'templateid', 'display_period', 'auto_start'],
'dashboardids' => array_column($dashboards, 'dashboardid'),
'editable' => true,
'preservekeys' => true
]);
if (count($db_dashboards) != count($dashboards)) {
self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
}
// Copy original dashboard names and templateids when not specified (for validation and error reporting).
$dashboards = $this->extendObjectsByKey($dashboards, $db_dashboards, 'dashboardid', ['name', 'templateid']);
$api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['name', 'templateid']], 'fields' => [
'name' => ['type' => API_STRING_UTF8],
'templateid' => ['type' => API_ID]
]];
if (!CApiInputValidator::validateUniqueness($api_input_rules, $dashboards, '/', $error)) {
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
}
// Add the existing pages, widgets and widget fields to $db_dashboards.
$this->addAffectedObjects($dashboards, $db_dashboards);
// Check ownership of the referenced pages and widgets.
$this->checkReferences($dashboards, $db_dashboards);
self::checkUuidDuplicates($dashboards, $db_dashboards);
$this->checkDuplicates($dashboards, $db_dashboards);
$this->checkWidgets($dashboards, $db_dashboards);
$this->checkWidgetFields($dashboards, $db_dashboards);
}
/**
* Check for unique dashboard names per template.
*
* @param array $dashboards
* @param array|null $db_dashboards
*
* @throws APIException if dashboard names are not unique.
*/
protected function checkDuplicates(array $dashboards, array $db_dashboards = null): void {
$names_by_templateid = [];
foreach ($dashboards as $dashboard) {
if ($db_dashboards === null || $dashboard['name'] !== $db_dashboards[$dashboard['dashboardid']]['name']) {
$names_by_templateid[$dashboard['templateid']][] = $dashboard['name'];
}
}
if (!$names_by_templateid) {
return;
}
$where = [];
foreach ($names_by_templateid as $templateid => $names) {
$where[] = '('.dbConditionId('d.templateid', [$templateid]).' AND '.dbConditionString('d.name', $names).')';
}
$duplicate = DBfetch(DBselect('SELECT d.name FROM dashboard d WHERE '.implode(' OR ', $where), 1));
if ($duplicate) {
self::exception(ZBX_API_ERROR_PARAMETERS, _s('Dashboard "%1$s" already exists.', $duplicate['name']));
}
}
}