['min_user_type' => USER_TYPE_ZABBIX_USER], 'getscriptsbyhosts' => ['min_user_type' => USER_TYPE_ZABBIX_USER], 'getscriptsbyevents' => ['min_user_type' => USER_TYPE_ZABBIX_USER], 'create' => ['min_user_type' => USER_TYPE_SUPER_ADMIN], 'update' => ['min_user_type' => USER_TYPE_SUPER_ADMIN], 'delete' => ['min_user_type' => USER_TYPE_SUPER_ADMIN], 'execute' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_EXECUTE_SCRIPTS] ]; protected $tableName = 'scripts'; protected $tableAlias = 's'; protected $sortColumns = ['scriptid', 'name']; /** * Fields from "actions" table. Used in get() validation and addRelatedObjects() when selecting action fields. */ private $action_fields = ['actionid', 'name', 'eventsource', 'status', 'esc_period', 'pause_suppressed', 'notify_if_canceled', 'pause_symptoms' ]; /** * This property, if filled out, will contain all hostrgroup ids * that requested scripts did inherit from. * Keyed by scriptid. * * @var array|HostGroup[] */ protected $parent_host_groups = []; /** * @param array $options * * @throws APIException if the input is invalid. * * @return array|int */ public function get(array $options) { $script_fields = ['scriptid', 'name', 'command', 'host_access', 'usrgrpid', 'groupid', 'description', 'confirmation', 'type', 'execute_on', 'timeout', 'parameters', 'scope', 'port', 'authtype', 'username', 'password', 'publickey', 'privatekey', 'menu_path', 'url', 'new_window' ]; $group_fields = ['groupid', 'name', 'flags', 'uuid']; $host_fields = ['hostid', 'host', 'name', 'description', 'status', 'proxyid', 'inventory_mode', 'flags', 'ipmi_authtype', 'ipmi_privilege', 'ipmi_username', 'ipmi_password', 'maintenanceid', 'maintenance_status', 'maintenance_type', 'maintenance_from', 'tls_connect', 'tls_accept', 'tls_issuer', 'tls_subject' ]; $api_input_rules = ['type' => API_OBJECT, 'fields' => [ // filter 'scriptids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 'hostids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 'groupids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 'usrgrpids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 'filter' => ['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['scriptid', 'name', 'command', 'host_access', 'usrgrpid', 'groupid', 'confirmation', 'type', 'url', 'new_window', 'execute_on', 'scope', 'menu_path']], 'search' => ['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['name', 'command', 'url', 'description', 'confirmation', 'username', 'menu_path']], '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(',', $script_fields), 'default' => API_OUTPUT_EXTEND], 'selectGroups' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL | API_DEPRECATED, 'in' => implode(',', $group_fields), 'default' => null], 'selectHostGroups' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $group_fields), 'default' => null], 'selectHosts' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $host_fields), 'default' => null], 'selectActions' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $this->action_fields), 'default' => null], 'countOutput' => ['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' => ['scripts' => 's.scriptid'], 'from' => ['scripts' => 'scripts s'], 'where' => [], 'order' => [] ]; // editable + permission check if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) { if ($options['editable']) { return $options['countOutput'] ? 0 : []; } $user_groups = getUserGroupsByUserId(self::$userData['userid']); $sql_parts['where'][] = '(s.usrgrpid IS NULL OR '.dbConditionInt('s.usrgrpid', $user_groups).')'; $sql_parts['where'][] = '(s.groupid IS NULL OR EXISTS ('. 'SELECT NULL'. ' FROM rights r'. ' WHERE s.groupid=r.id'. ' AND '.dbConditionInt('r.groupid', $user_groups). ' GROUP BY r.id'. ' HAVING MIN(r.permission)>'.PERM_DENY. '))'; } $host_groups = null; $host_groups_by_hostids = null; $host_groups_by_groupids = null; // Hostids and groupids selection API calls must be made separately because we must intersect enriched groupids. if ($options['hostids'] !== null) { $host_groups_by_hostids = enrichParentGroups(API::HostGroup()->get([ 'output' => ['groupid', 'name'], 'hostids' => $options['hostids'], 'preservekeys' => true ])); } if ($options['groupids'] !== null) { $host_groups_by_groupids = enrichParentGroups(API::HostGroup()->get([ 'output' => ['groupid', 'name'], 'groupids' => $options['groupids'], 'preservekeys' => true ])); } if ($host_groups_by_groupids !== null && $host_groups_by_hostids !== null) { $host_groups = array_intersect_key($host_groups_by_hostids, $host_groups_by_groupids); } elseif ($host_groups_by_hostids !== null) { $host_groups = $host_groups_by_hostids; } elseif ($host_groups_by_groupids !== null) { $host_groups = $host_groups_by_groupids; } if ($host_groups !== null) { $sql_parts['where'][] = '('.dbConditionInt('s.groupid', array_keys($host_groups)).' OR s.groupid IS NULL)'; $this->parent_host_groups = $host_groups; } // usrgrpids if ($options['usrgrpids'] !== null) { $sql_parts['where'][] = '(s.usrgrpid IS NULL OR '.dbConditionInt('s.usrgrpid', $options['usrgrpids']).')'; } // scriptids if ($options['scriptids'] !== null) { $sql_parts['where'][] = dbConditionInt('s.scriptid', $options['scriptids']); } // search if ($options['search'] !== null) { zbx_db_search('scripts s', $options, $sql_parts); } // filter if ($options['filter'] !== null) { $this->dbFilter('scripts s', $options, $sql_parts); } $db_scripts = []; $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']); while ($db_script = DBfetch($result)) { if ($options['countOutput']) { return $db_script['rowscount']; } $db_scripts[$db_script['scriptid']] = $db_script; } if ($db_scripts) { $db_scripts = $this->addRelatedObjects($options, $db_scripts); $db_scripts = $this->unsetExtraFields($db_scripts, ['scriptid', 'groupid', 'host_access'], $options['output'] ); if (!$options['preservekeys']) { $db_scripts = array_values($db_scripts); } } return $db_scripts; } /** * @param array $scripts * * @return array */ public function create(array $scripts) { if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) { self::exception(ZBX_API_ERROR_PERMISSIONS, _s('No permissions to call "%1$s.%2$s".', 'script', __FUNCTION__) ); } $this->validateCreate($scripts); $scriptids = DB::insert('scripts', $scripts); foreach ($scripts as $index => &$script) { $script['scriptid'] = $scriptids[$index]; } unset($script); self::updateParams($scripts, __FUNCTION__); self::addAuditLog(CAudit::ACTION_ADD, CAudit::RESOURCE_SCRIPT, $scripts); return ['scriptids' => $scriptids]; } /** * @param array $scripts * * @throws APIException if the input is invalid */ protected function validateCreate(array &$scripts) { /* * Get general validation rules and firstly validate name uniqueness and all the possible fields, so that there * are no invalid fields for any of the script types. Unfortunately there is also a drawback, since field types * validated before we know what rules belong to each script type. */ $api_input_rules = $this->getValidationRules('create', $common_fields); if (!CApiInputValidator::validate($api_input_rules, $scripts, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } /* * Then validate each script separately. Depending on script type, each script may have different set of allowed * fields. Then in case the type is SSH and authtype is set, validate parameters again. */ foreach ($scripts as $index => $script) { $type_rules = $this->getTypeValidationRules($script['type'], 'create', $type_fields); $scope_rules = $this->getScopeValidationRules($script['scope'], $scope_fields); // Temporary remove scope fields from script to validate type fields. $tmp = $script; $tmp_fields = array_intersect_key($scope_rules['fields'], $script); foreach ($tmp_fields as $field => $rules) { unset($tmp[$field]); } $type_rules['fields'] += $common_fields + $type_fields; if (!CApiInputValidator::validate($type_rules, $tmp, '/'.($index + 1), $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } // Validate all fields together. $scope_rules['fields'] += $type_rules['fields'] + $common_fields + $scope_fields; if (!CApiInputValidator::validate($scope_rules, $script, '/'.($index + 1), $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } if (array_key_exists('authtype', $script)) { $ssh_rules = $this->getAuthTypeValidationRules($script['authtype'], 'create'); $ssh_rules['fields'] += $common_fields + $type_fields + $scope_fields; if (!CApiInputValidator::validate($ssh_rules, $script, '/'.($index + 1), $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } } } $this->checkUniqueness($scripts); $this->checkDuplicates($scripts); // Finally check User and Host IDs. $this->checkUserGroups($scripts); $this->checkHostGroups($scripts); } /** * @param array $scripts * * @return array */ public function update(array $scripts) { if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) { self::exception(ZBX_API_ERROR_PERMISSIONS, _s('No permissions to call "%1$s.%2$s".', 'script', __FUNCTION__) ); } $this->validateUpdate($scripts, $db_scripts); $upd_scripts = []; foreach ($scripts as $script) { $upd_script = DB::getUpdatedValues('scripts', $script, $db_scripts[$script['scriptid']]); if ($upd_script) { $upd_scripts[] = [ 'values' => $upd_script, 'where' => ['scriptid' => $script['scriptid']] ]; } } if ($upd_scripts) { DB::update('scripts', $upd_scripts); } self::updateParams($scripts, __FUNCTION__, $db_scripts); self::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_SCRIPT, $scripts, $db_scripts); return ['scriptids' => array_column($scripts, 'scriptid')]; } /** * @param array $scripts * @param array $db_scripts * * @throws APIException if the input is invalid */ protected function validateUpdate(array &$scripts, array &$db_scripts = null) { /* * Get general validation rules and firstly validate name uniqueness and all the possible fields, so that there * are no invalid fields for any of the script types. Unfortunately there is also a drawback, since field types * validated before we know what rules belong to each script type. */ $api_input_rules = $this->getValidationRules('update', $common_fields); if (!CApiInputValidator::validate($api_input_rules, $scripts, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } // Continue to validate script name. $db_scripts = DB::select('scripts', [ 'output' => ['scriptid', 'name', 'command', 'host_access', 'usrgrpid', 'groupid', 'description', 'confirmation', 'type', 'execute_on', 'timeout', 'scope', 'port', 'authtype', 'username', 'password', 'publickey', 'privatekey', 'menu_path', 'url', 'new_window' ], 'scriptids' => array_column($scripts, 'scriptid'), 'preservekeys' => true ]); if (count($db_scripts) != count($scripts)) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } $this->checkUniqueness($scripts, 'update'); self::addAffectedObjects($scripts, $db_scripts); // Validate if scripts belong to actions and scope can be changed. $action_scriptids = []; foreach ($scripts as $script) { $db_script = $db_scripts[$script['scriptid']]; if (array_key_exists('scope', $script) && $script['scope'] != ZBX_SCRIPT_SCOPE_ACTION && $db_script['scope'] == ZBX_SCRIPT_SCOPE_ACTION) { $action_scriptids[$script['scriptid']] = true; } } if ($action_scriptids) { $actions = API::Action()->get([ 'output' => ['actionid', 'name'], 'selectOperations' => ['opcommand'], 'selectRecoveryOperations' => ['opcommand'], 'selectUpdateOperations' => ['opcommand'], 'scriptids' => array_keys($action_scriptids) ]); if ($actions) { foreach ($scripts as $script) { $db_script = $db_scripts[$script['scriptid']]; if (array_key_exists('scope', $script) && $script['scope'] != ZBX_SCRIPT_SCOPE_ACTION && $db_script['scope'] == ZBX_SCRIPT_SCOPE_ACTION) { foreach ($actions as $action) { if ($action['operations']) { // Find at least one usage of script in any of operations. foreach ($action['operations'] as $operation) { if (array_key_exists('opcommand', $operation) && bccomp($operation['opcommand']['scriptid'], $script['scriptid']) == 0) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update script scope. Script "%1$s" is used in action "%2$s".', $db_script['name'], $action['name'] ) ); } } } if ($action['recovery_operations']) { foreach ($action['recovery_operations'] as $operation) { if (array_key_exists('opcommand', $operation) && bccomp($operation['opcommand']['scriptid'], $script['scriptid']) == 0) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update script scope. Script "%1$s" is used in action "%2$s".', $db_script['name'], $action['name'] ) ); } } } if ($action['update_operations']) { foreach ($action['update_operations'] as $operation) { if (array_key_exists('opcommand', $operation) && bccomp($operation['opcommand']['scriptid'], $script['scriptid']) == 0) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update script scope. Script "%1$s" is used in action "%2$s".', $db_script['name'], $action['name'] ) ); } } } } } } } } // Populate common and mandatory fields. $scripts = $this->extendObjectsByKey($scripts, $db_scripts, 'scriptid', ['name', 'type', 'scope']); foreach ($scripts as $index => &$script) { $db_script = $db_scripts[$script['scriptid']]; $method = 'update'; if (array_key_exists('type', $script) && $script['type'] != $db_script['type'] || array_key_exists('scope', $script) && $script['scope'] == ZBX_SCRIPT_SCOPE_ACTION && $db_script['scope'] != ZBX_SCRIPT_SCOPE_ACTION && $db_script['type'] == ZBX_SCRIPT_TYPE_URL) { // This means that all other fields are now required just like create method. $method = 'create'; // Populate username field, if no new name is given and types are similar to previous. if (!array_key_exists('username', $script) && (($db_script['type'] == ZBX_SCRIPT_TYPE_TELNET && $script['type'] == ZBX_SCRIPT_TYPE_SSH) || ($db_script['type'] == ZBX_SCRIPT_TYPE_SSH && $script['type'] == ZBX_SCRIPT_TYPE_TELNET))) { $script['username'] = $db_script['username']; } } $type_rules = $this->getTypeValidationRules($script['type'], $method, $type_fields); $scope_rules = $this->getScopeValidationRules($script['scope'], $scope_fields); // Temporary remove scope fields from script to validate type fields. $tmp = $script; $tmp_fields = array_intersect_key($scope_rules['fields'], $script); foreach ($tmp_fields as $field => $rules) { unset($tmp[$field]); } $type_rules['fields'] += $common_fields + $type_fields; if (!CApiInputValidator::validate($type_rules, $tmp, '/'.($index + 1), $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } // Validate all fields together. $scope_rules['fields'] += $type_rules['fields'] + $common_fields + $scope_fields; if (!CApiInputValidator::validate($scope_rules, $script, '/'.($index + 1), $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } if ($script['type'] == ZBX_SCRIPT_TYPE_SSH) { $method = 'update'; if (array_key_exists('authtype', $script) && $script['authtype'] != $db_script['authtype']) { $method = 'create'; } $script += ['authtype' => $db_script['authtype']]; $ssh_rules = $this->getAuthTypeValidationRules($script['authtype'], $method); $ssh_rules['fields'] += $common_fields + $type_fields + $scope_fields; if (!CApiInputValidator::validate($ssh_rules, $script, '/'.($index + 1), $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } } } unset($script); // Clear and reset all unnecessary fields. foreach ($scripts as &$script) { $db_script = $db_scripts[$script['scriptid']]; if ($script['type'] != $db_script['type']) { switch ($script['type']) { case ZBX_SCRIPT_TYPE_IPMI: $script['execute_on'] = DB::getDefault('scripts', 'execute_on'); $script['url'] = ''; $script['new_window'] = DB::getDefault('scripts', 'new_window'); // break; is not missing here case ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT: $script['port'] = ''; $script['authtype'] = DB::getDefault('scripts', 'authtype'); $script['username'] = ''; $script['password'] = ''; $script['publickey'] = ''; $script['privatekey'] = ''; $script['parameters'] = []; $script['timeout'] = DB::getDefault('scripts', 'timeout'); $script['url'] = ''; $script['new_window'] = DB::getDefault('scripts', 'new_window'); break; case ZBX_SCRIPT_TYPE_SSH: $script['execute_on'] = DB::getDefault('scripts', 'execute_on'); $script['parameters'] = []; $script['timeout'] = DB::getDefault('scripts', 'timeout'); $script['url'] = ''; $script['new_window'] = DB::getDefault('scripts', 'new_window'); break; case ZBX_SCRIPT_TYPE_TELNET: $script['authtype'] = DB::getDefault('scripts', 'authtype'); $script['publickey'] = ''; $script['privatekey'] = ''; $script['execute_on'] = DB::getDefault('scripts', 'execute_on'); $script['parameters'] = []; $script['timeout'] = DB::getDefault('scripts', 'timeout'); $script['url'] = ''; $script['new_window'] = DB::getDefault('scripts', 'new_window'); break; case ZBX_SCRIPT_TYPE_WEBHOOK: $script['port'] = ''; $script['authtype'] = DB::getDefault('scripts', 'authtype'); $script['username'] = ''; $script['password'] = ''; $script['publickey'] = ''; $script['privatekey'] = ''; $script['execute_on'] = DB::getDefault('scripts', 'execute_on'); $script['url'] = ''; $script['new_window'] = DB::getDefault('scripts', 'new_window'); break; case ZBX_SCRIPT_TYPE_URL: $script['command'] = ''; $script['parameters'] = []; $script['timeout'] = DB::getDefault('scripts', 'timeout'); $script['port'] = ''; $script['authtype'] = DB::getDefault('scripts', 'authtype'); $script['username'] = ''; $script['password'] = ''; $script['publickey'] = ''; $script['privatekey'] = ''; $script['execute_on'] = DB::getDefault('scripts', 'execute_on'); break; } } elseif ($script['type'] == ZBX_SCRIPT_TYPE_SSH && $script['authtype'] != $db_script['authtype'] && $script['authtype'] == ITEM_AUTHTYPE_PASSWORD) { $script['publickey'] = ''; $script['privatekey'] = ''; } if ($script['scope'] != $db_script['scope'] && $script['scope'] == ZBX_SCRIPT_SCOPE_ACTION) { $script['menu_path'] = ''; $script['usrgrpid'] = 0; $script['host_access'] = DB::getDefault('scripts', 'host_access'); $script['confirmation'] = ''; } } unset($script); $this->checkDuplicates($scripts, $db_scripts); $this->checkUserGroups($scripts); $this->checkHostGroups($scripts); } /** * Get general validation rules. * * @param string $method [IN] API method "create" or "update". * @param array $common_fields [OUT] Returns common fields for all script types. * * @return array */ protected function getValidationRules(string $method, &$common_fields = []): array { $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'fields' => []]; $common_fields = [ 'name' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'name')], 'type' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT, ZBX_SCRIPT_TYPE_IPMI, ZBX_SCRIPT_TYPE_SSH, ZBX_SCRIPT_TYPE_TELNET, ZBX_SCRIPT_TYPE_WEBHOOK, ZBX_SCRIPT_TYPE_URL])], 'scope' => ['type' => API_INT32], 'groupid' => ['type' => API_ID], 'description' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'description')] ]; if ($method === 'create') { $common_fields['name']['flags'] |= API_REQUIRED; $common_fields['type']['flags'] = API_REQUIRED; $common_fields['scope']['flags'] = API_REQUIRED; } else { $api_input_rules['uniq'] = [['scriptid']]; $common_fields += ['scriptid' => ['type' => API_ID, 'flags' => API_REQUIRED]]; } /* * Merge together optional fields that depend on script type. Some of these fields are not required for some * script types. Set only type for now. Unique parameter names, lengths and other flags are set later. */ $api_input_rules['fields'] += $common_fields + [ 'command' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')], 'execute_on' => ['type' => API_INT32], 'menu_path' => ['type' => API_STRING_UTF8], 'usrgrpid' => ['type' => API_ID], 'host_access' => ['type' => API_INT32], 'confirmation' => ['type' => API_STRING_UTF8], 'port' => ['type' => API_PORT, 'flags' => API_ALLOW_USER_MACRO], 'authtype' => ['type' => API_INT32], 'username' => ['type' => API_STRING_UTF8], 'publickey' => ['type' => API_STRING_UTF8], 'privatekey' => ['type' => API_STRING_UTF8], 'password' => ['type' => API_STRING_UTF8], 'timeout' => ['type' => API_TIME_UNIT], 'parameters' => ['type' => API_OBJECTS, 'fields' => [ 'name' => ['type' => API_STRING_UTF8], 'value' => ['type' => API_STRING_UTF8] ]], 'url' => ['type' => API_URL], 'new_window' => ['type' => API_INT32] ]; return $api_input_rules; } /** * Get validation rules for script scope. * * @param int $scope [IN] Script scope. * @param array $common_fields [OUT] Returns common fields for specific script scope. * * @return array */ protected function getScopeValidationRules(int $scope, &$common_fields = []): array { $api_input_rules = ['type' => API_OBJECT, 'fields' => []]; $common_fields = []; if ($scope == ZBX_SCRIPT_SCOPE_HOST || $scope == ZBX_SCRIPT_SCOPE_EVENT) { $common_fields = [ 'menu_path' => ['type' => API_SCRIPT_MENU_PATH, 'length' => DB::getFieldLength('scripts', 'menu_path')], 'usrgrpid' => ['type' => API_ID], 'host_access' => ['type' => API_INT32, 'in' => implode(',', [PERM_READ, PERM_READ_WRITE])], 'confirmation' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'confirmation')] ]; $api_input_rules['fields'] += $common_fields; } return $api_input_rules; } /** * Get validation rules for each script type. * * @param int $type [IN] Script type. * @param string $method [IN] API method "create" or "update". * @param array $common_fields [OUT] Returns common fields for specific script type. * * @return array */ protected function getTypeValidationRules(int $type, string $method, &$common_fields = []): array { $api_input_rules = ['type' => API_OBJECT, 'fields' => []]; $common_fields = []; switch ($type) { case ZBX_SCRIPT_TYPE_CUSTOM_SCRIPT: $api_input_rules['fields'] += [ 'scope' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_SCOPE_ACTION, ZBX_SCRIPT_SCOPE_HOST, ZBX_SCRIPT_SCOPE_EVENT])], 'command' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')], 'execute_on' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_EXECUTE_ON_AGENT, ZBX_SCRIPT_EXECUTE_ON_SERVER, ZBX_SCRIPT_EXECUTE_ON_PROXY])] ]; if ($method === 'create') { $api_input_rules['fields']['scope']['flags'] = API_REQUIRED; $api_input_rules['fields']['command']['flags'] |= API_REQUIRED; } break; case ZBX_SCRIPT_TYPE_IPMI: $api_input_rules['fields'] += [ 'scope' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_SCOPE_ACTION, ZBX_SCRIPT_SCOPE_HOST, ZBX_SCRIPT_SCOPE_EVENT])], 'command' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')] ]; if ($method === 'create') { $api_input_rules['fields']['scope']['flags'] = API_REQUIRED; $api_input_rules['fields']['command']['flags'] |= API_REQUIRED; } break; case ZBX_SCRIPT_TYPE_SSH: $common_fields = [ 'scope' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_SCOPE_ACTION, ZBX_SCRIPT_SCOPE_HOST, ZBX_SCRIPT_SCOPE_EVENT])], 'command' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')], 'port' => ['type' => API_PORT, 'flags' => API_ALLOW_USER_MACRO], 'authtype' => ['type' => API_INT32, 'in' => implode(',', [ITEM_AUTHTYPE_PASSWORD, ITEM_AUTHTYPE_PUBLICKEY])], 'username' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'username')], 'password' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'password')] ]; if ($method === 'create') { $common_fields['scope']['flags'] = API_REQUIRED; $common_fields['command']['flags'] |= API_REQUIRED; $common_fields['username']['flags'] |= API_REQUIRED; } $api_input_rules['fields'] += $common_fields + [ 'publickey' => ['type' => API_STRING_UTF8], 'privatekey' => ['type' => API_STRING_UTF8] ]; break; case ZBX_SCRIPT_TYPE_TELNET: $api_input_rules['fields'] += [ 'scope' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_SCOPE_ACTION, ZBX_SCRIPT_SCOPE_HOST, ZBX_SCRIPT_SCOPE_EVENT])], 'command' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')], 'port' => ['type' => API_PORT, 'flags' => API_ALLOW_USER_MACRO], 'username' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'username')], 'password' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('scripts', 'password')] ]; if ($method === 'create') { $api_input_rules['fields']['scope']['flags'] = API_REQUIRED; $api_input_rules['fields']['command']['flags'] |= API_REQUIRED; $api_input_rules['fields']['username']['flags'] |= API_REQUIRED; } break; case ZBX_SCRIPT_TYPE_WEBHOOK: $api_input_rules['fields'] += [ 'scope' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_SCOPE_ACTION, ZBX_SCRIPT_SCOPE_HOST, ZBX_SCRIPT_SCOPE_EVENT])], 'command' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'command')], 'timeout' => ['type' => API_TIME_UNIT, 'flags' => API_NOT_EMPTY, 'in' => '1:'.SEC_PER_MIN], 'parameters' => ['type' => API_OBJECTS, 'uniq' => [['name']], 'fields' => [ 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('script_param', 'name')], 'value' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('script_param', 'value')] ]] ]; if ($method === 'create') { $api_input_rules['fields']['scope']['flags'] = API_REQUIRED; $api_input_rules['fields']['command']['flags'] |= API_REQUIRED; } break; case ZBX_SCRIPT_TYPE_URL: $api_input_rules['fields'] += [ 'scope' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_SCOPE_HOST, ZBX_SCRIPT_SCOPE_EVENT])], 'url' => ['type' => API_URL, 'flags' => API_NOT_EMPTY | API_ALLOW_USER_MACRO | API_ALLOW_MACRO, 'length' => DB::getFieldLength('scripts', 'url')], 'new_window' => ['type' => API_INT32, 'in' => implode(',', [ZBX_SCRIPT_URL_NEW_WINDOW_NO, ZBX_SCRIPT_URL_NEW_WINDOW_YES]), 'default' => DB::getDefault('scripts', 'new_window')] ]; if ($method === 'create') { $api_input_rules['fields']['scope']['flags'] = API_REQUIRED; $api_input_rules['fields']['url']['flags'] |= API_REQUIRED; } break; } return $api_input_rules; } /** * Get validation rules for each script authtype. * * @param int $authtype Script authtype. * @param string $method API method "create" or "update". * * @return array */ protected function getAuthTypeValidationRules(int $authtype, string $method): array { $api_input_rules = ['type' => API_OBJECT, 'fields' => []]; if ($authtype == ITEM_AUTHTYPE_PUBLICKEY) { $api_input_rules['fields'] += [ 'publickey' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'publickey')], 'privatekey' => ['type' => API_STRING_UTF8,'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('scripts', 'privatekey')] ]; if ($method === 'create') { $api_input_rules['fields']['publickey']['flags'] |= API_REQUIRED; $api_input_rules['fields']['privatekey']['flags'] |= API_REQUIRED; } } return $api_input_rules; } /** * Check for valid user groups. * * @param array $scripts * @param array $scripts[]['usrgrpid'] (optional) * * @throws APIException if user group is not exists. */ private function checkUserGroups(array $scripts) { $usrgrpids = []; foreach ($scripts as $script) { if (array_key_exists('usrgrpid', $script) && $script['usrgrpid'] != 0) { $usrgrpids[$script['usrgrpid']] = true; } } if (!$usrgrpids) { return; } $usrgrpids = array_keys($usrgrpids); $db_usrgrps = DB::select('usrgrp', [ 'output' => [], 'usrgrpids' => $usrgrpids, 'preservekeys' => true ]); foreach ($usrgrpids as $usrgrpid) { if (!array_key_exists($usrgrpid, $db_usrgrps)) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('User group with ID "%1$s" is not available.', $usrgrpid)); } } } /** * Check for valid host groups. * * @param array $scripts * @param array $scripts[]['groupid'] (optional) * * @throws APIException if host group is not exists. */ private function checkHostGroups(array $scripts) { $groupids = []; foreach ($scripts as $script) { if (array_key_exists('groupid', $script) && $script['groupid'] != 0) { $groupids[$script['groupid']] = true; } } if (!$groupids) { return; } $groupids = array_keys($groupids); $db_groups = API::HostGroup()->get([ 'output' => [], 'groupids' => $groupids, 'preservekeys' => true ]); foreach ($groupids as $groupid) { if (!array_key_exists($groupid, $db_groups)) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host group with ID "%1$s" is not available.', $groupid)); } } } /** * @param array $scriptids * * @return array */ public function delete(array $scriptids) { if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) { self::exception(ZBX_API_ERROR_PERMISSIONS, _s('No permissions to call "%1$s.%2$s".', 'script', __FUNCTION__) ); } self::validateDelete($scriptids, $db_scripts); DB::delete('scripts', ['scriptid' => $scriptids]); self::addAuditLog(CAudit::ACTION_DELETE, CAudit::RESOURCE_SCRIPT, $db_scripts); return ['scriptids' => $scriptids]; } /** * Validates parameters for script.delete method. * * @param array $scriptids * @param array|null $db_scripts * * @throws APIException if the input is invalid */ private static function validateDelete(array &$scriptids, array &$db_scripts = null) { $api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true]; if (!CApiInputValidator::validate($api_input_rules, $scriptids, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $db_scripts = DB::select('scripts', [ 'output' => ['scriptid', 'name'], 'scriptids' => $scriptids, 'preservekeys' => true ]); if (count($db_scripts) != count($scriptids)) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } // Check if deleted scripts used in actions. $db_actions = DBselect( 'SELECT a.name,oc.scriptid'. ' FROM opcommand oc,operations o,actions a'. ' WHERE oc.operationid=o.operationid'. ' AND o.actionid=a.actionid'. ' AND '.dbConditionInt('oc.scriptid', $scriptids), 1 ); if ($db_action = DBfetch($db_actions)) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot delete scripts. Script "%1$s" is used in action operation "%2$s".', $db_scripts[$db_action['scriptid']]['name'], $db_action['name'] ) ); } } /** * @param array $data * * @return array */ public function execute(array $data) { global $ZBX_SERVER, $ZBX_SERVER_PORT; $api_input_rules = ['type' => API_OBJECT, 'fields' => [ 'scriptid' => ['type' => API_ID, 'flags' => API_REQUIRED], 'hostid' => ['type' => API_ID], 'eventid' => ['type' => API_ID] ]]; if (!CApiInputValidator::validate($api_input_rules, $data, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } if (!array_key_exists('hostid', $data) && !array_key_exists('eventid', $data)) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', '/', _s('the parameter "%1$s" is missing', 'eventid')) ); } if (array_key_exists('hostid', $data) && array_key_exists('eventid', $data)) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', '/', _s('unexpected parameter "%1$s"', 'eventid')) ); } if (array_key_exists('eventid', $data)) { $db_events = API::Event()->get([ 'output' => [], 'selectHosts' => ['hostid'], 'eventids' => $data['eventid'] ]); if (!$db_events) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!') ); } $hostids = array_column($db_events[0]['hosts'], 'hostid'); $is_event = true; } else { $hostids = $data['hostid']; $is_event = false; $db_hosts = API::Host()->get([ 'output' => [], 'hostids' => $hostids ]); if (!$db_hosts) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!') ); } } $db_scripts = $this->get([ 'output' => ['type'], 'hostids' => $hostids, 'scriptids' => $data['scriptid'] ]); if (!$db_scripts) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } if ($db_scripts[0]['type'] == ZBX_SCRIPT_TYPE_URL) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('Cannot execute URL type script.')); } // execute script $zabbix_server = new CZabbixServer($ZBX_SERVER, $ZBX_SERVER_PORT, timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::CONNECT_TIMEOUT)), timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::SCRIPT_TIMEOUT)), ZBX_SOCKET_BYTES_LIMIT ); $result = $zabbix_server->executeScript($data['scriptid'], self::getAuthIdentifier(), $is_event ? null : $data['hostid'], $is_event ? $data['eventid'] : null ); if ($result !== false) { // return the result in a backwards-compatible format return [ 'response' => 'success', 'value' => $result, 'debug' => $zabbix_server->getDebug() ]; } else { self::exception(ZBX_API_ERROR_INTERNAL, $zabbix_server->getError()); } } /** * Returns all the scripts that are available on each given host. Automatically resolves macros in * confirmation and URL fields. * * @param $hostids * * @return array */ public function getScriptsByHosts($hostids) { zbx_value2array($hostids); $scripts_by_host = []; if (!$hostids) { return $scripts_by_host; } foreach ($hostids as $hostid) { $scripts_by_host[$hostid] = []; } $api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true]; if (!CApiInputValidator::validate($api_input_rules, $hostids, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $scripts = $this->get([ 'output' => ['scriptid', 'name', 'command', 'host_access', 'usrgrpid', 'groupid', 'description', 'confirmation', 'type', 'execute_on', 'timeout', 'scope', 'port', 'authtype', 'username', 'password', 'publickey', 'privatekey', 'menu_path', 'url', 'new_window' ], 'hostids' => $hostids, 'sortfield' => 'name', 'preservekeys' => true ]); $scripts = $this->addRelatedGroupsAndHosts([ 'selectGroups' => null, 'selectHostGroups' => null, 'selectHosts' => ['hostid'] ], $scripts, $hostids); if ($scripts) { $macros_data = []; foreach ($scripts as $scriptid => $script) { foreach ($script['hosts'] as $host) { $hostid = $host['hostid']; if (array_key_exists($hostid, $scripts_by_host)) { if (strpos($script['confirmation'], '{') !== false) { $macros_data[$hostid][$scriptid]['confirmation'] = $script['confirmation']; } if (strpos($script['url'], '{') !== false) { $macros_data[$hostid][$scriptid]['url'] = $script['url']; } } } } $macros_data = CMacrosResolverHelper::resolveManualHostActionScripts($macros_data); foreach ($scripts as $scriptid => $script) { $hosts = $script['hosts']; unset($script['hosts']); // Set script to host. foreach ($hosts as $host) { $hostid = $host['hostid']; if (array_key_exists($hostid, $scripts_by_host)) { $size = count($scripts_by_host[$hostid]); $scripts_by_host[$hostid][$size] = $script; // Set confirmation and URL with resolved macros. if (array_key_exists($hostid, $macros_data) && array_key_exists($scriptid, $macros_data[$hostid])) { $macro_values = $macros_data[$hostid][$scriptid]; if (strpos($script['confirmation'], '{') !== false) { $scripts_by_host[$hostid][$size]['confirmation'] = $macro_values['confirmation']; } if (strpos($script['url'], '{') !== false) { $scripts_by_host[$hostid][$size]['url'] = $macro_values['url']; } } } } } } return $scripts_by_host; } /** * Returns all the scripts that are available on each given event. Automatically resolves macros in * confirmation and URL fields. * * @param $eventids * * @return array */ public function getScriptsByEvents($eventids) { zbx_value2array($eventids); $scripts_by_events = []; if (!$eventids) { return $scripts_by_events; } $api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true]; if (!CApiInputValidator::validate($api_input_rules, $eventids, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } foreach ($eventids as $eventid) { $scripts_by_events[$eventid] = []; } $events = API::Event()->get([ 'output' => ['eventid', 'objectid', 'value', 'name', 'severity', 'cause_eventid'], 'selectHosts' => ['hostid'], 'object' => EVENT_OBJECT_TRIGGER, 'source' => EVENT_SOURCE_TRIGGERS, 'eventids' => $eventids, 'preservekeys' => true ]); if (!$events) { return $scripts_by_events; } $symptom_cause_eventids = []; foreach ($events as &$event) { if ($event['cause_eventid'] != 0) { // There is not need to select already preselected events again. if (array_key_exists($event['cause_eventid'], $events)) { $event['cause'] = [ 'eventid' => $events[$event['cause_eventid']]['eventid'], 'value' => $events[$event['cause_eventid']]['value'], 'name' => $events[$event['cause_eventid']]['name'], 'severity' => $events[$event['cause_eventid']]['severity'] ]; } else { $event['cause'] = []; // Collect cause event IDs for symptom events. $symptom_cause_eventids[] = $event['cause_eventid']; } } } unset($event); if ($symptom_cause_eventids) { $cause_events = API::Event()->get([ 'output' => ['eventid', 'value', 'name', 'severity'], 'object' => EVENT_OBJECT_TRIGGER, 'source' => EVENT_SOURCE_TRIGGERS, 'eventids' => $symptom_cause_eventids, 'preservekeys' => true ]); if ($cause_events) { foreach ($events as &$event) { foreach ($cause_events as $cause_event) { if (bccomp($event['cause_eventid'], $cause_event['eventid']) == 0) { $event['cause'] = [ 'eventid' => $cause_event['eventid'], 'value' => $cause_event['value'], 'name' => $cause_event['name'], 'severity' => $cause_event['severity'] ]; } } } unset($event); } } $hostids = []; foreach ($events as $event) { foreach ($event['hosts'] as $host) { $hostids[$host['hostid']] = true; } } $scripts = $this->get([ 'output' => ['scriptid', 'name', 'command', 'host_access', 'usrgrpid', 'groupid', 'description', 'confirmation', 'type', 'execute_on', 'timeout', 'scope', 'port', 'authtype', 'username', 'password', 'publickey', 'privatekey', 'menu_path', 'url', 'new_window' ], 'hostids' => array_keys($hostids), 'sortfield' => 'name', 'preservekeys' => true ]); $scripts = $this->addRelatedGroupsAndHosts([ 'selectGroups' => null, 'selectHostGroups' => null, 'selectHosts' => ['hostid'] ], $scripts, array_keys($hostids)); if ($scripts) { $macros_data = []; $processed_scripts_per_event = []; foreach ($scripts as $scriptid => $script) { foreach ($events as $eventid => $event) { foreach ($event['hosts'] as $event_host) { foreach ($script['hosts'] as $host) { if (bccomp($host['hostid'], $event_host['hostid']) == 0 && array_key_exists($eventid, $scripts_by_events)) { if (strpos($script['confirmation'], '{') !== false) { $macros_data[$eventid][$scriptid]['confirmation'] = $script['confirmation']; } if (strpos($script['url'], '{') !== false) { $macros_data[$eventid][$scriptid]['url'] = $script['url']; } } } } } } $macros_data = CMacrosResolverHelper::resolveManualEventActionScripts($macros_data, $events); foreach ($scripts as $scriptid => $script) { $hosts = $script['hosts']; unset($script['hosts']); foreach ($events as $eventid => $event) { if (!array_key_exists($eventid, $processed_scripts_per_event)) { $processed_scripts_per_event[$eventid] = []; } foreach ($event['hosts'] as $event_host) { foreach ($hosts as $host) { if (bccomp($host['hostid'], $event_host['hostid']) == 0 && array_key_exists($eventid, $scripts_by_events) && !array_key_exists($scriptid, $processed_scripts_per_event[$eventid])) { $size = count($scripts_by_events[$eventid]); $scripts_by_events[$eventid][$size] = $script; // Set confirmation and URL with resolved macros. if (array_key_exists($eventid, $macros_data) && array_key_exists($scriptid, $macros_data[$eventid])) { $macro_values = $macros_data[$eventid][$scriptid]; if (strpos($script['confirmation'], '{') !== false) { $scripts_by_events[$eventid][$size]['confirmation'] = $macro_values['confirmation']; } if (strpos($script['url'], '{') !== false) { $scripts_by_events[$eventid][$size]['url'] = $macro_values['url']; } } $processed_scripts_per_event[$eventid][$scriptid] = true; } } } } } } return $scripts_by_events; } protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) { $sqlParts = parent::applyQueryOutputOptions($tableName, $tableAlias, $options, $sqlParts); if ($options['selectGroups'] !== null || $options['selectHostGroups'] !== null || $options['selectHosts'] !== null) { $sqlParts = $this->addQuerySelect($this->fieldId('groupid'), $sqlParts); $sqlParts = $this->addQuerySelect($this->fieldId('host_access'), $sqlParts); } return $sqlParts; } /** * Applies relational subselect onto already fetched result. * * @param array $options * @param array $result * * @return array $result */ protected function addRelatedObjects(array $options, array $result) { $result = parent::addRelatedObjects($options, $result); // Adding actions. if ($options['selectActions'] !== null && $options['selectActions'] !== API_OUTPUT_COUNT) { foreach ($result as &$row) { $row['actions'] = []; } unset($row); $action_scriptids = []; if ($this->outputIsRequested('scope', $options['output'])) { foreach ($result as $scriptid => $row) { if ($row['scope'] == ZBX_SCRIPT_SCOPE_ACTION) { $action_scriptids[] = $scriptid; } } } else { $db_scripts = API::getApiService()->select('scripts', [ 'output' => ['scope'], 'filter' => ['scriptid' => array_keys($result)], 'preservekeys' => true ]); $db_scripts = $this->extendFromObjects($result, $db_scripts, ['scope']); foreach ($db_scripts as $scriptid => $db_script) { if ($db_script['scope'] == ZBX_SCRIPT_SCOPE_ACTION) { $action_scriptids[] = $scriptid; } } // Remove scope from output, since it's not requested. $result = $this->unsetExtraFields($result, ['scope']); } if ($action_scriptids) { if ($options['selectActions'] === API_OUTPUT_EXTEND) { $action_fields = array_map(function ($field) { return 'a.'.$field; }, $this->action_fields); $action_fields = implode(',', $action_fields); } elseif (is_array($options['selectActions'])) { $action_fields = $options['selectActions']; if (!in_array('actionid', $options['selectActions'])) { $action_fields[] = 'actionid'; } $action_fields = array_map(function ($field) { return 'a.'.$field; }, $action_fields); $action_fields = implode(',', $action_fields); } $db_script_actions = DBfetchArray(DBselect( 'SELECT DISTINCT oc.scriptid,'.$action_fields. ' FROM actions a,operations o,opcommand oc'. ' WHERE a.actionid=o.actionid'. ' AND o.operationid=oc.operationid'. ' AND '.dbConditionInt('oc.scriptid', $action_scriptids) )); foreach ($result as $scriptid => &$row) { if ($db_script_actions) { foreach ($db_script_actions as $db_script_action) { if (bccomp($db_script_action['scriptid'], $scriptid) == 0) { unset($db_script_action['scriptid']); $row['actions'][] = $db_script_action; } } $row['actions'] = $this->unsetExtraFields($row['actions'], ['actionid'], $options['selectActions'] ); } } unset($row); } } if ($this->outputIsRequested('parameters', $options['output'])) { foreach ($result as $scriptid => $script) { $result[$scriptid]['parameters'] = []; } $param_options = [ 'output' => ['script_paramid', 'scriptid', 'name', 'value'], 'filter' => ['scriptid' => array_keys($result)] ]; $db_parameters = DBselect(DB::makeSql('script_param', $param_options)); while ($db_param = DBfetch($db_parameters)) { $result[$db_param['scriptid']]['parameters'][] = [ 'name' => $db_param['name'], 'value' => $db_param['value'] ]; } } return $this->addRelatedGroupsAndHosts($options, $result); } /** * Applies relational subselect onto already fetched result. * * @param array $options * @param mixed $options['selectHostGroups'] * @param mixed $options['selectHosts'] * @param array $result * @param array $hostids An additional filter by hostids, which will be added to "hosts" key. * * @return array $result */ private function addRelatedGroupsAndHosts(array $options, array $result, array $hostids = null) { $is_groups_select = $options['selectGroups'] !== null; $is_hostgroups_select = $options['selectHostGroups'] !== null; $is_hosts_select = $options['selectHosts'] !== null; if (!$is_groups_select && !$is_hostgroups_select && !$is_hosts_select) { return $result; } $host_groups_with_write_access = []; $has_write_access_level = false; $group_search_names = []; foreach ($result as $script) { if ($script['host_access'] == PERM_READ_WRITE) { $has_write_access_level = true; } // If any script belongs to all host groups. if ($script['groupid'] == 0) { $group_search_names = null; } if ($group_search_names !== null) { /* * If scripts were requested by host or group filters, then we have already requested group names * for all groups linked to scripts. And then we can request less groups by adding them as search * condition in hostgroup.get. Otherwise we will need to request all groups, user has access to. */ if (array_key_exists($script['groupid'], $this->parent_host_groups)) { $group_search_names[] = $this->parent_host_groups[$script['groupid']]['name']; } } } if ($options['selectGroups'] === API_OUTPUT_EXTEND || $options['selectHostGroups'] === API_OUTPUT_EXTEND) { $select_groups = API_OUTPUT_EXTEND; } else { $select_groups = array_unique(array_merge( is_array($options['selectGroups']) ? $options['selectGroups'] : [], is_array($options['selectHostGroups']) ? $options['selectHostGroups'] : [] )); } $select_groups = $this->outputExtend($select_groups, ['groupid', 'name']); $host_groups = API::HostGroup()->get([ 'output' => $select_groups, 'search' => $group_search_names ? ['name' => $group_search_names] : null, 'searchByAny' => true, 'startSearch' => true, 'preservekeys' => true ]); if ($has_write_access_level && $host_groups) { $host_groups_with_write_access = API::HostGroup()->get([ 'output' => $select_groups, 'groupids' => array_keys($host_groups), 'preservekeys' => true, 'editable' => true ]); } else { $host_groups_with_write_access = $host_groups; } $nested = []; foreach ($host_groups as $groupid => $group) { $name = $group['name']; while (($pos = strrpos($name, '/')) !== false) { $name = substr($name, 0, $pos); $nested[$name][$groupid] = true; } } $hstgrp_branch = []; foreach ($host_groups as $groupid => $group) { $hstgrp_branch[$groupid] = [$groupid => true]; if (array_key_exists($group['name'], $nested)) { $hstgrp_branch[$groupid] += $nested[$group['name']]; } } if ($is_hosts_select) { $sql = 'SELECT hostid,groupid FROM hosts_groups'. ' WHERE '.dbConditionInt('groupid', array_keys($host_groups)); if ($hostids !== null) { $sql .= ' AND '.dbConditionInt('hostid', $hostids); } $db_group_hosts = DBSelect($sql); $all_hostids = []; $group_to_hosts = []; while ($row = DBFetch($db_group_hosts)) { if (!array_key_exists($row['groupid'], $group_to_hosts)) { $group_to_hosts[$row['groupid']] = []; } $group_to_hosts[$row['groupid']][$row['hostid']] = true; $all_hostids[] = $row['hostid']; } $used_hosts = API::Host()->get([ 'output' => $options['selectHosts'], 'hostids' => $all_hostids, 'preservekeys' => true ]); } foreach ($result as &$script) { if ($script['groupid'] == 0) { $script_groups = ($script['host_access'] == PERM_READ_WRITE) ? $host_groups_with_write_access : $host_groups; } else { $script_groups = ($script['host_access'] == PERM_READ_WRITE) ? array_intersect_key($host_groups_with_write_access, $hstgrp_branch[$script['groupid']]) : array_intersect_key($host_groups, $hstgrp_branch[$script['groupid']]); } if ($is_groups_select) { $script['groups'] = array_values($this->unsetExtraFields($script_groups, ['groupid', 'name', 'flags', 'uuid'], $options['selectGroups'] )); } if ($is_hostgroups_select) { $script['hostgroups'] = array_values($this->unsetExtraFields($script_groups, ['groupid', 'name', 'flags', 'uuid'], $options['selectHostGroups'] )); } if ($is_hosts_select) { $script['hosts'] = []; foreach (array_keys($script_groups) as $script_groupid) { if (array_key_exists($script_groupid, $group_to_hosts)) { $script['hosts'] += array_intersect_key($used_hosts, $group_to_hosts[$script_groupid]); } } $script['hosts'] = array_values($script['hosts']); } } unset($script); return $result; } /** * Check for unique script names within menu path in the input. * * @param array $scripts Array of scripts. * @param string $method API method "create" or "update". Default "create". * * $scripts = [[ * 'name' => (string) Script name (optional for update method). * 'menu_path' => (string) Script menu path (optional). * ]] * * @throws APIException if script names within menu paths are not unique. */ private function checkUniqueness(array $scripts, string $method = 'create'): void { if ($method === 'update') { $scripts = array_filter($scripts, static fn($script) => array_key_exists('name', $script) || array_key_exists('menu_path', $script) ); if (!$scripts) { return; } } foreach ($scripts as &$script) { $menu_path = ''; if (array_key_exists('menu_path', $script)) { $menu_path = trimPath($script['menu_path']); } // Trim preceeding and trailing slashes for comparison. $menu_path = trim($menu_path, '/'); $script['menu_path'] = $menu_path; } unset($script); $api_input_rules = $this->getValidationRules($method); $api_input_rules['uniq'] = [['name', 'menu_path']]; $api_input_rules['fields'] = array_intersect_key($api_input_rules['fields'], array_flip(['name', 'menu_path'])); $api_input_rules['flags'] |= API_ALLOW_UNEXPECTED; if (!CApiInputValidator::validate($api_input_rules, $scripts, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } } /** * Check for duplicate script names within menu path. * * @param array $scripts Array of scripts. * @param array|null $db_scripts Array of scripts from database. * * $scripts = [[ * 'scriptid' => (string) Script ID. * 'name' => (string) Script name. * 'menu_path' => (string) Script menu path (exists if scope = 1 for update method). * 'scope' => (string) Script scope. * ]] * * $db_scripts = [ * => [ * 'name' => (string) Script name. * 'menu_path' => (string) Script menu path. * 'scope' => (string) Script scope. * ] * ] * * @throws APIException if script names within menu paths have duplicates in DB. */ private function checkDuplicates(array $scripts, ?array $db_scripts = null): void { if ($db_scripts !== null) { $scripts = $this->extendFromObjects(zbx_toHash($scripts, 'scriptid'), $db_scripts, ['menu_path']); /* * Remove unchanged scripts and continue validation only for scripts that have changed name, menu path or * scope. If scope is changed to action, menu_path will be reset to empty string and that is a change. */ $scripts = array_filter($scripts, static fn($script) => $script['name'] !== $db_scripts[$script['scriptid']]['name'] || $script['menu_path'] !== $db_scripts[$script['scriptid']]['menu_path'] || ($script['scope'] !== $db_scripts[$script['scriptid']]['scope'] && $script['scope'] == ZBX_SCRIPT_SCOPE_ACTION) ); if (!$scripts) { return; } } $scripts_ex = DB::select('scripts', [ 'output' => ['scriptid', 'name', 'menu_path'], 'filter' => ['name' => array_column($scripts, 'name')] ]); if (!$scripts_ex) { return; } $db_scriptids = []; foreach ($scripts_ex as $script) { $name = self::getScriptNameAndPath($script); $db_scriptids[$name] = $script['scriptid']; } foreach ($scripts as $script) { $name = self::getScriptNameAndPath($script); if ($db_scripts === null && array_key_exists($name, $db_scriptids)) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Script "%1$s" already exists.', $script['name'])); } elseif (array_key_exists($name, $db_scriptids) && bccomp($script['scriptid'], $db_scriptids[$name]) != 0) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Script "%1$s" already exists.', $script['name'])); } } } /** * Update "script_param" table and populate script.parameters by "script_paramid" property. * * @param array $scripts * @param string $method * @param array|null $db_scripts */ private static function updateParams(array &$scripts, string $method, array $db_scripts = null): void { $ins_params = []; $upd_params = []; $del_paramids = []; foreach ($scripts as &$script) { if (!array_key_exists('parameters', $script)) { continue; } $db_params = ($method === 'update') ? array_column($db_scripts[$script['scriptid']]['parameters'], null, 'name') : []; foreach ($script['parameters'] as &$param) { if (array_key_exists($param['name'], $db_params)) { $db_param = $db_params[$param['name']]; $param['script_paramid'] = $db_param['script_paramid']; unset($db_params[$param['name']]); $upd_param = DB::getUpdatedValues('script_param', $param, $db_param); if ($upd_param) { $upd_params[] = [ 'values' => $upd_param, 'where' => ['script_paramid' => $db_param['script_paramid']] ]; } } else { $ins_params[] = ['scriptid' => $script['scriptid']] + $param; } } unset($param); $del_paramids = array_merge($del_paramids, array_column($db_params, 'script_paramid')); } unset($script); if ($ins_params) { $script_paramids = DB::insertBatch('script_param', $ins_params); } if ($upd_params) { DB::update('script_param', $upd_params); } if ($del_paramids) { DB::delete('script_param', ['script_paramid' => $del_paramids]); } foreach ($scripts as &$script) { if (!array_key_exists('parameters', $script)) { continue; } foreach ($script['parameters'] as &$param) { if (!array_key_exists('script_paramid', $param)) { $param['script_paramid'] = array_shift($script_paramids); } } unset($param); } unset($script); } /** * Add the existing parameters to $db_scripts whether these are affected by the update. * * @param array $scripts * @param array $db_scripts */ private static function addAffectedObjects(array $scripts, array &$db_scripts): void { $scriptids = []; foreach ($scripts as $script) { $scriptids[] = $script['scriptid']; $db_scripts[$script['scriptid']]['parameters'] = []; } if (!$scriptids) { return; } $options = [ 'output' => ['script_paramid', 'scriptid', 'name', 'value'], 'filter' => ['scriptid' => $scriptids] ]; $db_parameters = DBselect(DB::makeSql('script_param', $options)); while ($db_parameter = DBfetch($db_parameters)) { $db_scripts[$db_parameter['scriptid']]['parameters'][$db_parameter['script_paramid']] = array_diff_key($db_parameter, array_flip(['scriptid'])); } } /** * Helper function to combine trimmed menu path with name. * * @param array $script Script data. * * $script = [ * 'name' => (string) Script name. * 'menu_path' => (string) Script menu path (optional). * ] * * Example: * $script = [ * 'name' => 'ABC' * 'menu_path' => '/a/b' * ] * Output: a/b/ABC * * @return string */ private static function getScriptNameAndPath(array $script): string { $menu_path = ''; if (array_key_exists('menu_path', $script)) { $menu_path = trimPath($script['menu_path']); } $menu_path = trim($menu_path, '/'); return $menu_path === '' ? $script['name'] : $menu_path.'/'.$script['name']; } }