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']; 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 */ 589 public function addDependencies(array $triggerPrototypes) { 590 $this->validateAddDependencies($triggerPrototypes); 591 592 $insert = []; 593 594 foreach ($triggerPrototypes as $triggerPrototype) { 595 if (!array_key_exists('dependencies', $triggerPrototype)) { 596 continue; 597 } 598 599 foreach ($triggerPrototype['dependencies'] as $dependency) { 600 $insert[] = [ 601 'triggerid_down' => $triggerPrototype['triggerid'], 602 'triggerid_up' => $dependency['triggerid'] 603 ]; 604 } 605 } 606 607 DB::insertBatch('trigger_depends', $insert); 608 609 foreach ($triggerPrototypes as $triggerPrototype) { 610 // Propagate the dependencies to the child triggers. 611 612 $childTriggers = API::getApiService()->select($this->tableName(), [ 613 'output' => ['triggerid'], 614 'filter' => [ 615 'templateid' => $triggerPrototype['triggerid'] 616 ] 617 ]); 618 619 if ($childTriggers) { 620 foreach ($childTriggers as &$childTrigger) { 621 $childTrigger['dependencies'] = []; 622 $childHostsQuery = get_hosts_by_triggerid($childTrigger['triggerid']); 623 624 while ($childHost = DBfetch($childHostsQuery)) { 625 foreach ($triggerPrototype['dependencies'] as $dependency) { 626 $newDependency = [$childTrigger['triggerid'] => $dependency['triggerid']]; 627 $newDependency = replace_template_dependencies($newDependency, $childHost['hostid']); 628 629 $childTrigger['dependencies'][] = [ 630 'triggerid' => $newDependency[$childTrigger['triggerid']] 631 ]; 632 } 633 } 634 } 635 unset($childTrigger); 636 637 $this->addDependencies($childTriggers); 638 } 639 } 640 } 641 642 /** 643 * Validates the input for the addDependencies() method. 644 * 645 * @param array $trigger_prototypes 646 * @param string $trigger_prototypes[]['triggerid'] 647 * @param array $trigger_prototypes[]['dependencies'] 648 * @param string $trigger_prototypes[]['dependencies'][]['triggerid'] 649 * 650 * @throws APIException if the given dependencies are invalid. 651 */ 652 protected function validateAddDependencies(array $trigger_prototypes) { 653 $depTriggerIds = []; 654 655 foreach ($trigger_prototypes as $trigger_prototype) { 656 if (!array_key_exists('dependencies', $trigger_prototype)) { 657 continue; 658 } 659 660 foreach ($trigger_prototype['dependencies'] as $dependency) { 661 $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; 662 } 663 } 664 665 if (!$depTriggerIds) { 666 return; 667 } 668 669 // Check if given IDs are actual trigger prototypes and get discovery rules if they are. 670 $depTriggerPrototypes = $this->get([ 671 'output' => ['triggerid'], 672 'selectDiscoveryRule' => ['itemid'], 673 'triggerids' => $depTriggerIds, 674 'preservekeys' => true 675 ]); 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 'preservekeys' => true 684 ]); 685 686 foreach ($trigger_prototypes as $trigger_prototype) { 687 if (!array_key_exists('dependencies', $trigger_prototype)) { 688 continue; 689 } 690 691 $dRuleId = $dRules[$trigger_prototype['triggerid']]['discoveryRule']['itemid']; 692 693 // Check if current trigger prototype rules match dependent trigger prototype rules. 694 foreach ($trigger_prototype['dependencies'] as $dependency) { 695 if (isset($depTriggerPrototypes[$dependency['triggerid']])) { 696 $depTriggerDRuleId = $depTriggerPrototypes[$dependency['triggerid']]['discoveryRule']['itemid']; 697 698 if (bccomp($depTriggerDRuleId, $dRuleId) != 0) { 699 self::exception(ZBX_API_ERROR_PERMISSIONS, 700 _('No permissions to referred object or it does not exist!') 701 ); 702 } 703 } 704 } 705 } 706 } 707 708 // Check other dependency IDs if those are normal triggers. 709 $triggers = API::Trigger()->get([ 710 'output' => ['triggerid'], 711 'triggerids' => $depTriggerIds, 712 'filter' => [ 713 'flags' => [ZBX_FLAG_DISCOVERY_NORMAL] 714 ] 715 ]); 716 717 if (!$depTriggerPrototypes && !$triggers) { 718 self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); 719 } 720 721 $this->checkDependencies($trigger_prototypes); 722 $this->checkDependencyParents($trigger_prototypes); 723 $this->checkDependencyDuplicates($trigger_prototypes); 724 } 725 726 /** 727 * Check the dependencies of the given trigger prototypes. 728 * 729 * @param array $triggerPrototypes 730 * @param string $triggerPrototypes[]['triggerid'] 731 * @param array $triggerPrototypes[]['dependencies'] 732 * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] 733 * 734 * @throws APIException if any of the dependencies are invalid. 735 */ 736 protected function checkDependencies(array $triggerPrototypes) { 737 $triggerPrototypes = zbx_toHash($triggerPrototypes, 'triggerid'); 738 739 foreach ($triggerPrototypes as $triggerPrototype) { 740 if (!array_key_exists('dependencies', $triggerPrototype)) { 741 continue; 742 } 743 744 $triggerid_down = $triggerPrototype['triggerid']; 745 $triggerids_up = zbx_objectValues($triggerPrototype['dependencies'], 'triggerid'); 746 747 foreach ($triggerids_up as $triggerid_up) { 748 if (bccomp($triggerid_down, $triggerid_up) == 0) { 749 self::exception(ZBX_API_ERROR_PARAMETERS, 750 _('Cannot create dependency on trigger prototype itself.') 751 ); 752 } 753 } 754 } 755 756 foreach ($triggerPrototypes as $triggerPrototype) { 757 if (!array_key_exists('dependencies', $triggerPrototype)) { 758 continue; 759 } 760 761 $depTriggerIds = zbx_objectValues($triggerPrototype['dependencies'], 'triggerid'); 762 763 $triggerTemplates = API::Template()->get([ 764 'output' => ['hostid', 'status'], 765 'triggerids' => [$triggerPrototype['triggerid']], 766 'nopermissions' => true 767 ]); 768 769 if (!$triggerTemplates) { 770 // Current trigger prototype belongs to a host, so forbid dependencies from a host to a template. 771 772 $triggerDepTemplates = API::Template()->get([ 773 'output' => ['templateid'], 774 'triggerids' => $depTriggerIds, 775 'nopermissions' => true, 776 'limit' => 1 777 ]); 778 779 if ($triggerDepTemplates) { 780 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot add dependency from a host to a template.')); 781 } 782 } 783 784 // check circular dependency 785 $downTriggerIds = [$triggerPrototype['triggerid']]; 786 do { 787 // triggerid_down depends on triggerid_up 788 $res = DBselect( 789 'SELECT td.triggerid_up'. 790 ' FROM trigger_depends td'. 791 ' WHERE '.dbConditionInt('td.triggerid_down', $downTriggerIds) 792 ); 793 794 // combine db dependencies with those to be added 795 $upTriggersIds = []; 796 while ($row = DBfetch($res)) { 797 $upTriggersIds[] = $row['triggerid_up']; 798 } 799 foreach ($downTriggerIds as $id) { 800 if (isset($triggerPrototypes[$id]) && isset($triggerPrototypes[$id]['dependencies'])) { 801 $upTriggersIds = array_merge($upTriggersIds, 802 zbx_objectValues($triggerPrototypes[$id]['dependencies'], 'triggerid') 803 ); 804 } 805 } 806 807 // if found trigger id is in dependent triggerids, there is a dependency loop 808 $downTriggerIds = []; 809 foreach ($upTriggersIds as $id) { 810 if (bccomp($id, $triggerPrototype['triggerid']) == 0) { 811 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create circular dependencies.')); 812 } 813 $downTriggerIds[] = $id; 814 } 815 } while (!empty($downTriggerIds)); 816 817 // fetch all templates that are used in dependencies 818 $triggerDepTemplates = API::Template()->get([ 819 'output' => ['templateid'], 820 'triggerids' => $depTriggerIds, 821 'nopermissions' => true, 822 'preservekeys' => true 823 ]); 824 825 $depTemplateIds = array_keys($triggerDepTemplates); 826 827 // run the check only if a templated trigger has dependencies on other templates 828 $triggerTemplateIds = zbx_toHash(zbx_objectValues($triggerTemplates, 'templateid')); 829 $tdiff = array_diff($depTemplateIds, $triggerTemplateIds); 830 831 if (!empty($triggerTemplateIds) && !empty($depTemplateIds) && !empty($tdiff)) { 832 $affectedTemplateIds = zbx_array_merge($triggerTemplateIds, $depTemplateIds); 833 834 // create a list of all hosts, that are children of the affected templates 835 $dbLowlvltpl = DBselect( 836 'SELECT DISTINCT ht.templateid,ht.hostid,h.host'. 837 ' FROM hosts_templates ht,hosts h'. 838 ' WHERE h.hostid=ht.hostid'. 839 ' AND '.dbConditionInt('ht.templateid', $affectedTemplateIds) 840 ); 841 $map = []; 842 while ($lowlvltpl = DBfetch($dbLowlvltpl)) { 843 if (!isset($map[$lowlvltpl['hostid']])) { 844 $map[$lowlvltpl['hostid']] = []; 845 } 846 $map[$lowlvltpl['hostid']][$lowlvltpl['templateid']] = $lowlvltpl['host']; 847 } 848 849 // check that if some host is linked to the template, that the trigger belongs to, 850 // the host must also be linked to all of the templates, that trigger dependencies point to 851 foreach ($map as $templates) { 852 foreach ($triggerTemplateIds as $triggerTemplateId) { 853 // is the host linked to one of the trigger templates? 854 if (isset($templates[$triggerTemplateId])) { 855 // then make sure all of the dependency templates are also linked 856 foreach ($depTemplateIds as $depTemplateId) { 857 if (!isset($templates[$depTemplateId])) { 858 self::exception(ZBX_API_ERROR_PARAMETERS, 859 _s('Not all templates are linked to "%s".', reset($templates)) 860 ); 861 } 862 } 863 break; 864 } 865 } 866 } 867 } 868 } 869 } 870 871 /** 872 * Check that none of the triggers have dependencies on their children. Checks only one level of inheritance, but 873 * since it is called on each inheritance step, also works for multiple inheritance levels. 874 * 875 * @param array $triggerPrototypes 876 * @param string $triggerPrototypes[]['triggerid'] 877 * @param array $triggerPrototypes[]['dependencies'] 878 * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] 879 * 880 * @throws APIException if at least one trigger is dependent on its child. 881 */ 882 protected function checkDependencyParents(array $triggerPrototypes) { 883 // fetch all templated dependency trigger parents 884 $depTriggerIds = []; 885 886 foreach ($triggerPrototypes as $triggerPrototype) { 887 if (!array_key_exists('dependencies', $triggerPrototype)) { 888 continue; 889 } 890 891 foreach ($triggerPrototype['dependencies'] as $dependency) { 892 $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; 893 } 894 } 895 896 $parentDepTriggers = DBfetchArray(DBSelect( 897 'SELECT templateid,triggerid'. 898 ' FROM triggers'. 899 ' WHERE templateid>0'. 900 ' AND '.dbConditionInt('triggerid', $depTriggerIds) 901 )); 902 903 if ($parentDepTriggers) { 904 $parentDepTriggers = zbx_toHash($parentDepTriggers, 'triggerid'); 905 906 foreach ($triggerPrototypes as $triggerPrototype) { 907 foreach ($triggerPrototype['dependencies'] as $dependency) { 908 // Check if the current trigger is the parent of the dependency trigger. 909 910 $depTriggerId = $dependency['triggerid']; 911 912 if (isset($parentDepTriggers[$depTriggerId]) 913 && $parentDepTriggers[$depTriggerId]['templateid'] == $triggerPrototype['triggerid']) { 914 915 self::exception(ZBX_API_ERROR_PARAMETERS, 916 _s('Trigger prototype cannot be dependent on a trigger that is inherited from it.') 917 ); 918 } 919 } 920 } 921 } 922 } 923 924 /** 925 * Checks if the given dependencies contain duplicates. 926 * 927 * @param array $triggerPrototypes 928 * @param string $triggerPrototypes[]['triggerid'] 929 * @param array $triggerPrototypes[]['dependencies'] 930 * @param string $triggerPrototypes[]['dependencies'][]['triggerid'] 931 * 932 * @throws APIException if the given dependencies contain duplicates. 933 */ 934 protected function checkDependencyDuplicates(array $triggerPrototypes) { 935 // check duplicates in array 936 $uniqueTriggers = []; 937 $depTriggerIds = []; 938 $duplicateTriggerId = null; 939 940 foreach ($triggerPrototypes as $triggerPrototype) { 941 if (!array_key_exists('dependencies', $triggerPrototype)) { 942 continue; 943 } 944 945 foreach ($triggerPrototype['dependencies'] as $dependency) { 946 $depTriggerIds[$dependency['triggerid']] = $dependency['triggerid']; 947 948 if (isset($uniqueTriggers[$triggerPrototype['triggerid']][$dependency['triggerid']])) { 949 $duplicateTriggerId = $triggerPrototype['triggerid']; 950 break 2; 951 } 952 else { 953 $uniqueTriggers[$triggerPrototype['triggerid']][$dependency['triggerid']] = 1; 954 } 955 } 956 } 957 958 if ($duplicateTriggerId === null) { 959 // check if dependency already exists in DB 960 foreach ($triggerPrototypes as $triggerPrototype) { 961 $dbUpTriggers = DBselect( 962 'SELECT td.triggerid_up'. 963 ' FROM trigger_depends td'. 964 ' WHERE '.dbConditionInt('td.triggerid_up', $depTriggerIds). 965 ' AND td.triggerid_down='.zbx_dbstr($triggerPrototype['triggerid']) 966 , 1); 967 if (DBfetch($dbUpTriggers)) { 968 $duplicateTriggerId = $triggerPrototype['triggerid']; 969 break; 970 } 971 } 972 } 973 974 if ($duplicateTriggerId) { 975 $duplicateTrigger = DBfetch(DBselect( 976 'SELECT t.description'. 977 ' FROM triggers t'. 978 ' WHERE t.triggerid='.zbx_dbstr($duplicateTriggerId) 979 )); 980 self::exception(ZBX_API_ERROR_PARAMETERS, 981 _s('Duplicate dependencies in trigger prototype "%1$s".', $duplicateTrigger['description']) 982 ); 983 } 984 } 985 986 /** 987 * Synchronizes the templated trigger prototype dependencies on the given hosts inherited from the given templates. 988 * Update dependencies, do it after all triggers and trigger prototypes that can be dependent were created/updated 989 * on all child hosts/templates. Starting from highest level template trigger prototypes select trigger prototypes 990 * from one level lower, then for each lower trigger prototype look if it's parent has dependencies, if so 991 * find this dependency trigger prototype child on dependent trigger prototype host and add new dependency. 992 * 993 * @param array $data 994 * @param array|string $data['templateids'] 995 * @param array|string $data['hostids'] 996 */ 997 public function syncTemplateDependencies(array $data) { 998 $templateIds = zbx_toArray($data['templateids']); 999 $hostIds = zbx_toArray($data['hostids']); 1000 1001 $parentTriggers = $this->get([ 1002 'output' => ['triggerid'], 1003 'selectDependencies' => ['triggerid'], 1004 'hostids' => $templateIds, 1005 'preservekeys' => true 1006 ]); 1007 1008 if ($parentTriggers) { 1009 $childTriggers = $this->get([ 1010 'output' => ['triggerid', 'templateid'], 1011 'selectHosts' => ['hostid'], 1012 'hostids' => ($hostIds) ? $hostIds : null, 1013 'filter' => ['templateid' => array_keys($parentTriggers)], 1014 'nopermissions' => true, 1015 'preservekeys' => true 1016 ]); 1017 1018 if ($childTriggers) { 1019 $newDependencies = []; 1020 1021 foreach ($childTriggers as $childTrigger) { 1022 $parentDependencies = $parentTriggers[$childTrigger['templateid']]['dependencies']; 1023 1024 if ($parentDependencies) { 1025 $newDependencies[$childTrigger['triggerid']] = [ 1026 'triggerid' => $childTrigger['triggerid'], 1027 'dependencies' => [] 1028 ]; 1029 1030 $dependencies = []; 1031 foreach ($parentDependencies as $depTrigger) { 1032 $dependencies[] = $depTrigger['triggerid']; 1033 } 1034 1035 $host = reset($childTrigger['hosts']); 1036 $dependencies = replace_template_dependencies($dependencies, $host['hostid']); 1037 1038 foreach ($dependencies as $depTriggerId) { 1039 $newDependencies[$childTrigger['triggerid']]['dependencies'][] = [ 1040 'triggerid' => $depTriggerId 1041 ]; 1042 } 1043 } 1044 } 1045 1046 $this->deleteDependencies($childTriggers); 1047 1048 if ($newDependencies) { 1049 $this->addDependencies($newDependencies); 1050 } 1051 } 1052 } 1053 } 1054 1055 /** 1056 * Retrieves and adds additional requested data (options 'selectHosts', 'selectGroups', etc.) to result set. 1057 * 1058 * @param array $options 1059 * @param array $result 1060 * 1061 * @return array 1062 */ 1063 protected function addRelatedObjects(array $options, array $result) { 1064 $result = parent::addRelatedObjects($options, $result); 1065 1066 $triggerPrototypeIds = array_keys($result); 1067 1068 if ($options['selectDependencies'] !== null && $options['selectDependencies'] != API_OUTPUT_COUNT) { 1069 // Add trigger prototype dependencies. 1070 1071 $res = DBselect( 1072 'SELECT td.triggerid_up,td.triggerid_down'. 1073 ' FROM trigger_depends td'. 1074 ' WHERE '.dbConditionInt('td.triggerid_down', $triggerPrototypeIds) 1075 ); 1076 1077 $relationMap = new CRelationMap(); 1078 1079 while ($relation = DBfetch($res)) { 1080 $relationMap->addRelation($relation['triggerid_down'], $relation['triggerid_up']); 1081 } 1082 1083 $dependencies = API::getApiService()->select($this->tableName(), [ 1084 'output' => $options['selectDependencies'], 1085 'triggerids' => $relationMap->getRelatedIds(), 1086 'preservekeys' => true 1087 ]); 1088 1089 $result = $relationMap->mapMany($result, $dependencies, 'dependencies'); 1090 } 1091 1092 // adding items 1093 if ($options['selectItems'] !== null && $options['selectItems'] != API_OUTPUT_COUNT) { 1094 $relationMap = $this->createRelationMap($result, 'triggerid', 'itemid', 'functions'); 1095 $items = API::Item()->get([ 1096 'output' => $options['selectItems'], 1097 'itemids' => $relationMap->getRelatedIds(), 1098 'webitems' => true, 1099 'nopermissions' => true, 1100 'preservekeys' => true, 1101 'filter' => ['flags' => null] 1102 ]); 1103 $result = $relationMap->mapMany($result, $items, 'items'); 1104 } 1105 1106 // adding discovery rule 1107 if ($options['selectDiscoveryRule'] !== null && $options['selectDiscoveryRule'] != API_OUTPUT_COUNT) { 1108 $dbRules = DBselect( 1109 'SELECT id.parent_itemid,f.triggerid'. 1110 ' FROM item_discovery id,functions f'. 1111 ' WHERE '.dbConditionInt('f.triggerid', $triggerPrototypeIds). 1112 ' AND f.itemid=id.itemid' 1113 ); 1114 $relationMap = new CRelationMap(); 1115 while ($rule = DBfetch($dbRules)) { 1116 $relationMap->addRelation($rule['triggerid'], $rule['parent_itemid']); 1117 } 1118 1119 $discoveryRules = API::DiscoveryRule()->get([ 1120 'output' => $options['selectDiscoveryRule'], 1121 'itemids' => $relationMap->getRelatedIds(), 1122 'nopermissions' => true, 1123 'preservekeys' => true 1124 ]); 1125 $result = $relationMap->mapOne($result, $discoveryRules, 'discoveryRule'); 1126 } 1127 1128 return $result; 1129 } 1130 1131} 1132