1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22/** 23 * Class containing methods for operations with trigger prototypes. 24 */ 25class CTriggerPrototype extends CTriggerGeneral { 26 27 public const ACCESS_RULES = [ 28 'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER], 29 'create' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN], 30 'update' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN], 31 'delete' => ['min_user_type' => USER_TYPE_ZABBIX_ADMIN] 32 ]; 33 34 protected $tableName = 'triggers'; 35 protected $tableAlias = 't'; 36 protected $sortColumns = ['triggerid', 'description', 'status', 'priority', 'discover']; 37 38 /** 39 * Get trigger prototypes from database. 40 * 41 * @param array $options 42 * 43 * @return array|int 44 */ 45 public function get(array $options = []) { 46 $result = []; 47 48 $sqlParts = [ 49 'select' => ['triggers' => 't.triggerid'], 50 'from' => ['t' => 'triggers t'], 51 'where' => ['t.flags='.ZBX_FLAG_DISCOVERY_PROTOTYPE], 52 'group' => [], 53 'order' => [], 54 'limit' => null 55 ]; 56 57 $defOptions = [ 58 'groupids' => null, 59 'templateids' => null, 60 'hostids' => null, 61 'triggerids' => null, 62 'itemids' => null, 63 'discoveryids' => null, 64 'functions' => null, 65 'inherited' => null, 66 'templated' => null, 67 'monitored' => null, 68 'active' => null, 69 'maintenance' => null, 70 'nopermissions' => null, 71 'editable' => false, 72 // filter 73 'group' => null, 74 'host' => null, 75 'min_severity' => null, 76 'filter' => null, 77 'search' => null, 78 'searchByAny' => null, 79 'startSearch' => false, 80 'excludeSearch' => false, 81 'searchWildcardsEnabled' => null, 82 // output 83 'expandExpression' => null, 84 'output' => API_OUTPUT_EXTEND, 85 'selectGroups' => null, 86 'selectHosts' => null, 87 'selectItems' => null, 88 'selectFunctions' => null, 89 'selectDependencies' => null, 90 'selectDiscoveryRule' => null, 91 'selectTags' => null, 92 'countOutput' => false, 93 'groupCount' => false, 94 'preservekeys' => false, 95 'sortfield' => '', 96 'sortorder' => '', 97 'limit' => null, 98 'limitSelects' => null 99 ]; 100 $options = zbx_array_merge($defOptions, $options); 101 102 // editable + permission check 103 if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) { 104 $permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ; 105 $userGroups = getUserGroupsByUserId(self::$userData['userid']); 106 107 $sqlParts['where'][] = 'NOT EXISTS ('. 108 'SELECT NULL'. 109 ' FROM functions f,items i,hosts_groups hgg'. 110 ' LEFT JOIN rights r'. 111 ' ON r.id=hgg.groupid'. 112 ' AND '.dbConditionInt('r.groupid', $userGroups). 113 ' WHERE t.triggerid=f.triggerid'. 114 ' AND f.itemid=i.itemid'. 115 ' AND i.hostid=hgg.hostid'. 116 ' GROUP BY i.hostid'. 117 ' HAVING MAX(permission)<'.zbx_dbstr($permission). 118 ' OR MIN(permission) IS NULL'. 119 ' OR MIN(permission)='.PERM_DENY. 120 ')'; 121 } 122 123 // groupids 124 if ($options['groupids'] !== null) { 125 zbx_value2array($options['groupids']); 126 127 $sqlParts['from']['functions'] = 'functions f'; 128 $sqlParts['from']['items'] = 'items i'; 129 $sqlParts['from']['hosts_groups'] = 'hosts_groups hg'; 130 $sqlParts['where']['hgi'] = 'hg.hostid=i.hostid'; 131 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 132 $sqlParts['where']['fi'] = 'f.itemid=i.itemid'; 133 $sqlParts['where']['groupid'] = dbConditionInt('hg.groupid', $options['groupids']); 134 135 if ($options['groupCount']) { 136 $sqlParts['group']['hg'] = 'hg.groupid'; 137 } 138 } 139 140 // templateids 141 if ($options['templateids'] !== null) { 142 zbx_value2array($options['templateids']); 143 144 if ($options['hostids'] !== null) { 145 zbx_value2array($options['hostids']); 146 $options['hostids'] = array_merge($options['hostids'], $options['templateids']); 147 } 148 else { 149 $options['hostids'] = $options['templateids']; 150 } 151 } 152 153 // hostids 154 if ($options['hostids'] !== null) { 155 zbx_value2array($options['hostids']); 156 157 $sqlParts['from']['functions'] = 'functions f'; 158 $sqlParts['from']['items'] = 'items i'; 159 $sqlParts['where']['hostid'] = dbConditionInt('i.hostid', $options['hostids']); 160 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 161 $sqlParts['where']['fi'] = 'f.itemid=i.itemid'; 162 163 if ($options['groupCount']) { 164 $sqlParts['group']['i'] = 'i.hostid'; 165 } 166 } 167 168 // triggerids 169 if ($options['triggerids'] !== null) { 170 zbx_value2array($options['triggerids']); 171 172 $sqlParts['where']['triggerid'] = dbConditionInt('t.triggerid', $options['triggerids']); 173 } 174 175 // itemids 176 if ($options['itemids'] !== null) { 177 zbx_value2array($options['itemids']); 178 179 $sqlParts['from']['functions'] = 'functions f'; 180 $sqlParts['where']['itemid'] = dbConditionInt('f.itemid', $options['itemids']); 181 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 182 183 if ($options['groupCount']) { 184 $sqlParts['group']['f'] = 'f.itemid'; 185 } 186 } 187 188 // discoveryids 189 if ($options['discoveryids'] !== null) { 190 zbx_value2array($options['discoveryids']); 191 192 $sqlParts['from']['functions'] = 'functions f'; 193 $sqlParts['from']['item_discovery'] = 'item_discovery id'; 194 $sqlParts['where']['fid'] = 'f.itemid=id.itemid'; 195 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 196 $sqlParts['where'][] = dbConditionInt('id.parent_itemid', $options['discoveryids']); 197 198 if ($options['groupCount']) { 199 $sqlParts['group']['id'] = 'id.parent_itemid'; 200 } 201 } 202 203 // functions 204 if ($options['functions'] !== null) { 205 zbx_value2array($options['functions']); 206 207 $sqlParts['from']['functions'] = 'functions f'; 208 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 209 $sqlParts['where'][] = dbConditionString('f.name', $options['functions']); 210 } 211 212 // monitored 213 if ($options['monitored'] !== null) { 214 $sqlParts['where']['monitored'] = 215 ' NOT EXISTS ('. 216 ' SELECT NULL'. 217 ' FROM functions ff'. 218 ' WHERE ff.triggerid=t.triggerid'. 219 ' AND EXISTS ('. 220 ' SELECT NULL'. 221 ' FROM items ii,hosts hh'. 222 ' WHERE ff.itemid=ii.itemid'. 223 ' AND hh.hostid=ii.hostid'. 224 ' AND ('. 225 ' ii.status<>'.ITEM_STATUS_ACTIVE. 226 ' OR hh.status<>'.HOST_STATUS_MONITORED. 227 ' )'. 228 ' )'. 229 ' )'; 230 $sqlParts['where']['status'] = 't.status='.TRIGGER_STATUS_ENABLED; 231 } 232 233 // active 234 if ($options['active'] !== null) { 235 $sqlParts['where']['active'] = 236 ' NOT EXISTS ('. 237 ' SELECT NULL'. 238 ' FROM functions ff'. 239 ' WHERE ff.triggerid=t.triggerid'. 240 ' AND EXISTS ('. 241 ' SELECT NULL'. 242 ' FROM items ii,hosts hh'. 243 ' WHERE ff.itemid=ii.itemid'. 244 ' AND hh.hostid=ii.hostid'. 245 ' AND hh.status<>'.HOST_STATUS_MONITORED. 246 ' )'. 247 ' )'; 248 $sqlParts['where']['status'] = 't.status='.TRIGGER_STATUS_ENABLED; 249 } 250 251 // maintenance 252 if ($options['maintenance'] !== null) { 253 $sqlParts['where'][] = (($options['maintenance'] == 0) ? ' NOT ' : ''). 254 ' EXISTS ('. 255 ' SELECT NULL'. 256 ' FROM functions ff'. 257 ' WHERE ff.triggerid=t.triggerid'. 258 ' AND EXISTS ('. 259 ' SELECT NULL'. 260 ' FROM items ii,hosts hh'. 261 ' WHERE ff.itemid=ii.itemid'. 262 ' AND hh.hostid=ii.hostid'. 263 ' AND hh.maintenance_status=1'. 264 ' )'. 265 ' )'; 266 $sqlParts['where'][] = 't.status='.TRIGGER_STATUS_ENABLED; 267 } 268 269 // templated 270 if ($options['templated'] !== null) { 271 $sqlParts['from']['functions'] = 'functions f'; 272 $sqlParts['from']['items'] = 'items i'; 273 $sqlParts['from']['hosts'] = 'hosts h'; 274 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 275 $sqlParts['where']['fi'] = 'f.itemid=i.itemid'; 276 $sqlParts['where']['hi'] = 'h.hostid=i.hostid'; 277 278 if ($options['templated']) { 279 $sqlParts['where'][] = 'h.status='.HOST_STATUS_TEMPLATE; 280 } 281 else { 282 $sqlParts['where'][] = 'h.status<>'.HOST_STATUS_TEMPLATE; 283 } 284 } 285 286 // inherited 287 if ($options['inherited'] !== null) { 288 if ($options['inherited']) { 289 $sqlParts['where'][] = 't.templateid IS NOT NULL'; 290 } 291 else { 292 $sqlParts['where'][] = 't.templateid IS NULL'; 293 } 294 } 295 296 // search 297 if (is_array($options['search'])) { 298 zbx_db_search('triggers t', $options, $sqlParts); 299 } 300 301 // filter 302 if (is_array($options['filter'])) { 303 $this->dbFilter('triggers t', $options, $sqlParts); 304 305 if (isset($options['filter']['host']) && $options['filter']['host'] !== null) { 306 zbx_value2array($options['filter']['host']); 307 308 $sqlParts['from']['functions'] = 'functions f'; 309 $sqlParts['from']['items'] = 'items i'; 310 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 311 $sqlParts['where']['fi'] = 'f.itemid=i.itemid'; 312 313 $sqlParts['from']['hosts'] = 'hosts h'; 314 $sqlParts['where']['hi'] = 'h.hostid=i.hostid'; 315 $sqlParts['where']['host'] = dbConditionString('h.host', $options['filter']['host']); 316 } 317 318 if (isset($options['filter']['hostid']) && $options['filter']['hostid'] !== null) { 319 zbx_value2array($options['filter']['hostid']); 320 321 $sqlParts['from']['functions'] = 'functions f'; 322 $sqlParts['from']['items'] = 'items i'; 323 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 324 $sqlParts['where']['fi'] = 'f.itemid=i.itemid'; 325 326 $sqlParts['where']['hostid'] = dbConditionInt('i.hostid', $options['filter']['hostid']); 327 } 328 } 329 330 // group 331 if ($options['group'] !== null) { 332 $sqlParts['from']['functions'] = 'functions f'; 333 $sqlParts['from']['items'] = 'items i'; 334 $sqlParts['from']['hosts_groups'] = 'hosts_groups hg'; 335 $sqlParts['from']['hstgrp'] = 'hstgrp g'; 336 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 337 $sqlParts['where']['fi'] = 'f.itemid=i.itemid'; 338 $sqlParts['where']['hgi'] = 'hg.hostid=i.hostid'; 339 $sqlParts['where']['ghg'] = 'g.groupid=hg.groupid'; 340 $sqlParts['where']['group'] = ' g.name='.zbx_dbstr($options['group']); 341 } 342 343 // host 344 if ($options['host'] !== null) { 345 $sqlParts['from']['functions'] = 'functions f'; 346 $sqlParts['from']['items'] = 'items i'; 347 $sqlParts['from']['hosts'] = 'hosts h'; 348 $sqlParts['where']['i'] = dbConditionInt('i.hostid', $options['hostids']); 349 $sqlParts['where']['ft'] = 'f.triggerid=t.triggerid'; 350 $sqlParts['where']['fi'] = 'f.itemid=i.itemid'; 351 $sqlParts['where']['hi'] = 'h.hostid=i.hostid'; 352 $sqlParts['where']['host'] = ' h.host='.zbx_dbstr($options['host']); 353 } 354 355 // min_severity 356 if ($options['min_severity'] !== null) { 357 $sqlParts['where'][] = 't.priority>='.zbx_dbstr($options['min_severity']); 358 } 359 360 // limit 361 if (zbx_ctype_digit($options['limit']) && $options['limit']) { 362 $sqlParts['limit'] = $options['limit']; 363 } 364 365 $sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts); 366 $sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts); 367 $dbRes = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']); 368 while ($triggerPrototype = DBfetch($dbRes)) { 369 if ($options['countOutput']) { 370 if ($options['groupCount']) { 371 $result[] = $triggerPrototype; 372 } 373 else { 374 $result = $triggerPrototype['rowscount']; 375 } 376 } 377 else { 378 $result[$triggerPrototype['triggerid']] = $triggerPrototype; 379 } 380 } 381 382 if ($options['countOutput']) { 383 return $result; 384 } 385 386 if ($result) { 387 $result = $this->addRelatedObjects($options, $result); 388 } 389 390 // expand expressions 391 if ($options['expandExpression'] !== null && $result) { 392 $sources = []; 393 if (array_key_exists('expression', reset($result))) { 394 $sources[] = 'expression'; 395 } 396 if (array_key_exists('recovery_expression', reset($result))) { 397 $sources[] = 'recovery_expression'; 398 } 399 400 if ($sources) { 401 $result = CMacrosResolverHelper::resolveTriggerExpressions($result, 402 ['resolve_usermacros' => true, 'resolve_macros' => true, 'sources' => $sources] 403 ); 404 } 405 } 406 407 // removing keys (hash -> array) 408 if (!$options['preservekeys']) { 409 $result = zbx_cleanHashes($result); 410 } 411 412 return $result; 413 } 414 415 /** 416 * Create new trigger prototypes. 417 * 418 * @param array $trigger_prototypes 419 * 420 * @return array 421 */ 422 public function create(array $trigger_prototypes) { 423 $this->validateCreate($trigger_prototypes); 424 $this->createReal($trigger_prototypes); 425 $this->inherit($trigger_prototypes); 426 427 $addDependencies = false; 428 429 foreach ($trigger_prototypes as $trigger_prototype) { 430 if (isset($trigger_prototype['dependencies']) && is_array($trigger_prototype['dependencies']) 431 && $trigger_prototype['dependencies']) { 432 $addDependencies = true; 433 break; 434 } 435 } 436 437 if ($addDependencies) { 438 $this->addDependencies($trigger_prototypes); 439 } 440 441 return ['triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid')]; 442 } 443 444 /** 445 * Update existing trigger prototypes. 446 * 447 * @param array $trigger_prototypes 448 * 449 * @return array 450 */ 451 public function update(array $trigger_prototypes) { 452 $this->validateUpdate($trigger_prototypes, $db_triggers); 453 $this->updateReal($trigger_prototypes, $db_triggers); 454 $this->inherit($trigger_prototypes); 455 456 $updateDependencies = false; 457 458 foreach ($trigger_prototypes as $trigger_prototype) { 459 if (isset($trigger_prototype['dependencies']) && is_array($trigger_prototype['dependencies'])) { 460 $updateDependencies = true; 461 break; 462 } 463 } 464 465 if ($updateDependencies) { 466 $this->updateDependencies($trigger_prototypes); 467 } 468 469 return ['triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid')]; 470 } 471 472 /** 473 * Delete existing trigger prototypes. 474 * 475 * @param array $triggerids 476 * 477 * @throws APIException 478 * 479 * @return array 480 */ 481 public function delete(array $triggerids) { 482 $this->validateDelete($triggerids, $db_triggers); 483 484 CTriggerPrototypeManager::delete($triggerids); 485 486 $this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_TRIGGER_PROTOTYPE, $db_triggers); 487 488 return ['triggerids' => $triggerids]; 489 } 490 491 /** 492 * Validates the input parameters for the delete() method. 493 * 494 * @param array $triggerids [IN/OUT] 495 * @param array $db_triggers [OUT] 496 * 497 * @throws APIException if the input is invalid. 498 */ 499 protected function validateDelete(array &$triggerids, array &$db_triggers = null) { 500 $api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true]; 501 if (!CApiInputValidator::validate($api_input_rules, $triggerids, '/', $error)) { 502 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 503 } 504 505 $db_triggers = $this->get([ 506 'output' => ['triggerid', 'description', 'expression', 'templateid'], 507 'triggerids' => $triggerids, 508 'editable' => true, 509 'preservekeys' => true 510 ]); 511 512 foreach ($triggerids as $triggerid) { 513 if (!array_key_exists($triggerid, $db_triggers)) { 514 self::exception(ZBX_API_ERROR_PERMISSIONS, 515 _('No permissions to referred object or it does not exist!') 516 ); 517 } 518 519 $db_trigger = $db_triggers[$triggerid]; 520 521 if ($db_trigger['templateid'] != 0) { 522 self::exception(ZBX_API_ERROR_PARAMETERS, 523 _s('Cannot delete templated trigger prototype "%1$s:%2$s".', $db_trigger['description'], 524 CMacrosResolverHelper::resolveTriggerExpression($db_trigger['expression']) 525 ) 526 ); 527 } 528 } 529 } 530 531 /** 532 * Update the given dependencies and inherit them on all child triggers. 533 * 534 * @param array $triggerPrototypes 535 */ 536 protected function updateDependencies(array $triggerPrototypes) { 537 $this->deleteDependencies($triggerPrototypes); 538 539 $this->addDependencies($triggerPrototypes); 540 } 541 542 /** 543 * Deletes all trigger and trigger prototype dependencies from the given trigger prototypes and their children. 544 * 545 * @param array $triggerPrototypes 546 * @param string $triggerPrototypes[]['triggerid'] 547 */ 548 protected function deleteDependencies(array $triggerPrototypes) { 549 $triggerPrototypeIds = zbx_objectValues($triggerPrototypes, 'triggerid'); 550 551 try { 552 // Delete the dependencies from the child trigger prototypes. 553 554 $childTriggerPrototypes = API::getApiService()->select($this->tableName(), [ 555 'output' => ['triggerid'], 556 'filter' => [ 557 'templateid' => $triggerPrototypeIds 558 ] 559 ]); 560 561 if ($childTriggerPrototypes) { 562 $this->deleteDependencies($childTriggerPrototypes); 563 } 564 565 DB::delete('trigger_depends', [ 566 'triggerid_down' => $triggerPrototypeIds 567 ]); 568 } 569 catch (APIException $e) { 570 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot delete dependency')); 571 } 572 } 573 574 /** 575 * Add the given dependencies and inherit them on all child triggers. 576 * 577 * @param array $triggerPrototypes 578 * @param string $triggerPrototypes[]['triggerid'] 579 * @param array $triggerPrototypes[]['dependencies'] 580 * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] 581 * @param bool $inherited Determines either to check permissions for 582 * added dependencies. Permissions are not 583 * validated for inherited triggers. 584 */ 585 public function addDependencies(array $triggerPrototypes, bool $inherited = false) { 586 $this->validateAddDependencies($triggerPrototypes, $inherited); 587 588 $insert = []; 589 590 foreach ($triggerPrototypes as $triggerPrototype) { 591 if (!array_key_exists('dependencies', $triggerPrototype)) { 592 continue; 593 } 594 595 foreach ($triggerPrototype['dependencies'] as $dependency) { 596 $insert[] = [ 597 'triggerid_down' => $triggerPrototype['triggerid'], 598 'triggerid_up' => $dependency['triggerid'] 599 ]; 600 } 601 } 602 603 DB::insertBatch('trigger_depends', $insert); 604 605 foreach ($triggerPrototypes as $triggerPrototype) { 606 // Propagate the dependencies to the child triggers. 607 608 $childTriggers = API::getApiService()->select($this->tableName(), [ 609 'output' => ['triggerid'], 610 'filter' => [ 611 'templateid' => $triggerPrototype['triggerid'] 612 ] 613 ]); 614 615 if ($childTriggers) { 616 foreach ($childTriggers as &$childTrigger) { 617 $childTrigger['dependencies'] = []; 618 $childHostsQuery = get_hosts_by_triggerid($childTrigger['triggerid']); 619 620 while ($childHost = DBfetch($childHostsQuery)) { 621 foreach ($triggerPrototype['dependencies'] as $dependency) { 622 $newDependency = [$childTrigger['triggerid'] => $dependency['triggerid']]; 623 $newDependency = replace_template_dependencies($newDependency, $childHost['hostid']); 624 625 $childTrigger['dependencies'][] = [ 626 'triggerid' => $newDependency[$childTrigger['triggerid']] 627 ]; 628 } 629 } 630 } 631 unset($childTrigger); 632 633 $this->addDependencies($childTriggers, true); 634 } 635 } 636 } 637 638 /** 639 * Validates the input for the addDependencies() method. 640 * 641 * @param array $trigger_prototypes 642 * @param string $trigger_prototypes[]['triggerid'] 643 * @param array $trigger_prototypes[]['dependencies'] 644 * @param string $trigger_prototypes[]['dependencies'][]['triggerid'] 645 * @param bool $inherited 646 * 647 * @throws APIException if the given dependencies are invalid. 648 */ 649 protected function validateAddDependencies(array $trigger_prototypes, bool $inherited = false): void { 650 $depTriggerIds = []; 651 652 foreach ($trigger_prototypes as $trigger_prototype) { 653 if (!array_key_exists('dependencies', $trigger_prototype)) { 654 continue; 655 } 656 657 foreach ($trigger_prototype['dependencies'] as $dependency) { 658 $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; 659 } 660 } 661 662 if (!$depTriggerIds) { 663 return; 664 } 665 666 // Check if given IDs are actual trigger prototypes and get discovery rules if they are. 667 $depTriggerPrototypes = $this->get([ 668 'output' => ['triggerid'], 669 'selectDiscoveryRule' => ['itemid'], 670 'triggerids' => $depTriggerIds, 671 'nopermissions' => $inherited ? true : null, 672 'preservekeys' => true 673 ]); 674 675 $dep_triggerids = array_diff($depTriggerIds, array_keys($depTriggerPrototypes)); 676 677 if ($depTriggerPrototypes) { 678 // Get current trigger prototype discovery rules. 679 $dRules = $this->get([ 680 'output' => ['triggerid'], 681 'selectDiscoveryRule' => ['itemid'], 682 'triggerids' => zbx_objectValues($trigger_prototypes, 'triggerid'), 683 'nopermissions' => $inherited ? true : null, 684 'preservekeys' => true 685 ]); 686 687 foreach ($trigger_prototypes as $trigger_prototype) { 688 if (!array_key_exists('dependencies', $trigger_prototype)) { 689 continue; 690 } 691 692 $dRuleId = $dRules[$trigger_prototype['triggerid']]['discoveryRule']['itemid']; 693 694 // Check if current trigger prototype rules match dependent trigger prototype rules. 695 foreach ($trigger_prototype['dependencies'] as $dependency) { 696 if (isset($depTriggerPrototypes[$dependency['triggerid']])) { 697 $depTriggerDRuleId = $depTriggerPrototypes[$dependency['triggerid']]['discoveryRule']['itemid']; 698 699 if (bccomp($depTriggerDRuleId, $dRuleId) != 0) { 700 self::exception(ZBX_API_ERROR_PERMISSIONS, 701 _('No permissions to referred object or it does not exist!') 702 ); 703 } 704 } 705 } 706 } 707 } 708 elseif (!$dep_triggerids) { 709 self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); 710 } 711 712 if ($dep_triggerids && !$inherited) { 713 // Check other dependency IDs if those are normal triggers. 714 $count = API::Trigger()->get([ 715 'countOutput' => true, 716 'triggerids' => $dep_triggerids, 717 'filter' => [ 718 'flags' => [ZBX_FLAG_DISCOVERY_NORMAL] 719 ] 720 ]); 721 722 if ($count != count($dep_triggerids)) { 723 self::exception(ZBX_API_ERROR_PERMISSIONS, 724 _('No permissions to referred object or it does not exist!') 725 ); 726 } 727 } 728 729 $this->checkDependencies($trigger_prototypes); 730 $this->checkDependencyParents($trigger_prototypes); 731 $this->checkDependencyDuplicates($trigger_prototypes); 732 } 733 734 /** 735 * Check the dependencies of the given trigger prototypes. 736 * 737 * @param array $triggerPrototypes 738 * @param string $triggerPrototypes[]['triggerid'] 739 * @param array $triggerPrototypes[]['dependencies'] 740 * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] 741 * 742 * @throws APIException if any of the dependencies are invalid. 743 */ 744 protected function checkDependencies(array $triggerPrototypes) { 745 $triggerPrototypes = zbx_toHash($triggerPrototypes, 'triggerid'); 746 747 foreach ($triggerPrototypes as $triggerPrototype) { 748 if (!array_key_exists('dependencies', $triggerPrototype)) { 749 continue; 750 } 751 752 $triggerid_down = $triggerPrototype['triggerid']; 753 $triggerids_up = zbx_objectValues($triggerPrototype['dependencies'], 'triggerid'); 754 755 foreach ($triggerids_up as $triggerid_up) { 756 if (bccomp($triggerid_down, $triggerid_up) == 0) { 757 self::exception(ZBX_API_ERROR_PARAMETERS, 758 _('Cannot create dependency on trigger prototype itself.') 759 ); 760 } 761 } 762 } 763 764 foreach ($triggerPrototypes as $triggerPrototype) { 765 if (!array_key_exists('dependencies', $triggerPrototype)) { 766 continue; 767 } 768 769 $depTriggerIds = zbx_objectValues($triggerPrototype['dependencies'], 'triggerid'); 770 771 $triggerTemplates = API::Template()->get([ 772 'output' => ['hostid', 'status'], 773 'triggerids' => [$triggerPrototype['triggerid']], 774 'nopermissions' => true 775 ]); 776 777 if (!$triggerTemplates) { 778 // Current trigger prototype belongs to a host, so forbid dependencies from a host to a template. 779 780 $triggerDepTemplates = API::Template()->get([ 781 'output' => ['templateid'], 782 'triggerids' => $depTriggerIds, 783 'nopermissions' => true, 784 'limit' => 1 785 ]); 786 787 if ($triggerDepTemplates) { 788 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot add dependency from a host to a template.')); 789 } 790 } 791 792 // check circular dependency 793 $downTriggerIds = [$triggerPrototype['triggerid']]; 794 do { 795 // triggerid_down depends on triggerid_up 796 $res = DBselect( 797 'SELECT td.triggerid_up'. 798 ' FROM trigger_depends td'. 799 ' WHERE '.dbConditionInt('td.triggerid_down', $downTriggerIds) 800 ); 801 802 // combine db dependencies with those to be added 803 $upTriggersIds = []; 804 while ($row = DBfetch($res)) { 805 $upTriggersIds[] = $row['triggerid_up']; 806 } 807 foreach ($downTriggerIds as $id) { 808 if (isset($triggerPrototypes[$id]) && isset($triggerPrototypes[$id]['dependencies'])) { 809 $upTriggersIds = array_merge($upTriggersIds, 810 zbx_objectValues($triggerPrototypes[$id]['dependencies'], 'triggerid') 811 ); 812 } 813 } 814 815 // if found trigger id is in dependent triggerids, there is a dependency loop 816 $downTriggerIds = []; 817 foreach ($upTriggersIds as $id) { 818 if (bccomp($id, $triggerPrototype['triggerid']) == 0) { 819 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create circular dependencies.')); 820 } 821 $downTriggerIds[] = $id; 822 } 823 } while (!empty($downTriggerIds)); 824 825 // fetch all templates that are used in dependencies 826 $triggerDepTemplates = API::Template()->get([ 827 'output' => ['templateid'], 828 'triggerids' => $depTriggerIds, 829 'nopermissions' => true, 830 'preservekeys' => true 831 ]); 832 833 $depTemplateIds = array_keys($triggerDepTemplates); 834 835 // run the check only if a templated trigger has dependencies on other templates 836 $triggerTemplateIds = zbx_toHash(zbx_objectValues($triggerTemplates, 'templateid')); 837 $tdiff = array_diff($depTemplateIds, $triggerTemplateIds); 838 839 if (!empty($triggerTemplateIds) && !empty($depTemplateIds) && !empty($tdiff)) { 840 $affectedTemplateIds = zbx_array_merge($triggerTemplateIds, $depTemplateIds); 841 842 // create a list of all hosts, that are children of the affected templates 843 $dbLowlvltpl = DBselect( 844 'SELECT DISTINCT ht.templateid,ht.hostid,h.host'. 845 ' FROM hosts_templates ht,hosts h'. 846 ' WHERE h.hostid=ht.hostid'. 847 ' AND '.dbConditionInt('ht.templateid', $affectedTemplateIds) 848 ); 849 $map = []; 850 while ($lowlvltpl = DBfetch($dbLowlvltpl)) { 851 if (!isset($map[$lowlvltpl['hostid']])) { 852 $map[$lowlvltpl['hostid']] = []; 853 } 854 $map[$lowlvltpl['hostid']][$lowlvltpl['templateid']] = $lowlvltpl['host']; 855 } 856 857 // check that if some host is linked to the template, that the trigger belongs to, 858 // the host must also be linked to all of the templates, that trigger dependencies point to 859 foreach ($map as $templates) { 860 foreach ($triggerTemplateIds as $triggerTemplateId) { 861 // is the host linked to one of the trigger templates? 862 if (isset($templates[$triggerTemplateId])) { 863 // then make sure all of the dependency templates are also linked 864 foreach ($depTemplateIds as $depTemplateId) { 865 if (!isset($templates[$depTemplateId])) { 866 self::exception(ZBX_API_ERROR_PARAMETERS, 867 _s('Not all templates are linked to "%1$s".', reset($templates)) 868 ); 869 } 870 } 871 break; 872 } 873 } 874 } 875 } 876 } 877 } 878 879 /** 880 * Check that none of the triggers have dependencies on their children. Checks only one level of inheritance, but 881 * since it is called on each inheritance step, also works for multiple inheritance levels. 882 * 883 * @param array $triggerPrototypes 884 * @param string $triggerPrototypes[]['triggerid'] 885 * @param array $triggerPrototypes[]['dependencies'] 886 * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] 887 * 888 * @throws APIException if at least one trigger is dependent on its child. 889 */ 890 protected function checkDependencyParents(array $triggerPrototypes) { 891 // fetch all templated dependency trigger parents 892 $depTriggerIds = []; 893 894 foreach ($triggerPrototypes as $triggerPrototype) { 895 if (!array_key_exists('dependencies', $triggerPrototype)) { 896 continue; 897 } 898 899 foreach ($triggerPrototype['dependencies'] as $dependency) { 900 $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; 901 } 902 } 903 904 $parentDepTriggers = DBfetchArray(DBSelect( 905 'SELECT templateid,triggerid'. 906 ' FROM triggers'. 907 ' WHERE templateid>0'. 908 ' AND '.dbConditionInt('triggerid', $depTriggerIds) 909 )); 910 911 if ($parentDepTriggers) { 912 $parentDepTriggers = zbx_toHash($parentDepTriggers, 'triggerid'); 913 914 foreach ($triggerPrototypes as $triggerPrototype) { 915 foreach ($triggerPrototype['dependencies'] as $dependency) { 916 // Check if the current trigger is the parent of the dependency trigger. 917 918 $depTriggerId = $dependency['triggerid']; 919 920 if (isset($parentDepTriggers[$depTriggerId]) 921 && $parentDepTriggers[$depTriggerId]['templateid'] == $triggerPrototype['triggerid']) { 922 923 self::exception(ZBX_API_ERROR_PARAMETERS, 924 _s('Trigger prototype cannot be dependent on a trigger that is inherited from it.') 925 ); 926 } 927 } 928 } 929 } 930 } 931 932 /** 933 * Checks if the given dependencies contain duplicates. 934 * 935 * @param array $triggerPrototypes 936 * @param string $triggerPrototypes[]['triggerid'] 937 * @param array $triggerPrototypes[]['dependencies'] 938 * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] 939 * 940 * @throws APIException if the given dependencies contain duplicates. 941 */ 942 protected function checkDependencyDuplicates(array $triggerPrototypes) { 943 // check duplicates in array 944 $uniqueTriggers = []; 945 $depTriggerIds = []; 946 $duplicateTriggerId = null; 947 948 foreach ($triggerPrototypes as $triggerPrototype) { 949 if (!array_key_exists('dependencies', $triggerPrototype)) { 950 continue; 951 } 952 953 foreach ($triggerPrototype['dependencies'] as $dependency) { 954 $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; 955 956 if (isset($uniqueTriggers[$triggerPrototype['triggerid']][$dependency['triggerid']])) { 957 $duplicateTriggerId = $triggerPrototype['triggerid']; 958 break 2; 959 } 960 else { 961 $uniqueTriggers[$triggerPrototype['triggerid']][$dependency['triggerid']] = 1; 962 } 963 } 964 } 965 966 if ($duplicateTriggerId === null) { 967 // check if dependency already exists in DB 968 foreach ($triggerPrototypes as $triggerPrototype) { 969 $dbUpTriggers = DBselect( 970 'SELECT td.triggerid_up'. 971 ' FROM trigger_depends td'. 972 ' WHERE '.dbConditionInt('td.triggerid_up', $depTriggerIds). 973 ' AND td.triggerid_down='.zbx_dbstr($triggerPrototype['triggerid']) 974 , 1); 975 if (DBfetch($dbUpTriggers)) { 976 $duplicateTriggerId = $triggerPrototype['triggerid']; 977 break; 978 } 979 } 980 } 981 982 if ($duplicateTriggerId) { 983 $duplicateTrigger = DBfetch(DBselect( 984 'SELECT t.description'. 985 ' FROM triggers t'. 986 ' WHERE t.triggerid='.zbx_dbstr($duplicateTriggerId) 987 )); 988 self::exception(ZBX_API_ERROR_PARAMETERS, 989 _s('Duplicate dependencies in trigger prototype "%1$s".', $duplicateTrigger['description']) 990 ); 991 } 992 } 993 994 /** 995 * Synchronizes the templated trigger prototype dependencies on the given hosts inherited from the given templates. 996 * Update dependencies, do it after all triggers and trigger prototypes that can be dependent were created/updated 997 * on all child hosts/templates. Starting from highest level template trigger prototypes select trigger prototypes 998 * from one level lower, then for each lower trigger prototype look if it's parent has dependencies, if so 999 * find this dependency trigger prototype child on dependent trigger prototype host and add new dependency. 1000 * 1001 * @param array $data 1002 * @param array|string $data['templateids'] 1003 * @param array|string $data['hostids'] 1004 */ 1005 public function syncTemplateDependencies(array $data) { 1006 $templateIds = zbx_toArray($data['templateids']); 1007 $hostIds = zbx_toArray($data['hostids']); 1008 1009 $parentTriggers = $this->get([ 1010 'output' => ['triggerid'], 1011 'selectDependencies' => ['triggerid'], 1012 'hostids' => $templateIds, 1013 'preservekeys' => true 1014 ]); 1015 1016 if ($parentTriggers) { 1017 $childTriggers = $this->get([ 1018 'output' => ['triggerid', 'templateid'], 1019 'selectHosts' => ['hostid'], 1020 'hostids' => ($hostIds) ? $hostIds : null, 1021 'filter' => ['templateid' => array_keys($parentTriggers)], 1022 'nopermissions' => true, 1023 'preservekeys' => true 1024 ]); 1025 1026 if ($childTriggers) { 1027 $newDependencies = []; 1028 1029 foreach ($childTriggers as $childTrigger) { 1030 $parentDependencies = $parentTriggers[$childTrigger['templateid']]['dependencies']; 1031 1032 if ($parentDependencies) { 1033 $newDependencies[$childTrigger['triggerid']] = [ 1034 'triggerid' => $childTrigger['triggerid'], 1035 'dependencies' => [] 1036 ]; 1037 1038 $dependencies = []; 1039 foreach ($parentDependencies as $depTrigger) { 1040 $dependencies[] = $depTrigger['triggerid']; 1041 } 1042 1043 $host = reset($childTrigger['hosts']); 1044 $dependencies = replace_template_dependencies($dependencies, $host['hostid']); 1045 1046 foreach ($dependencies as $depTriggerId) { 1047 $newDependencies[$childTrigger['triggerid']]['dependencies'][] = [ 1048 'triggerid' => $depTriggerId 1049 ]; 1050 } 1051 } 1052 } 1053 1054 $this->deleteDependencies($childTriggers); 1055 1056 if ($newDependencies) { 1057 $this->addDependencies($newDependencies); 1058 } 1059 } 1060 } 1061 } 1062 1063 /** 1064 * Retrieves and adds additional requested data (options 'selectHosts', 'selectGroups', etc.) to result set. 1065 * 1066 * @param array $options 1067 * @param array $result 1068 * 1069 * @return array 1070 */ 1071 protected function addRelatedObjects(array $options, array $result) { 1072 $result = parent::addRelatedObjects($options, $result); 1073 1074 $triggerPrototypeIds = array_keys($result); 1075 1076 // Add trigger prototype dependencies. 1077 if ($options['selectDependencies'] !== null && $options['selectDependencies'] != API_OUTPUT_COUNT) { 1078 $dependencies = []; 1079 $relationMap = new CRelationMap(); 1080 $res = DBselect( 1081 'SELECT td.triggerid_up,td.triggerid_down'. 1082 ' FROM trigger_depends td'. 1083 ' WHERE '.dbConditionInt('td.triggerid_down', $triggerPrototypeIds) 1084 ); 1085 1086 while ($relation = DBfetch($res)) { 1087 $relationMap->addRelation($relation['triggerid_down'], $relation['triggerid_up']); 1088 } 1089 1090 $related_ids = $relationMap->getRelatedIds(); 1091 1092 if ($related_ids) { 1093 $dependencies = API::getApiService()->select($this->tableName(), [ 1094 'output' => $options['selectDependencies'], 1095 'triggerids' => $related_ids, 1096 'preservekeys' => true 1097 ]); 1098 } 1099 1100 $result = $relationMap->mapMany($result, $dependencies, 'dependencies'); 1101 } 1102 1103 // adding items 1104 if ($options['selectItems'] !== null && $options['selectItems'] != API_OUTPUT_COUNT) { 1105 $relationMap = $this->createRelationMap($result, 'triggerid', 'itemid', 'functions'); 1106 $items = API::Item()->get([ 1107 'output' => $options['selectItems'], 1108 'itemids' => $relationMap->getRelatedIds(), 1109 'webitems' => true, 1110 'nopermissions' => true, 1111 'preservekeys' => true, 1112 'filter' => ['flags' => null] 1113 ]); 1114 $result = $relationMap->mapMany($result, $items, 'items'); 1115 } 1116 1117 // adding discovery rule 1118 if ($options['selectDiscoveryRule'] !== null && $options['selectDiscoveryRule'] != API_OUTPUT_COUNT) { 1119 $dbRules = DBselect( 1120 'SELECT id.parent_itemid,f.triggerid'. 1121 ' FROM item_discovery id,functions f'. 1122 ' WHERE '.dbConditionInt('f.triggerid', $triggerPrototypeIds). 1123 ' AND f.itemid=id.itemid' 1124 ); 1125 $relationMap = new CRelationMap(); 1126 while ($rule = DBfetch($dbRules)) { 1127 $relationMap->addRelation($rule['triggerid'], $rule['parent_itemid']); 1128 } 1129 1130 $discoveryRules = API::DiscoveryRule()->get([ 1131 'output' => $options['selectDiscoveryRule'], 1132 'itemids' => $relationMap->getRelatedIds(), 1133 'nopermissions' => true, 1134 'preservekeys' => true 1135 ]); 1136 $result = $relationMap->mapOne($result, $discoveryRules, 'discoveryRule'); 1137 } 1138 1139 return $result; 1140 } 1141 1142} 1143