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