diff options
author | Alexander Vladishev <aleksander.vladishev@zabbix.com> | 2022-05-24 22:58:34 +0300 |
---|---|---|
committer | Alexander Vladishev <aleksander.vladishev@zabbix.com> | 2022-05-24 22:58:34 +0300 |
commit | 8ab28dcf078f9530b56f38e412fc49c05a5edc52 (patch) | |
tree | 5bf72422987f49719fc877b2f0c4564fa27b4d03 | |
parent | ed570e4843186cf4900524f3a0f87bcfbf2896b8 (diff) | |
parent | d3b516db8f924d879f77f7eb8943b22f237013f9 (diff) |
.......... [ZBX-20859] updated to the latest master; no conflicts
30 files changed, 2316 insertions, 1604 deletions
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index ecd95aa8ed7..dc1e9086d74 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -15,7 +15,7 @@ jobs: env: SONAR_SCANNER_VERSION: 4.7.0.2747 SONAR_SERVER_URL: "https://sonarcloud.io" - BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed + BUILD_WRAPPER_OUT_DIR: $HOME/.sonar/build_wrapper_output_directory # Directory where build-wrapper output will be placed steps: - uses: actions/checkout@v2 with: diff --git a/ChangeLog b/ChangeLog index 84c13da75db..f8583af7920 100644 --- a/ChangeLog +++ b/ChangeLog @@ -14,6 +14,7 @@ A.F....... [ZBXNEXT-7476] disabled php 8 strict type validation (ashubin) A.F....... [ZBXNEXT-7491] fixed PHP 8.1 runtime warnings with PostgreSQL backend (Sasha) Bug fixes: +..F....... [ZBX-20242] fixed inability to change LDAP bind password (agriscenko) ...G...... [ZBX-20911] added process state check in PROC_NUM() for Oracle Solaris (Andris) ..F....... [ZBX-20752] fixed saved properties of classic graphs being lost when reopening graph configuration (agriscenko) ..F....... [ZBX-19381] fixed resolving of macros in item key parameters when testing items in web interface (Sasha) diff --git a/ChangeLog.d/bugfix/ZBX-20613 b/ChangeLog.d/bugfix/ZBX-20613 new file mode 100644 index 00000000000..3fded15f049 --- /dev/null +++ b/ChangeLog.d/bugfix/ZBX-20613 @@ -0,0 +1 @@ +A.F....... [ZBX-20613] returned the ability to edit the dependencies of inherited triggers; improved validation upon unlinking templates with triggers that have dependencies; improved performance of trigger dependency functionality; prevented the ability to fully replace trigger dependencies of inherited triggers if the new dependency was added to parent trigger; supplemented validation of trigger dependencies on templates to prevent linkage dead-ends (vmaksimovs) diff --git a/ChangeLog.d/bugfix/ZBX-20814 b/ChangeLog.d/bugfix/ZBX-20814 new file mode 100644 index 00000000000..5fb3ccd4389 --- /dev/null +++ b/ChangeLog.d/bugfix/ZBX-20814 @@ -0,0 +1 @@ +..F....... [ZBX-20814] fixed Map->Sharing user group shares table not showing saved user groups (esekace) diff --git a/ChangeLog.d/bugfix/ZBX-20898 b/ChangeLog.d/bugfix/ZBX-20898 new file mode 100644 index 00000000000..d630940c0b3 --- /dev/null +++ b/ChangeLog.d/bugfix/ZBX-20898 @@ -0,0 +1 @@ +.........T [ZBX-20898] fixed duplicate UUID in official template (abakaldin) diff --git a/templates/app/ftp_service/template_app_ftp_service.yaml b/templates/app/ftp_service/template_app_ftp_service.yaml index 8e3d1e5223b..1fb9f225237 100644 --- a/templates/app/ftp_service/template_app_ftp_service.yaml +++ b/templates/app/ftp_service/template_app_ftp_service.yaml @@ -32,7 +32,7 @@ zabbix_export: value: network triggers: - - uuid: b299d73cebcd430c8bfc54cf9b84e853 + uuid: aee485c125f94e37ac97c3ce1a654757 expression: 'max(/FTP Service/net.tcp.service[ftp],#3)=0' name: 'FTP service is down on {HOST.NAME}' priority: AVERAGE diff --git a/templates/db/postgresql_agent2/README.md b/templates/db/postgresql_agent2/README.md index 88727664850..10b94efc64c 100644 --- a/templates/db/postgresql_agent2/README.md +++ b/templates/db/postgresql_agent2/README.md @@ -19,7 +19,7 @@ This template was tested on: 1\. Create PostgreSQL user for monitoring (`<password>` at your discretion): ```bash -CREATE USER 'zbx_monitor' WITH PASSWORD '<PASSWORD>' INHERIT; +CREATE USER zbx_monitor WITH PASSWORD '<PASSWORD>' INHERIT; GRANT EXECUTE ON FUNCTION pg_catalog.pg_ls_dir(text) TO zbx_monitor; GRANT EXECUTE ON FUNCTION pg_catalog.pg_stat_file(text) TO zbx_monitor; GRANT EXECUTE ON FUNCTION pg_catalog.pg_ls_waldir() TO zbx_monitor; diff --git a/ui/app/controllers/CControllerAuthenticationEdit.php b/ui/app/controllers/CControllerAuthenticationEdit.php index e4b6d51d1c5..c7807cec589 100644 --- a/ui/app/controllers/CControllerAuthenticationEdit.php +++ b/ui/app/controllers/CControllerAuthenticationEdit.php @@ -92,8 +92,6 @@ class CControllerAuthenticationEdit extends CController { $openssl_status = (new CFrontendSetup())->checkPhpOpenSsl(); $data = [ - 'action_submit' => 'authentication.update', - 'action_passw_change' => 'authentication.edit', 'ldap_error' => ($ldap_status['result'] == CFrontendSetup::CHECK_OK) ? '' : $ldap_status['error'], 'ldap_test_password' => '', 'ldap_test_user' => CWebUser::$data['username'], diff --git a/ui/app/controllers/CControllerHostCreate.php b/ui/app/controllers/CControllerHostCreate.php index 89565b41cd9..c7f835922d0 100644 --- a/ui/app/controllers/CControllerHostCreate.php +++ b/ui/app/controllers/CControllerHostCreate.php @@ -106,11 +106,11 @@ class CControllerHostCreate extends CControllerHostUpdateGeneral { $host = $this->extendHostCloneEncryption($host, $src_hostid); } - $hostids = API::Host()->create($host); + $result = API::Host()->create($host); - if ($hostids === false - || !$this->createValueMaps($hostids['hostids'][0]) - || ($full_clone && !$this->copyFromCloneSourceHost($src_hostid, $hostids['hostids'][0]))) { + if ($result === false + || !$this->createValueMaps($result['hostids'][0]) + || ($full_clone && !$this->copyFromCloneSourceHost($src_hostid, $result['hostids'][0]))) { throw new Exception(); } @@ -210,14 +210,7 @@ class CControllerHostCreate extends CControllerHostUpdateGeneral { } // Copy triggers. - $db_triggers = API::Trigger()->get([ - 'output' => ['triggerid'], - 'hostids' => $src_hostid, - 'inherited' => false, - 'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL] - ]); - - if ($db_triggers && !copyTriggersToHosts(array_column($db_triggers, 'triggerid'), $hostid, $src_hostid)) { + if (!copyTriggersToHosts([$hostid], $src_hostid)) { return false; } diff --git a/ui/app/views/administration.authentication.edit.php b/ui/app/views/administration.authentication.edit.php index 1f6d1385d7a..0d18f9075fb 100644 --- a/ui/app/views/administration.authentication.edit.php +++ b/ui/app/views/administration.authentication.edit.php @@ -136,19 +136,23 @@ $http_tab = (new CFormList('list_http')) // LDAP configuration fields. if ($data['change_bind_password']) { - $password_box = [ - new CVar('change_bind_password', 1), - (new CPassBox('ldap_bind_password', $data['ldap_bind_password'])) - ->setEnabled($data['ldap_enabled']) - ->setWidth(ZBX_TEXTAREA_SMALL_WIDTH) - ]; + $password_box = (new CPassBox('ldap_bind_password', $data['ldap_bind_password'], + DB::getFieldLength('config', 'ldap_bind_password')) + ) + ->setEnabled($data['ldap_enabled']) + ->setWidth(ZBX_TEXTAREA_SMALL_WIDTH) + ; } else { $password_box = [ - new CVar('action_passw_change', $data['action_passw_change']), - (new CButton('change_bind_password', _('Change password'))) + (new CSimpleButton(_('Change password'))) + ->setId('bind-password-btn') ->setEnabled($data['ldap_enabled']) - ->addClass(ZBX_STYLE_BTN_GREY) + ->addClass(ZBX_STYLE_BTN_GREY), + (new CPassBox('ldap_bind_password', '', DB::getFieldLength('config', 'ldap_bind_password'))) + ->setWidth(ZBX_TEXTAREA_SMALL_WIDTH) + ->addClass(ZBX_STYLE_DISPLAY_NONE) + ->setEnabled(false) ]; } @@ -322,7 +326,8 @@ $saml_tab = (new CFormList('list_saml')) (new CWidget()) ->setTitle(_('Authentication')) ->addItem((new CForm()) - ->addVar('action', $data['action_submit']) + ->addVar('action', 'authentication.update') + ->addVar('change_bind_password', $data['change_bind_password']) ->addVar('db_authentication_type', $data['db_authentication_type']) ->setId('authentication-form') ->setName('form_auth') diff --git a/ui/app/views/js/administration.authentication.edit.js.php b/ui/app/views/js/administration.authentication.edit.js.php index 1baac8907f4..1b8d80a1b84 100644 --- a/ui/app/views/js/administration.authentication.edit.js.php +++ b/ui/app/views/js/administration.authentication.edit.js.php @@ -51,7 +51,7 @@ fields = $form.find('[name^=http_]'); } else if ($(this).is('#ldap_configured')) { - fields = $form.find('[name^=ldap_],button[name=change_bind_password]'); + fields = $form.find('[name^=ldap_],#bind-password-btn'); } else { fields = $form.find('[name^=saml_]'); @@ -62,15 +62,22 @@ .prop('disabled', !this.checked); }); - $form.find('button#change_bind_password').click(function() { - $form.find('[name=action]') - .val($form.find('[name=action_passw_change]').val()); - - submitFormWithParam('form_auth', 'change_bind_password', '1'); - }); + $form.find('#bind-password-btn').on('click', showPasswordField); $form.find('[name=ldap_test]').click(function() { warn = false; }); }); + + function showPasswordField(e) { + const form_field = e.target.parentNode; + const password_field = form_field.querySelector('[name="ldap_bind_password"]'); + + password_field.disabled = false; + password_field.classList.remove('<?= ZBX_STYLE_DISPLAY_NONE ?>'); + + form_field.removeChild(e.target); + + document.getElementById('change_bind_password').value = 1; + } </script> diff --git a/ui/include/classes/api/services/CDiscoveryRule.php b/ui/include/classes/api/services/CDiscoveryRule.php index 7d906a2ebc8..fe91fd2520e 100644 --- a/ui/include/classes/api/services/CDiscoveryRule.php +++ b/ui/include/classes/api/services/CDiscoveryRule.php @@ -714,196 +714,225 @@ class CDiscoveryRule extends CItemGeneral { /** * Copies all of the triggers from the source discovery to the target discovery rule. * - * @throws APIException if trigger saving fails - * - * @param array $srcDiscovery The source discovery rule to copy from - * @param array $srcHost The host the source discovery belongs to - * @param array $dstHost The host the target discovery belongs to + * @param array $src_discovery The source discovery rule to copy from. + * @param array $src_host The host the source discovery belongs to. + * @param string $src_host['hostid'] + * @param string $src_host['host'] + * @param array $dst_host The host the target discovery belongs to. + * @param string $dst_host['hostid'] + * @param string $dst_host['host'] * * @return array + * + * @throws APIException */ - protected function copyTriggerPrototypes(array $srcDiscovery, array $srcHost, array $dstHost) { - $srcTriggers = API::TriggerPrototype()->get([ - 'discoveryids' => $srcDiscovery['itemid'], - 'output' => ['triggerid', 'expression', 'description', 'url', 'status', 'priority', 'comments', - 'templateid', 'type', 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag', - 'opdata', 'discover', 'event_name' + protected function copyTriggerPrototypes(array $src_discovery, array $src_host, array $dst_host): array { + $src_triggers = API::TriggerPrototype()->get([ + 'output' => ['triggerid', 'expression', 'description', 'url', 'status', 'priority', 'comments', 'type', + 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag', 'opdata', 'discover', + 'event_name' ], - 'selectHosts' => API_OUTPUT_EXTEND, 'selectItems' => ['itemid', 'type'], - 'selectDiscoveryRule' => API_OUTPUT_EXTEND, - 'selectFunctions' => API_OUTPUT_EXTEND, - 'selectDependencies' => ['triggerid'], 'selectTags' => ['tag', 'value'], - 'preservekeys' => true + 'selectDependencies' => ['triggerid'], + 'discoveryids' => $src_discovery['itemid'] ]); - foreach ($srcTriggers as $id => $trigger) { + $dst_triggers = []; + + foreach ($src_triggers as $i => $src_trigger) { // Skip trigger prototypes with web items and remove them from source. - if (httpItemExists($trigger['items'])) { - unset($srcTriggers[$id]); + if (httpItemExists($src_trigger['items'])) { + unset($src_triggers[$i]); + } + else { + $dst_triggers[] = array_intersect_key($src_trigger, array_flip(['expression', 'description', 'url', + 'status', 'priority', 'comments','type', 'recovery_mode', 'recovery_expression', 'correlation_mode', + 'correlation_tag', 'opdata', 'discover', 'event_name', 'tags' + ])); } } - if (!$srcTriggers) { + if (!$dst_triggers) { return []; } - /* - * Copy the remaining trigger prototypes to a new source. These will contain IDs and original dependencies. - * The dependencies from $srcTriggers will be removed. - */ - $trigger_prototypes = $srcTriggers; - - // Contains original trigger prototype dependency IDs. - $dep_triggerids = []; - - /* - * Collect dependency trigger IDs and remove them from source. Otherwise these IDs do not pass - * validation, since they don't belong to destination discovery rule. - */ - $add_dependencies = false; - - foreach ($srcTriggers as $id => &$trigger) { - if ($trigger['dependencies']) { - foreach ($trigger['dependencies'] as $dep_trigger) { - $dep_triggerids[] = $dep_trigger['triggerid']; - } - $add_dependencies = true; - } - unset($trigger['dependencies']); - } - unset($trigger); + $src_triggers = array_values($src_triggers); - // Save new trigger prototypes and without dependencies for now. - $dstTriggers = $srcTriggers; - $dstTriggers = CMacrosResolverHelper::resolveTriggerExpressions($dstTriggers, + $dst_triggers = CMacrosResolverHelper::resolveTriggerExpressions($dst_triggers, ['sources' => ['expression', 'recovery_expression']] ); - foreach ($dstTriggers as $id => &$trigger) { - unset($trigger['triggerid'], $trigger['templateid'], $trigger['hosts'], $trigger['functions'], - $trigger['items'], $trigger['discoveryRule'] - ); - // Update the destination expressions. - $trigger['expression'] = triggerExpressionReplaceHost($trigger['expression'], $srcHost['host'], - $dstHost['host'] + foreach ($dst_triggers as &$trigger) { + $trigger['expression'] = triggerExpressionReplaceHost($trigger['expression'], $src_host['host'], + $dst_host['host'] ); + if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { $trigger['recovery_expression'] = triggerExpressionReplaceHost($trigger['recovery_expression'], - $srcHost['host'], $dstHost['host'] + $src_host['host'], $dst_host['host'] ); } } unset($trigger); - $result = API::TriggerPrototype()->create($dstTriggers); + $result = API::TriggerPrototype()->create($dst_triggers); + if (!$result) { self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot clone trigger prototypes.')); } - // Process dependencies, if at least one trigger prototype has a dependency. - if ($add_dependencies) { - $trigger_prototypeids = array_keys($trigger_prototypes); + $dst_triggerids = $result['triggerids']; + $src_trigger_indexes = array_flip(array_column($src_triggers, 'triggerid')); - foreach ($result['triggerids'] as $i => $triggerid) { - $new_trigger_prototypes[$trigger_prototypeids[$i]] = [ - 'new_triggerid' => $triggerid, - 'new_hostid' => $dstHost['hostid'], - 'new_host' => $dstHost['host'], - 'src_hostid' => $srcHost['hostid'], - 'src_host' => $srcHost['host'] - ]; + $dst_triggers = []; + + /* + * A check that the trigger-up belongs to the source host needs to be performed on copying the dependencies + * on triggers. + * If it does, we need to check that the triggers with the same description and expression exist on the + * destination host. + * If not, we need to check if the dependencies from destination triggers to these triggers are valid. + */ + $src_triggerids_up = []; + + foreach ($dst_triggerids as $i => $dst_triggerid) { + if (!$src_triggers[$i]['dependencies']) { + unset($dst_triggerids[$i]); + continue; } - /* - * Search for original dependent triggers and expressions to find corresponding triggers on destination host - * with same expression. - */ - $dep_triggers = API::Trigger()->get([ - 'output' => ['description', 'expression'], - 'selectHosts' => ['hostid'], - 'triggerids' => $dep_triggerids, - 'preservekeys' => true - ]); - $dep_triggers = CMacrosResolverHelper::resolveTriggerExpressions($dep_triggers); + $dst_triggers[$dst_triggerid] = ['triggerid' => $dst_triggerid]; - // Map dependencies to the new trigger IDs and save. - foreach ($trigger_prototypes as &$trigger_prototype) { - // Get corresponding created trigger prototype ID. - $new_trigger_prototype = $new_trigger_prototypes[$trigger_prototype['triggerid']]; + foreach ($src_triggers[$i]['dependencies'] as $i2 => $src_trigger_up) { + if (array_key_exists($src_trigger_up['triggerid'], $src_trigger_indexes)) { + // Add dependency on the trigger prototype of the same LLD rule. + $dst_triggers[$dst_triggerid]['dependencies'][] = + ['triggerid' => $result['triggerids'][$src_trigger_indexes[$src_trigger_up['triggerid']]]]; - if ($trigger_prototype['dependencies']) { - foreach ($trigger_prototype['dependencies'] as &$dependency) { - $dep_triggerid = $dependency['triggerid']; + unset($src_triggers[$i]['dependencies'][$i2]); + } + else { + $src_triggerids_up[$src_trigger_up['triggerid']] = true; + } + } - /* - * We have added a dependent trigger prototype and we know corresponding trigger prototype ID - * for newly created trigger prototype. - */ - if (array_key_exists($dependency['triggerid'], $new_trigger_prototypes)) { - /* - * Dependency is within same host according to $srcHostId parameter or dep trigger has - * single host. - */ - if ($new_trigger_prototype['src_hostid'] == - $new_trigger_prototypes[$dep_triggerid]['src_hostid']) { - $dependency['triggerid'] = $new_trigger_prototypes[$dep_triggerid]['new_triggerid']; - } - } - elseif (in_array(['hostid' => $new_trigger_prototype['src_hostid']], - $dep_triggers[$dep_triggerid]['hosts'])) { - // Get all possible $depTrigger matching triggers by description. - $target_triggers = API::Trigger()->get([ - 'output' => ['hosts', 'triggerid', 'expression'], - 'hostids' => $new_trigger_prototype['new_hostid'], - 'filter' => ['description' => $dep_triggers[$dep_triggerid]['description']], - 'preservekeys' => true - ]); - $target_triggers = CMacrosResolverHelper::resolveTriggerExpressions($target_triggers); - - // Compare exploded expressions for exact match. - $expr1 = $dep_triggers[$dep_triggerid]['expression']; - $dependency['triggerid'] = null; - - foreach ($target_triggers as $target_trigger) { - $expr2 = triggerExpressionReplaceHost($target_trigger['expression'], - $new_trigger_prototype['new_host'], - $new_trigger_prototype['src_host'] - ); + if (!$src_triggers[$i]['dependencies']) { + unset($dst_triggerids[$i]); + } + } - if ($expr2 === $expr1) { - // Matching trigger has been found. - $dependency['triggerid'] = $target_trigger['triggerid']; - break; - } - } + if ($src_triggerids_up) { + $src_host_triggers_up = DBfetchArrayAssoc(DBselect( + 'SELECT DISTINCT t.triggerid,t.description,t.expression,t.recovery_expression'. + ' FROM triggers t,functions f,items i'. + ' WHERE t.triggerid=f.triggerid'. + ' AND f.itemid=i.itemid'. + ' AND '.dbConditionId('t.triggerid', array_keys($src_triggerids_up)). + ' AND '.dbConditionId('i.hostid', [$src_host['hostid']]) + ), 'triggerid'); - // If matching trigger was not found, raise exception. - if ($dependency['triggerid'] === null) { - $expr2 = triggerExpressionReplaceHost($dep_triggers[$dep_triggerid]['expression'], - $new_trigger_prototype['src_host'], - $new_trigger_prototype['new_host'] - ); - self::exception(ZBX_API_ERROR_PARAMETERS, _s( - 'Cannot add dependency from trigger "%1$s:%2$s" to non existing trigger "%3$s:%4$s".', - $trigger_prototype['description'], - $trigger_prototype['expression'], - $dep_triggers[$dep_triggerid]['description'], - $expr2 - )); - } + $src_host_triggers_up = CMacrosResolverHelper::resolveTriggerExpressions($src_host_triggers_up, + ['sources' => ['expression', 'recovery_expression']] + ); + + $src_host_dependencies = []; + $other_host_dependencies = []; + + foreach ($dst_triggerids as $i => $dst_triggerid) { + $src_trigger = $src_triggers[$i]; + + foreach ($src_trigger['dependencies'] as $src_trigger_up) { + if (array_key_exists($src_trigger_up['triggerid'], $src_host_triggers_up)) { + $src_host_dependencies[$src_trigger_up['triggerid']][$src_trigger['triggerid']] = true; + } + else { + // Add dependency on the trigger of the other templates or hosts. + $dst_triggers[$dst_triggerid]['dependencies'][] = ['triggerid' => $src_trigger_up['triggerid']]; + $other_host_dependencies[$src_trigger_up['triggerid']][$dst_triggerid] = true; + } + } + } + + if ($src_host_dependencies) { + $dst_host_triggers = DBfetchArrayAssoc(DBselect( + 'SELECT DISTINCT t.triggerid,t.description,t.expression,t.recovery_expression'. + ' FROM items i,functions f,triggers t'. + ' WHERE i.itemid=f.itemid'. + ' AND f.triggerid=t.triggerid'. + ' AND '.dbConditionId('i.hostid', [$dst_host['hostid']]). + ' AND '.dbConditionString('t.description', + array_unique(array_column($src_host_triggers_up, 'description')) + ) + ), 'triggerid'); + + $dst_host_triggers = CMacrosResolverHelper::resolveTriggerExpressions($dst_host_triggers); + + $dst_host_triggerids = []; + + foreach ($dst_host_triggers as $i => $trigger) { + $expression = triggerExpressionReplaceHost($trigger['expression'], $dst_host['host'], + $src_host['host'] + ); + $recovery_expression = $trigger['recovery_expression']; + + if ($recovery_expression !== '') { + $recovery_expression = triggerExpressionReplaceHost($trigger['recovery_expression'], + $dst_host['host'], $src_host['host'] + ); + } + + $dst_host_triggerids[$trigger['description']][$expression][$recovery_expression] = + $trigger['triggerid']; + } + + foreach ($src_host_triggers_up as $src_trigger_up) { + $description = $src_trigger_up['description']; + $expression = $src_trigger_up['expression']; + $recovery_expression = $src_trigger_up['recovery_expression']; + + if (array_key_exists($description, $dst_host_triggerids) + && array_key_exists($expression, $dst_host_triggerids[$description]) + && array_key_exists($recovery_expression, $dst_host_triggerids[$description][$expression])) { + $dst_triggerid_up = $dst_host_triggerids[$description][$expression][$recovery_expression]; + + foreach ($src_host_dependencies[$src_trigger_up['triggerid']] as $src_triggerid => $foo) { + $dst_triggerid = $dst_triggerids[$src_trigger_indexes[$src_triggerid]]; + + $dst_triggers[$dst_triggerid]['dependencies'][] = ['triggerid' => $dst_triggerid_up]; } } - unset($dependency); + else { + $src_triggerid = key($src_host_dependencies[$src_trigger_up['triggerid']]); + $src_trigger = $src_triggers[$src_trigger_indexes[$src_triggerid]]; + + $hosts = DB::select('hosts', [ + 'output' => ['status'], + 'hostids' => $dst_host['hostid'] + ]); - $trigger_prototype['triggerid'] = $new_trigger_prototype['new_triggerid']; + $error = ($hosts[0]['status'] == HOST_STATUS_TEMPLATE) + ? _('Trigger prototype "%1$s" cannot depend on the non-existent trigger "%2$s" on the template "%3$s".') + : _('Trigger prototype "%1$s" cannot depend on the non-existent trigger "%2$s" on the host "%3$s".'); + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $src_trigger['description'], + $src_trigger_up['description'], $dst_host['host'] + )); + } } } - unset($trigger_prototype); - // If adding a dependency fails, the exception will be raised in TriggerPrototype API. - API::TriggerPrototype()->addDependencies($trigger_prototypes); + if ($other_host_dependencies) { + $trigger_hosts = CTriggerGeneral::getTriggerHosts($other_host_dependencies); + + CTriggerGeneral::checkDependenciesOfHostTriggers($other_host_dependencies, $trigger_hosts); + CTriggerGeneral::checkDependenciesOfTemplateTriggers($other_host_dependencies, $trigger_hosts); + } + } + + if ($dst_triggers) { + $dst_triggers = array_values($dst_triggers); + CTriggerGeneral::updateDependencies($dst_triggers); } return $result; diff --git a/ui/include/classes/api/services/CHostBase.php b/ui/include/classes/api/services/CHostBase.php index 8d05cc8225c..14c0f2bf742 100644 --- a/ui/include/classes/api/services/CHostBase.php +++ b/ui/include/classes/api/services/CHostBase.php @@ -128,6 +128,7 @@ abstract class CHostBase extends CApiService { $del_links = []; $check_double_linkage = false; $del_templates = []; + $del_links_clear = []; foreach ($hosts as $host) { if (array_key_exists('templates', $host)) { @@ -187,13 +188,22 @@ abstract class CHostBase extends CApiService { } } } + + if (($this instanceof CHost || $this instanceof CTemplate) && array_key_exists('templates_clear', $host)) { + foreach ($host['templates_clear'] as $template) { + $del_links_clear[$template['templateid']][$host[$id_field_name]] = true; + } + } } if ($del_templates) { - $this->checkTriggerDependenciesOfUpdTemplates($del_templates); $this->checkTriggerExpressionsOfDelTemplates($del_templates); } + if ($del_links_clear) { + $this->checkTriggerDependenciesOfHostTriggers($del_links_clear); + } + if ($ins_templates) { if ($this instanceof CTemplate && $db_hosts !== null) { self::checkCircularLinkageNew($ins_templates, $del_links); @@ -209,32 +219,21 @@ abstract class CHostBase extends CApiService { } /** - * Check whether triggers of existing templates have not dependencies on triggers of unlinked templates on target - * hosts or templates. + * Check whether all templates of triggers of unlinking templates are unlinked from target hosts or templates. * * @param array $del_templates * @param array $del_templates[<templateid>][<hostid>] Array of IDs of existing templates. * - * @throws APIException + * @throws APIException if not linked template is found. */ - protected function checkTriggerDependenciesOfUpdTemplates(array $del_templates): void { - $all_upd_templates = []; - - foreach ($del_templates as $hosts) { - foreach ($hosts as $hostid => $upd_templateids) { - $all_upd_templates += array_flip($upd_templateids); - } - } - + protected function checkTriggerExpressionsOfDelTemplates(array $del_templates): void { $result = DBselect( - 'SELECT DISTINCT i.hostid AS del_templateid,td.triggerid_down,ii.hostid'. - ' FROM items i,functions f,trigger_depends td,functions ff,items ii'. + 'SELECT DISTINCT i.hostid AS del_templateid,f.triggerid,ii.hostid'. + ' FROM items i,functions f,functions ff,items ii'. ' WHERE i.itemid=f.itemid'. - ' AND f.triggerid=td.triggerid_up'. - ' AND td.triggerid_down=ff.triggerid'. + ' AND f.triggerid=ff.triggerid'. ' AND ff.itemid=ii.itemid'. - ' AND '.dbConditionInt('i.hostid', array_keys($del_templates)). - ' AND '.dbConditionInt('ii.hostid', array_keys($all_upd_templates)) + ' AND '.dbConditionInt('i.hostid', array_keys($del_templates)) ); while ($row = DBfetch($result)) { @@ -248,12 +247,12 @@ abstract class CHostBase extends CApiService { $triggers = DB::select('triggers', [ 'output' => ['description'], - 'triggerids' => $row['triggerid_down'] + 'triggerids' => $row['triggerid'] ]); $error = ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) - ? _('Cannot unlink template "%1$s" without template "%2$s" from template "%3$s" due to dependency of trigger "%4$s".') - : _('Cannot unlink template "%1$s" without template "%2$s" from host "%3$s" due to dependency of trigger "%4$s".'); + ? _('Cannot unlink template "%1$s" without template "%2$s" from template "%3$s" due to expression of trigger "%4$s".') + : _('Cannot unlink template "%1$s" without template "%2$s" from host "%3$s" due to expression of trigger "%4$s".'); self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$row['del_templateid']]['host'], $objects[$row['hostid']]['host'], $objects[$hostid]['host'], $triggers[0]['description'] @@ -264,47 +263,165 @@ abstract class CHostBase extends CApiService { } /** - * Check whether all templates of triggers of unlinking templates are unlinked from target hosts or templates. + * Check whether the triggers of the target hosts or templates don't have a dependencies on the triggers of the + * unlinking (with cleaning) templates. * - * @param array $del_templates - * @param array $del_templates[<templateid>][<hostid>] Array of IDs of existing templates. + * @param array $del_links_clear[<templateid>][<hostid>] * - * @throws APIException if not linked template is found. + * @throws APIException */ - protected function checkTriggerExpressionsOfDelTemplates(array $del_templates): void { + protected function checkTriggerDependenciesOfHostTriggers(array $del_links_clear): void { + $del_host_templates = []; + + foreach ($del_links_clear as $templateid => $hosts) { + foreach ($hosts as $hostid => $foo) { + $del_host_templates[$hostid][] = $templateid; + } + } + $result = DBselect( - 'SELECT DISTINCT i.hostid AS del_templateid,f.triggerid,ii.hostid'. - ' FROM items i,functions f,functions ff,items ii'. + 'SELECT DISTINCT i.hostid AS templateid,t.triggerid,ii.hostid'. + ' FROM items i,functions f,triggers t,functions ff,items ii'. ' WHERE i.itemid=f.itemid'. - ' AND f.triggerid=ff.triggerid'. + ' AND f.triggerid=t.templateid'. + ' AND t.triggerid=ff.triggerid'. ' AND ff.itemid=ii.itemid'. - ' AND '.dbConditionInt('i.hostid', array_keys($del_templates)) + ' AND '.dbConditionId('i.hostid', array_keys($del_links_clear)). + ' AND '.dbConditionId('ii.hostid', array_keys($del_host_templates)) ); + $trigger_links = []; + while ($row = DBfetch($result)) { - foreach ($del_templates[$row['del_templateid']] as $hostid => $upd_templateids) { - if (in_array($row['hostid'], $upd_templateids)) { + if (in_array($row['templateid'], $del_host_templates[$row['hostid']])) { + $trigger_links[$row['triggerid']][$row['hostid']] = $row['templateid']; + } + } + + if (!$trigger_links) { + return; + } + + $result = DBselect( + 'SELECT DISTINCT td.triggerid_up,td.triggerid_down,i.hostid'. + ' FROM trigger_depends td,functions f,items i'. + ' WHERE td.triggerid_down=f.triggerid'. + ' AND f.itemid=i.itemid'. + ' AND '.dbConditionId('td.triggerid_up', array_keys($trigger_links)). + ' AND '.dbConditionId('td.triggerid_down', array_keys($trigger_links), true). + ' AND '.dbConditionId('i.hostid', array_keys($del_host_templates)) + ); + + while ($row = DBfetch($result)) { + foreach ($trigger_links[$row['triggerid_up']] as $hostid => $templateid) { + if (bccomp($row['hostid'], $hostid) == 0) { $objects = DB::select('hosts', [ 'output' => ['host', 'status'], - 'hostids' => [$row['del_templateid'], $row['hostid'], $hostid], + 'hostids' => [$templateid, $hostid], 'preservekeys' => true ]); $triggers = DB::select('triggers', [ 'output' => ['description'], - 'triggerids' => $row['triggerid'] + 'triggerids' => $row['triggerid_down'] ]); $error = ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) - ? _('Cannot unlink template "%1$s" without template "%2$s" from template "%3$s" due to expression of trigger "%4$s".') - : _('Cannot unlink template "%1$s" without template "%2$s" from host "%3$s" due to expression of trigger "%4$s".'); + ? _('Cannot unlink template "%1$s" from template "%2$s" due to dependency of trigger "%3$s".') + : _('Cannot unlink template "%1$s" from host "%2$s" due to dependency of trigger "%3$s".'); - self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$row['del_templateid']]['host'], - $objects[$row['hostid']]['host'], $objects[$hostid]['host'], $triggers[0]['description'] + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$templateid]['host'], + $objects[$hostid]['host'], $triggers[0]['description'] )); } } } + + if ($this instanceof CTemplate) { + $trigger_hosts = []; + + foreach ($trigger_links as $triggerid => $hostids) { + $trigger_hosts[$triggerid] = array_keys($hostids); + } + + $trigger_map = []; + + while (true) { + $result = DBselect( + 'SELECT DISTINCT t.templateid,t.triggerid,i.hostid'. + ' FROM triggers t,functions f,items i'. + ' WHERE t.triggerid=f.triggerid'. + ' AND f.itemid=i.itemid'. + ' AND '.dbConditionInt('t.templateid', array_keys($trigger_hosts)) + ); + + $_trigger_hosts = []; + $hostids = []; + + while ($row = DBfetch($result)) { + foreach ($trigger_hosts[$row['templateid']] as $hostid) { + if (array_key_exists($row['hostid'], $del_host_templates) + && in_array($hostid, $del_host_templates[$row['hostid']])) { + continue; + } + + $trigger_map[$row['triggerid']] = $row['templateid']; + $_trigger_hosts[$row['triggerid']][] = $row['hostid']; + $hostids[$row['hostid']] = true; + } + } + + if (!$_trigger_hosts) { + break; + } + + $trigger_hosts = $_trigger_hosts; + + $result = DBselect( + 'SELECT DISTINCT td.triggerid_up,td.triggerid_down,i.hostid'. + ' FROM trigger_depends td,functions f,items i'. + ' WHERE td.triggerid_down=f.triggerid'. + ' AND f.itemid=i.itemid'. + ' AND '.dbConditionId('td.triggerid_up', array_keys($trigger_hosts)). + ' AND '.dbConditionId('td.triggerid_down', array_keys($trigger_hosts), true). + ' AND '.dbConditionId('i.hostid', array_keys($hostids)) + ); + + while ($row = DBfetch($result)) { + foreach ($trigger_hosts[$row['triggerid_up']] as $hostid) { + if (bccomp($row['hostid'], $hostid) == 0) { + $triggerid = $row['triggerid_up']; + + do { + $triggerid = $trigger_map[$triggerid]; + } while (array_key_exists($triggerid, $trigger_map)); + + $from_hostid = key($trigger_links[$triggerid]); + $templateid = $trigger_links[$triggerid][$from_hostid]; + + $objects = DB::select('hosts', [ + 'output' => ['host', 'status'], + 'hostids' => [$templateid, $from_hostid, $hostid], + 'preservekeys' => true + ]); + + $triggers = DB::select('triggers', [ + 'output' => ['description'], + 'triggerids' => $row['triggerid_down'] + ]); + + $error = ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) + ? _('Cannot unlink template "%1$s" from template "%2$s" due to dependency of trigger "%3$s" on template "%4$s".') + : _('Cannot unlink template "%1$s" from template "%2$s" due to dependency of trigger "%3$s" on host "%4$s".'); + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$templateid]['host'], + $objects[$from_hostid]['host'], $triggers[0]['description'], $objects[$hostid]['host'] + )); + } + } + } + } + } } /** @@ -367,18 +484,22 @@ abstract class CHostBase extends CApiService { ]); foreach ($templates as $template) { + $description = '"'.$template['host'].'"'; + if (bccomp($template['hostid'], $templateid) == 0) { - $template_name = '"'.$template['host'].'"'; + $template_name = $description; } else { - $links_path[$template['hostid']] = '"'.$template['host'].'"'; + $links_path[$template['hostid']] = $description; } } - $circular_linkage = $template_name.' -> '.implode(' -> ', $links_path).' -> '.$template_name; + $circular_linkage = (bccomp($templateid, $hostid) == 0) + ? $template_name.' -> '.$template_name + : $template_name.' -> '.implode(' -> ', $links_path).' -> '.$template_name; self::exception(ZBX_API_ERROR_PARAMETERS, _s( - 'Cannot link template "%1$s" to template "%2$s" because circular linkage (%3$s) will occurs.', + 'Cannot link template "%1$s" to template "%2$s", because a circular linkage (%3$s) would occur.', $templates[$templateid]['host'], $templates[$hostid]['host'], $circular_linkage )); } @@ -412,7 +533,10 @@ abstract class CHostBase extends CApiService { if ($hostid_links) { $links_path[$hostid] = true; - return self::circularLinkageExists($links, $templateid, $hostid_links, $links_path); + + if (self::circularLinkageExists($links, $templateid, $hostid_links, $links_path)) { + return true; + } } } } @@ -511,13 +635,13 @@ abstract class CHostBase extends CApiService { ]); if ($objects[$hostid]['status'] == HOST_STATUS_TEMPLATE) { - $error = _('Cannot link template "%1$s" to template "%2$s" because its parent template "%3$s" will be linked twice.'); + $error = _('Cannot link template "%1$s" to template "%2$s", because its parent template "%3$s" would be linked twice.'); } elseif ($objects[$hostid]['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) { - $error = _('Cannot link template "%1$s" to host prototype "%2$s" because its parent template "%3$s" will be linked twice.'); + $error = _('Cannot link template "%1$s" to host prototype "%2$s", because its parent template "%3$s" would be linked twice.'); } else { - $error = _('Cannot link template "%1$s" to host "%2$s" because its parent template "%3$s" will be linked twice.'); + $error = _('Cannot link template "%1$s" to host "%2$s", because its parent template "%3$s" would be linked twice.'); } self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $objects[$ins_templateid]['host'], diff --git a/ui/include/classes/api/services/CHostGeneral.php b/ui/include/classes/api/services/CHostGeneral.php index 8961d3b508a..1f48b1e1952 100644 --- a/ui/include/classes/api/services/CHostGeneral.php +++ b/ui/include/classes/api/services/CHostGeneral.php @@ -158,11 +158,13 @@ abstract class CHostGeneral extends CHostBase { * @param array $templateids * @param array $db_hosts */ - protected function massCheckTemplatesLinks(string $method, array $templateids, array $db_hosts): void { + protected function massCheckTemplatesLinks(string $method, array $templateids, array $db_hosts, + array $templateids_clear = []): void { $ins_templates = []; $del_links = []; $check_double_linkage = false; $del_templates = []; + $del_links_clear = []; foreach ($db_hosts as $hostid => $db_host) { $db_templateids = array_column($db_host['templates'], 'templateid'); @@ -211,14 +213,21 @@ abstract class CHostGeneral extends CHostBase { if ($upd_templateids) { $del_templates[$db_templateid][$hostid] = $upd_templateids; } + + if (array_key_exists($db_templateid, $templateids_clear)) { + $del_links_clear[$db_templateid][$hostid] = true; + } } } if ($del_templates) { - $this->checkTriggerDependenciesOfUpdTemplates($del_templates); $this->checkTriggerExpressionsOfDelTemplates($del_templates); } + if ($del_links_clear) { + $this->checkTriggerDependenciesOfHostTriggers($del_links_clear); + } + if ($ins_templates) { if ($this instanceof CTemplate) { self::checkCircularLinkageNew($ins_templates, $del_links); @@ -760,11 +769,7 @@ abstract class CHostGeneral extends CHostBase { API::Graph()->syncTemplates($link_request); - API::Trigger()->syncTemplateDependencies($link_request); - - if ($ruleids) { - API::TriggerPrototype()->syncTemplateDependencies($link_request); - } + CTriggerGeneral::syncTemplateDependencies($link_request['templateids'], $link_request['hostids']); } /** @@ -998,8 +1003,7 @@ abstract class CHostGeneral extends CHostBase { } foreach ($link_requests as $link_request){ - API::Trigger()->syncTemplateDependencies($link_request); - API::TriggerPrototype()->syncTemplateDependencies($link_request); + CTriggerGeneral::syncTemplateDependencies($link_request['templateids'], $link_request['hostids']); } return $hosts_linkage_inserts; diff --git a/ui/include/classes/api/services/CTemplate.php b/ui/include/classes/api/services/CTemplate.php index 2a5117aad1a..e08aeac8c7c 100644 --- a/ui/include/classes/api/services/CTemplate.php +++ b/ui/include/classes/api/services/CTemplate.php @@ -718,18 +718,36 @@ class CTemplate extends CHostGeneral { ' FROM hosts_templates ht,hosts_templates htt'. ' WHERE ht.hostid=htt.hostid'. ' AND ht.templateid!=htt.templateid'. - ' AND '.dbConditionInt('ht.templateid', $templateids). - ' AND '.dbConditionInt('htt.templateid', $templateids, true) + ' AND '.dbConditionId('ht.templateid', $templateids). + ' AND '.dbConditionId('htt.templateid', $templateids, true) ); while ($row = DBfetch($result)) { $del_templates[$row['del_templateid']][$row['hostid']][] = $row['templateid']; } + $del_links_clear = []; + $options = [ + 'output' => ['templateid', 'hostid'], + 'filter' => [ + 'templateid' => $templateids + ] + ]; + $result = DBselect(DB::makeSql('hosts_templates', $options)); + + while ($row = DBfetch($result)) { + if (!in_array($row['hostid'], $templateids)) { + $del_links_clear[$row['templateid']][$row['hostid']] = true; + } + } + if ($del_templates) { - $this->checkTriggerDependenciesOfUpdTemplates($del_templates); $this->checkTriggerExpressionsOfDelTemplates($del_templates); } + + if ($del_links_clear) { + $this->checkTriggerDependenciesOfHostTriggers($del_links_clear); + } } /** @@ -1057,10 +1075,10 @@ class CTemplate extends CHostGeneral { } if (array_key_exists('templates_link', $data)) { - $this->massCheckTemplatesLinks('massupdate', $templateids_link, $db_templates); + $this->massCheckTemplatesLinks('massupdate', $templateids_link, $db_templates, $templateids_clear); } else { - $this->massCheckTemplatesLinks('massremove', $templateids_clear, $db_templates); + $this->massCheckTemplatesLinks('massremove', $templateids_clear, $db_templates, $templateids_clear); } } } @@ -1144,7 +1162,9 @@ class CTemplate extends CHostGeneral { $this->massAddAffectedObjects('templates', $templateids, $db_templates); - $this->massCheckTemplatesLinks('massremove', $templateids, $db_templates); + $this->massCheckTemplatesLinks('massremove', $templateids, $db_templates, + array_key_exists('templateids_clear', $data) ? $data['templateids_clear'] : [] + ); } } diff --git a/ui/include/classes/api/services/CTrigger.php b/ui/include/classes/api/services/CTrigger.php index 50c5b972f1e..db00f0c359a 100644 --- a/ui/include/classes/api/services/CTrigger.php +++ b/ui/include/classes/api/services/CTrigger.php @@ -524,29 +524,15 @@ class CTrigger extends CTriggerGeneral { */ public function create(array $triggers) { $this->validateCreate($triggers); + $this->createReal($triggers); + $this->checkDependenciesLinks($triggers); + $this->inherit($triggers); - // Clear all dependencies on inherited triggers. - $this->deleteDependencies($triggers); + $this->updateDependencies($triggers); - // Add new dependencies. - foreach ($triggers as $trigger) { - if (!array_key_exists('dependencies', $trigger) || !$trigger['dependencies']) { - continue; - } - - $new_dependencies = []; - foreach ($trigger['dependencies'] as $dependency) { - $new_dependencies[] = [ - 'triggerid' => $trigger['triggerid'], - 'dependsOnTriggerid' => $dependency['triggerid'] - ]; - } - $this->addDependencies($new_dependencies); - } - - return ['triggerids' => zbx_objectValues($triggers, 'triggerid')]; + return ['triggerids' => array_column($triggers, 'triggerid')]; } /** @@ -558,51 +544,17 @@ class CTrigger extends CTriggerGeneral { * * @return array */ - public function update(array $triggers) { + public function update(array $triggers): array { $this->validateUpdate($triggers, $db_triggers); - $validate_dependencies = []; - foreach ($triggers as $tnum => $trigger) { - $db_trigger = $db_triggers[$tnum]; - - $expressions_changed = ($trigger['expression'] !== $db_trigger['expression'] - || $trigger['recovery_expression'] !== $db_trigger['recovery_expression']); - - if ($expressions_changed && $db_trigger['dependencies'] && !array_key_exists('dependencies', $trigger)) { - $validate_dependencies[] = [ - 'triggerid' => $trigger['triggerid'], - 'dependencies' => zbx_objectValues($db_trigger['dependencies'], 'triggerid') - ]; - } - } - - if ($validate_dependencies) { - $this->checkDependencies($validate_dependencies); - $this->checkDependencyParents($validate_dependencies); - } - $this->updateReal($triggers, $db_triggers); + self::checkExistingDependencies($triggers, $db_triggers); + $this->inherit($triggers); - foreach ($triggers as $trigger) { - // Replace dependencies. - if (array_key_exists('dependencies', $trigger)) { - $this->deleteDependencies($trigger); - - if ($trigger['dependencies']) { - $new_dependencies = []; - foreach ($trigger['dependencies'] as $dependency) { - $new_dependencies[] = [ - 'triggerid' => $trigger['triggerid'], - 'dependsOnTriggerid' => $dependency['triggerid'] - ]; - } - $this->addDependencies($new_dependencies); - } - } - } + $this->updateDependencies($triggers, $db_triggers); - return ['triggerids' => zbx_objectValues($triggers, 'triggerid')]; + return ['triggerids' => array_column($triggers, 'triggerid')]; } /** @@ -663,498 +615,235 @@ class CTrigger extends CTriggerGeneral { } /** - * Validates the input for the addDependencies() method. - * - * @param array $triggers_data - * @param bool $inherited + * Check the existing dependencies if the trigger expressions were changed. * - * @throws APIException if the given dependencies are invalid. + * @param array $triggers + * @param array $db_triggers */ - protected function validateAddDependencies(array &$triggers_data, $inherited = false) { - $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['triggerid', 'dependsOnTriggerid']], 'fields' => [ - 'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED], - 'dependsOnTriggerid' => ['type' => API_ID, 'flags' => API_REQUIRED] - ]]; - if (!CApiInputValidator::validate($api_input_rules, $triggers_data, '/', $error)) { - self::exception(ZBX_API_ERROR_PARAMETERS, $error); - } + private static function checkExistingDependencies(array $triggers, array $db_triggers): void { + $triggerids = []; + $hostids = []; - $triggerids = zbx_objectValues($triggers_data, 'triggerid'); - $triggerids = array_keys(array_flip($triggerids)); + foreach ($triggers as $trigger) { + if (array_key_exists('hosts', $db_triggers[$trigger['triggerid']])) { + $triggerids[$trigger['triggerid']] = true; + $hostids += $db_triggers[$trigger['triggerid']]['hosts']; + } + } - $permission_check = $inherited - ? ['nopermissions' => true] - : ['editable' => true]; + if (!$triggerids) { + return; + } - $triggers = $this->get([ - 'output' => ['triggerid', 'description', 'templateid', 'flags'], - 'triggerids' => $triggerids, - 'preservekeys' => true - ] + $permission_check); + /* + * It's necessary to perform the check of existing dependencies only if, as the result of the expression change, + * the trigger no longer belongs to any of the previous hosts. + */ + $result = DBselect( + 'SELECT DISTINCT f.triggerid,i.hostid'. + ' FROM functions f,items i'. + ' WHERE f.itemid=i.itemid'. + ' AND '.dbConditionId('f.triggerid', array_keys($triggerids)). + ' AND '.dbConditionId('i.hostid', array_keys($hostids)) + ); - if (count($triggerids) != count($triggers)) { - self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); + while ($row = DBfetch($result)) { + if (array_key_exists($row['hostid'], $db_triggers[$row['triggerid']]['hosts'])) { + unset($db_triggers[$row['triggerid']]['hosts'][$row['hostid']]); + } } + $trigger_dependencies = []; + foreach ($triggers as $trigger) { - if ($trigger['templateid'] && !$inherited) { - self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Cannot update dependencies of inherited trigger "%1$s".', - $trigger['description'] - )); + if (!array_key_exists($trigger['triggerid'], $triggerids) + || !$db_triggers[$trigger['triggerid']]['hosts']) { + continue; } - if ($trigger['flags'] == ZBX_FLAG_DISCOVERY_CREATED) { - self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Cannot update "%2$s" for a discovered trigger "%1$s".', - $trigger['description'], 'dependencies' - )); + $dependencies = array_key_exists('dependencies', $trigger) + ? $trigger['dependencies'] + : $db_triggers[$trigger['triggerid']]['dependencies']; + + foreach ($dependencies as $trigger_up) { + $trigger_dependencies[$trigger_up['triggerid']][$trigger['triggerid']] = true; } } - $dep_triggerids = []; - $triggers = []; - foreach ($triggers_data as $dep) { - $triggerid = $dep['triggerid']; - - if (!array_key_exists($dep['triggerid'], $triggers)) { - $triggers[$triggerid] = [ - 'triggerid' => $triggerid, - 'dependencies' => [] - ]; - } - $triggers[$triggerid]['dependencies'][] = $dep['dependsOnTriggerid']; - $dep_triggerids[$dep['dependsOnTriggerid']] = $dep['dependsOnTriggerid']; + if (!$trigger_dependencies) { + return; } - if (!$inherited) { - $count = $this->get([ - 'countOutput' => true, - 'triggerids' => $dep_triggerids - ]); + /* + * There is no need to perform a check for dependency duplicates, because we are checking for existing + * dependencies that cannot have them. Also there is no need to perform the check on circular + * dependencies, because the dependencies are based on trigger IDs. Even if the expressions have changed, the + * trigger IDs remain the same. + */ - if ($count != count($dep_triggerids)) { - self::exception(ZBX_API_ERROR_PERMISSIONS, - _('No permissions to referred object or it does not exist!') - ); - } - } + $trigger_hosts = self::getTriggerHosts($trigger_dependencies); - $this->checkDependencies($triggers); - $this->checkDependencyParents($triggers); - $this->checkDependencyDuplicates($triggers); + /* + * The template trigger can become a host trigger. Therefore the template trigger dependencies may remain + * after the update. + */ + self::checkDependenciesOfHostTriggers($trigger_dependencies, $trigger_hosts); + + /* + * The template (or host) trigger can become a trigger of another template. If after the update + * the trigger became owned by another template and it also has a dependency on a trigger from another + * template remaining, then we should check that: + * - Such dependency did not become a dependency on a trigger from the parent template. + * - Such dependency did not become a dependency on a trigger from the child template or host. + * - The template of the trigger-up is linked to all child templates of the new trigger's template. + */ + self::checkDependenciesOfTemplateTriggers($trigger_dependencies, $trigger_hosts); } /** - * Add the given dependencies and inherit them on all child triggers. + * Validates the input for the addDependencies() method. * - * @param array $triggers_data An array of trigger dependency pairs, each pair in the form of - * ['triggerid' => 1, 'dependsOnTriggerid' => 2]. - * @param bool $inherited Determines either to check permissions for added dependencies. Permissions are not - * validated for inherited triggers. + * @param array $triggers_data + * @param array|null $triggers + * @param array|null $db_triggers * - * @return array + * @throws APIException if the given dependencies are invalid. */ - public function addDependencies(array $triggers_data, $inherited = false) { - $this->validateAddDependencies($triggers_data, $inherited); - - foreach ($triggers_data as $dep) { - $triggerId = $dep['triggerid']; - $depTriggerId = $dep['dependsOnTriggerid']; - - DB::insert('trigger_depends', [[ - 'triggerid_down' => $triggerId, - 'triggerid_up' => $depTriggerId - ]]); - - // propagate the dependencies to the child triggers - $childTriggers = API::getApiService()->select($this->tableName(), [ - 'output' => ['triggerid'], - 'filter' => [ - 'templateid' => $triggerId - ] - ]); - if ($childTriggers) { - foreach ($childTriggers as $childTrigger) { - $childHostsQuery = get_hosts_by_triggerid($childTrigger['triggerid']); - while ($childHost = DBfetch($childHostsQuery)) { - $newDep = [$childTrigger['triggerid'] => $depTriggerId]; - $newDep = replace_template_dependencies($newDep, $childHost['hostid']); - - $this->addDependencies([[ - 'triggerid' => $childTrigger['triggerid'], - 'dependsOnTriggerid' => $newDep[$childTrigger['triggerid']] - ]], true); - } - } - } + protected function validateAddDependencies(array $triggers_data, ?array &$triggers, ?array &$db_triggers): void { + $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['triggerid', 'dependsOnTriggerid']], 'fields' => [ + 'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED], + 'dependsOnTriggerid' => ['type' => API_ID, 'flags' => API_REQUIRED] + ]]; + + if (!CApiInputValidator::validate($api_input_rules, $triggers_data, '/', $error)) { + self::exception(ZBX_API_ERROR_PARAMETERS, $error); } - return ['triggerids' => array_unique(zbx_objectValues($triggers_data, 'triggerid'))]; - } + $triggers = []; + $trigger_dependencies = []; - /** - * Validates the input for the deleteDependencies() method. - * - * @param array $triggers - * @param bool $inherited - * - * @throws APIException if the given input is invalid - */ - protected function validateDeleteDependencies(array $triggers, $inherited) { - if (!$triggers) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.')); - } + foreach ($triggers_data as $dependency) { + $triggers[$dependency['triggerid']]['triggerid'] = $dependency['triggerid']; + $triggers[$dependency['triggerid']]['dependencies'][] = ['triggerid' => $dependency['dependsOnTriggerid']]; - foreach ($triggers as $trigger) { - if (!check_db_fields(['triggerid' => null], $trigger)) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect input parameters.')); - } + $trigger_dependencies[$dependency['dependsOnTriggerid']][$dependency['triggerid']] = true; } - $triggerids = zbx_objectValues($triggers, 'triggerid'); - $triggerids = array_keys(array_flip($triggerids)); + $triggers = array_values($triggers); - $permission_check = $inherited - ? ['nopermissions' => true] - : ['editable' => true]; - - $triggers = $this->get([ - 'output' => ['triggerid', 'description', 'templateid', 'flags'], - 'triggerids' => $triggerids, + $db_triggers = $this->get([ + 'output' => ['triggerid', 'description', 'flags'], + 'triggerids' => array_column($triggers, 'triggerid'), + 'editable' => true, 'preservekeys' => true - ] + $permission_check); + ]); - foreach ($triggerids as $triggerid) { - if (!array_key_exists($triggerid, $triggers)) { - self::exception(ZBX_API_ERROR_PERMISSIONS, - _('No permissions to referred object or it does not exist!') - ); - } + if (count($db_triggers) != count($triggers)) { + self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } - foreach ($triggers as $trigger) { - if ($trigger['templateid'] && !$inherited) { - self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Cannot update dependencies of inherited trigger "%1$s".', - $trigger['description'] - )); - } - - if ($trigger['flags'] == ZBX_FLAG_DISCOVERY_CREATED) { + foreach ($db_triggers as $db_trigger) { + if ($db_trigger['flags'] == ZBX_FLAG_DISCOVERY_CREATED) { self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Cannot update "%2$s" for a discovered trigger "%1$s".', - $trigger['description'], 'dependencies' + $db_trigger['description'], 'dependencies' )); } } - } - /** - * Deletes all trigger dependencies from the given triggers and their children. - * - * @param array $triggers an array of triggers with the 'triggerid' field defined - * @param bool $inherited Determines either to check permissions for deleted dependencies. Permissions are not - * validated for inherited triggers. - * - * @return array - */ - public function deleteDependencies(array $triggers, $inherited = false) { - $triggers = zbx_toArray($triggers); + $count = $this->get([ + 'countOutput' => true, + 'triggerids' => array_keys($trigger_dependencies) + ]); - $this->validateDeleteDependencies($triggers, $inherited); + if ($count != count($trigger_dependencies)) { + self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); + } - $triggerids = zbx_objectValues($triggers, 'triggerid'); + self::checkDependencyDuplicates($trigger_dependencies); + self::checkCircularDependencies($trigger_dependencies); - try { - // delete the dependencies from the child triggers - $childTriggers = DB::select($this->tableName(), [ - 'output' => ['triggerid'], - 'filter' => [ - 'templateid' => $triggerids - ] - ]); - if ($childTriggers) { - $this->deleteDependencies($childTriggers, true); - } + $trigger_hosts = self::getTriggerHosts($trigger_dependencies); - DB::delete('trigger_depends', [ - 'triggerid_down' => $triggerids - ]); - } - catch (APIException $e) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete dependency')); - } + self::checkDependenciesOfHostTriggers($trigger_dependencies, $trigger_hosts); + self::checkDependenciesOfTemplateTriggers($trigger_dependencies, $trigger_hosts); - return ['triggerids' => $triggerids]; + foreach ($db_triggers as &$db_trigger) { + $db_trigger['dependencies'] = []; + } + unset($db_trigger); } /** - * Synchronizes the templated trigger dependencies on the given hosts inherited from the given - * templates. - * Update dependencies, do it after all triggers that can be dependent were created/updated on - * all child hosts/templates. Starting from highest level template triggers select triggers from - * one level lower, then for each lower trigger look if it's parent has dependencies, if so - * find this dependency trigger child on dependent trigger host and add new dependency. + * Add the given dependencies and inherit them on all child triggers. * - * @param array $data + * @param array $triggers_data An array of trigger dependency pairs, each pair in the form of + * ['triggerid' => 1, 'dependsOnTriggerid' => 2]. + * + * @return array */ - public function syncTemplateDependencies(array $data) { - $templateIds = zbx_toArray($data['templateids']); - $hostIds = zbx_toArray($data['hostids']); - - $parentTriggers = $this->get([ - 'output' => ['triggerid'], - 'selectDependencies' => ['triggerid'], - 'hostids' => $templateIds, - 'preservekeys' => true, - 'nopermissions' => true - ]); - - if ($parentTriggers) { - $childTriggers = $this->get([ - 'output' => ['triggerid', 'templateid'], - 'hostids' => ($hostIds) ? $hostIds : null, - 'filter' => ['templateid' => array_keys($parentTriggers)], - 'nopermissions' => true, - 'preservekeys' => true, - 'selectHosts' => ['hostid'] - ]); + public function addDependencies(array $triggers_data) { + $this->validateAddDependencies($triggers_data, $triggers, $db_triggers); - if ($childTriggers) { - $newDependencies = []; - foreach ($childTriggers as $childTrigger) { - $parentDependencies = $parentTriggers[$childTrigger['templateid']]['dependencies']; - if ($parentDependencies) { - $dependencies = []; - foreach ($parentDependencies as $depTrigger) { - $dependencies[] = $depTrigger['triggerid']; - } - $host = reset($childTrigger['hosts']); - $dependencies = replace_template_dependencies($dependencies, $host['hostid']); - foreach ($dependencies as $depTriggerId) { - $newDependencies[] = [ - 'triggerid' => $childTrigger['triggerid'], - 'dependsOnTriggerid' => $depTriggerId - ]; - } - } - } - $this->deleteDependencies($childTriggers, true); + self::updateDependencies($triggers, $db_triggers); - if ($newDependencies) { - $this->addDependencies($newDependencies, true); - } - } - } + return ['triggerids' => array_column($triggers, 'triggerid')]; } /** - * Validates the dependencies of the given triggers. + * @param array $triggers + * @param array|null $db_triggers * - * @param array $triggers list of triggers and corresponding dependencies - * @param int $triggers[]['triggerid'] trigger id - * @param array $triggers[]['dependencies'] list of trigger ids on which depends given trigger - * - * @trows APIException if any of the dependencies is invalid + * @throws APIException if the given input is invalid */ - protected function checkDependencies(array $triggers) { - foreach ($triggers as $trigger) { - if (empty($trigger['dependencies'])) { - continue; - } - - // trigger templates - $triggerTemplates = API::Template()->get([ - 'output' => ['status', 'hostid'], - 'triggerids' => $trigger['triggerid'], - 'nopermissions' => true - ]); - - // forbid dependencies from hosts to templates - if (!$triggerTemplates) { - $triggerDependencyTemplates = API::Template()->get([ - 'output' => ['templateid'], - 'triggerids' => $trigger['dependencies'], - 'nopermissions' => true, - 'limit' => 1 - ]); - if ($triggerDependencyTemplates) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot add dependency from a host to a template.')); - } - } - - // the trigger can't depend on itself - if (in_array($trigger['triggerid'], $trigger['dependencies'])) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create dependency on trigger itself.')); - } - - // check circular dependency - $downTriggerIds = [$trigger['triggerid']]; - do { - // triggerid_down depends on triggerid_up - $res = DBselect( - 'SELECT td.triggerid_up'. - ' FROM trigger_depends td'. - ' WHERE '.dbConditionInt('td.triggerid_down', $downTriggerIds) - ); - - // combine db dependencies with those to be added - $upTriggersIds = []; - while ($row = DBfetch($res)) { - $upTriggersIds[] = $row['triggerid_up']; - } - foreach ($downTriggerIds as $id) { - if (isset($triggers[$id]) && isset($triggers[$id]['dependencies'])) { - $upTriggersIds = array_merge($upTriggersIds, $triggers[$id]['dependencies']); - } - } + private function validateDeleteDependencies(array &$triggers, ?array &$db_triggers): void { + $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['triggerid']], 'fields' => [ + 'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED] + ]]; - // if found trigger id is in dependent triggerids, there is a dependency loop - $downTriggerIds = []; - foreach ($upTriggersIds as $id) { - if (bccomp($id, $trigger['triggerid']) == 0) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create circular dependencies.')); - } - $downTriggerIds[] = $id; - } - } while (!empty($downTriggerIds)); + if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) { + self::exception(ZBX_API_ERROR_PARAMETERS, $error); + } - // fetch all templates that are used in dependencies - $triggerDependencyTemplates = API::Template()->get([ - 'output' => ['templateid'], - 'triggerids' => $trigger['dependencies'], - 'nopermissions' => true - ]); - $depTemplateIds = zbx_toHash(zbx_objectValues($triggerDependencyTemplates, 'templateid')); - - // run the check only if a templated trigger has dependencies on other templates - $triggerTemplateIds = zbx_toHash(zbx_objectValues($triggerTemplates, 'templateid')); - $tdiff = array_diff($depTemplateIds, $triggerTemplateIds); - if (!empty($triggerTemplateIds) && !empty($depTemplateIds) && !empty($tdiff)) { - $affectedTemplateIds = zbx_array_merge($triggerTemplateIds, $depTemplateIds); - - // create a list of all hosts, that are children of the affected templates - $dbLowlvltpl = DBselect( - 'SELECT DISTINCT ht.templateid,ht.hostid,h.host'. - ' FROM hosts_templates ht,hosts h'. - ' WHERE h.hostid=ht.hostid'. - ' AND '.dbConditionInt('ht.templateid', $affectedTemplateIds) - ); - $map = []; - while ($lowlvltpl = DBfetch($dbLowlvltpl)) { - if (!isset($map[$lowlvltpl['hostid']])) { - $map[$lowlvltpl['hostid']] = []; - } - $map[$lowlvltpl['hostid']][$lowlvltpl['templateid']] = $lowlvltpl['host']; - } + $db_triggers = $this->get([ + 'output' => ['description', 'flags'], + 'triggerids' => array_column($triggers, 'triggerid'), + 'editable' => true, + 'preservekeys' => true + ]); - // check that if some host is linked to the template, that the trigger belongs to, - // the host must also be linked to all of the templates, that trigger dependencies point to - foreach ($map as $templates) { - foreach ($triggerTemplateIds as $triggerTemplateId) { - // is the host linked to one of the trigger templates? - if (isset($templates[$triggerTemplateId])) { - // then make sure all of the dependency templates are also linked - foreach ($depTemplateIds as $depTemplateId) { - if (!isset($templates[$depTemplateId])) { - self::exception(ZBX_API_ERROR_PARAMETERS, - _s('Not all templates are linked to "%1$s".', reset($templates)) - ); - } - } - break; - } - } - } - } + if (count($db_triggers) != count($triggers)) { + self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } - } - /** - * Check that none of the triggers have dependencies on their children. Checks only one level of inheritance, but - * since it is called on each inheritance step, also works for multiple inheritance levels. - * - * @throws APIException if at least one trigger is dependent on its child - * - * @param array $triggers - */ - protected function checkDependencyParents(array $triggers) { - // fetch all templated dependency trigger parents - $depTriggerIds = []; - foreach ($triggers as $trigger) { - foreach ($trigger['dependencies'] as $depTriggerId) { - $depTriggerIds[$depTriggerId] = $depTriggerId; + foreach ($db_triggers as $db_trigger) { + if ($db_trigger['flags'] == ZBX_FLAG_DISCOVERY_CREATED) { + self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Cannot update "%2$s" for a discovered trigger "%1$s".', + $db_trigger['description'], 'dependencies' + )); } } - $parentDepTriggers = DBfetchArray(DBSelect( - 'SELECT templateid,triggerid'. - ' FROM triggers'. - ' WHERE templateid>0'. - ' AND '.dbConditionInt('triggerid', $depTriggerIds) - )); - if ($parentDepTriggers) { - $parentDepTriggers = zbx_toHash($parentDepTriggers, 'triggerid'); - foreach ($triggers as $trigger) { - foreach ($trigger['dependencies'] as $depTriggerId) { - // check if the current trigger is the parent of the dependency trigger - if (isset($parentDepTriggers[$depTriggerId]) - && $parentDepTriggers[$depTriggerId]['templateid'] == $trigger['triggerid']) { - - self::exception(ZBX_API_ERROR_PARAMETERS, - _s('Trigger cannot be dependent on a trigger that is inherited from it.') - ); - } - } - } + + foreach ($triggers as &$trigger) { + $trigger['dependencies'] = []; } + unset($trigger); + + self::addAffectedDependencies($triggers, $db_triggers); } /** - * Checks if the given dependencies contain duplicates. + * Deletes all trigger dependencies from the given triggers and their children. * - * @throws APIException if the given dependencies contain duplicates + * @param array $triggers An array of triggers with the 'triggerid' field defined. * - * @param array $triggers + * @return array */ - protected function checkDependencyDuplicates(array $triggers) { - // check duplicates in array - $uniqueTriggers = []; - $duplicateTriggerId = null; - foreach ($triggers as $trigger) { - foreach ($trigger['dependencies'] as $dep) { - if (isset($uniqueTriggers[$trigger['triggerid']][$dep])) { - $duplicateTriggerId = $trigger['triggerid']; - break 2; - } - else { - $uniqueTriggers[$trigger['triggerid']][$dep] = 1; - } - } - } + public function deleteDependencies(array $triggers) { + $this->validateDeleteDependencies($triggers, $db_triggers); - if ($duplicateTriggerId === null) { - // check if dependency already exists in DB - foreach ($triggers as $trigger) { - $dbUpTriggers = DBselect( - 'SELECT td.triggerid_up'. - ' FROM trigger_depends td'. - ' WHERE '.dbConditionInt('td.triggerid_up', $trigger['dependencies']). - ' AND td.triggerid_down='.zbx_dbstr($trigger['triggerid']) - , 1); - if (DBfetch($dbUpTriggers)) { - $duplicateTriggerId = $trigger['triggerid']; - break; - } - } - } + self::updateDependencies($triggers, $db_triggers); - if ($duplicateTriggerId) { - $dplTrigger = DBfetch(DBselect( - 'SELECT t.description'. - ' FROM triggers t'. - ' WHERE t.triggerid='.zbx_dbstr($duplicateTriggerId) - )); - self::exception(ZBX_API_ERROR_PARAMETERS, - _s('Duplicate dependencies in trigger "%1$s".', $dplTrigger['description']) - ); - } + return ['triggerids' => array_column($triggers, 'triggerid')]; } protected function applyQueryOutputOptions($tableName, $tableAlias, array $options, array $sqlParts) { diff --git a/ui/include/classes/api/services/CTriggerGeneral.php b/ui/include/classes/api/services/CTriggerGeneral.php index 0a17831081b..1c01099ed24 100644 --- a/ui/include/classes/api/services/CTriggerGeneral.php +++ b/ui/include/classes/api/services/CTriggerGeneral.php @@ -203,7 +203,7 @@ abstract class CTriggerGeneral extends CApiService { 'triggerid' => $chd_trigger['triggerid'], 'templateid' => $tpl_trigger['triggerid'] ]; - $db_triggers[] = $chd_trigger; + $db_triggers[$chd_trigger['triggerid']] = $chd_trigger; $triggerids[] = $chd_trigger['triggerid']; $check_duplicates = ($chd_trigger['description'] !== $new_trigger['description'] @@ -247,11 +247,12 @@ abstract class CTriggerGeneral extends CApiService { ]; } - foreach ($db_triggers as $tnum => $db_trigger) { - $db_triggers[$tnum]['tags'] = array_key_exists($db_trigger['triggerid'], $trigger_tags) + foreach ($db_triggers as &$db_trigger) { + $db_trigger['tags'] = array_key_exists($db_trigger['triggerid'], $trigger_tags) ? $trigger_tags[$db_trigger['triggerid']] : []; } + unset($db_trigger); // Add discovery rule IDs. if ($this instanceof CTriggerPrototype) { @@ -268,9 +269,10 @@ abstract class CTriggerGeneral extends CApiService { $drule_by_triggerid[$row['triggerid']] = $row['parent_itemid']; } - foreach ($db_triggers as $tnum => $db_trigger) { - $db_triggers[$tnum]['discoveryRule']['itemid'] = $drule_by_triggerid[$db_trigger['triggerid']]; + foreach ($db_triggers as &$db_trigger) { + $db_trigger['discoveryRule']['itemid'] = $drule_by_triggerid[$db_trigger['triggerid']]; } + unset($db_trigger); } } @@ -508,9 +510,9 @@ abstract class CTriggerGeneral extends CApiService { } /** - * Updates the children of the triggers on the given hosts and propagates the inheritance to all child hosts. - * If the given triggers was assigned to a different template or a host, all of the child triggers, that became - * obsolete will be deleted. + * Updates children of triggers on the given hosts and propagates the inheritance to all child hosts. + * All of the child triggers that became obsolete will be deleted if the given triggers were assigned to a different + * template or host. * * @param array $triggers * @param string $triggers[]['triggerid'] @@ -914,6 +916,7 @@ abstract class CTriggerGeneral extends CApiService { $descriptions = $this->populateHostIds($descriptions); $this->checkAndAddUuid($triggers, $descriptions); $this->checkDuplicates($descriptions); + $this->checkDependencies($triggers); } /** @@ -963,8 +966,6 @@ abstract class CTriggerGeneral extends CApiService { * @throws APIException if validation failed. */ protected function validateUpdate(array &$triggers, array &$db_triggers = null) { - $db_triggers = []; - $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['description', 'expression']], 'fields' => [ 'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED], 'description' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')], @@ -1005,7 +1006,6 @@ abstract class CTriggerGeneral extends CApiService { 'templateid', 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag', 'manual_close', 'opdata', 'event_name' ], - 'selectDependencies' => ['triggerid'], 'triggerids' => zbx_objectValues($triggers, 'triggerid'), 'editable' => true, 'preservekeys' => true @@ -1035,21 +1035,17 @@ abstract class CTriggerGeneral extends CApiService { self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.')); } - $_db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($this->get($options), - ['sources' => ['expression', 'recovery_expression']] - ); + $db_triggers = $this->get($options); - $db_trigger_tags = $_db_triggers - ? DB::select('trigger_tag', [ - 'output' => ['triggertagid', 'triggerid', 'tag', 'value'], - 'filter' => ['triggerid' => array_keys($_db_triggers)], - 'preservekeys' => true - ]) - : []; + if (count($db_triggers) != count($triggers)) { + self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); + } + + $db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($db_triggers, [ + 'sources' => ['expression', 'recovery_expression'] + ]); - $_db_triggers = $this - ->createRelationMap($db_trigger_tags, 'triggerid', 'triggertagid') - ->mapMany($_db_triggers, $db_trigger_tags, 'tags'); + $this->addAffectedObjects($triggers, $db_triggers); $read_only_fields = ['description', 'expression', 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag', 'manual_close' @@ -1057,12 +1053,8 @@ abstract class CTriggerGeneral extends CApiService { $descriptions = []; - foreach ($triggers as $key => &$trigger) { - if (!array_key_exists($trigger['triggerid'], $_db_triggers)) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('No permissions to referred object or it does not exist!')); - } - - $db_trigger = $_db_triggers[$trigger['triggerid']]; + foreach ($triggers as &$trigger) { + $db_trigger = $db_triggers[$trigger['triggerid']]; $description = array_key_exists('description', $trigger) ? $trigger['description'] : $db_trigger['description']; @@ -1110,8 +1102,6 @@ abstract class CTriggerGeneral extends CApiService { 'recovery_expression' => $trigger['recovery_expression'] ]; } - - $db_triggers[$key] = $db_trigger; } unset($trigger); @@ -1119,6 +1109,303 @@ abstract class CTriggerGeneral extends CApiService { $descriptions = $this->populateHostIds($descriptions); $this->checkDuplicates($descriptions); } + + $this->checkDependencies($triggers, $db_triggers); + $this->checkDependenciesLinks($triggers, $db_triggers); + } + + /** + * @param array $triggers + * @param array $db_triggers + */ + private function addAffectedObjects(array $triggers, array &$db_triggers): void { + if ($this instanceof CTrigger) { + self::addAffectedHosts($triggers, $db_triggers); + } + + self::addAffectedTags($triggers, $db_triggers); + self::addAffectedDependencies($triggers, $db_triggers); + } + + /** + * @param array $triggers + * @param array $db_triggers + */ + protected static function addAffectedHosts(array $triggers, array &$db_triggers): void { + $triggerids = []; + + foreach ($triggers as $trigger) { + $db_trigger = $db_triggers[$trigger['triggerid']]; + + if ((array_key_exists('expression', $trigger) && $trigger['expression'] !== $db_trigger['expression']) + || (array_key_exists('recovery_expression', $trigger) + && $trigger['recovery_expression'] !== $db_trigger['recovery_expression'])) { + $triggerids[] = $trigger['triggerid']; + $db_triggers[$trigger['triggerid']]['hosts'] = []; + } + } + + if (!$triggerids) { + return; + } + + $result = DBselect( + 'SELECT DISTINCT f.triggerid,i.hostid'. + ' FROM functions f,items i'. + ' WHERE f.itemid=i.itemid'. + ' AND '.dbConditionId('f.triggerid', $triggerids) + ); + + while ($row = DBfetch($result)) { + $db_triggers[$row['triggerid']]['hosts'][$row['hostid']] = true; + } + } + + /** + * @param array $triggers + * @param array $db_triggers + */ + private static function addAffectedTags(array $triggers, array &$db_triggers): void { + $triggerids = []; + + foreach ($triggers as $trigger) { + if (array_key_exists('tags', $trigger)) { + $triggerids[] = $trigger['triggerid']; + $db_triggers[$trigger['triggerid']]['tags'] = []; + } + } + + if (!$triggerids) { + return; + } + + $options = [ + 'output' => ['triggertagid', 'triggerid', 'tag', 'value'], + 'filter' => ['triggerid' => $triggerids] + ]; + $db_tags = DBselect(DB::makeSql('trigger_tag', $options)); + + while ($db_tag = DBfetch($db_tags)) { + $db_triggers[$db_tag['triggerid']]['tags'][$db_tag['triggertagid']] = + array_diff_key($db_tag, array_flip(['triggerid'])); + } + } + + /** + * @param array $triggers + * @param array $db_triggers + */ + protected static function addAffectedDependencies(array $triggers, array &$db_triggers): void { + $triggerids = []; + + foreach ($triggers as $trigger) { + $db_trigger = $db_triggers[$trigger['triggerid']]; + + if (array_key_exists('dependencies', $trigger) || array_key_exists('hosts', $db_trigger)) { + $triggerids[] = $trigger['triggerid']; + $db_triggers[$trigger['triggerid']]['dependencies'] = []; + } + } + + if (!$triggerids) { + return; + } + + $options = [ + 'output' => ['triggerdepid', 'triggerid_down', 'triggerid_up'], + 'filter' => ['triggerid_down' => $triggerids] + ]; + $db_deps = DBselect(DB::makeSql('trigger_depends', $options)); + + while ($db_dep = DBfetch($db_deps)) { + $db_triggers[$db_dep['triggerid_down']]['dependencies'][$db_dep['triggerdepid']] = [ + 'triggerdepid' => $db_dep['triggerdepid'], + 'triggerid' => $db_dep['triggerid_up'] + ]; + } + } + + /** + * Check trigger dependencies of the given triggers. + * + * @param array $triggers + * @param array|null $db_triggers + * + * @throws APIException + */ + private function checkDependencies(array $triggers, array $db_triggers = null): void { + $edit_triggerids_up = []; + + foreach ($triggers as $trigger) { + if (!array_key_exists('dependencies', $trigger)) { + continue; + } + + $triggerids_up = array_column($trigger['dependencies'], 'triggerid'); + + if ($db_triggers === null) { + $edit_triggerids_up += array_flip($triggerids_up); + } + else { + $db_triggerids_up = array_column($db_triggers[$trigger['triggerid']]['dependencies'], 'triggerid'); + + $ins_triggerids_up = array_flip(array_diff($triggerids_up, $db_triggerids_up)); + $del_triggerids_up = array_flip(array_diff($db_triggerids_up, $triggerids_up)); + + $edit_triggerids_up += $ins_triggerids_up + $del_triggerids_up; + } + } + + if (!$edit_triggerids_up) { + return; + } + + $count = API::Trigger()->get([ + 'countOutput' => true, + 'triggerids' => array_keys($edit_triggerids_up) + ]); + + if ($this instanceof CTriggerPrototype) { + $trigger_prototypes_up = API::TriggerPrototype()->get([ + 'output' => [], + 'triggerids' => array_keys($edit_triggerids_up), + 'preservekeys' => true + ]); + + $count += count($trigger_prototypes_up); + } + + if ($count != count($edit_triggerids_up)) { + self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); + } + + if ($this instanceof CTriggerPrototype) { + $ins_dependencies = []; + $triggerids = []; + + foreach ($triggers as $trigger) { + if (!array_key_exists('dependencies', $triggers)) { + continue; + } + + $db_triggers_up = ($db_triggers !== null) + ? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid') + : []; + + foreach ($trigger['dependencies'] as $trigger_up) { + if (!array_key_exists($trigger_up['triggerid'], $db_triggers_up) + && array_key_exists($trigger_up['triggerid'], $trigger_prototypes_up)) { + $ins_dependencies[$trigger_up['triggerid']][$trigger['triggerid']] = true; + $triggerids[$trigger['triggerid']] = true; + } + } + } + + if (!$ins_dependencies) { + return; + } + + $result = DBselect( + 'SELECT DISTINCT f.triggerid,id.parent_itemid'. + ' FROM functions f,item_discovery id'. + ' WHERE f.itemid=id.itemid'. + ' AND '.dbConditionId('f.triggerid', array_keys($ins_dependencies + $triggerids)) + ); + + $lld_rules = []; + + while ($row = DBfetch($result)) { + $lld_rules[$row['triggerid']] = $row['parent_itemid']; + } + + foreach ($ins_dependencies as $triggerid_up => $triggerids) { + foreach ($triggerids as $triggerid) { + if (bccomp($lld_rules[$triggerid_up], $lld_rules[$triggerid]) != 0) { + self::exception(ZBX_API_ERROR_PARAMETERS, + _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because dependencies on trigger prototypes from another LLD rule are not allowed.') + ); + } + } + } + } + } + + /** + * Check linkage of dependencies. + * + * @param array $triggers + * @param array|null $db_triggers + */ + protected function checkDependenciesLinks(array $triggers, array $db_triggers = null): void { + $ins_dependencies = []; + $del_dependencies = []; + + foreach ($triggers as $trigger) { + if (!array_key_exists('dependencies', $trigger)) { + continue; + } + + $db_triggers_up = ($db_triggers !== null) + ? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid') + : []; + + foreach ($trigger['dependencies'] as $trigger_up) { + if (array_key_exists($trigger_up['triggerid'], $db_triggers_up)) { + unset($db_triggers_up[$trigger_up['triggerid']]); + } + else { + $ins_dependencies[$trigger_up['triggerid']][$trigger['triggerid']] = true; + } + } + + foreach ($db_triggers_up as $db_trigger_up) { + $del_dependencies[$db_trigger_up['triggerid']][$trigger['triggerid']] = true; + } + } + + if ($ins_dependencies) { + if ($this instanceof CTriggerPrototype) { + $options = [ + 'output' => ['triggerid', 'flags'], + 'triggerids' => array_keys($ins_dependencies) + ]; + $result = DBselect(DB::makeSql('triggers', $options)); + + $ins_dependencies_prototypes = []; + + while ($row = DBfetch($result)) { + if ($row['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) { + + /* + * Trigger cannot depend on trigger prototypes, so it's enough to check circular dependencies + * only among the trigger prototypes. + */ + $ins_dependencies_prototypes[$row['triggerid']] = $ins_dependencies[$row['triggerid']]; + + /* + * Since the trigger prototypes can depend only on trigger prototypes from the same LLD rule, + * for such dependencies it is not necessary to check the dependencies of host and template + * triggers. Only dependencies on triggers are required to be checked. + */ + unset($ins_dependencies[$row['triggerid']]); + } + } + } + + if ($db_triggers !== null) { + if ($this instanceof CTriggerPrototype && $ins_dependencies_prototypes) { + self::checkCircularDependencies($ins_dependencies_prototypes, $del_dependencies); + } + else { + self::checkCircularDependencies($ins_dependencies, $del_dependencies); + } + } + + $trigger_hosts = self::getTriggerHosts($ins_dependencies); + + self::checkDependenciesOfHostTriggers($ins_dependencies, $trigger_hosts); + self::checkDependenciesOfTemplateTriggers($ins_dependencies, $trigger_hosts); + } } /** @@ -1248,7 +1535,7 @@ abstract class CTriggerGeneral extends CApiService { $this->implode_expressions($triggers, $db_triggers, $triggers_functions, $inherited); foreach ($triggers as $tnum => $trigger) { - $db_trigger = $db_triggers[$tnum]; + $db_trigger = $db_triggers[$trigger['triggerid']]; $upd_trigger = ['values' => [], 'where' => ['triggerid' => $trigger['triggerid']]]; if (array_key_exists($tnum, $triggers_functions)) { @@ -1361,7 +1648,7 @@ abstract class CTriggerGeneral extends CApiService { if (!$inherited) { $resource = ($this instanceof CTrigger) ? CAudit::RESOURCE_TRIGGER : CAudit::RESOURCE_TRIGGER_PROTOTYPE; - $this->addAuditBulk(CAudit::ACTION_UPDATE, $resource, $save_triggers, zbx_toHash($db_triggers, 'triggerid')); + $this->addAuditBulk(CAudit::ACTION_UPDATE, $resource, $save_triggers, $db_triggers); } } @@ -1379,9 +1666,9 @@ abstract class CTriggerGeneral extends CApiService { * @param int $triggers[<tnum>]['recovery_mode'] [IN] * @param string $triggers[<tnum>]['recovery_expression'] [IN/OUT] * @param array|null $db_triggers [IN] - * @param string $db_triggers[<tnum>]['triggerid'] [IN] - * @param string $db_triggers[<tnum>]['expression'] [IN] - * @param string $db_triggers[<tnum>]['recovery_expression'] [IN] + * @param string $db_triggers[<triggerid>]['triggerid'] [IN] + * @param string $db_triggers[<triggerid>]['expression'] [IN] + * @param string $db_triggers[<triggerid>]['recovery_expression'] [IN] * @param array $triggers_functions [OUT] array of the new functions which must be * inserted into DB * @param string $triggers_functions[<tnum>][]['functionid'] [OUT] @@ -1438,10 +1725,10 @@ abstract class CTriggerGeneral extends CApiService { $hosts_keys = []; $functions_num = 0; - foreach ($triggers as $tnum => $trigger) { + foreach ($triggers as $trigger) { $expressions_changed = ($db_triggers === null - || ($trigger['expression'] !== $db_triggers[$tnum]['expression'] - || $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression'])); + || ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression'] + || $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression'])); if (!$expressions_changed) { continue; @@ -1577,8 +1864,8 @@ abstract class CTriggerGeneral extends CApiService { foreach ($triggers as $tnum => &$trigger) { $expressions_changed = ($db_triggers === null - || ($trigger['expression'] !== $db_triggers[$tnum]['expression'] - || $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression'])); + || ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression'] + || $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression'])); if (!$expressions_changed) { continue; @@ -1672,12 +1959,12 @@ abstract class CTriggerGeneral extends CApiService { // Triggers with children cannot be moved from one template to another host or template. if ($class === 'CTrigger' && $db_triggers !== null && $expressions_changed) { - $expression_parser->parse($db_triggers[$tnum]['expression']); + $expression_parser->parse($db_triggers[$trigger['triggerid']]['expression']); $old_hosts1 = $expression_parser->getResult()->getHosts(); $old_hosts2 = []; if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { - $expression_parser->parse($db_triggers[$tnum]['recovery_expression']); + $expression_parser->parse($db_triggers[$trigger['triggerid']]['recovery_expression']); $old_hosts2 = $expression_parser->getResult()->getHosts(); } @@ -1690,7 +1977,7 @@ abstract class CTriggerGeneral extends CApiService { } if ($is_moved) { - $moved_triggers[$db_triggers[$tnum]['triggerid']] = ['description' => $trigger['description']]; + $moved_triggers[$trigger['triggerid']] = ['description' => $trigger['description']]; } } @@ -1717,7 +2004,7 @@ abstract class CTriggerGeneral extends CApiService { )); } elseif ($db_triggers !== null - && !idcmp($lld_ruleids[0], $db_triggers[$tnum]['discoveryRule']['itemid'])) { + && !idcmp($lld_ruleids[0], $db_triggers[$trigger['triggerid']]['discoveryRule']['itemid'])) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger prototype "%1$s": %2$s.', $trigger['description'], _('trigger prototype cannot be moved to another template or host') )); @@ -1742,8 +2029,8 @@ abstract class CTriggerGeneral extends CApiService { // Replace func(/host/item) macros with {<functionid>}. foreach ($triggers as $tnum => &$trigger) { $expressions_changed = ($db_triggers === null - || ($trigger['expression'] !== $db_triggers[$tnum]['expression'] - || $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression'])); + || ($trigger['expression'] !== $db_triggers[$trigger['triggerid']]['expression'] + || $trigger['recovery_expression'] !== $db_triggers[$trigger['triggerid']]['recovery_expression'])); if (!$expressions_changed) { continue; @@ -1921,4 +2208,1039 @@ abstract class CTriggerGeneral extends CApiService { $this->inherit($triggers, $data['hostids']); } + + /** + * Checks if the given dependencies already exist. + * + * @param array $trigger_dependencies[<triggerid_up>][<triggerid>] + * + * @throws APIException + */ + protected static function checkDependencyDuplicates(array $trigger_dependencies) { + $all_triggerids = []; + + foreach ($trigger_dependencies as $triggerids) { + $all_triggerids += $triggerids; + } + + $options = [ + 'output' => ['triggerid_down', 'triggerid_up'], + 'filter' => [ + 'triggerid_down' => array_keys($all_triggerids), + 'triggerid_up' => array_keys($trigger_dependencies) + ] + ]; + $result = DBselect(DB::makeSql('trigger_depends', $options)); + + while ($row = DBfetch($result)) { + if (array_key_exists($row['triggerid_up'], $trigger_dependencies) + && in_array($row['triggerid_down'], $trigger_dependencies[$row['triggerid_up']])) { + $duplicates = DB::select('triggers', [ + 'output' => ['description', 'flags'], + 'triggerids' => [$row['triggerid_down'], $row['triggerid_up']], + 'preservekeys' => true + ]); + + if ($duplicates[$row['triggerid_down']]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) { + $error = _('The dependency of trigger "%1$s" on trigger "%2$s" already exists.'); + } + else { + $error = ($duplicates[$row['triggerid_up']]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) + ? _('The dependency of trigger prototype "%1$s" on trigger "%2$s" already exists.') + : _('The dependency of trigger prototype "%1$s" on trigger prototype "%2$s" already exists.'); + } + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, + $duplicates[$row['triggerid_down']]['description'], + $duplicates[$row['triggerid_up']]['description'] + )); + } + } + } + + /** + * Check whether circular linkage occurs as a result of the given changes in trigger dependencies. + * + * @param array $ins_dependencies[<triggerid_up>][<triggerid>] + * @param array $del_dependencies[<triggerid_up>][<triggerid>] + * @param bool $inherited Whether the check gets performed during inherit. + * + * @throws APIException + */ + protected static function checkCircularDependencies(array $ins_dependencies, array $del_dependencies = [], + bool $inherited = false): void { + $links = []; + $_triggerids_down = $ins_dependencies; + + do { + $options = [ + 'output' => ['triggerid_up', 'triggerid_down'], + 'filter' => [ + 'triggerid_down' => array_keys($_triggerids_down) + ] + ]; + $result = DBselect(DB::makeSql('trigger_depends', $options)); + + $_triggerids_down = []; + + while ($row = DBfetch($result)) { + if (array_key_exists($row['triggerid_up'], $del_dependencies) + && array_key_exists($row['triggerid_down'], $del_dependencies[$row['triggerid_up']])) { + continue; + } + + if (!array_key_exists($row['triggerid_up'], $links)) { + $_triggerids_down[$row['triggerid_up']] = true; + } + + $links[$row['triggerid_up']][$row['triggerid_down']] = true; + } + } while ($_triggerids_down); + + foreach ($ins_dependencies as $triggerid_up => $triggerids) { + if (array_key_exists($triggerid_up, $links)) { + $links[$triggerid_up] += $triggerids; + } + else { + $links[$triggerid_up] = $triggerids; + } + } + + foreach ($ins_dependencies as $triggerid_up => $triggerids) { + foreach ($triggerids as $triggerid => $foo) { + if (array_key_exists($triggerid, $links)) { + $links_path = [$triggerid => true]; + + if (self::circularLinkageExists($links, $triggerid_up, $links[$triggerid], $links_path)) { + $trigger_up_name = ''; + + $triggers = DB::select('triggers', [ + 'output' => ['description', 'flags'], + 'triggerids' => array_keys($links_path + [$triggerid_up => true]), + 'preservekeys' => true + ]); + + foreach ($triggers as $_triggerid => $trigger) { + $description = '"'.$trigger['description'].'"'; + + if (bccomp($_triggerid, $triggerid_up) == 0) { + $trigger_up_name = $description; + } + else { + $links_path[$_triggerid] = $description; + } + } + + $circular_linkage = (bccomp($triggerid_up, $triggerid) == 0) + ? $trigger_up_name.' -> '.$trigger_up_name + : $trigger_up_name.' -> '.implode(' -> ', $links_path).' -> '.$trigger_up_name; + + if ($inherited) { + $host = DBfetch(DBselect( + 'SELECT DISTINCT h.host,h.status'. + ' FROM functions f,items i,hosts h'. + ' WHERE f.itemid=i.itemid'. + ' AND i.hostid=h.hostid'. + ' AND '.dbConditionId('f.triggerid', [$triggerid_up]) + )); + + if ($host['status'] == HOST_STATUS_TEMPLATE) { + $error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) + ? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur for template "%4$s".') + : _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur for template "%4$s".'); + } + else { + $error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) + ? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur for host "%4$s".') + : _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur for host "%4$s".'); + } + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, + $triggers[$triggerid]['description'], $triggers[$triggerid_up]['description'], + $circular_linkage, $host['host'] + )); + } + else { + $error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) + ? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because a circular linkage (%3$s) would occur.') + : _('Trigger prototype "%1$s" cannot depend on the trigger prototype "%2$s", because a circular linkage (%3$s) would occur.'); + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, + $triggers[$triggerid]['description'], $triggers[$triggerid_up]['description'], + $circular_linkage + )); + } + } + } + } + } + } + + /** + * Recursively check whether the trigger, which a dependency is being set on, produces a circular linkage. + * + * @param array $links[<triggerid_up>][<triggerid>] + * @param string $triggerid_up + * @param array $triggerids[<triggerid>] + * @param array $links_path Circular linkage path, collected performing the check. + * + * @return bool + */ + private static function circularLinkageExists(array $links, string $triggerid_up, array $triggerids, + array &$links_path): bool { + if (array_key_exists($triggerid_up, $triggerids)) { + return true; + } + + $_links_path = $links_path; + + foreach ($triggerids as $triggerid => $foo) { + if (array_key_exists($triggerid, $links)) { + $links_path = $_links_path; + $triggerid_links = array_diff_key($links[$triggerid], $links_path); + + if ($triggerid_links) { + $links_path[$triggerid] = true; + + if (self::circularLinkageExists($links, $triggerid_up, $triggerid_links, $links_path)) { + return true; + } + } + } + } + + return false; + } + + /** + * Get hosts data of all triggers given in trigger dependencies array. + * + * @param array $trigger_dependencies[<triggerid_up>][<triggerid>] + * + * @return array + */ + public static function getTriggerHosts(array $trigger_dependencies): array { + $all_triggerids = $trigger_dependencies; + + foreach ($trigger_dependencies as $triggerids) { + $all_triggerids += $triggerids; + } + + $result = DBselect( + 'SELECT DISTINCT f.triggerid,h.hostid,h.status'. + ' FROM functions f,items i,hosts h'. + ' WHERE f.itemid=i.itemid'. + ' AND i.hostid=h.hostid'. + ' AND '.dbConditionId('f.triggerid', array_keys($all_triggerids)) + ); + + $trigger_hosts = []; + + while ($row = DBfetch($result)) { + // Each trigger can have either only templateids or only hostids. + if ($row['status'] == HOST_STATUS_TEMPLATE) { + $trigger_hosts[$row['triggerid']]['templateids'][$row['hostid']] = true; + } + else { + $trigger_hosts[$row['triggerid']]['hostids'][$row['hostid']] = true; + } + } + + return $trigger_hosts; + } + + /** + * Check whether the trigger dependencies are correctly set for host triggers. + * + * @param array $trigger_dependencies[<triggerid_up>][<triggerid>] + * @param array $trigger_hosts + * + * @throws APIException + */ + public static function checkDependenciesOfHostTriggers(array $trigger_dependencies, array $trigger_hosts): void { + foreach ($trigger_dependencies as $triggerid_up => $triggerids) { + if (array_key_exists('templateids', $trigger_hosts[$triggerid_up])) { + foreach ($triggerids as $triggerid => $foo) { + if (array_key_exists('hostids', $trigger_hosts[$triggerid])) { + $triggers = DB::select('triggers', [ + 'output' => ['description', 'flags'], + 'triggerids' => [$triggerid, $triggerid_up], + 'preservekeys' => true + ]); + + $error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) + ? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because dependencies of host triggers on template triggers are not allowed.') + : _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because dependencies of host triggers on template triggers are not allowed.'); + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'], + $triggers[$triggerid_up]['description'] + )); + } + } + } + } + } + + /** + * Check whether the trigger dependencies are correctly set for template triggers. + * + * @param array $trigger_dependencies[<triggerid_up>][<triggerid>] + * @param array $trigger_hosts + * + * @throws APIException + */ + public static function checkDependenciesOfTemplateTriggers(array $trigger_dependencies, + array $trigger_hosts): void { + /* + * From the given trigger dependencies we should keep only the dependencies of the template triggers. There is + * also no need to check the dependencies on triggers from the same template, because that is considered a + * valid case. Thus, among the triggers that the template triggers depends on, we should only keep triggers from + * other templates and hosts. + */ + foreach ($trigger_dependencies as $triggerid_up => $triggerids) { + $templateids_up = array_key_exists('templateids', $trigger_hosts[$triggerid_up]) + ? $trigger_hosts[$triggerid_up]['templateids'] + : []; + + foreach ($triggerids as $triggerid => $foo) { + /* + * If trigger-up and dependent trigger have at least one common template, even if they also have + * other templates, it is important to understand that at the moment of the trigger dependency + * validation all of those different templates are already linked to all child templates, + * so there is no need to check them again. + */ + if (!array_key_exists('templateids', $trigger_hosts[$triggerid]) + || array_intersect_key($templateids_up, $trigger_hosts[$triggerid]['templateids'])) { + unset($trigger_dependencies[$triggerid_up][$triggerid]); + } + } + + if (!$trigger_dependencies[$triggerid_up]) { + unset($trigger_dependencies[$triggerid_up]); + } + } + + if (!$trigger_dependencies) { + return; + } + + self::checkTriggersUpNotFromParentTemplates($trigger_dependencies, $trigger_hosts); + self::checkTriggersUpNotFromChildTemplatesOrHosts($trigger_dependencies, $trigger_hosts); + self::checkTriggersUpTemplatesAreLinkedToChildTemplates($trigger_dependencies, $trigger_hosts); + } + + /** + * Check that the triggers-up of the given trigger dependencies do not come from parent templates of dependent + * triggers. + * + * @param array $trigger_dependencies[<triggerid_up>][<triggerid>] + * @param array $trigger_hosts + * + * @throws APIException + */ + private static function checkTriggersUpNotFromParentTemplates(array $trigger_dependencies, array $trigger_hosts): void { + $templateids = []; + $template_triggers_up = []; + $dependency_templates = []; + + foreach ($trigger_dependencies as $triggerid_up => $triggerids) { + if (!array_key_exists('templateids', $trigger_hosts[$triggerid_up])) { + continue; + } + + $templateids_up = []; + + foreach ($trigger_hosts[$triggerid_up]['templateids'] as $templateid => $foo) { + $template_triggers_up[$templateid][] = $triggerid_up; + $templateids_up[$templateid] = true; + } + + foreach ($triggerids as $triggerid => $foo) { + foreach ($trigger_hosts[$triggerid]['templateids'] as $templateid => $foo) { + if (!array_key_exists($templateid, $dependency_templates)) { + $dependency_templates[$templateid] = []; + } + + $dependency_templates[$templateid] += $templateids_up; + } + + $templateids += $trigger_hosts[$triggerid]['templateids']; + } + } + + $_templateids = $templateids; + $template_links = []; + + do { + $options = [ + 'output' => ['hostid', 'templateid'], + 'filter' => ['hostid' => array_keys($_templateids)] + ]; + $result = DBselect(DB::makeSql('hosts_templates', $options)); + + $_templateids = []; + + while ($row = DBfetch($result)) { + $template_links[$row['hostid']][$row['templateid']] = true; + + if (!array_key_exists($row['templateid'], $template_links)) { + $_templateids[$row['templateid']] = true; + } + } + } while ($_templateids); + + if (!$template_links) { + return; + } + + // Check if the trigger-up is a trigger from the parent template of the dependent trigger. + foreach ($dependency_templates as $templateid => $templateids_up) { + if (array_key_exists($templateid, $template_links)) { + foreach ($templateids_up as $templateid_up => $foo) { + if (self::checkTemplateUpExistsInTemplateLinks($template_links, $templateid, $templateid_up)) { + foreach ($template_triggers_up[$templateid_up] as $triggerid_up) { + foreach ($trigger_dependencies[$triggerid_up] as $triggerid => $foo) { + if (array_key_exists($templateid, $trigger_hosts[$triggerid]['templateids'])) { + break 2; + } + } + } + + $triggers = DB::select('triggers', [ + 'output' => ['description', 'flags'], + 'triggerids' => [$triggerid, $triggerid_up], + 'preservekeys' => true + ]); + + $templates = DB::select('hosts', [ + 'output' => ['host'], + 'hostids' => $templateid_up + ]); + + $error = ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) + ? _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from the parent template are not allowed.') + : _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from the parent template are not allowed.'); + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'], + $triggers[$triggerid_up]['description'], $templates[0]['host'] + )); + } + } + } + } + } + + /** + * Check that the triggers-up of the given trigger dependencies are not from the child templates or hosts of the + * dependent triggers. + * + * @param array $trigger_dependencies[<triggerid_up>][<triggerid>] + * @param array $trigger_hosts + * + * @throws APIException + */ + private static function checkTriggersUpNotFromChildTemplatesOrHosts(array $trigger_dependencies, + array $trigger_hosts): void { + $templateids = []; + $template_triggers_up = []; + $dependency_templates = []; + + foreach ($trigger_dependencies as $triggerid_up => $triggerids) { + $templateids_up = []; + + if (array_key_exists('templateids', $trigger_hosts[$triggerid_up])) { + foreach ($trigger_hosts[$triggerid_up]['templateids'] as $templateid => $foo) { + $template_triggers_up[$templateid][] = $triggerid_up; + $templateids_up[$templateid] = true; + } + } + else { + foreach ($trigger_hosts[$triggerid_up]['hostids'] as $hostid => $foo) { + $template_triggers_up[$hostid][] = $triggerid_up; + $templateids_up[$hostid] = true; + } + } + + foreach ($triggerids as $triggerid => $foo) { + foreach ($trigger_hosts[$triggerid]['templateids'] as $templateid => $foo) { + if (!array_key_exists($templateid, $dependency_templates)) { + $dependency_templates[$templateid] = []; + } + + $dependency_templates[$templateid] += $templateids_up; + } + + $templateids += $trigger_hosts[$triggerid]['templateids']; + } + } + + $template_links = []; + + do { + $options = [ + 'output' => ['hostid', 'templateid'], + 'filter' => ['templateid' => array_keys($templateids)] + ]; + $result = DBselect(DB::makeSql('hosts_templates', $options)); + + $templateids = []; + + while ($row = DBfetch($result)) { + $template_links[$row['templateid']][$row['hostid']] = true; + + if (!array_key_exists($row['hostid'], $template_links)) { + $templateids[$row['hostid']] = true; + } + } + } while ($templateids); + + if (!$template_links) { + return; + } + + // Check if each trigger-up is a trigger from the child template or host of the dependent trigger. + foreach ($dependency_templates as $templateid => $templateids_up) { + if (array_key_exists($templateid, $template_links)) { + foreach ($templateids_up as $templateid_up => $foo) { + if (self::checkTemplateUpExistsInTemplateLinks($template_links, $templateid, $templateid_up)) { + foreach ($template_triggers_up[$templateid_up] as $triggerid_up) { + foreach ($trigger_dependencies[$triggerid_up] as $triggerid => $foo) { + if (array_key_exists($templateid, $trigger_hosts[$triggerid]['templateids'])) { + break 2; + } + } + } + + $triggers = DB::select('triggers', [ + 'output' => ['description', 'flags'], + 'triggerids' => [$triggerid, $triggerid_up], + 'preservekeys' => true + ]); + + $templates = DB::select('hosts', [ + 'output' => ['host', 'status'], + 'hostids' => $templateid_up + ]); + + if ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) { + $error = ($templates[0]['status'] == HOST_STATUS_TEMPLATE) + ? _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from a child template or host are not allowed.') + : _('Trigger "%1$s" cannot depend on the trigger "%2$s" from the host "%3$s", because dependencies on triggers from a child template or host are not allowed.'); + } + else { + $error = ($templates[0]['status'] == HOST_STATUS_TEMPLATE) + ? _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the template "%3$s", because dependencies on triggers from a child template or host are not allowed.') + : _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s" from the host "%3$s", because dependencies on triggers from a child template or host are not allowed.'); + } + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'], + $triggers[$triggerid_up]['description'], $templates[0]['host'] + )); + } + } + } + } + } + + /** + * Recursively check if the given template-up exists in the chain of the given template links. + * + * @param array $template_links[<templateid>][<templateid_up>] + * @param string $templateid + * @param string $templateid_up + * + * @return bool + */ + private static function checkTemplateUpExistsInTemplateLinks(array $template_links, string $templateid, + string $templateid_up): bool { + if (array_key_exists($templateid_up, $template_links[$templateid])) { + return true; + } + + foreach ($template_links[$templateid] as $_templateid => $foo) { + if (array_key_exists($_templateid, $template_links) + && self::checkTemplateUpExistsInTemplateLinks($template_links, $_templateid, $templateid_up)) { + return true; + } + } + + return false; + } + + /** + * Check if the triggers-up templates of the given trigger dependencies are linked to child templates of the + * dependent triggers. + * + * @param array $trigger_dependencies[<triggerid_up>][<triggerid>] + * @param array $trigger_hosts + * + * @throws APIException + */ + private static function checkTriggersUpTemplatesAreLinkedToChildTemplates(array $trigger_dependencies, + array $trigger_hosts): void { + $templateids_up = []; + $templateids = []; + + foreach ($trigger_dependencies as $triggerid_up => $triggerids) { + if (!array_key_exists('templateids', $trigger_hosts[$triggerid_up])) { + unset($trigger_dependencies[$triggerid_up]); + continue; + } + + $templateids_up += $trigger_hosts[$triggerid_up]['templateids']; + + foreach ($triggerids as $triggerid => $foo) { + $templateids += $trigger_hosts[$triggerid]['templateids']; + } + } + + $options = [ + 'output' => ['templateid', 'hostid'], + 'filter' => [ + 'templateid' => array_keys($templateids) + ] + ]; + $result = DBselect(DB::makeSql('hosts_templates', $options)); + + $template_links = []; + + while ($row = DBfetch($result)) { + $template_links[$row['templateid']][$row['hostid']] = []; + } + + $result = DBselect( + 'SELECT ht.templateid,ht.hostid,htt.templateid AS host_templateid'. + ' FROM hosts_templates ht,hosts_templates htt'. + ' WHERE ht.hostid=htt.hostid'. + ' AND ht.templateid!=htt.templateid'. + ' AND '.dbConditionId('ht.templateid', array_keys($templateids)). + ' AND '.dbConditionId('htt.templateid', array_keys($templateids_up)) + ); + + while ($row = DBfetch($result)) { + $template_links[$row['templateid']][$row['hostid']][$row['host_templateid']] = true; + } + + foreach ($trigger_dependencies as $triggerid_up => $triggerids) { + /* + * If trigger belongs to more than one template, then it is not possible to link only part of them to + * another host or template. That means each template ID of that trigger would have the same hosts + * in template links. And vice versa, if at least one of the trigger templates was not found in template + * links, then the trigger is not inherited further. + */ + $templateid_up = key($trigger_hosts[$triggerid_up]['templateids']); + + foreach ($triggerids as $triggerid => $foo) { + $templateid = key($trigger_hosts[$triggerid]['templateids']); + + if (!array_key_exists($templateid, $template_links)) { + continue; + } + + foreach ($template_links[$templateid] as $hostid => $host_templateids) { + if (!array_key_exists($templateid_up, $host_templateids)) { + $triggers = DB::select('triggers', [ + 'output' => ['description', 'flags'], + 'triggerids' => [$triggerid, $triggerid_up], + 'preservekeys' => true + ]); + + $hosts = DB::select('hosts', [ + 'output' => ['host', 'status'], + 'hostids' => [$templateid_up, $hostid], + 'preservekeys' => true + ]); + + if ($triggers[$triggerid]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) { + $error = ($hosts[$hostid]['status'] == HOST_STATUS_TEMPLATE) + ? _('Trigger "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the template "%4$s".') + : _('Trigger "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the host "%4$s".'); + } + else { + $error = ($hosts[$hostid]['status'] == HOST_STATUS_TEMPLATE) + ? _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the template "%4$s".') + : _('Trigger prototype "%1$s" cannot depend on the trigger "%2$s", because the template "%3$s" is not linked to the host "%4$s".'); + } + + self::exception(ZBX_API_ERROR_PARAMETERS, sprintf($error, $triggers[$triggerid]['description'], + $triggers[$triggerid_up]['description'], $hosts[$templateid_up]['host'], + $hosts[$hostid]['host'] + )); + } + } + } + } + } + + /** + * Update the trigger dependencies. + * + * @param array $triggers + * @param array|null $db_triggers + */ + public static function updateDependencies(array &$triggers, array $db_triggers = null): void { + $ins_trigger_deps = []; + $del_triggerdepids = []; + $edit_dependencies = []; + + foreach ($triggers as &$trigger) { + if (!array_key_exists('dependencies', $trigger)) { + continue; + } + + $db_triggers_up = ($db_triggers !== null) + ? array_column($db_triggers[$trigger['triggerid']]['dependencies'], null, 'triggerid') + : []; + + foreach ($trigger['dependencies'] as &$trigger_up) { + if (array_key_exists($trigger_up['triggerid'], $db_triggers_up)) { + $trigger_up['triggerdepid'] = $db_triggers_up[$trigger_up['triggerid']]['triggerdepid']; + unset($db_triggers_up[$trigger_up['triggerid']]); + } + else { + $ins_trigger_deps[] = [ + 'triggerid_down' => $trigger['triggerid'], + 'triggerid_up' => $trigger_up['triggerid'] + ]; + + $edit_dependencies[$trigger['triggerid']][$trigger_up['triggerid']] = true; + } + } + unset($trigger_up); + + foreach ($db_triggers_up as $db_trigger_up) { + $del_triggerdepids[] = $db_trigger_up['triggerdepid']; + + $edit_dependencies[$trigger['triggerid']][$db_trigger_up['triggerid']] = false; + } + } + unset($trigger); + + if ($del_triggerdepids) { + DB::delete('trigger_depends', ['triggerdepid' => $del_triggerdepids]); + } + + if ($ins_trigger_deps) { + $triggerdepids = DB::insertBatch('trigger_depends', $ins_trigger_deps); + } + + foreach ($triggers as &$trigger) { + if (!array_key_exists('dependencies', $trigger)) { + continue; + } + + foreach ($trigger['dependencies'] as &$trigger_up) { + if (!array_key_exists('triggerdepid', $trigger_up)) { + $trigger_up['triggerdepid'] = array_shift($triggerdepids); + } + } + unset($trigger_up); + } + unset($trigger); + + if ($edit_dependencies) { + $edit_dependencies = self::getTemplatedDependencies($edit_dependencies); + + if ($edit_dependencies) { + self::inheritDependencies($edit_dependencies); + } + } + } + + /** + * Filter for dependencies whose dependent triggers belong to templates that are further linked + * to some templates or hosts. + * + * @param array $edit_dependencies[<triggerid>][<triggerid_up>] + * + * @return array + */ + protected static function getTemplatedDependencies(array $edit_dependencies): array { + $triggerids = DBfetchColumn(DBselect( + 'SELECT DISTINCT f.triggerid'. + ' FROM functions f,items i,hosts h'. + ' WHERE f.itemid=i.itemid'. + ' AND i.hostid=h.hostid'. + ' AND '.dbConditionId('f.triggerid', array_keys($edit_dependencies)). + ' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE]). + ' AND EXISTS('. + 'SELECT NULL'. + ' FROM hosts_templates ht'. + ' WHERE ht.templateid=i.hostid'. + ')' + ), 'triggerid'); + + return array_intersect_key($edit_dependencies, array_flip($triggerids)); + } + + /** + * Inherit the given trigger dependencies. + * + * @param array $edit_dependencies[<triggerid>][<triggerid_up>] + * @param array $hostids + */ + protected static function inheritDependencies(array $edit_dependencies, array $hostids = null): void { + $all_triggerids = $edit_dependencies; + $all_triggerids_up = []; + + foreach ($edit_dependencies as $triggerids_up) { + $all_triggerids += $triggerids_up; + $all_triggerids_up += $triggerids_up; + } + + $hostids_condition = ($hostids !== null) ? ' AND '.dbConditionId('i.hostid', $hostids) : ''; + + $result = DBselect( + 'SELECT DISTINCT t.templateid,t.triggerid,t.flags,i.hostid'. + ' FROM triggers t,functions f,items i'. + ' WHERE t.triggerid=f.triggerid'. + ' AND f.itemid=i.itemid'. + ' AND '.dbConditionId('t.templateid', array_keys($all_triggerids)). + $hostids_condition + ); + + $trigger_flags = []; + $trigger_links = []; + $tpl_triggerids_up = []; + + while ($row = DBfetch($result)) { + $trigger_links[$row['templateid']][$row['hostid']] = $row['triggerid']; + $trigger_flags[$row['triggerid']] = $row['flags']; + + if (array_key_exists($row['templateid'], $all_triggerids_up)) { + $tpl_triggerids_up[$row['templateid']] = true; + } + } + + $del_triggerdepids = []; + $_edit_dependencies = []; + + if ($tpl_triggerids_up) { + if ($hostids === null) { + $result = DBselect( + 'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid,i.hostid'. + ' FROM triggers t,trigger_depends td,triggers tt,functions f,items i'. + ' WHERE t.triggerid=td.triggerid_down'. + ' AND td.triggerid_up=tt.triggerid'. + ' AND tt.triggerid=f.triggerid'. + ' AND f.itemid=i.itemid'. + ' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)). + ' AND '.dbConditionId('tt.templateid', array_keys($tpl_triggerids_up)) + ); + } + else { + $result = DBselect( + 'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid,ii.hostid'. + ' FROM triggers t,functions f,items i,trigger_depends td,triggers tt,functions ff,items ii'. + ' WHERE t.triggerid=f.triggerid'. + ' AND f.itemid=i.itemid'. + ' AND t.triggerid=td.triggerid_down'. + ' AND td.triggerid_up=tt.triggerid'. + ' AND tt.triggerid=ff.triggerid'. + ' AND ff.itemid=ii.itemid'. + ' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)). + ' AND '.dbConditionId('i.hostid', $hostids). + ' AND '.dbConditionId('tt.templateid', array_keys($tpl_triggerids_up)) + ); + } + + $tpl_child_dependencies = []; + + while ($row = DBfetch($result)) { + $tpl_child_dependencies[$row['triggerid']][$row['triggerid_up']][$row['hostid']] = $row['triggerdepid']; + } + + foreach ($edit_dependencies as $triggerid => $triggerids_up) { + $triggerids_up = array_intersect_key($triggerids_up, $tpl_triggerids_up); + + if (!$triggerids_up) { + continue; + } + + foreach ($trigger_links[$triggerid] as $hostid => $child_triggerid) { + $upd_child_triggerids_up = []; + + if (array_key_exists($child_triggerid, $tpl_child_dependencies)) { + foreach ($tpl_child_dependencies[$child_triggerid] as $child_triggerid_up => $hostids_up) { + $hostid_up = key($hostids_up); + $triggerdepid = reset($hostids_up); + + if (in_array($child_triggerid_up, $upd_child_triggerids_up) + || in_array($triggerdepid, $del_triggerdepids)) { + continue; + } + + foreach ($triggerids_up as $triggerid_up => $add) { + if (array_key_exists($hostid_up, $trigger_links[$triggerid_up]) + && bccomp($child_triggerid_up, $trigger_links[$triggerid_up][$hostid_up]) == 0) { + if ($add) { + $upd_child_triggerids_up[] = $child_triggerid_up; + } + else { + $del_triggerdepids[] = $hostids_up[$hostid_up]; + + $_edit_dependencies[$child_triggerid][$child_triggerid_up] = false; + } + } + } + } + } + + foreach ($triggerids_up as $triggerid_up => $add) { + if ($add) { + $child_triggerid_up = $trigger_links[$triggerid_up][$hostid]; + + if (!in_array($child_triggerid_up, $upd_child_triggerids_up)) { + $_edit_dependencies[$child_triggerid][$child_triggerid_up] = true; + } + } + } + } + } + } + + $host_triggerids_up = array_diff_key($all_triggerids_up, $tpl_triggerids_up); + + if ($host_triggerids_up) { + if ($hostids === null) { + $result = DBselect( + 'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid'. + ' FROM triggers t,trigger_depends td'. + ' WHERE t.triggerid=td.triggerid_down'. + ' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)). + ' AND '.dbConditionId('td.triggerid_up', array_keys($host_triggerids_up)) + ); + } + else { + $result = DBselect( + 'SELECT DISTINCT t.triggerid,td.triggerid_up,td.triggerdepid'. + ' FROM triggers t,functions f,items i,trigger_depends td'. + ' WHERE t.triggerid=f.triggerid'. + ' AND f.itemid=i.itemid'. + ' AND t.triggerid=td.triggerid_down'. + ' AND '.dbConditionId('t.templateid', array_keys($edit_dependencies)). + ' AND '.dbConditionId('i.hostid', $hostids). + ' AND '.dbConditionId('td.triggerid_up', array_keys($host_triggerids_up)) + ); + } + + $host_child_dependencies = []; + + while ($row = DBfetch($result)) { + $host_child_dependencies[$row['triggerid']][$row['triggerid_up']] = $row['triggerdepid']; + } + + foreach ($edit_dependencies as $triggerid => $triggerids_up) { + $triggerids_up = array_intersect_key($triggerids_up, $host_triggerids_up); + + if (!$triggerids_up) { + continue; + } + + foreach ($trigger_links[$triggerid] as $hostid => $child_triggerid) { + $upd_child_triggerids_up = []; + + if (array_key_exists($child_triggerid, $host_child_dependencies)) { + foreach ($host_child_dependencies[$child_triggerid] as $child_triggerid_up => $triggerdepid) { + if (array_key_exists($child_triggerid_up, $triggerids_up)) { + $add = $triggerids_up[$child_triggerid_up]; + + if ($add) { + $upd_child_triggerids_up[] = $child_triggerid_up; + } + else { + $del_triggerdepids[] = $triggerdepid; + + $_edit_dependencies[$child_triggerid][$child_triggerid_up] = false; + } + } + } + } + + foreach ($triggerids_up as $triggerid_up => $add) { + if ($add && !in_array($triggerid_up, $upd_child_triggerids_up)) { + $_edit_dependencies[$child_triggerid][$triggerid_up] = true; + } + } + } + } + } + + $ins_dependencies = []; + $del_dependencies = []; + $ins_trigger_deps = []; + + foreach ($_edit_dependencies as $triggerid => $triggerids_up) { + foreach ($triggerids_up as $triggerid_up => $add) { + if ($add) { + if ($trigger_flags[$triggerid] == ZBX_FLAG_DISCOVERY_PROTOTYPE) { + if ($trigger_flags[$triggerid_up] == ZBX_FLAG_DISCOVERY_PROTOTYPE) { + $ins_dependencies[$triggerid_up][$triggerid] = true; + } + } + else { + $ins_dependencies[$triggerid_up][$triggerid] = true; + } + + $ins_trigger_deps[] = [ + 'triggerid_down' => $triggerid, + 'triggerid_up' => $triggerid_up + ]; + } + else { + $del_dependencies[$triggerid_up][$triggerid] = true; + } + } + } + + if ($ins_dependencies) { + self::checkCircularDependencies($ins_dependencies, $del_dependencies, true); + } + + if ($del_triggerdepids) { + DB::delete('trigger_depends', ['triggerdepid' => $del_triggerdepids]); + } + + if ($ins_trigger_deps) { + DB::insertBatch('trigger_depends', $ins_trigger_deps); + } + + if ($_edit_dependencies) { + $_edit_dependencies = self::getTemplatedDependencies($_edit_dependencies); + + if ($_edit_dependencies) { + self::inheritDependencies($_edit_dependencies); + } + } + } + + /** + * Inherit the trigger dependencies of the given templates to the given hosts. + * + * @param array $templateids + * @param array $hostids + */ + public static function syncTemplateDependencies(array $templateids, array $hostids): void { + $result = DBselect( + 'SELECT DISTINCT f.triggerid,td.triggerid_up'. + ' FROM items i,functions f,trigger_depends td'. + ' WHERE i.itemid=f.itemid'. + ' AND f.triggerid=td.triggerid_down'. + ' AND '.dbConditionId('i.hostid', $templateids) + ); + + $edit_dependencies = []; + + while ($row = DBfetch($result)) { + $edit_dependencies[$row['triggerid']][$row['triggerid_up']] = true; + } + + if ($edit_dependencies) { + $edit_dependencies = self::getTemplatedDependencies($edit_dependencies); + + if ($edit_dependencies) { + self::inheritDependencies($edit_dependencies, $hostids); + } + } + } } diff --git a/ui/include/classes/api/services/CTriggerPrototype.php b/ui/include/classes/api/services/CTriggerPrototype.php index 3ffc3760604..3ccaa2db145 100644 --- a/ui/include/classes/api/services/CTriggerPrototype.php +++ b/ui/include/classes/api/services/CTriggerPrototype.php @@ -421,22 +421,13 @@ class CTriggerPrototype extends CTriggerGeneral { */ public function create(array $trigger_prototypes) { $this->validateCreate($trigger_prototypes); - $this->createReal($trigger_prototypes); - $this->inherit($trigger_prototypes); - $addDependencies = false; + $this->createReal($trigger_prototypes); + $this->checkDependenciesLinks($trigger_prototypes); - foreach ($trigger_prototypes as $trigger_prototype) { - if (isset($trigger_prototype['dependencies']) && is_array($trigger_prototype['dependencies']) - && $trigger_prototype['dependencies']) { - $addDependencies = true; - break; - } - } + $this->inherit($trigger_prototypes); - if ($addDependencies) { - $this->addDependencies($trigger_prototypes); - } + $this->updateDependencies($trigger_prototypes); return ['triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid')]; } @@ -448,25 +439,15 @@ class CTriggerPrototype extends CTriggerGeneral { * * @return array */ - public function update(array $trigger_prototypes) { + public function update(array $trigger_prototypes): array { $this->validateUpdate($trigger_prototypes, $db_triggers); + $this->updateReal($trigger_prototypes, $db_triggers); $this->inherit($trigger_prototypes); - $updateDependencies = false; - - foreach ($trigger_prototypes as $trigger_prototype) { - if (isset($trigger_prototype['dependencies']) && is_array($trigger_prototype['dependencies'])) { - $updateDependencies = true; - break; - } - } - - if ($updateDependencies) { - $this->updateDependencies($trigger_prototypes); - } + $this->updateDependencies($trigger_prototypes, $db_triggers); - return ['triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid')]; + return ['triggerids' => array_column($trigger_prototypes, 'triggerid')]; } /** @@ -529,539 +510,6 @@ class CTriggerPrototype extends CTriggerGeneral { } /** - * Update the given dependencies and inherit them on all child triggers. - * - * @param array $triggerPrototypes - */ - protected function updateDependencies(array $triggerPrototypes) { - $this->deleteDependencies($triggerPrototypes); - - $this->addDependencies($triggerPrototypes); - } - - /** - * Deletes all trigger and trigger prototype dependencies from the given trigger prototypes and their children. - * - * @param array $triggerPrototypes - * @param string $triggerPrototypes[]['triggerid'] - */ - protected function deleteDependencies(array $triggerPrototypes) { - $triggerPrototypeIds = zbx_objectValues($triggerPrototypes, 'triggerid'); - - try { - // Delete the dependencies from the child trigger prototypes. - - $childTriggerPrototypes = API::getApiService()->select($this->tableName(), [ - 'output' => ['triggerid'], - 'filter' => [ - 'templateid' => $triggerPrototypeIds - ] - ]); - - if ($childTriggerPrototypes) { - $this->deleteDependencies($childTriggerPrototypes); - } - - DB::delete('trigger_depends', [ - 'triggerid_down' => $triggerPrototypeIds - ]); - } - catch (APIException $e) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete dependency')); - } - } - - /** - * Add the given dependencies and inherit them on all child triggers. - * - * @param array $triggerPrototypes - * @param string $triggerPrototypes[]['triggerid'] - * @param array $triggerPrototypes[]['dependencies'] - * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] - * @param bool $inherited Determines either to check permissions for - * added dependencies. Permissions are not - * validated for inherited triggers. - */ - public function addDependencies(array $triggerPrototypes, bool $inherited = false) { - $this->validateAddDependencies($triggerPrototypes, $inherited); - - $insert = []; - - foreach ($triggerPrototypes as $triggerPrototype) { - if (!array_key_exists('dependencies', $triggerPrototype)) { - continue; - } - - foreach ($triggerPrototype['dependencies'] as $dependency) { - $insert[] = [ - 'triggerid_down' => $triggerPrototype['triggerid'], - 'triggerid_up' => $dependency['triggerid'] - ]; - } - } - - DB::insertBatch('trigger_depends', $insert); - - foreach ($triggerPrototypes as $triggerPrototype) { - // Propagate the dependencies to the child triggers. - - $childTriggers = API::getApiService()->select($this->tableName(), [ - 'output' => ['triggerid'], - 'filter' => [ - 'templateid' => $triggerPrototype['triggerid'] - ] - ]); - - if ($childTriggers) { - foreach ($childTriggers as &$childTrigger) { - $childTrigger['dependencies'] = []; - $childHostsQuery = get_hosts_by_triggerid($childTrigger['triggerid']); - - while ($childHost = DBfetch($childHostsQuery)) { - foreach ($triggerPrototype['dependencies'] as $dependency) { - $newDependency = [$childTrigger['triggerid'] => $dependency['triggerid']]; - $newDependency = replace_template_dependencies($newDependency, $childHost['hostid']); - - $childTrigger['dependencies'][] = [ - 'triggerid' => $newDependency[$childTrigger['triggerid']] - ]; - } - } - } - unset($childTrigger); - - $this->addDependencies($childTriggers, true); - } - } - } - - /** - * Validates the input for the addDependencies() method. - * - * @param array $trigger_prototypes - * @param string $trigger_prototypes[]['triggerid'] - * @param array $trigger_prototypes[]['dependencies'] - * @param string $trigger_prototypes[]['dependencies'][]['triggerid'] - * @param bool $inherited - * - * @throws APIException if the given dependencies are invalid. - */ - protected function validateAddDependencies(array $trigger_prototypes, bool $inherited = false): void { - $depTriggerIds = []; - - foreach ($trigger_prototypes as $trigger_prototype) { - if (!array_key_exists('dependencies', $trigger_prototype)) { - continue; - } - - foreach ($trigger_prototype['dependencies'] as $dependency) { - $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; - } - } - - if (!$depTriggerIds) { - return; - } - - // Check if given IDs are actual trigger prototypes and get discovery rules if they are. - $depTriggerPrototypes = $this->get([ - 'output' => ['triggerid'], - 'selectDiscoveryRule' => ['itemid'], - 'triggerids' => $depTriggerIds, - 'nopermissions' => $inherited ? true : null, - 'preservekeys' => true - ]); - - $dep_triggerids = array_diff($depTriggerIds, array_keys($depTriggerPrototypes)); - - if ($depTriggerPrototypes) { - // Get current trigger prototype discovery rules. - $dRules = $this->get([ - 'output' => ['triggerid'], - 'selectDiscoveryRule' => ['itemid'], - 'triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid'), - 'nopermissions' => $inherited ? true : null, - 'preservekeys' => true - ]); - - foreach ($trigger_prototypes as $trigger_prototype) { - if (!array_key_exists('dependencies', $trigger_prototype)) { - continue; - } - - $dRuleId = $dRules[$trigger_prototype['triggerid']]['discoveryRule']['itemid']; - - // Check if current trigger prototype rules match dependent trigger prototype rules. - foreach ($trigger_prototype['dependencies'] as $dependency) { - if (isset($depTriggerPrototypes[$dependency['triggerid']])) { - $depTriggerDRuleId = $depTriggerPrototypes[$dependency['triggerid']]['discoveryRule']['itemid']; - - if (bccomp($depTriggerDRuleId, $dRuleId) != 0) { - self::exception(ZBX_API_ERROR_PERMISSIONS, - _('No permissions to referred object or it does not exist!') - ); - } - } - } - } - } - elseif (!$dep_triggerids) { - self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); - } - - if ($dep_triggerids && !$inherited) { - // Check other dependency IDs if those are normal triggers. - $count = API::Trigger()->get([ - 'countOutput' => true, - 'triggerids' => $dep_triggerids, - 'filter' => [ - 'flags' => [ZBX_FLAG_DISCOVERY_NORMAL] - ] - ]); - - if ($count != count($dep_triggerids)) { - self::exception(ZBX_API_ERROR_PERMISSIONS, - _('No permissions to referred object or it does not exist!') - ); - } - } - - $this->checkDependencies($trigger_prototypes); - $this->checkDependencyParents($trigger_prototypes); - $this->checkDependencyDuplicates($trigger_prototypes); - } - - /** - * Check the dependencies of the given trigger prototypes. - * - * @param array $triggerPrototypes - * @param string $triggerPrototypes[]['triggerid'] - * @param array $triggerPrototypes[]['dependencies'] - * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] - * - * @throws APIException if any of the dependencies are invalid. - */ - protected function checkDependencies(array $triggerPrototypes) { - $triggerPrototypes = zbx_toHash($triggerPrototypes, 'triggerid'); - - foreach ($triggerPrototypes as $triggerPrototype) { - if (!array_key_exists('dependencies', $triggerPrototype)) { - continue; - } - - $triggerid_down = $triggerPrototype['triggerid']; - $triggerids_up = zbx_objectValues($triggerPrototype['dependencies'], 'triggerid'); - - foreach ($triggerids_up as $triggerid_up) { - if (bccomp($triggerid_down, $triggerid_up) == 0) { - self::exception(ZBX_API_ERROR_PARAMETERS, - _('Cannot create dependency on trigger prototype itself.') - ); - } - } - } - - foreach ($triggerPrototypes as $triggerPrototype) { - if (!array_key_exists('dependencies', $triggerPrototype)) { - continue; - } - - $depTriggerIds = zbx_objectValues($triggerPrototype['dependencies'], 'triggerid'); - - $triggerTemplates = API::Template()->get([ - 'output' => ['hostid', 'status'], - 'triggerids' => [$triggerPrototype['triggerid']], - 'nopermissions' => true - ]); - - if (!$triggerTemplates) { - // Current trigger prototype belongs to a host, so forbid dependencies from a host to a template. - - $triggerDepTemplates = API::Template()->get([ - 'output' => ['templateid'], - 'triggerids' => $depTriggerIds, - 'nopermissions' => true, - 'limit' => 1 - ]); - - if ($triggerDepTemplates) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot add dependency from a host to a template.')); - } - } - - // check circular dependency - $downTriggerIds = [$triggerPrototype['triggerid']]; - do { - // triggerid_down depends on triggerid_up - $res = DBselect( - 'SELECT td.triggerid_up'. - ' FROM trigger_depends td'. - ' WHERE '.dbConditionInt('td.triggerid_down', $downTriggerIds) - ); - - // combine db dependencies with those to be added - $upTriggersIds = []; - while ($row = DBfetch($res)) { - $upTriggersIds[] = $row['triggerid_up']; - } - foreach ($downTriggerIds as $id) { - if (isset($triggerPrototypes[$id]) && isset($triggerPrototypes[$id]['dependencies'])) { - $upTriggersIds = array_merge($upTriggersIds, - zbx_objectValues($triggerPrototypes[$id]['dependencies'], 'triggerid') - ); - } - } - - // if found trigger id is in dependent triggerids, there is a dependency loop - $downTriggerIds = []; - foreach ($upTriggersIds as $id) { - if (bccomp($id, $triggerPrototype['triggerid']) == 0) { - self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create circular dependencies.')); - } - $downTriggerIds[] = $id; - } - } while (!empty($downTriggerIds)); - - // fetch all templates that are used in dependencies - $triggerDepTemplates = API::Template()->get([ - 'output' => ['templateid'], - 'triggerids' => $depTriggerIds, - 'nopermissions' => true, - 'preservekeys' => true - ]); - - $depTemplateIds = array_keys($triggerDepTemplates); - - // run the check only if a templated trigger has dependencies on other templates - $triggerTemplateIds = zbx_toHash(zbx_objectValues($triggerTemplates, 'templateid')); - $tdiff = array_diff($depTemplateIds, $triggerTemplateIds); - - if (!empty($triggerTemplateIds) && !empty($depTemplateIds) && !empty($tdiff)) { - $affectedTemplateIds = zbx_array_merge($triggerTemplateIds, $depTemplateIds); - - // create a list of all hosts, that are children of the affected templates - $dbLowlvltpl = DBselect( - 'SELECT DISTINCT ht.templateid,ht.hostid,h.host'. - ' FROM hosts_templates ht,hosts h'. - ' WHERE h.hostid=ht.hostid'. - ' AND '.dbConditionInt('ht.templateid', $affectedTemplateIds) - ); - $map = []; - while ($lowlvltpl = DBfetch($dbLowlvltpl)) { - if (!isset($map[$lowlvltpl['hostid']])) { - $map[$lowlvltpl['hostid']] = []; - } - $map[$lowlvltpl['hostid']][$lowlvltpl['templateid']] = $lowlvltpl['host']; - } - - // check that if some host is linked to the template, that the trigger belongs to, - // the host must also be linked to all of the templates, that trigger dependencies point to - foreach ($map as $templates) { - foreach ($triggerTemplateIds as $triggerTemplateId) { - // is the host linked to one of the trigger templates? - if (isset($templates[$triggerTemplateId])) { - // then make sure all of the dependency templates are also linked - foreach ($depTemplateIds as $depTemplateId) { - if (!isset($templates[$depTemplateId])) { - self::exception(ZBX_API_ERROR_PARAMETERS, - _s('Not all templates are linked to "%1$s".', reset($templates)) - ); - } - } - break; - } - } - } - } - } - } - - /** - * Check that none of the triggers have dependencies on their children. Checks only one level of inheritance, but - * since it is called on each inheritance step, also works for multiple inheritance levels. - * - * @param array $triggerPrototypes - * @param string $triggerPrototypes[]['triggerid'] - * @param array $triggerPrototypes[]['dependencies'] - * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] - * - * @throws APIException if at least one trigger is dependent on its child. - */ - protected function checkDependencyParents(array $triggerPrototypes) { - // fetch all templated dependency trigger parents - $depTriggerIds = []; - - foreach ($triggerPrototypes as $triggerPrototype) { - if (!array_key_exists('dependencies', $triggerPrototype)) { - continue; - } - - foreach ($triggerPrototype['dependencies'] as $dependency) { - $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; - } - } - - $parentDepTriggers = DBfetchArray(DBSelect( - 'SELECT templateid,triggerid'. - ' FROM triggers'. - ' WHERE templateid>0'. - ' AND '.dbConditionInt('triggerid', $depTriggerIds) - )); - - if ($parentDepTriggers) { - $parentDepTriggers = zbx_toHash($parentDepTriggers, 'triggerid'); - - foreach ($triggerPrototypes as $triggerPrototype) { - foreach ($triggerPrototype['dependencies'] as $dependency) { - // Check if the current trigger is the parent of the dependency trigger. - - $depTriggerId = $dependency['triggerid']; - - if (isset($parentDepTriggers[$depTriggerId]) - && $parentDepTriggers[$depTriggerId]['templateid'] == $triggerPrototype['triggerid']) { - - self::exception(ZBX_API_ERROR_PARAMETERS, - _s('Trigger prototype cannot be dependent on a trigger that is inherited from it.') - ); - } - } - } - } - } - - /** - * Checks if the given dependencies contain duplicates. - * - * @param array $triggerPrototypes - * @param string $triggerPrototypes[]['triggerid'] - * @param array $triggerPrototypes[]['dependencies'] - * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] - * - * @throws APIException if the given dependencies contain duplicates. - */ - protected function checkDependencyDuplicates(array $triggerPrototypes) { - // check duplicates in array - $uniqueTriggers = []; - $depTriggerIds = []; - $duplicateTriggerId = null; - - foreach ($triggerPrototypes as $triggerPrototype) { - if (!array_key_exists('dependencies', $triggerPrototype)) { - continue; - } - - foreach ($triggerPrototype['dependencies'] as $dependency) { - $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; - - if (isset($uniqueTriggers[$triggerPrototype['triggerid']][$dependency['triggerid']])) { - $duplicateTriggerId = $triggerPrototype['triggerid']; - break 2; - } - else { - $uniqueTriggers[$triggerPrototype['triggerid']][$dependency['triggerid']] = 1; - } - } - } - - if ($duplicateTriggerId === null) { - // check if dependency already exists in DB - foreach ($triggerPrototypes as $triggerPrototype) { - $dbUpTriggers = DBselect( - 'SELECT td.triggerid_up'. - ' FROM trigger_depends td'. - ' WHERE '.dbConditionInt('td.triggerid_up', $depTriggerIds). - ' AND td.triggerid_down='.zbx_dbstr($triggerPrototype['triggerid']) - , 1); - if (DBfetch($dbUpTriggers)) { - $duplicateTriggerId = $triggerPrototype['triggerid']; - break; - } - } - } - - if ($duplicateTriggerId) { - $duplicateTrigger = DBfetch(DBselect( - 'SELECT t.description'. - ' FROM triggers t'. - ' WHERE t.triggerid='.zbx_dbstr($duplicateTriggerId) - )); - self::exception(ZBX_API_ERROR_PARAMETERS, - _s('Duplicate dependencies in trigger prototype "%1$s".', $duplicateTrigger['description']) - ); - } - } - - /** - * Synchronizes the templated trigger prototype dependencies on the given hosts inherited from the given templates. - * Update dependencies, do it after all triggers and trigger prototypes that can be dependent were created/updated - * on all child hosts/templates. Starting from highest level template trigger prototypes select trigger prototypes - * from one level lower, then for each lower trigger prototype look if it's parent has dependencies, if so - * find this dependency trigger prototype child on dependent trigger prototype host and add new dependency. - * - * @param array $data - * @param array|string $data['templateids'] - * @param array|string $data['hostids'] - */ - public function syncTemplateDependencies(array $data) { - $templateIds = zbx_toArray($data['templateids']); - $hostIds = zbx_toArray($data['hostids']); - - $parentTriggers = $this->get([ - 'output' => ['triggerid'], - 'selectDependencies' => ['triggerid'], - 'hostids' => $templateIds, - 'preservekeys' => true, - 'nopermissions' => true - ]); - - if ($parentTriggers) { - $childTriggers = $this->get([ - 'output' => ['triggerid', 'templateid'], - 'selectHosts' => ['hostid'], - 'hostids' => ($hostIds) ? $hostIds : null, - 'filter' => ['templateid' => array_keys($parentTriggers)], - 'nopermissions' => true, - 'preservekeys' => true - ]); - - if ($childTriggers) { - $newDependencies = []; - - foreach ($childTriggers as $childTrigger) { - $parentDependencies = $parentTriggers[$childTrigger['templateid']]['dependencies']; - - if ($parentDependencies) { - $newDependencies[$childTrigger['triggerid']] = [ - 'triggerid' => $childTrigger['triggerid'], - 'dependencies' => [] - ]; - - $dependencies = []; - foreach ($parentDependencies as $depTrigger) { - $dependencies[] = $depTrigger['triggerid']; - } - - $host = reset($childTrigger['hosts']); - $dependencies = replace_template_dependencies($dependencies, $host['hostid']); - - foreach ($dependencies as $depTriggerId) { - $newDependencies[$childTrigger['triggerid']]['dependencies'][] = [ - 'triggerid' => $depTriggerId - ]; - } - } - } - - $this->deleteDependencies($childTriggers, true); - - if ($newDependencies) { - $this->addDependencies($newDependencies, true); - } - } - } - } - - /** * Retrieves and adds additional requested data (options 'selectHosts', 'selectGroups', etc.) to result set. * * @param array $options diff --git a/ui/include/forms.inc.php b/ui/include/forms.inc.php index 2075ce4e3a3..0c194700f7d 100644 --- a/ui/include/forms.inc.php +++ b/ui/include/forms.inc.php @@ -1573,9 +1573,16 @@ function getCopyElementsFormData($elements_field, $title = null) { 'elements' => getRequest($elements_field, []), 'copy_type' => getRequest('copy_type', COPY_TYPE_TO_HOST_GROUP), 'copy_targetids' => getRequest('copy_targetids', []), - 'hostid' => getRequest('hostid', 0) + 'hostid' => 0 ]; + $prefix = (getRequest('context') === 'host') ? 'web.hosts.' : 'web.templates.'; + $filter_hostids = getRequest('filter_hostids', CProfile::getArray($prefix.'triggers.filter_hostids', [])); + + if (count($filter_hostids) == 1) { + $data['hostid'] = reset($filter_hostids); + } + if (!$data['elements'] || !is_array($data['elements'])) { show_error_message(_('Incorrect list of items.')); diff --git a/ui/include/triggers.inc.php b/ui/include/triggers.inc.php index 2bdd76b93ad..bbd85dc35f5 100644 --- a/ui/include/triggers.inc.php +++ b/ui/include/triggers.inc.php @@ -93,18 +93,6 @@ function get_trigger_by_triggerid($triggerid) { return false; } -function get_hosts_by_triggerid($triggerids) { - zbx_value2array($triggerids); - - return DBselect( - 'SELECT DISTINCT h.*'. - ' FROM hosts h,functions f,items i'. - ' WHERE i.itemid=f.itemid'. - ' AND h.hostid=i.hostid'. - ' AND '.dbConditionInt('f.triggerid', $triggerids) - ); -} - function get_triggers_by_hostid($hostid) { return DBselect( 'SELECT DISTINCT t.*'. @@ -148,27 +136,68 @@ function utf8RawUrlDecode($source) { } /** - * Copies the given triggers to the given hosts or templates. - * - * Without the $src_hostid parameter it will only be able to copy triggers that belong to only one host. If the - * $src_hostid parameter is not passed, and a trigger has multiple hosts, it will throw an error. If the - * $src_hostid parameter is passed, the given host will be replaced with the destination host. + * Copy the given triggers to the target hosts or templates, taking care of copied trigger dependencies. * - * This function takes care of copied trigger dependencies. - * If trigger is copied alongside with trigger on which it depends, then dependencies is replaced directly using new ids, - * If there is target host within dependency trigger, algorithm will search for potential matching trigger in target host, - * if matching trigger is found, then id from this trigger is used, if not rise exception, - * otherwise original dependency will be left. + * If the $src_hostid parameter is passed, the given host will be replaced with the destination host. + * Without $src_hostid, only triggers that belong to a single host or template can be copied. * + * If a trigger is copied alongside with the trigger which it depends on, then dependencies are replaced directly, + * using new IDs. + * If the source trigger depends on the trigger from the same host or template, the same trigger-up should exist on the + * target host or template. * - * @param array $src_triggerids Triggers which will be copied to $dst_hostids - * @param array $dst_hostids Hosts and templates to whom add triggers. IDs not present in DB (host table) - * will be ignored. - * @param int $src_hostid Host ID in which context trigger with multiple hosts will be treated. + * @param array $dst_hostids Hosts and templates to copy triggers to. + * IDs not present in the database will be ignored. + * @param string|null $src_hostid ID of host to use as context for trigger when multiple hosts are involved. + * @param array|null $src_triggerids Triggers which will be copied to destination host(s). * * @return bool */ -function copyTriggersToHosts($src_triggerids, $dst_hostids, $src_hostid = null) { +function copyTriggersToHosts(array $dst_hostids, ?string $src_hostid, array $src_triggerids = null): bool { + $dst_templates = API::Template()->get([ + 'output' => ['host'], + 'templateids' => $dst_hostids, + 'editable' => true, + 'preservekeys' => true + ]); + + $_dst_hostids = array_diff($dst_hostids, array_keys($dst_templates)); + + $dst_hosts = $_dst_hostids + ? API::Host()->get([ + 'output' => ['host', 'status'], + 'hostids' => $_dst_hostids, + 'editable' => true, + 'preservekeys' => true + ]) + : []; + + $dst_hosts = $dst_templates + $dst_hosts; + + if (!$dst_hosts || count($dst_hosts) != count($dst_hostids)) { + return false; + } + + if ($src_hostid) { + $src_hosts = API::Template()->get([ + 'output' => ['host'], + 'templateids' => $src_hostid + ]); + + $src_hosts = $src_hosts + ? $src_hosts + : API::Host()->get([ + 'output' => ['host'], + 'hostids' => $src_hostid + ]); + + if (!$src_hosts) { + return false; + } + + $src_host = $src_hosts[0]['host']; + } + $options = [ 'output' => ['triggerid', 'expression', 'description', 'url', 'status', 'priority', 'comments', 'type', 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag', 'manual_close', 'opdata', @@ -176,225 +205,249 @@ function copyTriggersToHosts($src_triggerids, $dst_hostids, $src_hostid = null) ], 'selectDependencies' => ['triggerid'], 'selectTags' => ['tag', 'value'], - 'triggerids' => $src_triggerids + 'preservekeys' => true ]; - if ($src_hostid) { - $srcHost = API::Host()->get([ - 'output' => ['host'], + if (!$src_hostid) { + $options += ['selectHosts' => ['hostid', 'host']]; + } + + if ($src_triggerids) { + $options += ['triggerids' => $src_triggerids]; + } + else { + $options += [ 'hostids' => $src_hostid, - 'preservekeys' => true, - 'templated_hosts' => true - ]); + 'inherited' => false, + 'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL] + ]; + } - if (!$srcHost = reset($srcHost)) { + $src_triggers = API::Trigger()->get($options); + + if ($src_triggerids) { + if (count($src_triggers) != count($src_triggerids)) { return false; } } else { - // Select source trigger first host 'host'. - $options['selectHosts'] = ['host']; + if (!$src_triggers) { + return true; + } } - $dbSrcTriggers = API::Trigger()->get($options); - - $dbSrcTriggers = CMacrosResolverHelper::resolveTriggerExpressions($dbSrcTriggers, + $src_triggers = CMacrosResolverHelper::resolveTriggerExpressions($src_triggers, ['sources' => ['expression', 'recovery_expression']] ); - $dbDstHosts = API::Host()->get([ - 'output' => ['hostid', 'host'], - 'hostids' => $dst_hostids, - 'preservekeys' => true, - 'templated_hosts' => true - ]); + if (!$src_hostid) { + foreach ($src_triggers as $src_trigger) { + if (count($src_trigger['hosts']) > 1) { + error(_s('Cannot copy trigger "%1$s", because it has multiple hosts in the expression.', + $src_trigger['description'] + )); + + return false; + } + } + } - $newTriggers = []; + $dst_triggers = []; + $trigger_links = []; + $i = 0; - foreach ($dbDstHosts as $dstHost) { - // Create each trigger for each host. + foreach ($dst_hosts as $dst_hostid => $dst_host) { + foreach ($src_triggers as $src_triggerid => $src_trigger) { + $dst_trigger = array_intersect_key($src_trigger, array_flip(['expression', 'description', 'url', 'status', + 'priority', 'comments', 'type', 'recovery_mode', 'recovery_expression', 'correlation_mode', + 'correlation_tag', 'manual_close', 'opdata', 'event_name', 'tags' + ])); - foreach ($dbSrcTriggers as $srcTrigger) { - if ($src_hostid) { - // Get host 'host' for triggerExpressionReplaceHost(). + $_src_host = $src_hostid ? $src_host : $src_trigger['hosts'][0]['host']; - $host = $srcHost['host']; - $srcTriggerContextHostId = $src_hostid; + $dst_trigger['expression'] = + triggerExpressionReplaceHost($src_trigger['expression'], $_src_host, $dst_host['host']); + + if ($src_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { + $dst_trigger['recovery_expression'] = + triggerExpressionReplaceHost($src_trigger['recovery_expression'], $_src_host, $dst_host['host']); } - else { - if (count($srcTrigger['hosts']) > 1) { - error(_s('Cannot copy trigger "%1$s:%2$s", because it has multiple hosts in the expression.', - $srcTrigger['description'], $srcTrigger['expression'] - )); - return false; - } + $dst_triggers[] = $dst_trigger; + $trigger_links[$src_triggerid][$dst_hostid] = $i; - // Use source trigger first host 'host'. - $host = $srcTrigger['hosts'][0]['host']; - $srcTriggerContextHostId = $srcTrigger['hosts'][0]['hostid']; - } + $i++; + } + } - $srcTrigger['expression'] = triggerExpressionReplaceHost($srcTrigger['expression'], $host, - $dstHost['host'] - ); + $result = API::Trigger()->create($dst_triggers); - if ($srcTrigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { - $srcTrigger['recovery_expression'] = triggerExpressionReplaceHost($srcTrigger['recovery_expression'], - $host, $dstHost['host'] - ); - } + if (!$result) { + return false; + } - // The dependencies must be added after all triggers are created. - $result = API::Trigger()->create([[ - 'description' => $srcTrigger['description'], - 'event_name' => $srcTrigger['event_name'], - 'opdata' => $srcTrigger['opdata'], - 'expression' => $srcTrigger['expression'], - 'url' => $srcTrigger['url'], - 'status' => $srcTrigger['status'], - 'priority' => $srcTrigger['priority'], - 'comments' => $srcTrigger['comments'], - 'type' => $srcTrigger['type'], - 'recovery_mode' => $srcTrigger['recovery_mode'], - 'recovery_expression' => $srcTrigger['recovery_expression'], - 'correlation_mode' => $srcTrigger['correlation_mode'], - 'correlation_tag' => $srcTrigger['correlation_tag'], - 'tags' => $srcTrigger['tags'], - 'manual_close' => $srcTrigger['manual_close'] - ]]); - - if (!$result) { - return false; + $dst_triggerids = $result['triggerids']; + + $dst_triggers = []; + $src_triggerids_up = []; + + foreach ($trigger_links as $src_triggerid => $links) { + foreach ($links as $dst_hostid => $i) { + if (!$src_triggers[$src_triggerid]['dependencies']) { + continue; } - $newTriggers[$srcTrigger['triggerid']][] = [ - 'newTriggerId' => reset($result['triggerids']), - 'newTriggerExpression' => $srcTrigger['expression'], - 'newTriggerHostId' => $dstHost['hostid'], - 'newTriggerHost' => $dstHost['host'], - 'srcTriggerContextHostId' => $srcTriggerContextHostId, - 'srcTriggerContextHost' => $host - ]; + $dst_triggers[$i] = ['triggerid' => $dst_triggerids[$i]]; + + foreach ($src_triggers[$src_triggerid]['dependencies'] as $src_trigger_up) { + if (array_key_exists($src_trigger_up['triggerid'], $trigger_links)) { + $dst_triggers[$i]['dependencies'][] = [ + 'triggerid' => $dst_triggerids[$trigger_links[$src_trigger_up['triggerid']][$dst_hostid]] + ]; + } + elseif ($src_triggerids) { + $src_triggerids_up[$src_trigger_up['triggerid']] = true; + } + else { + $dst_triggers[$i]['dependencies'][] = ['triggerid' => $src_trigger_up['triggerid']]; + } + } } } - $depids = []; - foreach ($dbSrcTriggers as $srcTrigger) { - foreach ($srcTrigger['dependencies'] as $depTrigger) { - $depids[] = $depTrigger['triggerid']; + if ($src_triggerids_up) { + $src_triggers_up = API::Trigger()->get([ + 'output' => ['description', 'expression', 'recovery_mode', 'recovery_expression'], + 'selectHosts' => ['hostid'], + 'triggerids' => array_keys($src_triggerids_up), + 'preservekeys' => true + ]); + + $src_triggers_up = CMacrosResolverHelper::resolveTriggerExpressions($src_triggers_up, + ['sources' => ['expression', 'recovery_expression']] + ); + + $src_host_dependencies = []; + + foreach ($trigger_links as $src_triggerid => $links) { + $_src_hostid = $src_hostid ? $src_hostid : $src_triggers[$src_triggerid]['hosts'][0]['hostid']; + + foreach ($links as $dst_hostid => $i) { + foreach ($src_triggers[$src_triggerid]['dependencies'] as $src_trigger_up) { + if (!array_key_exists($src_trigger_up['triggerid'], $src_triggers_up)) { + continue; + } + + $src_hostids_up = array_column($src_triggers_up[$src_trigger_up['triggerid']]['hosts'], 'hostid'); + + if (in_array($_src_hostid, $src_hostids_up)) { + $src_host_dependencies[$src_trigger_up['triggerid']][$src_triggerid] = true; + } + else { + $dst_triggers[$i]['dependencies'][] = ['triggerid' => $src_trigger_up['triggerid']]; + } + } + } } - } - $depTriggers = API::Trigger()->get([ - 'triggerids' => $depids, - 'output' => ['description', 'expression', 'recovery_mode', 'recovery_expression'], - 'selectHosts' => ['hostid'], - 'preservekeys' => true - ]); - $depTriggers = CMacrosResolverHelper::resolveTriggerExpressions($depTriggers, - ['sources' => ['expression', 'recovery_expression']] - ); + if ($src_host_dependencies) { + $descriptions = array_unique(array_column(array_intersect_key($src_triggers_up, $src_host_dependencies), + 'description' + )); - if ($newTriggers) { - // Map dependencies to the new trigger IDs and save. - - $dependencies = []; - - foreach ($dbSrcTriggers as $srcTrigger) { - if ($srcTrigger['dependencies']) { - // Get corresponding created triggers. - $dst_triggers = $newTriggers[$srcTrigger['triggerid']]; - - foreach ($dst_triggers as $dst_trigger) { - foreach ($srcTrigger['dependencies'] as $depTrigger) { - /* - * We have added $depTrigger trigger and we know corresponding trigger ID for newly - * created trigger. - */ - if (array_key_exists($depTrigger['triggerid'], $newTriggers)) { - $dst_dep_triggers = $newTriggers[$depTrigger['triggerid']]; - - foreach ($dst_dep_triggers as $dst_dep_trigger) { - /* - * Dependency is within same host according to $src_hostid parameter or dep trigger has - * single host. - */ - if ($dst_trigger['srcTriggerContextHostId'] == $dst_dep_trigger['srcTriggerContextHostId'] - && $dst_dep_trigger['newTriggerHostId'] == $dst_trigger['newTriggerHostId']) { - $depTriggerId = $dst_dep_trigger['newTriggerId']; - break; - } - // Dependency is to trigger from another host. - else { - $depTriggerId = $depTrigger['triggerid']; - } - } - } - // We need to search for $depTrigger trigger if target host is within dependency hosts. - elseif (in_array(['hostid' => $dst_trigger['srcTriggerContextHostId']], - $depTriggers[$depTrigger['triggerid']]['hosts'])) { - // Get all possible $depTrigger matching triggers by description. - $targetHostTriggersByDescription = API::Trigger()->get([ - 'hostids' => $dst_trigger['newTriggerHostId'], - 'output' => ['hosts', 'triggerid', 'expression'], - 'filter' => ['description' => $depTriggers[$depTrigger['triggerid']]['description']], - 'preservekeys' => true - ]); - - $targetHostTriggersByDescription = - CMacrosResolverHelper::resolveTriggerExpressions($targetHostTriggersByDescription); - - // Compare exploded expressions for exact match. - $expr1 = $depTriggers[$depTrigger['triggerid']]['expression']; - $depTriggerId = null; - - foreach ($targetHostTriggersByDescription as $potentialTargetTrigger) { - $expr2 = triggerExpressionReplaceHost($potentialTargetTrigger['expression'], - $dst_trigger['newTriggerHost'], $dst_trigger['srcTriggerContextHost'] - ); - - if ($expr2 == $expr1) { - // Matching trigger has been found. - $depTriggerId = $potentialTargetTrigger['triggerid']; - break; - } - } + $dst_host_triggers = API::Trigger()->get([ + 'output' => ['triggerid', 'description', 'expression', 'recovery_expression'], + 'selectHosts' => ['hostid'], + 'hostids' => array_keys($dst_hosts), + 'filter' => ['description' => $descriptions], + 'preservekeys' => true + ]); - // If matching trigger wasn't found raise exception. - if ($depTriggerId === null) { - $expr2 = triggerExpressionReplaceHost($expr1, $dst_trigger['srcTriggerContextHost'], - $dst_trigger['newTriggerHost'] - ); + if (!$dst_host_triggers) { + $src_triggerid_up = key($src_host_dependencies); + $src_triggerid = key($src_host_dependencies[$src_triggerid_up]); + $dst_hostid = key($trigger_links[$src_triggerid]); - error(_s( - 'Cannot add dependency from trigger "%1$s:%2$s" to non existing trigger "%3$s:%4$s".', - $srcTrigger['description'], $dst_trigger['newTriggerExpression'], - $depTriggers[$depTrigger['triggerid']]['description'], $expr2 - )); + $error = array_key_exists('status', $dst_hosts[$dst_hostid]) + ? _('Trigger "%1$s" cannot depend on the non-existent trigger "%2$s" on the host "%3$s".') + : _('Trigger "%1$s" cannot depend on the non-existent trigger "%2$s" on the template "%3$s".'); - return false; - } + error(sprintf($error, $src_triggers[$src_triggerid]['description'], + $src_triggers_up[$src_triggerid_up]['description'], $dst_hosts[$dst_hostid]['host'] + )); + + return false; + } + + $dst_host_triggers = CMacrosResolverHelper::resolveTriggerExpressions($dst_host_triggers, + ['sources' => ['expression', 'recovery_expression']] + ); + + $dst_host_triggerids = []; + + foreach ($dst_host_triggers as $i => $trigger) { + $description = $trigger['description']; + $expression = $trigger['expression']; + $recovery_expression = $trigger['recovery_expression']; + + if ($src_hostid) { + foreach ($trigger['hosts'] as $host) { + if (array_key_exists($host['hostid'], $dst_hosts)) { + $dst_host_triggerids[$host['hostid']][$description][$expression][$recovery_expression] = + $trigger['triggerid']; } - else { - // Leave original dependency. + } + } + else { + $dst_hostid = $trigger['hosts'][0]['hostid']; - $depTriggerId = $depTrigger['triggerid']; + $dst_host_triggerids[$dst_hostid][$description][$expression][$recovery_expression] = + $trigger['triggerid']; + } + } + + foreach ($src_host_dependencies as $src_triggerid_up => $src_triggerids) { + foreach ($src_triggerids as $src_triggerid => $foo) { + foreach ($trigger_links[$src_triggerid] as $dst_hostid => $i) { + $src_trigger_up = $src_triggers_up[$src_triggerid_up]; + $_src_host = $src_hostid ? $src_host : $src_trigger['hosts'][0]['host']; + $dst_host = $dst_hosts[$dst_hostid]['host']; + + $description = $src_trigger_up['description']; + $expression = + triggerExpressionReplaceHost($src_trigger_up['expression'], $_src_host, $dst_host); + $recovery_expression = + triggerExpressionReplaceHost($src_trigger_up['recovery_expression'], $_src_host, $dst_host); + + if (array_key_exists($dst_hostid, $dst_host_triggerids) + && array_key_exists($description, $dst_host_triggerids[$dst_hostid]) + && array_key_exists($expression, $dst_host_triggerids[$dst_hostid][$description]) + && array_key_exists($recovery_expression, $dst_host_triggerids[$dst_hostid][$description][$expression])) { + $dst_triggerid_up = + $dst_host_triggerids[$dst_hostid][$description][$expression][$recovery_expression]; + + $dst_triggers[$i]['dependencies'][] = ['triggerid' => $dst_triggerid_up]; } + else { + $error = array_key_exists('status', $dst_hosts[$dst_hostid]) + ? _('Trigger "%1$s" cannot depend on the non-existent trigger "%2$s" on the host "%3$s".') + : _('Trigger "%1$s" cannot depend on the non-existent trigger "%2$s" on the template "%3$s".'); + + error(sprintf($error, $description, $src_trigger_up['description'], $dst_host)); - $dependencies[] = [ - 'triggerid' => $dst_trigger['newTriggerId'], - 'dependsOnTriggerid' => $depTriggerId - ]; + return false; + } } } } } + } - if ($dependencies) { - if (!API::Trigger()->addDependencies($dependencies)) { - return false; - } + if ($dst_triggers) { + $result = API::Trigger()->update(array_values($dst_triggers)); + + if (!$result) { + return false; } } @@ -433,22 +486,6 @@ function triggerExpressionReplaceHost(string $expression, string $src_host, stri return $expression; } -function replace_template_dependencies($deps, $hostid) { - foreach ($deps as $id => $val) { - $sql = 'SELECT t.triggerid'. - ' FROM triggers t,functions f,items i'. - ' WHERE t.triggerid=f.triggerid'. - ' AND f.itemid=i.itemid'. - ' AND t.templateid='.zbx_dbstr($val). - ' AND i.hostid='.zbx_dbstr($hostid); - if ($db_new_dep = DBfetch(DBselect($sql))) { - $deps[$id] = $db_new_dep['triggerid']; - } - } - - return $deps; -} - /** * Prepare arrays containing only hosts and triggers that will be shown results table. * diff --git a/ui/include/views/configuration.triggers.edit.php b/ui/include/views/configuration.triggers.edit.php index 83e6255c3b3..fc3efc04ca9 100644 --- a/ui/include/views/configuration.triggers.edit.php +++ b/ui/include/views/configuration.triggers.edit.php @@ -600,7 +600,7 @@ $dependenciesFormList = new CFormList('dependenciesFormList'); $dependenciesTable = (new CTable()) ->setId('dependency-table') ->setAttribute('style', 'width: 100%;') - ->setHeader([_('Name'), $readonly ? null : _('Action')]); + ->setHeader([_('Name'), $discovered_trigger ? null : _('Action')]); foreach ($data['db_dependencies'] as $dependency) { $triggersForm->addVar('dependencies[]', $dependency['triggerid'], 'dependencies_'.$dependency['triggerid']); @@ -618,7 +618,7 @@ foreach ($data['db_dependencies'] as $dependency) { ->setArgument('context', $data['context']) ))->setTarget('_blank'), (new CCol( - $readonly + $discovered_trigger ? null : (new CButton('remove', _('Remove'))) ->onClick('view.removeDependency('.json_encode($dependency['triggerid']).')') @@ -632,7 +632,7 @@ foreach ($data['db_dependencies'] as $dependency) { $dependenciesFormList->addRow(_('Dependencies'), (new CDiv([ $dependenciesTable, - $readonly + $discovered_trigger ? null : (new CButton('bnt1', _('Add'))) ->onClick( diff --git a/ui/include/views/monitoring.sysmap.edit.php b/ui/include/views/monitoring.sysmap.edit.php index c0345f7682c..a7731419c56 100644 --- a/ui/include/views/monitoring.sysmap.edit.php +++ b/ui/include/views/monitoring.sysmap.edit.php @@ -344,7 +344,7 @@ $user_groups = []; foreach ($data['sysmap']['userGroups'] as $user_group) { $user_groupid = $user_group['usrgrpid']; - $user_groups[$user_groupid] = [ + $user_groups[] = [ 'usrgrpid' => $user_groupid, 'name' => $data['user_groups'][$user_groupid]['name'], 'permission' => $user_group['permission'] @@ -381,14 +381,14 @@ $users = []; foreach ($data['sysmap']['users'] as $user) { $userid = $user['userid']; - $users[$userid] = [ + $users[] = [ 'id' => $userid, 'name' => getUserFullname($data['users'][$userid]), 'permission' => $user['permission'] ]; } -$js_insert .= 'window.addPopupValues('.zbx_jsvalue(['object' => 'userid', 'values' => $users]).');'; +$js_insert .= 'window.addPopupValues('.json_encode(['object' => 'userid', 'values' => $users]).');'; zbx_add_post_js($js_insert); diff --git a/ui/templates.php b/ui/templates.php index 400c9ac5d99..f5623caeb35 100644 --- a/ui/templates.php +++ b/ui/templates.php @@ -333,17 +333,8 @@ elseif (hasRequest('add') || hasRequest('update')) { } // copy triggers - $dbTriggers = API::Trigger()->get([ - 'output' => ['triggerid'], - 'hostids' => $cloneTemplateId, - 'inherited' => false - ]); - - if ($dbTriggers) { - if (!copyTriggersToHosts(zbx_objectValues($dbTriggers, 'triggerid'), $input_templateid, - $cloneTemplateId)) { - throw new Exception(); - } + if (!copyTriggersToHosts([$input_templateid], $cloneTemplateId)) { + throw new Exception(); } // copy graphs diff --git a/ui/tests/api_json/testTemplate.php b/ui/tests/api_json/testTemplate.php index 14bec3e5311..8441ad15ca1 100644 --- a/ui/tests/api_json/testTemplate.php +++ b/ui/tests/api_json/testTemplate.php @@ -272,7 +272,7 @@ class testTemplate extends CAPITest { ['templateid' => $templateids[2]] ] ] - ], 'Cannot link template "test-template-double-link-01" to template "test-template-double-link-04" because its parent template "test-template-double-link-01" will be linked twice.'); + ], 'Cannot link template "test-template-double-link-01" to template "test-template-double-link-04", because its parent template "test-template-double-link-01" would be linked twice.'); } public function testTemplate_CreateTriggerDependency() { diff --git a/ui/tests/api_json/testTriggerValidation.php b/ui/tests/api_json/testTriggerValidation.php index cae85a10781..fb59239a390 100644 --- a/ui/tests/api_json/testTriggerValidation.php +++ b/ui/tests/api_json/testTriggerValidation.php @@ -107,7 +107,7 @@ class testTriggerValidation extends CAPITest { ] ] ], - 'expected_error' => 'Cannot create circular dependencies.' + 'expected_error' => 'Trigger "test-trigger-1" cannot depend on the trigger "test-trigger-2", because a circular linkage ("test-trigger-2" -> "test-trigger-1" -> "test-trigger-2") would occur.' ], 'delete trigger name' => [ 'triggers' => [ @@ -129,7 +129,7 @@ class testTriggerValidation extends CAPITest { ] ] ], - 'expected_error' => 'Cannot create dependency on trigger itself.' + 'expected_error' => 'Trigger "test-trigger-1" cannot depend on the trigger "test-trigger-1", because a circular linkage ("test-trigger-1" -> "test-trigger-1") would occur.' ], 'update read-only properties' => [ 'triggers' => [ @@ -392,7 +392,7 @@ class testTriggerValidation extends CAPITest { ]] ] ], - 'expected_error' => 'Cannot add dependency from a host to a template.' + 'expected_error' => 'Trigger "Trigger with invalid dependencies" cannot depend on the trigger "template-trigger", because dependencies of host triggers on template triggers are not allowed.' ], 'Trigger with invalid dependencies #3' => [ 'triggers' => [ diff --git a/ui/tests/include/web/CElement.php b/ui/tests/include/web/CElement.php index 6cd81b83988..1fd95f14912 100644 --- a/ui/tests/include/web/CElement.php +++ b/ui/tests/include/web/CElement.php @@ -500,7 +500,8 @@ class CElement extends CBaseElement implements IWaitable { * @inheritdoc */ public function isEnabled($enabled = true) { - $classes = explode(' ', parent::getAttribute('class')); + $attribute = parent::getAttribute('class'); + $classes = ($attribute !== null) ? explode(' ', $attribute) : []; $is_enabled = parent::isEnabled() && (parent::getAttribute('disabled') === null) diff --git a/ui/tests/include/web/CElementCollection.php b/ui/tests/include/web/CElementCollection.php index 67ab8c9e349..76835c261f2 100644 --- a/ui/tests/include/web/CElementCollection.php +++ b/ui/tests/include/web/CElementCollection.php @@ -61,13 +61,14 @@ class CElementCollection implements Iterator { /** * @inheritdoc */ - public function rewind() { + public function rewind(): void { reset($this->elements); } /** * @inheritdoc */ + #[\ReturnTypeWillChange] public function current() { return current($this->elements); } @@ -75,6 +76,7 @@ class CElementCollection implements Iterator { /** * @inheritdoc */ + #[\ReturnTypeWillChange] public function key() { return key($this->elements); } @@ -82,14 +84,14 @@ class CElementCollection implements Iterator { /** * @inheritdoc */ - public function next() { - return next($this->elements); + public function next(): void { + next($this->elements); } /** * @inheritdoc */ - public function valid() { + public function valid(): bool { $key = $this->key(); return ($key !== null && $key !== false); @@ -100,7 +102,7 @@ class CElementCollection implements Iterator { * * @return integer */ - public function count() { + public function count(): int { return count($this->elements); } @@ -109,7 +111,7 @@ class CElementCollection implements Iterator { * * @return boolean */ - public function isEmpty() { + public function isEmpty(): bool { return ($this->elements === []); } @@ -120,7 +122,7 @@ class CElementCollection implements Iterator { * * @throws Exception */ - public function first() { + public function first(): CElement { $element = reset($this->elements); if ($element === false) { @@ -137,7 +139,7 @@ class CElementCollection implements Iterator { * * @throws Exception */ - public function last() { + public function last(): CElement { $element = end($this->elements); if ($element === false) { @@ -154,7 +156,7 @@ class CElementCollection implements Iterator { * * @return boolean */ - public function exists($key) { + public function exists($key): bool { return array_key_exists($key, $this->elements); } @@ -163,9 +165,9 @@ class CElementCollection implements Iterator { * * @param mixed $key array key * - * @return boolean + * @return CElement */ - public function get($key) { + public function get($key): CElement { return $this->elements[$key]; } @@ -175,7 +177,7 @@ class CElementCollection implements Iterator { * @param mixed $key array key * @param mixed $element element to be set */ - public function set($key, $element) { + public function set($key, $element): void { $this->elements[$key] = $element; } diff --git a/ui/tests/include/web/elements/CTableElement.php b/ui/tests/include/web/elements/CTableElement.php index 44d4bc764e5..41215230a25 100644 --- a/ui/tests/include/web/elements/CTableElement.php +++ b/ui/tests/include/web/elements/CTableElement.php @@ -186,7 +186,7 @@ class CTableElement extends CElement { public function findRows($param, $data = []) { $rows = []; - if (is_callable($param)) { + if ($param instanceof \Closure) { foreach ($this->getRows() as $i => $row) { if (call_user_func($param, $row)) { $rows[$i] = $row; diff --git a/ui/tests/selenium/testTriggerDependencies.php b/ui/tests/selenium/testTriggerDependencies.php index fe4c9aa408a..1e4404740ba 100644 --- a/ui/tests/selenium/testTriggerDependencies.php +++ b/ui/tests/selenium/testTriggerDependencies.php @@ -20,6 +20,7 @@ require_once dirname(__FILE__).'/../include/CLegacyWebTest.php'; require_once dirname(__FILE__).'/../include/helpers/CDataHelper.php'; +require_once dirname(__FILE__).'/behaviors/CMessageBehavior.php'; use Facebook\WebDriver\WebDriverBy; @@ -29,6 +30,14 @@ use Facebook\WebDriver\WebDriverBy; * @onBefore prepareTemplateData */ class testTriggerDependencies extends CLegacyWebTest { + + /** + * Attach MessageBehavior to the test. + */ + public function getBehaviors() { + return [CMessageBehavior::class]; + } + const TEMPLATE_AGENT = 'Zabbix agent'; const TEMPLATE_FREEBSD = 'FreeBSD by Zabbix agent'; const TEMPLATE_APACHE = 'Apache by HTTP'; @@ -64,58 +73,80 @@ class testTriggerDependencies extends CLegacyWebTest { /** * @dataProvider getTriggerDependenciesData */ - public function testTriggerDependenciesFromHost_SimpleTest($trigger, $template, $dependencies, $expected) { + public function testTriggerDependenciesFromHost_SimpleTest($data) { // Get the id of template to be updated based on the template that owns the trigger in dependencies tab. $ids = [ self::TEMPLATE_AGENT => self::$agent_templateid, self::TEMPLATE_APACHE => self::$apache_templateid ]; - $update_id = ($template === self::TEMPLATE_APACHE) ? $ids[self::TEMPLATE_APACHE] : $ids[self::TEMPLATE_AGENT]; + $update_id = ($data['template'] === self::TEMPLATE_APACHE) ? $ids[self::TEMPLATE_APACHE] : $ids[self::TEMPLATE_AGENT]; $this->zbxTestLogin('triggers.php?filter_set=1&context=template&filter_hostids[0]='.$update_id); $this->zbxTestCheckTitle('Configuration of triggers'); - $this->zbxTestClickLinkTextWait($trigger); + $this->zbxTestClickLinkTextWait($data['trigger']); $this->zbxTestClickWait('tab_dependenciesTab'); $this->zbxTestClick('bnt1'); $this->zbxTestLaunchOverlayDialog('Triggers'); $host = COverlayDialogElement::find()->one()->query('class:multiselect-control')->asMultiselect()->one(); $host->fill([ - 'values' => $template, + 'values' => $data['template'], 'context' => 'Templates' ]); - $this->zbxTestClickLinkTextWait($dependencies); + $this->zbxTestClickLinkTextWait($data['dependency']); $this->zbxTestWaitUntilElementVisible(WebDriverBy::id('bnt1')); $this->zbxTestClickWait('update'); - $this->zbxTestTextPresent($expected); + if ($data['expected'] === TEST_BAD) { + $this->assertMessage(TEST_BAD, 'Cannot update trigger', $data['error_message']); + } + else { + $this->assertMessage(TEST_GOOD, 'Trigger updated'); + } } public function getTriggerDependenciesData() { return [ [ - 'Zabbix agent is not available', - self::TEMPLATE_FREEBSD, - '/etc/passwd has been changed on FreeBSD by Zabbix agent', - 'Not all templates are linked to' + [ + 'expected' => TEST_BAD, + 'trigger' => 'Zabbix agent is not available', + 'template' => self::TEMPLATE_FREEBSD, + 'dependency' => '/etc/passwd has been changed on FreeBSD by Zabbix agent', + 'error_message' => 'Trigger "Zabbix agent is not available" cannot depend on the trigger "/etc/passwd has been changed'. + ' on {HOST.NAME}" from the template "FreeBSD by Zabbix agent", because dependencies on triggers'. + ' from a child template or host are not allowed.' + ] ], [ - 'Apache: Service is down', - self::TEMPLATE_APACHE, - 'Apache: Service response time is too high', - 'Cannot create circular dependencies.' + [ + 'expected' => TEST_BAD, + 'trigger' => 'Apache: Service is down', + 'template' => self::TEMPLATE_APACHE, + 'dependency' => 'Apache: Service response time is too high', + 'error_message' => 'Trigger "Apache: Service is down" cannot depend on the trigger "Apache: Service response'. + ' time is too high", because a circular linkage ("Apache: Service response time is too high" ->'. + ' "Apache: Service is down" -> "Apache: Service response time is too high") would occur.' + ] ], [ - 'Apache: has been restarted', - self::TEMPLATE_APACHE, - 'Apache: has been restarted', - 'Cannot create dependency on trigger itself.' + [ + 'expected' => TEST_BAD, + 'trigger' => 'Apache: has been restarted', + 'template' => self::TEMPLATE_APACHE, + 'dependency' => 'Apache: has been restarted', + 'error_message' => 'Trigger "Apache: has been restarted" cannot depend on the trigger "Apache: '. + 'has been restarted", because a circular linkage ("Apache: has been restarted" -> "Apache: '. + 'has been restarted") would occur.' + ] ], [ - 'Apache: has been restarted', - self::TEMPLATE_APACHE, - 'Apache: Service is down', - 'Trigger updated' + [ + 'expected' => TEST_GOOD, + 'trigger' => 'Apache: has been restarted', + 'template' => self::TEMPLATE_APACHE, + 'dependency' => 'Apache: Service is down' + ] ] ]; } diff --git a/ui/triggers.php b/ui/triggers.php index f79c209ca10..62ec7d9506b 100644 --- a/ui/triggers.php +++ b/ui/triggers.php @@ -523,7 +523,7 @@ elseif (hasRequest('action') && getRequest('action') === 'trigger.masscopyto' && DBstart(); - $result = copyTriggersToHosts(getRequest('g_triggerid'), $hosts_ids, getRequest('hostid')); + $result = copyTriggersToHosts($hosts_ids, getRequest('hostid'), getRequest('g_triggerid')); $result = DBend($result); $triggers_count = count(getRequest('g_triggerid')); |