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 common methods for operations with triggers. 24 */ 25abstract class CTriggerGeneral extends CApiService { 26 27 /** 28 * @abstract 29 * 30 * @param array $options 31 * 32 * @return array 33 */ 34 abstract public function get(array $options = []); 35 36 /** 37 * Prepares and returns an array of child triggers, inherited from triggers $tpl_triggers on the given hosts. 38 * 39 * @param array $tpl_triggers 40 * @param string $tpl_triggers[<tnum>]['triggerid'] 41 */ 42 private function prepareInheritedTriggers(array $tpl_triggers, array $hostids = null, array &$ins_triggers = null, 43 array &$upd_triggers = null, array &$db_triggers = null) { 44 $ins_triggers = []; 45 $upd_triggers = []; 46 $db_triggers = []; 47 48 $result = DBselect( 49 'SELECT DISTINCT t.triggerid,h.hostid'. 50 ' FROM triggers t,functions f,items i,hosts h'. 51 ' WHERE t.triggerid=f.triggerid'. 52 ' AND f.itemid=i.itemid'. 53 ' AND i.hostid=h.hostid'. 54 ' AND '.dbConditionInt('t.triggerid', zbx_objectValues($tpl_triggers, 'triggerid')). 55 ' AND '.dbConditionInt('h.status', [HOST_STATUS_TEMPLATE]) 56 ); 57 58 $tpl_hostids_by_triggerid = []; 59 $tpl_hostids = []; 60 61 while ($row = DBfetch($result)) { 62 $tpl_hostids_by_triggerid[$row['triggerid']][] = $row['hostid']; 63 $tpl_hostids[$row['hostid']] = true; 64 } 65 66 // Unset host-level triggers. 67 foreach ($tpl_triggers as $tnum => $tpl_trigger) { 68 if (!array_key_exists($tpl_trigger['triggerid'], $tpl_hostids_by_triggerid)) { 69 unset($tpl_triggers[$tnum]); 70 } 71 } 72 73 if (!$tpl_triggers) { 74 // Nothing to inherit, just exit. 75 return; 76 } 77 78 $hosts_by_tpl_hostid = self::getLinkedHosts(array_keys($tpl_hostids), $hostids); 79 $chd_triggers_tpl = $this->getHostTriggersByTemplateId(array_keys($tpl_hostids_by_triggerid), $hostids); 80 $tpl_triggers_by_description = []; 81 82 // Preparing list of missing triggers on linked hosts. 83 foreach ($tpl_triggers as $tpl_trigger) { 84 $hostids = []; 85 86 foreach ($tpl_hostids_by_triggerid[$tpl_trigger['triggerid']] as $tpl_hostid) { 87 if (array_key_exists($tpl_hostid, $hosts_by_tpl_hostid)) { 88 foreach ($hosts_by_tpl_hostid[$tpl_hostid] as $host) { 89 if (array_key_exists($host['hostid'], $chd_triggers_tpl) 90 && array_key_exists($tpl_trigger['triggerid'], $chd_triggers_tpl[$host['hostid']])) { 91 continue; 92 } 93 94 $hostids[$host['hostid']] = true; 95 } 96 } 97 } 98 99 if ($hostids) { 100 $tpl_triggers_by_description[$tpl_trigger['description']][] = [ 101 'triggerid' => $tpl_trigger['triggerid'], 102 'expression' => $tpl_trigger['expression'], 103 'recovery_mode' => $tpl_trigger['recovery_mode'], 104 'recovery_expression' => $tpl_trigger['recovery_expression'], 105 'hostids' => $hostids 106 ]; 107 } 108 } 109 110 $chd_triggers_all = array_replace_recursive($chd_triggers_tpl, 111 $this->getHostTriggersByDescription($tpl_triggers_by_description) 112 ); 113 114 $expression_data = new CTriggerExpression(['lldmacros' => $this instanceof CTriggerPrototype]); 115 $recovery_expression_data = new CTriggerExpression(['lldmacros' => $this instanceof CTriggerPrototype]); 116 117 // List of triggers to check for duplicates. Grouped by description. 118 $descriptions = []; 119 $triggerids = []; 120 121 $output = ['url', 'status', 'priority', 'comments', 'type', 'correlation_mode', 'correlation_tag', 122 'manual_close', 'opdata' 123 ]; 124 if ($this instanceof CTriggerPrototype) { 125 $output[] = 'discover'; 126 } 127 128 $db_tpl_triggers = DB::select('triggers', [ 129 'output' => $output, 130 'triggerids' => array_keys($tpl_hostids_by_triggerid), 131 'preservekeys' => true 132 ]); 133 134 foreach ($tpl_triggers as $tpl_trigger) { 135 $db_tpl_trigger = $db_tpl_triggers[$tpl_trigger['triggerid']]; 136 137 $tpl_hostid = $tpl_hostids_by_triggerid[$tpl_trigger['triggerid']][0]; 138 139 // expression: {template:item.func()} => {host:item.func()} 140 if (!$expression_data->parse($tpl_trigger['expression'])) { 141 self::exception(ZBX_API_ERROR_PARAMETERS, $expression_data->error); 142 } 143 144 // recovery_expression: {template:item.func()} => {host:item.func()} 145 if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 146 if (!$recovery_expression_data->parse($tpl_trigger['recovery_expression'])) { 147 self::exception(ZBX_API_ERROR_PARAMETERS, $recovery_expression_data->error); 148 } 149 } 150 151 $new_trigger = $tpl_trigger; 152 unset($new_trigger['triggerid'], $new_trigger['templateid']); 153 154 if (array_key_exists($tpl_hostid, $hosts_by_tpl_hostid)) { 155 foreach ($hosts_by_tpl_hostid[$tpl_hostid] as $host) { 156 $new_trigger['expression'] = $tpl_trigger['expression']; 157 $expr_part = end($expression_data->expressions); 158 do { 159 $new_trigger['expression'] = substr_replace($new_trigger['expression'], 160 '{'.$host['host'].':'.$expr_part['item'].'.'.$expr_part['function'].'}', 161 $expr_part['pos'], strlen($expr_part['expression']) 162 ); 163 } 164 while ($expr_part = prev($expression_data->expressions)); 165 166 if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 167 $new_trigger['recovery_expression'] = $tpl_trigger['recovery_expression']; 168 $expr_part = end($recovery_expression_data->expressions); 169 do { 170 $new_trigger['recovery_expression'] = substr_replace($new_trigger['recovery_expression'], 171 '{'.$host['host'].':'.$expr_part['item'].'.'.$expr_part['function'].'}', 172 $expr_part['pos'], strlen($expr_part['expression']) 173 ); 174 } 175 while ($expr_part = prev($recovery_expression_data->expressions)); 176 } 177 178 if (array_key_exists($host['hostid'], $chd_triggers_all) 179 && array_key_exists($tpl_trigger['triggerid'], $chd_triggers_all[$host['hostid']])) { 180 $chd_trigger = $chd_triggers_all[$host['hostid']][$tpl_trigger['triggerid']]; 181 182 $upd_triggers[] = $new_trigger + [ 183 'triggerid' => $chd_trigger['triggerid'], 184 'templateid' => $tpl_trigger['triggerid'] 185 ]; 186 $db_triggers[] = $chd_trigger; 187 $triggerids[] = $chd_trigger['triggerid']; 188 189 $check_duplicates = ($chd_trigger['description'] !== $new_trigger['description'] 190 || $chd_trigger['expression'] !== $new_trigger['expression'] 191 || $chd_trigger['recovery_expression'] !== $new_trigger['recovery_expression']); 192 } 193 else { 194 $ins_triggers[] = $new_trigger + $db_tpl_trigger + ['templateid' => $tpl_trigger['triggerid']]; 195 $check_duplicates = true; 196 } 197 198 if ($check_duplicates) { 199 $descriptions[$new_trigger['description']][] = [ 200 'expression' => $new_trigger['expression'], 201 'recovery_expression' => $new_trigger['recovery_expression'], 202 'hostid' => $host['hostid'] 203 ]; 204 } 205 } 206 } 207 } 208 209 if ($triggerids) { 210 // Add trigger tags. 211 $result = DBselect( 212 'SELECT tt.triggertagid,tt.triggerid,tt.tag,tt.value'. 213 ' FROM trigger_tag tt'. 214 ' WHERE '.dbConditionInt('tt.triggerid', $triggerids) 215 ); 216 217 $trigger_tags = []; 218 219 while ($row = DBfetch($result)) { 220 $trigger_tags[$row['triggerid']][] = [ 221 'triggertagid' => $row['triggertagid'], 222 'tag' => $row['tag'], 223 'value' => $row['value'] 224 ]; 225 } 226 227 foreach ($db_triggers as $tnum => $db_trigger) { 228 $db_triggers[$tnum]['tags'] = array_key_exists($db_trigger['triggerid'], $trigger_tags) 229 ? $trigger_tags[$db_trigger['triggerid']] 230 : []; 231 } 232 233 // Add discovery rule IDs. 234 if ($this instanceof CTriggerPrototype) { 235 $result = DBselect( 236 'SELECT id.parent_itemid,f.triggerid'. 237 ' FROM item_discovery id,functions f'. 238 ' WHERE '.dbConditionInt('f.triggerid', $triggerids). 239 ' AND f.itemid=id.itemid' 240 ); 241 242 $drule_by_triggerid = []; 243 244 while ($row = DBfetch($result)) { 245 $drule_by_triggerid[$row['triggerid']] = $row['parent_itemid']; 246 } 247 248 foreach ($db_triggers as $tnum => $db_trigger) { 249 $db_triggers[$tnum]['discoveryRule']['itemid'] = $drule_by_triggerid[$db_trigger['triggerid']]; 250 } 251 } 252 } 253 254 $this->checkDuplicates($descriptions); 255 } 256 257 /** 258 * Returns list of linked hosts. 259 * 260 * Output format: 261 * [ 262 * <tpl_hostid> => [ 263 * [ 264 * 'hostid' => <hostid>, 265 * 'host' => <host> 266 * ], 267 * ... 268 * ], 269 * ... 270 * ] 271 * 272 * @param array $tpl_hostids 273 * @param array $hostids The function will return a list of all linked hosts if no hostids are specified. 274 * 275 * @return array 276 */ 277 private static function getLinkedHosts(array $tpl_hostids, array $hostids = null) { 278 // Fetch all child hosts and templates 279 $sql = 'SELECT ht.hostid,ht.templateid,h.host'. 280 ' FROM hosts_templates ht,hosts h'. 281 ' WHERE ht.hostid=h.hostid'. 282 ' AND '.dbConditionInt('ht.templateid', $tpl_hostids). 283 ' AND '.dbConditionInt('h.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]); 284 if ($hostids !== null) { 285 $sql .= ' AND '.dbConditionInt('ht.hostid', $hostids); 286 } 287 $result = DBselect($sql); 288 289 $hosts_by_tpl_hostid = []; 290 291 while ($row = DBfetch($result)) { 292 $hosts_by_tpl_hostid[$row['templateid']][] = [ 293 'hostid' => $row['hostid'], 294 'host' => $row['host'] 295 ]; 296 } 297 298 return $hosts_by_tpl_hostid; 299 } 300 301 /** 302 * Returns list of already linked triggers. 303 * 304 * Output format: 305 * [ 306 * <hostid> => [ 307 * <tpl_triggerid> => ['triggerid' => <triggerid>], 308 * ... 309 * ], 310 * ... 311 * ] 312 * 313 * @param array $tpl_triggerids 314 * @param array $hostids The function will return a list of all linked triggers if no hosts are specified. 315 * 316 * @return array 317 */ 318 private function getHostTriggersByTemplateId(array $tpl_triggerids, array $hostids = null) { 319 $output = 't.triggerid,t.expression,t.description,t.url,t.status,t.priority,t.comments,t.type,t.recovery_mode,'. 320 't.recovery_expression,t.correlation_mode,t.correlation_tag,t.manual_close,t.opdata,t.templateid,i.hostid'; 321 if ($this instanceof CTriggerPrototype) { 322 $output .= ',t.discover'; 323 } 324 325 // Preparing list of triggers by templateid. 326 $sql = 'SELECT DISTINCT '.$output. 327 ' FROM triggers t,functions f,items i'. 328 ' WHERE t.triggerid=f.triggerid'. 329 ' AND f.itemid=i.itemid'. 330 ' AND '.dbConditionInt('t.templateid', $tpl_triggerids); 331 if ($hostids !== null) { 332 $sql .= ' AND '.dbConditionInt('i.hostid', $hostids); 333 } 334 335 $chd_triggers = DBfetchArray(DBselect($sql)); 336 $chd_triggers = CMacrosResolverHelper::resolveTriggerExpressions($chd_triggers, 337 ['sources' => ['expression', 'recovery_expression']] 338 ); 339 340 $chd_triggers_tpl = []; 341 342 foreach ($chd_triggers as $chd_trigger) { 343 $hostid = $chd_trigger['hostid']; 344 unset($chd_trigger['hostid']); 345 346 $chd_triggers_tpl[$hostid][$chd_trigger['templateid']] = $chd_trigger; 347 } 348 349 return $chd_triggers_tpl; 350 } 351 352 /** 353 * Returns list of not inherited triggers with same name and expression. 354 * 355 * Output format: 356 * [ 357 * <hostid> => [ 358 * <tpl_triggerid> => ['triggerid' => <triggerid>], 359 * ... 360 * ], 361 * ... 362 * ] 363 * 364 * @param array $tpl_triggers_by_description The list of hostids, grouped by trigger description and expression. 365 * 366 * @return array 367 */ 368 private function getHostTriggersByDescription(array $tpl_triggers_by_description) { 369 $chd_triggers_description = []; 370 371 $expression_data = new CTriggerExpression(['lldmacros' => $this instanceof CTriggerPrototype]); 372 $recovery_expression_data = new CTriggerExpression(['lldmacros' => $this instanceof CTriggerPrototype]); 373 374 $output = 't.triggerid,t.expression,t.description,t.url,t.status,t.priority,t.comments,t.type,t.recovery_mode,'. 375 't.recovery_expression,t.correlation_mode,t.correlation_tag,t.manual_close,t.opdata,i.hostid,h.host'; 376 if ($this instanceof CTriggerPrototype) { 377 $output .= ',t.discover'; 378 } 379 380 foreach ($tpl_triggers_by_description as $description => $tpl_triggers) { 381 $hostids = []; 382 383 foreach ($tpl_triggers as $tpl_trigger) { 384 $hostids += $tpl_trigger['hostids']; 385 } 386 387 $chd_triggers = DBfetchArray(DBselect( 388 'SELECT DISTINCT '.$output. 389 ' FROM triggers t,functions f,items i,hosts h'. 390 ' WHERE t.triggerid=f.triggerid'. 391 ' AND f.itemid=i.itemid'. 392 ' AND i.hostid=h.hostid'. 393 ' AND '.dbConditionString('t.description', [$description]). 394 ' AND '.dbConditionInt('i.hostid', array_keys($hostids)) 395 )); 396 397 $chd_triggers = CMacrosResolverHelper::resolveTriggerExpressions($chd_triggers, 398 ['sources' => ['expression', 'recovery_expression']] 399 ); 400 401 foreach ($tpl_triggers as $tpl_trigger) { 402 // expression: {template:item.func()} => {host:item.func()} 403 if (!$expression_data->parse($tpl_trigger['expression'])) { 404 self::exception(ZBX_API_ERROR_PARAMETERS, $expression_data->error); 405 } 406 407 // recovery_expression: {template:item.func()} => {host:item.func()} 408 if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 409 if (!$recovery_expression_data->parse($tpl_trigger['recovery_expression'])) { 410 self::exception(ZBX_API_ERROR_PARAMETERS, $recovery_expression_data->error); 411 } 412 } 413 414 foreach ($chd_triggers as $chd_trigger) { 415 if (!array_key_exists($chd_trigger['hostid'], $tpl_trigger['hostids'])) { 416 continue; 417 } 418 419 if ($chd_trigger['recovery_mode'] != $tpl_trigger['recovery_mode']) { 420 continue; 421 } 422 423 $expression = $tpl_trigger['expression']; 424 $expr_part = end($expression_data->expressions); 425 do { 426 $expression = substr_replace($expression, 427 '{'.$chd_trigger['host'].':'.$expr_part['item'].'.'.$expr_part['function'].'}', 428 $expr_part['pos'], strlen($expr_part['expression']) 429 ); 430 } 431 while ($expr_part = prev($expression_data->expressions)); 432 433 if ($chd_trigger['expression'] !== $expression) { 434 continue; 435 } 436 437 if ($tpl_trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 438 $recovery_expression = $tpl_trigger['recovery_expression']; 439 $expr_part = end($recovery_expression_data->expressions); 440 do { 441 $recovery_expression = substr_replace($recovery_expression, 442 '{'.$chd_trigger['host'].':'.$expr_part['item'].'.'.$expr_part['function'].'}', 443 $expr_part['pos'], strlen($expr_part['expression']) 444 ); 445 } 446 while ($expr_part = prev($recovery_expression_data->expressions)); 447 448 if ($chd_trigger['recovery_expression'] !== $recovery_expression) { 449 continue; 450 } 451 } 452 453 $hostid = $chd_trigger['hostid']; 454 unset($chd_trigger['hostid'], $chd_trigger['host']); 455 $chd_triggers_description[$hostid][$tpl_trigger['triggerid']] = $chd_trigger + ['templateid' => 0]; 456 } 457 } 458 } 459 460 return $chd_triggers_description; 461 } 462 463 /** 464 * Updates the children of the triggers on the given hosts and propagates the inheritance to all child hosts. 465 * If the given triggers was assigned to a different template or a host, all of the child triggers, that became 466 * obsolete will be deleted. 467 * 468 * @param array $triggers 469 * @param string $triggers[]['triggerid'] 470 * @param string $triggers[]['description'] 471 * @param string $triggers[]['expression'] 472 * @param int $triggers[]['recovery mode'] 473 * @param string $triggers[]['recovery_expression'] 474 * @param array $hostids 475 */ 476 protected function inherit(array $triggers, array $hostids = null) { 477 $this->prepareInheritedTriggers($triggers, $hostids, $ins_triggers, $upd_triggers, $db_triggers); 478 479 if ($ins_triggers) { 480 $this->createReal($ins_triggers, true); 481 } 482 483 if ($upd_triggers) { 484 $this->updateReal($upd_triggers, $db_triggers, true); 485 } 486 487 if ($ins_triggers || $upd_triggers) { 488 $this->inherit(array_merge($ins_triggers + $upd_triggers)); 489 } 490 } 491 492 /** 493 * Populate an array by "hostid" keys. 494 * 495 * @param array $descriptions 496 * @param string $descriptions[<description>][]['expression'] 497 * 498 * @throws APIException If host or template does not exists. 499 * 500 * @return array 501 */ 502 protected function populateHostIds($descriptions) { 503 $expression_data = new CTriggerExpression(['lldmacros' => $this instanceof CTriggerPrototype]); 504 505 $hosts = []; 506 507 foreach ($descriptions as $description => $triggers) { 508 foreach ($triggers as $index => $trigger) { 509 $expression_data->parse($trigger['expression']); 510 $hosts[$expression_data->getHosts()[0]][$description][] = $index; 511 } 512 } 513 514 $db_hosts = DBselect( 515 'SELECT h.hostid,h.host'. 516 ' FROM hosts h'. 517 ' WHERE '.dbConditionString('h.host', array_keys($hosts)). 518 ' AND '.dbConditionInt('h.status', 519 [HOST_STATUS_MONITORED, HOST_STATUS_NOT_MONITORED, HOST_STATUS_TEMPLATE] 520 ) 521 ); 522 523 while ($db_host = DBfetch($db_hosts)) { 524 foreach ($hosts[$db_host['host']] as $description => $indexes) { 525 foreach ($indexes as $index) { 526 $descriptions[$description][$index]['hostid'] = $db_host['hostid']; 527 } 528 } 529 unset($hosts[$db_host['host']]); 530 } 531 532 if ($hosts) { 533 $error_wrong_host = ($this instanceof CTrigger) 534 ? _('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.') 535 : _('Incorrect trigger prototype expression. Host "%1$s" does not exist or you have no access to this host.'); 536 self::exception(ZBX_API_ERROR_PARAMETERS, _params($error_wrong_host, [key($hosts)])); 537 } 538 539 return $descriptions; 540 } 541 542 /** 543 * Checks triggers for duplicates. 544 * 545 * @param array $descriptions 546 * @param string $descriptions[<description>][]['expression'] 547 * @param string $descriptions[<description>][]['recovery_expression'] 548 * @param string $descriptions[<description>][]['hostid'] 549 * 550 * @throws APIException if at least one trigger exists 551 */ 552 protected function checkDuplicates(array $descriptions) { 553 foreach ($descriptions as $description => $triggers) { 554 $hostids = []; 555 $expressions = []; 556 557 foreach ($triggers as $trigger) { 558 $hostids[$trigger['hostid']] = true; 559 $expressions[$trigger['expression']][$trigger['recovery_expression']] = $trigger['hostid']; 560 } 561 562 $db_triggers = DBfetchArray(DBselect( 563 'SELECT DISTINCT t.expression,t.recovery_expression'. 564 ' FROM triggers t,functions f,items i,hosts h'. 565 ' WHERE t.triggerid=f.triggerid'. 566 ' AND f.itemid=i.itemid'. 567 ' AND i.hostid=h.hostid'. 568 ' AND '.dbConditionString('t.description', [$description]). 569 ' AND '.dbConditionInt('i.hostid', array_keys($hostids)) 570 )); 571 572 $db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($db_triggers, 573 ['sources' => ['expression', 'recovery_expression']] 574 ); 575 576 foreach ($db_triggers as $db_trigger) { 577 $expression = $db_trigger['expression']; 578 $recovery_expression = $db_trigger['recovery_expression']; 579 580 if (array_key_exists($expression, $expressions) 581 && array_key_exists($recovery_expression, $expressions[$expression])) { 582 $error_already_exists = ($this instanceof CTrigger) 583 ? _('Trigger "%1$s" already exists on "%2$s".') 584 : _('Trigger prototype "%1$s" already exists on "%2$s".'); 585 586 $db_hosts = DB::select('hosts', [ 587 'output' => ['name'], 588 'hostids' => $expressions[$expression][$recovery_expression] 589 ]); 590 591 self::exception(ZBX_API_ERROR_PARAMETERS, 592 _params($error_already_exists, [$description, $db_hosts[0]['name']]) 593 ); 594 } 595 } 596 } 597 } 598 599 protected function addRelatedObjects(array $options, array $result) { 600 $result = parent::addRelatedObjects($options, $result); 601 602 $triggerids = array_keys($result); 603 604 // adding groups 605 if ($options['selectGroups'] !== null && $options['selectGroups'] != API_OUTPUT_COUNT) { 606 $res = DBselect( 607 'SELECT f.triggerid,hg.groupid'. 608 ' FROM functions f,items i,hosts_groups hg'. 609 ' WHERE '.dbConditionInt('f.triggerid', $triggerids). 610 ' AND f.itemid=i.itemid'. 611 ' AND i.hostid=hg.hostid' 612 ); 613 $relationMap = new CRelationMap(); 614 while ($relation = DBfetch($res)) { 615 $relationMap->addRelation($relation['triggerid'], $relation['groupid']); 616 } 617 618 $groups = API::HostGroup()->get([ 619 'output' => $options['selectGroups'], 620 'groupids' => $relationMap->getRelatedIds(), 621 'preservekeys' => true 622 ]); 623 $result = $relationMap->mapMany($result, $groups, 'groups'); 624 } 625 626 // adding hosts 627 if ($options['selectHosts'] !== null && $options['selectHosts'] != API_OUTPUT_COUNT) { 628 $res = DBselect( 629 'SELECT f.triggerid,i.hostid'. 630 ' FROM functions f,items i'. 631 ' WHERE '.dbConditionInt('f.triggerid', $triggerids). 632 ' AND f.itemid=i.itemid' 633 ); 634 $relationMap = new CRelationMap(); 635 while ($relation = DBfetch($res)) { 636 $relationMap->addRelation($relation['triggerid'], $relation['hostid']); 637 } 638 639 $hosts = API::Host()->get([ 640 'output' => $options['selectHosts'], 641 'hostids' => $relationMap->getRelatedIds(), 642 'templated_hosts' => true, 643 'nopermissions' => true, 644 'preservekeys' => true 645 ]); 646 if (!is_null($options['limitSelects'])) { 647 order_result($hosts, 'host'); 648 } 649 $result = $relationMap->mapMany($result, $hosts, 'hosts', $options['limitSelects']); 650 } 651 652 // adding functions 653 if ($options['selectFunctions'] !== null && $options['selectFunctions'] != API_OUTPUT_COUNT) { 654 $functions = API::getApiService()->select('functions', [ 655 'output' => $this->outputExtend($options['selectFunctions'], ['triggerid', 'functionid']), 656 'filter' => ['triggerid' => $triggerids], 657 'preservekeys' => true 658 ]); 659 660 // Rename column 'name' to 'function'. 661 $function = reset($functions); 662 if ($function && array_key_exists('name', $function)) { 663 $functions = CArrayHelper::renameObjectsKeys($functions, ['name' => 'function']); 664 } 665 666 $relationMap = $this->createRelationMap($functions, 'triggerid', 'functionid'); 667 668 $functions = $this->unsetExtraFields($functions, ['triggerid', 'functionid'], $options['selectFunctions']); 669 $result = $relationMap->mapMany($result, $functions, 'functions'); 670 } 671 672 // Adding trigger tags. 673 if ($options['selectTags'] !== null && $options['selectTags'] != API_OUTPUT_COUNT) { 674 $tags = API::getApiService()->select('trigger_tag', [ 675 'output' => $this->outputExtend($options['selectTags'], ['triggerid']), 676 'filter' => ['triggerid' => $triggerids], 677 'preservekeys' => true 678 ]); 679 680 $relationMap = $this->createRelationMap($tags, 'triggerid', 'triggertagid'); 681 $tags = $this->unsetExtraFields($tags, ['triggertagid', 'triggerid'], []); 682 $result = $relationMap->mapMany($result, $tags, 'tags'); 683 } 684 685 return $result; 686 } 687 688 /** 689 * Validate integrity of trigger recovery properties. 690 * 691 * @static 692 * 693 * @param array $trigger 694 * @param int $trigger['recovery_mode'] 695 * @param string $trigger['recovery_expression'] 696 * 697 * @throws APIException if validation failed. 698 */ 699 private static function checkTriggerRecoveryMode(array $trigger) { 700 if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 701 if ($trigger['recovery_expression'] === '') { 702 self::exception(ZBX_API_ERROR_PARAMETERS, 703 _s('Incorrect value for field "%1$s": %2$s.', 'recovery_expression', _('cannot be empty')) 704 ); 705 } 706 } 707 elseif ($trigger['recovery_expression'] !== '') { 708 self::exception(ZBX_API_ERROR_PARAMETERS, 709 _s('Incorrect value for field "%1$s": %2$s.', 'recovery_expression', _('should be empty')) 710 ); 711 } 712 } 713 714 /** 715 * Validate trigger correlation mode and related properties. 716 * 717 * @static 718 * 719 * @param array $trigger 720 * @param int $trigger['correlation_mode'] 721 * @param string $trigger['correlation_tag'] 722 * @param int $trigger['recovery_mode'] 723 * 724 * @throws APIException if validation failed. 725 */ 726 private static function checkTriggerCorrelationMode(array $trigger) { 727 if ($trigger['correlation_mode'] == ZBX_TRIGGER_CORRELATION_TAG) { 728 if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_NONE) { 729 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.', 730 'correlation_mode', _s('unexpected value "%1$s"', $trigger['correlation_mode']) 731 )); 732 } 733 734 if ($trigger['correlation_tag'] === '') { 735 self::exception(ZBX_API_ERROR_PARAMETERS, 736 _s('Incorrect value for field "%1$s": %2$s.', 'correlation_tag', _('cannot be empty')) 737 ); 738 } 739 } 740 elseif ($trigger['correlation_tag'] !== '') { 741 self::exception(ZBX_API_ERROR_PARAMETERS, 742 _s('Incorrect value for field "%1$s": %2$s.', 'correlation_tag', _('should be empty')) 743 ); 744 } 745 } 746 747 /** 748 * Validate trigger to be created. 749 * 750 * @param array $triggers [IN/OUT] 751 * @param array $triggers[]['description'] [IN] 752 * @param string $triggers[]['expression'] [IN] 753 * @param string $triggers[]['opdata'] [IN] 754 * @param string $triggers[]['comments'] [IN] (optional) 755 * @param int $triggers[]['priority'] [IN] (optional) 756 * @param int $triggers[]['status'] [IN] (optional) 757 * @param int $triggers[]['type'] [IN] (optional) 758 * @param string $triggers[]['url'] [IN] (optional) 759 * @param int $triggers[]['recovery_mode'] [IN/OUT] (optional) 760 * @param string $triggers[]['recovery_expression'] [IN/OUT] (optional) 761 * @param int $triggers[]['correlation_mode'] [IN/OUT] (optional) 762 * @param string $triggers[]['correlation_tag'] [IN/OUT] (optional) 763 * @param int $triggers[]['manual_close'] [IN] (optional) 764 * @param int $triggers[]['discover'] [IN] (optional) for trigger prototypes only 765 * @param array $triggers[]['tags'] [IN] (optional) 766 * @param string $triggers[]['tags'][]['tag'] [IN] 767 * @param string $triggers[]['tags'][]['value'] [IN/OUT] (optional) 768 * @param array $triggers[]['dependencies'] [IN] (optional) 769 * @param string $triggers[]['dependencies'][]['triggerid'] [IN] 770 * 771 * @throws APIException if validation failed. 772 */ 773 protected function validateCreate(array &$triggers) { 774 $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['description', 'expression']], 'fields' => [ 775 'description' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')], 776 'expression' => ['type' => API_TRIGGER_EXPRESSION, 'flags' => API_REQUIRED | API_NOT_EMPTY | API_ALLOW_LLD_MACRO], 777 'opdata' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'opdata')], 778 'comments' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'comments')], 779 'priority' => ['type' => API_INT32, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))], 780 'status' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])], 781 'type' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_MULT_EVENT_DISABLED, TRIGGER_MULT_EVENT_ENABLED])], 782 'url' => ['type' => API_URL, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url')], 783 'recovery_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_RECOVERY_MODE_EXPRESSION, ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION, ZBX_RECOVERY_MODE_NONE]), 'default' => DB::getDefault('triggers', 'recovery_mode')], 784 'recovery_expression' => ['type' => API_TRIGGER_EXPRESSION, 'flags' => API_ALLOW_LLD_MACRO, 'default' => DB::getDefault('triggers', 'recovery_expression')], 785 'correlation_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_CORRELATION_NONE, ZBX_TRIGGER_CORRELATION_TAG]), 'default' => DB::getDefault('triggers', 'correlation_mode')], 786 'correlation_tag' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'correlation_tag'), 'default' => DB::getDefault('triggers', 'correlation_tag')], 787 'manual_close' => ['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED, ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED])], 788 'tags' => ['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [ 789 'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('trigger_tag', 'tag')], 790 'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('trigger_tag', 'value'), 'default' => DB::getDefault('trigger_tag', 'value')] 791 ]], 792 'dependencies' => ['type' => API_OBJECTS, 'uniq' => [['triggerid']], 'fields'=> [ 793 'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED] 794 ]] 795 ]]; 796 if ($this instanceof CTriggerPrototype) { 797 $api_input_rules['fields']['discover'] = ['type' => API_INT32, 'in' => implode(',', [TRIGGER_DISCOVER, TRIGGER_NO_DISCOVER])]; 798 } 799 else { 800 $api_input_rules['fields']['expression']['flags'] &= ~API_ALLOW_LLD_MACRO; 801 $api_input_rules['fields']['recovery_expression']['flags'] &= ~API_ALLOW_LLD_MACRO; 802 } 803 if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) { 804 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 805 } 806 807 $descriptions = []; 808 foreach ($triggers as $trigger) { 809 self::checkTriggerRecoveryMode($trigger); 810 self::checkTriggerCorrelationMode($trigger); 811 812 $descriptions[$trigger['description']][] = [ 813 'expression' => $trigger['expression'], 814 'recovery_expression' => $trigger['recovery_expression'] 815 ]; 816 } 817 $descriptions = $this->populateHostIds($descriptions); 818 $this->checkDuplicates($descriptions); 819 } 820 821 /** 822 * Validate trigger to be updated. 823 * 824 * @param array $triggers [IN/OUT] 825 * @param array $triggers[]['triggerid'] [IN] 826 * @param array $triggers[]['description'] [IN/OUT] (optional) 827 * @param string $triggers[]['expression'] [IN/OUT] (optional) 828 * @param string $triggers[]['opdata'] [IN] (optional) 829 * @param string $triggers[]['comments'] [IN] (optional) 830 * @param int $triggers[]['priority'] [IN] (optional) 831 * @param int $triggers[]['status'] [IN] (optional) 832 * @param int $triggers[]['type'] [IN] (optional) 833 * @param string $triggers[]['url'] [IN] (optional) 834 * @param int $triggers[]['recovery_mode'] [IN/OUT] (optional) 835 * @param string $triggers[]['recovery_expression'] [IN/OUT] (optional) 836 * @param int $triggers[]['correlation_mode'] [IN/OUT] (optional) 837 * @param string $triggers[]['correlation_tag'] [IN/OUT] (optional) 838 * @param int $triggers[]['manual_close'] [IN] (optional) 839 * @param int $triggers[]['discover'] [IN] (optional) for trigger prototypes only 840 * @param array $triggers[]['tags'] [IN] (optional) 841 * @param string $triggers[]['tags'][]['tag'] [IN] 842 * @param string $triggers[]['tags'][]['value'] [IN/OUT] (optional) 843 * @param array $triggers[]['dependencies'] [IN] (optional) 844 * @param string $triggers[]['dependencies'][]['triggerid'] [IN] 845 * @param array $db_triggers [OUT] 846 * @param array $db_triggers[<tnum>]['triggerid'] [OUT] 847 * @param array $db_triggers[<tnum>]['description'] [OUT] 848 * @param string $db_triggers[<tnum>]['expression'] [OUT] 849 * @param string $db_triggers[<tnum>]['opdata'] [OUT] 850 * @param int $db_triggers[<tnum>]['recovery_mode'] [OUT] 851 * @param string $db_triggers[<tnum>]['recovery_expression'] [OUT] 852 * @param string $db_triggers[<tnum>]['url'] [OUT] 853 * @param int $db_triggers[<tnum>]['status'] [OUT] 854 * @param int $db_triggers[<tnum>]['discover'] [OUT] 855 * @param int $db_triggers[<tnum>]['priority'] [OUT] 856 * @param string $db_triggers[<tnum>]['comments'] [OUT] 857 * @param int $db_triggers[<tnum>]['type'] [OUT] 858 * @param string $db_triggers[<tnum>]['templateid'] [OUT] 859 * @param int $db_triggers[<tnum>]['correlation_mode'] [OUT] 860 * @param string $db_triggers[<tnum>]['correlation_tag'] [OUT] 861 * @param int $db_triggers[<tnum>]['discover'] [OUT] for trigger prototypes only 862 * 863 * @throws APIException if validation failed. 864 */ 865 protected function validateUpdate(array &$triggers, array &$db_triggers = null) { 866 $db_triggers = []; 867 868 $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['description', 'expression']], 'fields' => [ 869 'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED], 870 'description' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('triggers', 'description')], 871 'expression' => ['type' => API_TRIGGER_EXPRESSION, 'flags' => API_NOT_EMPTY | API_ALLOW_LLD_MACRO], 872 'opdata' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'opdata')], 873 'comments' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'comments')], 874 'priority' => ['type' => API_INT32, 'in' => implode(',', range(TRIGGER_SEVERITY_NOT_CLASSIFIED, TRIGGER_SEVERITY_COUNT - 1))], 875 'status' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_STATUS_ENABLED, TRIGGER_STATUS_DISABLED])], 876 'type' => ['type' => API_INT32, 'in' => implode(',', [TRIGGER_MULT_EVENT_DISABLED, TRIGGER_MULT_EVENT_ENABLED])], 877 'url' => ['type' => API_URL, 'flags' => API_ALLOW_USER_MACRO, 'length' => DB::getFieldLength('triggers', 'url')], 878 'recovery_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_RECOVERY_MODE_EXPRESSION, ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION, ZBX_RECOVERY_MODE_NONE])], 879 'recovery_expression' => ['type' => API_TRIGGER_EXPRESSION, 'flags' => API_ALLOW_LLD_MACRO], 880 'correlation_mode' => ['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_CORRELATION_NONE, ZBX_TRIGGER_CORRELATION_TAG])], 881 'correlation_tag' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('triggers', 'correlation_tag')], 882 'manual_close' => ['type' => API_INT32, 'in' => implode(',', [ZBX_TRIGGER_MANUAL_CLOSE_NOT_ALLOWED, ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED])], 883 'tags' => ['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [ 884 'tag' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('trigger_tag', 'tag')], 885 'value' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('trigger_tag', 'value'), 'default' => DB::getDefault('trigger_tag', 'value')] 886 ]], 887 'dependencies' => ['type' => API_OBJECTS, 'uniq' => [['triggerid']], 'fields'=> [ 888 'triggerid' => ['type' => API_ID, 'flags' => API_REQUIRED] 889 ]] 890 ]]; 891 if ($this instanceof CTriggerPrototype) { 892 $api_input_rules['fields']['discover'] = ['type' => API_INT32, 'in' => implode(',', [TRIGGER_DISCOVER, TRIGGER_NO_DISCOVER])]; 893 } 894 else { 895 $api_input_rules['fields']['expression']['flags'] &= ~API_ALLOW_LLD_MACRO; 896 $api_input_rules['fields']['recovery_expression']['flags'] &= ~API_ALLOW_LLD_MACRO; 897 } 898 if (!CApiInputValidator::validate($api_input_rules, $triggers, '/', $error)) { 899 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 900 } 901 902 $options = [ 903 'output' => ['triggerid', 'description', 'expression', 'url', 'status', 'priority', 'comments', 'type', 904 'templateid', 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag', 905 'manual_close', 'opdata' 906 ], 907 'selectDependencies' => ['triggerid'], 908 'triggerids' => zbx_objectValues($triggers, 'triggerid'), 909 'editable' => true, 910 'preservekeys' => true 911 ]; 912 913 $class = get_class($this); 914 915 switch ($class) { 916 case 'CTrigger': 917 $error_cannot_update = _('Cannot update "%1$s" for templated trigger "%2$s".'); 918 $options['output'][] = 'flags'; 919 920 // Discovered fields, except status, cannot be updated. 921 $update_discovered_validator = new CUpdateDiscoveredValidator([ 922 'allowed' => ['triggerid', 'status'], 923 'messageAllowedField' => _('Cannot update "%2$s" for a discovered trigger "%1$s".') 924 ]); 925 break; 926 927 case 'CTriggerPrototype': 928 $error_cannot_update = _('Cannot update "%1$s" for templated trigger prototype "%2$s".'); 929 $options['output'][] = 'discover'; 930 $options['selectDiscoveryRule'] = ['itemid']; 931 break; 932 933 default: 934 self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.')); 935 } 936 937 $_db_triggers = CMacrosResolverHelper::resolveTriggerExpressions($this->get($options), 938 ['sources' => ['expression', 'recovery_expression']] 939 ); 940 941 $db_trigger_tags = $_db_triggers 942 ? DB::select('trigger_tag', [ 943 'output' => ['triggertagid', 'triggerid', 'tag', 'value'], 944 'filter' => ['triggerid' => array_keys($_db_triggers)], 945 'preservekeys' => true 946 ]) 947 : []; 948 949 $_db_triggers = $this 950 ->createRelationMap($db_trigger_tags, 'triggerid', 'triggertagid') 951 ->mapMany($_db_triggers, $db_trigger_tags, 'tags'); 952 953 $read_only_fields = ['description', 'expression', 'recovery_mode', 'recovery_expression', 'correlation_mode', 954 'correlation_tag', 'manual_close' 955 ]; 956 957 $descriptions = []; 958 959 foreach ($triggers as $key => &$trigger) { 960 if (!array_key_exists($trigger['triggerid'], $_db_triggers)) { 961 self::exception(ZBX_API_ERROR_PARAMETERS, _('No permissions to referred object or it does not exist!')); 962 } 963 964 $db_trigger = $_db_triggers[$trigger['triggerid']]; 965 $description = array_key_exists('description', $trigger) 966 ? $trigger['description'] 967 : $db_trigger['description']; 968 969 if ($class === 'CTrigger') { 970 $update_discovered_validator->setObjectName($description); 971 $this->checkPartialValidator($trigger, $update_discovered_validator, $db_trigger); 972 } 973 974 if ($db_trigger['templateid'] != 0) { 975 $this->checkNoParameters($trigger, $read_only_fields, $error_cannot_update, $description); 976 } 977 978 $field_names = ['description', 'expression', 'recovery_mode', 'manual_close']; 979 foreach ($field_names as $field_name) { 980 if (!array_key_exists($field_name, $trigger)) { 981 $trigger[$field_name] = $db_trigger[$field_name]; 982 } 983 } 984 985 if (!array_key_exists('recovery_expression', $trigger)) { 986 $trigger['recovery_expression'] = ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) 987 ? $db_trigger['recovery_expression'] 988 : ''; 989 } 990 if (!array_key_exists('correlation_mode', $trigger)) { 991 $trigger['correlation_mode'] = ($trigger['recovery_mode'] != ZBX_RECOVERY_MODE_NONE) 992 ? $db_trigger['correlation_mode'] 993 : ZBX_TRIGGER_CORRELATION_NONE; 994 } 995 if (!array_key_exists('correlation_tag', $trigger)) { 996 $trigger['correlation_tag'] = ($trigger['correlation_mode'] == ZBX_TRIGGER_CORRELATION_TAG) 997 ? $db_trigger['correlation_tag'] 998 : ''; 999 } 1000 1001 self::checkTriggerRecoveryMode($trigger); 1002 self::checkTriggerCorrelationMode($trigger); 1003 1004 if ($trigger['expression'] !== $db_trigger['expression'] 1005 || $trigger['recovery_expression'] !== $db_trigger['recovery_expression'] 1006 || $trigger['description'] !== $db_trigger['description']) { 1007 $descriptions[$trigger['description']][] = [ 1008 'expression' => $trigger['expression'], 1009 'recovery_expression' => $trigger['recovery_expression'] 1010 ]; 1011 } 1012 1013 $db_triggers[$key] = $db_trigger; 1014 } 1015 unset($trigger); 1016 1017 if ($descriptions) { 1018 $descriptions = $this->populateHostIds($descriptions); 1019 $this->checkDuplicates($descriptions); 1020 } 1021 } 1022 1023 /** 1024 * Inserts trigger or trigger prototypes records into the database. 1025 * 1026 * @param array $triggers [IN/OUT] 1027 * @param array $triggers[]['triggerid'] [OUT] 1028 * @param array $triggers[]['description'] [IN] 1029 * @param string $triggers[]['expression'] [IN] 1030 * @param int $triggers[]['recovery_mode'] [IN] 1031 * @param string $triggers[]['recovery_expression'] [IN] 1032 * @param string $triggers[]['url'] [IN] (optional) 1033 * @param int $triggers[]['status'] [IN] (optional) 1034 * @param int $triggers[]['priority'] [IN] (optional) 1035 * @param string $triggers[]['comments'] [IN] (optional) 1036 * @param int $triggers[]['type'] [IN] (optional) 1037 * @param string $triggers[]['templateid'] [IN] (optional) 1038 * @param array $triggers[]['tags'] [IN] (optional) 1039 * @param string $triggers[]['tags'][]['tag'] [IN] 1040 * @param string $triggers[]['tags'][]['value'] [IN] 1041 * @param int $triggers[]['correlation_mode'] [IN] (optional) 1042 * @param string $triggers[]['correlation_tag'] [IN] (optional) 1043 * @param bool $inherited [IN] (optional) If set to true, trigger will be created for 1044 * non-editable host/template. 1045 * 1046 * @throws APIException 1047 */ 1048 protected function createReal(array &$triggers, $inherited = false) { 1049 $class = get_class($this); 1050 1051 switch ($class) { 1052 case 'CTrigger': 1053 $resource = AUDIT_RESOURCE_TRIGGER; 1054 break; 1055 1056 case 'CTriggerPrototype': 1057 $resource = AUDIT_RESOURCE_TRIGGER_PROTOTYPE; 1058 break; 1059 1060 default: 1061 self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.')); 1062 } 1063 1064 $new_triggers = $triggers; 1065 $new_functions = []; 1066 $triggers_functions = []; 1067 $new_tags = []; 1068 $this->implode_expressions($new_triggers, null, $triggers_functions, $inherited); 1069 1070 $triggerid = DB::reserveIds('triggers', count($new_triggers)); 1071 1072 foreach ($new_triggers as $tnum => &$new_trigger) { 1073 $new_trigger['triggerid'] = $triggerid; 1074 $triggers[$tnum]['triggerid'] = $triggerid; 1075 1076 foreach ($triggers_functions[$tnum] as $trigger_function) { 1077 $trigger_function['triggerid'] = $triggerid; 1078 $new_functions[] = $trigger_function; 1079 } 1080 1081 if ($class === 'CTriggerPrototype') { 1082 $new_trigger['flags'] = ZBX_FLAG_DISCOVERY_PROTOTYPE; 1083 } 1084 1085 if (array_key_exists('tags', $new_trigger)) { 1086 foreach ($new_trigger['tags'] as $tag) { 1087 $tag['triggerid'] = $triggerid; 1088 $new_tags[] = $tag; 1089 } 1090 } 1091 1092 $triggerid = bcadd($triggerid, 1, 0); 1093 } 1094 unset($new_trigger); 1095 1096 DB::insert('triggers', $new_triggers, false); 1097 DB::insertBatch('functions', $new_functions, false); 1098 1099 if ($new_tags) { 1100 DB::insert('trigger_tag', $new_tags); 1101 } 1102 1103 if (!$inherited) { 1104 $this->addAuditBulk(AUDIT_ACTION_ADD, $resource, $triggers); 1105 } 1106 } 1107 1108 /** 1109 * Update trigger or trigger prototypes records in the database. 1110 * 1111 * @param array $triggers [IN] list of triggers to be updated 1112 * @param array $triggers[<tnum>]['triggerid'] [IN] 1113 * @param array $triggers[<tnum>]['description'] [IN] 1114 * @param string $triggers[<tnum>]['expression'] [IN] 1115 * @param int $triggers[<tnum>]['recovery_mode'] [IN] 1116 * @param string $triggers[<tnum>]['recovery_expression'] [IN] 1117 * @param string $triggers[<tnum>]['url'] [IN] (optional) 1118 * @param int $triggers[<tnum>]['status'] [IN] (optional) 1119 * @param int $triggers[<tnum>]['priority'] [IN] (optional) 1120 * @param string $triggers[<tnum>]['comments'] [IN] (optional) 1121 * @param int $triggers[<tnum>]['type'] [IN] (optional) 1122 * @param string $triggers[<tnum>]['templateid'] [IN] (optional) 1123 * @param array $triggers[<tnum>]['tags'] [IN] 1124 * @param string $triggers[<tnum>]['tags'][]['tag'] [IN] 1125 * @param string $triggers[<tnum>]['tags'][]['value'] [IN] 1126 * @param int $triggers[<tnum>]['correlation_mode'] [IN] 1127 * @param string $triggers[<tnum>]['correlation_tag'] [IN] 1128 * @param array $db_triggers [IN] 1129 * @param array $db_triggers[<tnum>]['triggerid'] [IN] 1130 * @param array $db_triggers[<tnum>]['description'] [IN] 1131 * @param string $db_triggers[<tnum>]['expression'] [IN] 1132 * @param int $db_triggers[<tnum>]['recovery_mode'] [IN] 1133 * @param string $db_triggers[<tnum>]['recovery_expression'] [IN] 1134 * @param string $db_triggers[<tnum>]['url'] [IN] 1135 * @param int $db_triggers[<tnum>]['status'] [IN] 1136 * @param int $db_triggers[<tnum>]['priority'] [IN] 1137 * @param string $db_triggers[<tnum>]['comments'] [IN] 1138 * @param int $db_triggers[<tnum>]['type'] [IN] 1139 * @param string $db_triggers[<tnum>]['templateid'] [IN] 1140 * @param array $db_triggers[<tnum>]['discoveryRule'] [IN] For trigger prototypes only. 1141 * @param string $db_triggers[<tnum>]['discoveryRule']['itemid'] [IN] 1142 * @param array $db_triggers[<tnum>]['tags'] [IN] 1143 * @param string $db_triggers[<tnum>]['tags'][]['tag'] [IN] 1144 * @param string $db_triggers[<tnum>]['tags'][]['value'] [IN] 1145 * @param int $db_triggers[<tnum>]['correlation_mode'] [IN] 1146 * @param string $db_triggers[<tnum>]['correlation_tag'] [IN] 1147 * @param bool $inherited [IN] (optional) If set to true, trigger will be 1148 * created for non-editable 1149 * host/template. 1150 * 1151 * @throws APIException 1152 */ 1153 protected function updateReal(array $triggers, array $db_triggers, $inherited = false) { 1154 $class = get_class($this); 1155 1156 switch ($class) { 1157 case 'CTrigger': 1158 $resource = AUDIT_RESOURCE_TRIGGER; 1159 break; 1160 1161 case 'CTriggerPrototype': 1162 $resource = AUDIT_RESOURCE_TRIGGER_PROTOTYPE; 1163 break; 1164 1165 default: 1166 self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.')); 1167 } 1168 1169 $upd_triggers = []; 1170 $new_functions = []; 1171 $del_functions_triggerids = []; 1172 $triggers_functions = []; 1173 $new_tags = []; 1174 $del_triggertagids = []; 1175 $save_triggers = $triggers; 1176 $this->implode_expressions($triggers, $db_triggers, $triggers_functions, $inherited); 1177 1178 if ($class === 'CTrigger') { 1179 // The list of the triggers with changed priority. 1180 $changed_priority_triggerids = []; 1181 } 1182 1183 foreach ($triggers as $tnum => $trigger) { 1184 $db_trigger = $db_triggers[$tnum]; 1185 $upd_trigger = ['values' => [], 'where' => ['triggerid' => $trigger['triggerid']]]; 1186 1187 if (array_key_exists($tnum, $triggers_functions)) { 1188 $del_functions_triggerids[] = $trigger['triggerid']; 1189 1190 foreach ($triggers_functions[$tnum] as $trigger_function) { 1191 $trigger_function['triggerid'] = $trigger['triggerid']; 1192 $new_functions[] = $trigger_function; 1193 } 1194 1195 $upd_trigger['values']['expression'] = $trigger['expression']; 1196 $upd_trigger['values']['recovery_expression'] = $trigger['recovery_expression']; 1197 } 1198 1199 if ($trigger['description'] !== $db_trigger['description']) { 1200 $upd_trigger['values']['description'] = $trigger['description']; 1201 } 1202 if (array_key_exists('opdata', $trigger) && $trigger['opdata'] !== $db_trigger['opdata']) { 1203 $upd_trigger['values']['opdata'] = $trigger['opdata']; 1204 } 1205 if ($trigger['recovery_mode'] != $db_trigger['recovery_mode']) { 1206 $upd_trigger['values']['recovery_mode'] = $trigger['recovery_mode']; 1207 } 1208 if (array_key_exists('url', $trigger) && $trigger['url'] !== $db_trigger['url']) { 1209 $upd_trigger['values']['url'] = $trigger['url']; 1210 } 1211 if (array_key_exists('status', $trigger) && $trigger['status'] != $db_trigger['status']) { 1212 $upd_trigger['values']['status'] = $trigger['status']; 1213 } 1214 if ($class === 'CTriggerPrototype' 1215 && array_key_exists('discover', $trigger) && $trigger['discover'] != $db_trigger['discover']) { 1216 $upd_trigger['values']['discover'] = $trigger['discover']; 1217 } 1218 if (array_key_exists('priority', $trigger) && $trigger['priority'] != $db_trigger['priority']) { 1219 $upd_trigger['values']['priority'] = $trigger['priority']; 1220 1221 if ($class === 'CTrigger') { 1222 $changed_priority_triggerids[] = $trigger['triggerid']; 1223 } 1224 } 1225 if (array_key_exists('comments', $trigger) && $trigger['comments'] !== $db_trigger['comments']) { 1226 $upd_trigger['values']['comments'] = $trigger['comments']; 1227 } 1228 if (array_key_exists('type', $trigger) && $trigger['type'] != $db_trigger['type']) { 1229 $upd_trigger['values']['type'] = $trigger['type']; 1230 } 1231 if (array_key_exists('templateid', $trigger) && $trigger['templateid'] != $db_trigger['templateid']) { 1232 $upd_trigger['values']['templateid'] = $trigger['templateid']; 1233 } 1234 if ($trigger['correlation_mode'] != $db_trigger['correlation_mode']) { 1235 $upd_trigger['values']['correlation_mode'] = $trigger['correlation_mode']; 1236 } 1237 if ($trigger['correlation_tag'] !== $db_trigger['correlation_tag']) { 1238 $upd_trigger['values']['correlation_tag'] = $trigger['correlation_tag']; 1239 } 1240 if ($trigger['manual_close'] != $db_trigger['manual_close']) { 1241 $upd_trigger['values']['manual_close'] = $trigger['manual_close']; 1242 } 1243 1244 if ($upd_trigger['values']) { 1245 $upd_triggers[] = $upd_trigger; 1246 } 1247 1248 if (array_key_exists('tags', $trigger)) { 1249 // Add new trigger tags and replace changed ones. 1250 1251 CArrayHelper::sort($db_trigger['tags'], ['tag', 'value']); 1252 CArrayHelper::sort($trigger['tags'], ['tag', 'value']); 1253 1254 $tags_delete = $db_trigger['tags']; 1255 $tags_add = $trigger['tags']; 1256 1257 foreach ($tags_delete as $dt_key => $tag_delete) { 1258 foreach ($tags_add as $nt_key => $tag_add) { 1259 if ($tag_delete['tag'] === $tag_add['tag'] && $tag_delete['value'] === $tag_add['value']) { 1260 unset($tags_delete[$dt_key], $tags_add[$nt_key]); 1261 continue 2; 1262 } 1263 } 1264 } 1265 1266 foreach ($tags_delete as $tag_delete) { 1267 $del_triggertagids[] = $tag_delete['triggertagid']; 1268 } 1269 1270 foreach ($tags_add as $tag_add) { 1271 $tag_add['triggerid'] = $trigger['triggerid']; 1272 $new_tags[] = $tag_add; 1273 } 1274 } 1275 } 1276 1277 if ($upd_triggers) { 1278 DB::update('triggers', $upd_triggers); 1279 } 1280 if ($del_functions_triggerids) { 1281 DB::delete('functions', ['triggerid' => $del_functions_triggerids]); 1282 } 1283 if ($new_functions) { 1284 DB::insertBatch('functions', $new_functions, false); 1285 } 1286 if ($del_triggertagids) { 1287 DB::delete('trigger_tag', ['triggertagid' => $del_triggertagids]); 1288 } 1289 if ($new_tags) { 1290 DB::insert('trigger_tag', $new_tags); 1291 } 1292 1293 if ($class === 'CTrigger' && $changed_priority_triggerids 1294 && CTriggerManager::usedInItServices($changed_priority_triggerids)) { 1295 updateItServices(); 1296 } 1297 1298 if (!$inherited) { 1299 $this->addAuditBulk(AUDIT_ACTION_UPDATE, $resource, $save_triggers, zbx_toHash($db_triggers, 'triggerid')); 1300 } 1301 } 1302 1303 /** 1304 * Implodes expression and recovery_expression for each trigger. Also returns array of functions and 1305 * array of hostnames for each trigger. 1306 * 1307 * For example: {localhost:system.cpu.load.last(0)}>10 will be translated to {12}>10 and 1308 * created database representation. 1309 * 1310 * Note: All expressions must be already validated and exploded. 1311 * 1312 * @param array $triggers [IN] 1313 * @param string $triggers[<tnum>]['description'] [IN] 1314 * @param string $triggers[<tnum>]['expression'] [IN/OUT] 1315 * @param int $triggers[<tnum>]['recovery_mode'] [IN] 1316 * @param string $triggers[<tnum>]['recovery_expression'] [IN/OUT] 1317 * @param array|null $db_triggers [IN] 1318 * @param string $db_triggers[<tnum>]['triggerid'] [IN] 1319 * @param string $db_triggers[<tnum>]['expression'] [IN] 1320 * @param string $db_triggers[<tnum>]['recovery_expression'] [IN] 1321 * @param array $triggers_functions [OUT] array of the new functions which must be 1322 * inserted into DB 1323 * @param string $triggers_functions[<tnum>][]['functionid'] [OUT] 1324 * @param null $triggers_functions[<tnum>][]['triggerid'] [OUT] must be initialized before insertion into DB 1325 * @param string $triggers_functions[<tnum>][]['itemid'] [OUT] 1326 * @param string $triggers_functions[<tnum>][]['name'] [OUT] 1327 * @param string $triggers_functions[<tnum>][]['parameter'] [OUT] 1328 * @param bool $inherited [IN] (optional) If set to true, triggers will be 1329 * created for non-editable 1330 * hosts/templates. 1331 * 1332 * @throws APIException if error occurred 1333 */ 1334 private function implode_expressions(array &$triggers, array $db_triggers = null, array &$triggers_functions, 1335 $inherited = false) { 1336 $class = get_class($this); 1337 1338 switch ($class) { 1339 case 'CTrigger': 1340 $expressionData = new CTriggerExpression(['lldmacros' => false]); 1341 $error_wrong_host = _('Incorrect trigger expression. Host "%1$s" does not exist or you have no access to this host.'); 1342 $error_host_and_template = _('Incorrect trigger expression. Trigger expression elements should not belong to a template and a host simultaneously.'); 1343 $triggerFunctionValidator = new CFunctionValidator(['lldmacros' => false]); 1344 break; 1345 1346 case 'CTriggerPrototype': 1347 $expressionData = new CTriggerExpression(); 1348 $error_wrong_host = _('Incorrect trigger prototype expression. Host "%1$s" does not exist or you have no access to this host.'); 1349 $error_host_and_template = _('Incorrect trigger prototype expression. Trigger prototype expression elements should not belong to a template and a host simultaneously.'); 1350 $triggerFunctionValidator = new CFunctionValidator(); 1351 break; 1352 1353 default: 1354 self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.')); 1355 } 1356 1357 /* 1358 * [ 1359 * <host> => [ 1360 * 'hostid' => <hostid>, 1361 * 'host' => <host>, 1362 * 'status' => <status>, 1363 * 'keys' => [ 1364 * <key> => [ 1365 * 'itemid' => <itemid>, 1366 * 'key' => <key>, 1367 * 'value_type' => <value_type>, 1368 * 'flags' => <flags>, 1369 * 'lld_ruleid' => <itemid> (CTriggerProrotype only) 1370 * ] 1371 * ] 1372 * ] 1373 * ] 1374 */ 1375 $hosts_keys = []; 1376 $functions_num = 0; 1377 1378 foreach ($triggers as $tnum => $trigger) { 1379 $expressions_changed = ($db_triggers === null 1380 || ($trigger['expression'] !== $db_triggers[$tnum]['expression'] 1381 || $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression'])); 1382 1383 if (!$expressions_changed) { 1384 continue; 1385 } 1386 1387 $expressionData->parse($trigger['expression']); 1388 $expressions = $expressionData->expressions; 1389 1390 if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 1391 $expressionData->parse($trigger['recovery_expression']); 1392 $expressions = array_merge($expressions, $expressionData->expressions); 1393 } 1394 1395 foreach ($expressions as $exprPart) { 1396 if (!array_key_exists($exprPart['host'], $hosts_keys)) { 1397 $hosts_keys[$exprPart['host']] = [ 1398 'hostid' => null, 1399 'host' => $exprPart['host'], 1400 'status' => null, 1401 'keys' => [] 1402 ]; 1403 } 1404 1405 $hosts_keys[$exprPart['host']]['keys'][$exprPart['item']] = [ 1406 'itemid' => null, 1407 'key' => $exprPart['item'], 1408 'value_type' => null, 1409 'flags' => null 1410 ]; 1411 } 1412 } 1413 1414 if (!$hosts_keys) { 1415 return; 1416 } 1417 1418 $permission_check = $inherited 1419 ? ['nopermissions' => true] 1420 : ['editable' => true]; 1421 1422 $_db_hosts = API::Host()->get([ 1423 'output' => ['hostid', 'host', 'status'], 1424 'filter' => ['host' => array_keys($hosts_keys)] 1425 ] + $permission_check); 1426 1427 if (count($hosts_keys) != count($_db_hosts)) { 1428 $_db_templates = API::Template()->get([ 1429 'output' => ['templateid', 'host', 'status'], 1430 'filter' => ['host' => array_keys($hosts_keys)] 1431 ] + $permission_check); 1432 1433 foreach ($_db_templates as &$_db_template) { 1434 $_db_template['hostid'] = $_db_template['templateid']; 1435 unset($_db_template['templateid']); 1436 } 1437 unset($_db_template); 1438 1439 $_db_hosts = array_merge($_db_hosts, $_db_templates); 1440 } 1441 1442 foreach ($_db_hosts as $_db_host) { 1443 $host_keys = &$hosts_keys[$_db_host['host']]; 1444 1445 $host_keys['hostid'] = $_db_host['hostid']; 1446 $host_keys['status'] = $_db_host['status']; 1447 1448 if ($class === 'CTriggerPrototype') { 1449 $sql = 'SELECT i.itemid,i.key_,i.value_type,i.flags,id.parent_itemid'. 1450 ' FROM items i'. 1451 ' LEFT JOIN item_discovery id ON i.itemid=id.itemid'. 1452 ' WHERE i.hostid='.$host_keys['hostid']. 1453 ' AND '.dbConditionString('i.key_', array_keys($host_keys['keys'])). 1454 ' AND '.dbConditionInt('i.flags', 1455 [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_PROTOTYPE, ZBX_FLAG_DISCOVERY_CREATED] 1456 ); 1457 } 1458 else { 1459 $sql = 'SELECT i.itemid,i.key_,i.value_type,i.flags'. 1460 ' FROM items i'. 1461 ' WHERE i.hostid='.$host_keys['hostid']. 1462 ' AND '.dbConditionString('i.key_', array_keys($host_keys['keys'])). 1463 ' AND '.dbConditionInt('i.flags', [ZBX_FLAG_DISCOVERY_NORMAL, ZBX_FLAG_DISCOVERY_CREATED]); 1464 } 1465 1466 $_db_items = DBselect($sql); 1467 1468 while ($_db_item = DBfetch($_db_items)) { 1469 $host_keys['keys'][$_db_item['key_']]['itemid'] = $_db_item['itemid']; 1470 $host_keys['keys'][$_db_item['key_']]['value_type'] = $_db_item['value_type']; 1471 $host_keys['keys'][$_db_item['key_']]['flags'] = $_db_item['flags']; 1472 1473 if ($class === 'CTriggerPrototype' && $_db_item['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) { 1474 $host_keys['keys'][$_db_item['key_']]['lld_ruleid'] = $_db_item['parent_itemid']; 1475 } 1476 } 1477 1478 unset($host_keys); 1479 } 1480 1481 /* 1482 * The list of triggers with multiple templates. 1483 * 1484 * [ 1485 * [ 1486 * 'description' => <description>, 1487 * 'templateids' => [<templateid>, ...] 1488 * ], 1489 * ... 1490 * ] 1491 */ 1492 $mt_triggers = []; 1493 1494 if ($class === 'CTrigger') { 1495 /* 1496 * The list of triggers which are moved from one host or template to another. 1497 * 1498 * [ 1499 * <triggerid> => [ 1500 * 'description' => <description> 1501 * ], 1502 * ... 1503 * ] 1504 */ 1505 $moved_triggers = []; 1506 } 1507 1508 foreach ($triggers as $tnum => &$trigger) { 1509 $expressions_changed = $db_triggers === null 1510 || ($trigger['expression'] !== $db_triggers[$tnum]['expression'] 1511 || $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression']); 1512 1513 if (!$expressions_changed) { 1514 continue; 1515 } 1516 1517 $expressionData->parse($trigger['expression']); 1518 $expressions1 = $expressionData->expressions; 1519 $expressions2 = []; 1520 1521 if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 1522 $expressionData->parse($trigger['recovery_expression']); 1523 $expressions2 = $expressionData->expressions; 1524 } 1525 1526 $triggers_functions[$tnum] = []; 1527 if ($class === 'CTriggerPrototype') { 1528 $lld_ruleids = []; 1529 } 1530 1531 /* 1532 * 0x01 - with templates 1533 * 0x02 - with hosts 1534 */ 1535 $status_mask = 0x00; 1536 // The lists of hostids and hosts which are used in the current trigger. 1537 $hostids = []; 1538 $hosts = []; 1539 1540 // Common checks. 1541 foreach (array_merge($expressions1, $expressions2) as $exprPart) { 1542 $host_keys = $hosts_keys[$exprPart['host']]; 1543 $key = $host_keys['keys'][$exprPart['item']]; 1544 1545 if ($host_keys['hostid'] === null) { 1546 self::exception(ZBX_API_ERROR_PARAMETERS, _params($error_wrong_host, [$host_keys['host']])); 1547 } 1548 1549 if ($key['itemid'] === null) { 1550 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 1551 'Incorrect item key "%1$s" provided for trigger expression on "%2$s".', $key['key'], 1552 $host_keys['host'] 1553 )); 1554 } 1555 1556 if (!$triggerFunctionValidator->validate([ 1557 'function' => $exprPart['function'], 1558 'functionName' => $exprPart['functionName'], 1559 'functionParamList' => $exprPart['functionParamList'], 1560 'valueType' => $key['value_type']])) { 1561 self::exception(ZBX_API_ERROR_PARAMETERS, $triggerFunctionValidator->getError()); 1562 } 1563 1564 if (!array_key_exists($exprPart['expression'], $triggers_functions[$tnum])) { 1565 $triggers_functions[$tnum][$exprPart['expression']] = [ 1566 'functionid' => null, 1567 'triggerid' => null, 1568 'itemid' => $key['itemid'], 1569 'name' => $exprPart['functionName'], 1570 'parameter' => $exprPart['functionParam'] 1571 ]; 1572 $functions_num++; 1573 } 1574 1575 if ($class === 'CTriggerPrototype' && $key['flags'] == ZBX_FLAG_DISCOVERY_PROTOTYPE) { 1576 $lld_ruleids[$key['lld_ruleid']] = true; 1577 } 1578 1579 $status_mask |= ($host_keys['status'] == HOST_STATUS_TEMPLATE ? 0x01 : 0x02); 1580 $hostids[$host_keys['hostid']] = true; 1581 $hosts[$exprPart['host']] = true; 1582 } 1583 1584 // When both templates and hosts are referenced in expressions. 1585 if ($status_mask == 0x03) { 1586 self::exception(ZBX_API_ERROR_PARAMETERS, $error_host_and_template); 1587 } 1588 1589 // Triggers with children cannot be moved from one template to another host or template. 1590 if ($class === 'CTrigger' && $db_triggers !== null && $expressions_changed) { 1591 $expressionData->parse($db_triggers[$tnum]['expression']); 1592 $old_hosts1 = $expressionData->getHosts(); 1593 $old_hosts2 = []; 1594 1595 if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 1596 $expressionData->parse($db_triggers[$tnum]['recovery_expression']); 1597 $old_hosts2 = $expressionData->getHosts(); 1598 } 1599 1600 $is_moved = true; 1601 foreach (array_merge($old_hosts1, $old_hosts2) as $old_host) { 1602 if (array_key_exists($old_host, $hosts)) { 1603 $is_moved = false; 1604 break; 1605 } 1606 } 1607 1608 if ($is_moved) { 1609 $moved_triggers[$db_triggers[$tnum]['triggerid']] = ['description' => $trigger['description']]; 1610 } 1611 } 1612 1613 // The trigger with multiple templates. 1614 if ($status_mask == 0x01 && count($hostids) > 1) { 1615 $mt_triggers[] = [ 1616 'description' => $trigger['description'], 1617 'templateids' => array_keys($hostids) 1618 ]; 1619 } 1620 1621 if ($class === 'CTriggerPrototype') { 1622 $lld_ruleids = array_keys($lld_ruleids); 1623 1624 if (!$lld_ruleids) { 1625 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 1626 'Trigger prototype "%1$s" must contain at least one item prototype.', $trigger['description'] 1627 )); 1628 } 1629 elseif (count($lld_ruleids) > 1) { 1630 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 1631 'Trigger prototype "%1$s" contains item prototypes from multiple discovery rules.', 1632 $trigger['description'] 1633 )); 1634 } 1635 elseif ($db_triggers !== null 1636 && !idcmp($lld_ruleids[0], $db_triggers[$tnum]['discoveryRule']['itemid'])) { 1637 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger prototype "%1$s": %2$s.', 1638 $trigger['description'], _('trigger prototype cannot be moved to another template or host') 1639 )); 1640 } 1641 } 1642 } 1643 unset($trigger); 1644 1645 if ($mt_triggers) { 1646 $this->validateTriggersWithMultipleTemplates($mt_triggers); 1647 } 1648 1649 if ($class === 'CTrigger' && $moved_triggers) { 1650 $this->validateMovedTriggers($moved_triggers); 1651 } 1652 1653 $functionid = DB::reserveIds('functions', $functions_num); 1654 1655 $expression_max_length = DB::getFieldLength('triggers', 'expression'); 1656 $recovery_expression_max_length = DB::getFieldLength('triggers', 'recovery_expression'); 1657 1658 // Replace {host:item.func()} macros with {<functionid>}. 1659 foreach ($triggers as $tnum => &$trigger) { 1660 $expressions_changed = $db_triggers === null 1661 || ($trigger['expression'] !== $db_triggers[$tnum]['expression'] 1662 || $trigger['recovery_expression'] !== $db_triggers[$tnum]['recovery_expression']); 1663 1664 if (!$expressions_changed) { 1665 continue; 1666 } 1667 1668 foreach ($triggers_functions[$tnum] as &$trigger_function) { 1669 $trigger_function['functionid'] = $functionid; 1670 $functionid = bcadd($functionid, 1, 0); 1671 } 1672 unset($function); 1673 1674 $expressionData->parse($trigger['expression']); 1675 $exprPart = end($expressionData->expressions); 1676 do { 1677 $trigger['expression'] = substr_replace($trigger['expression'], 1678 '{'.$triggers_functions[$tnum][$exprPart['expression']]['functionid'].'}', 1679 $exprPart['pos'], strlen($exprPart['expression']) 1680 ); 1681 } 1682 while ($exprPart = prev($expressionData->expressions)); 1683 1684 if (mb_strlen($trigger['expression']) > $expression_max_length) { 1685 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 1686 'Invalid parameter "%1$s": %2$s.', '/'.($tnum + 1).'/expression', _('value is too long') 1687 )); 1688 } 1689 1690 if ($trigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 1691 $expressionData->parse($trigger['recovery_expression']); 1692 $exprPart = end($expressionData->expressions); 1693 do { 1694 $trigger['recovery_expression'] = substr_replace($trigger['recovery_expression'], 1695 '{'.$triggers_functions[$tnum][$exprPart['expression']]['functionid'].'}', 1696 $exprPart['pos'], strlen($exprPart['expression']) 1697 ); 1698 } 1699 while ($exprPart = prev($expressionData->expressions)); 1700 1701 if (mb_strlen($trigger['recovery_expression']) > $recovery_expression_max_length) { 1702 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid parameter "%1$s": %2$s.', 1703 '/'.($tnum + 1).'/recovery_expression', _('value is too long') 1704 )); 1705 } 1706 } 1707 } 1708 unset($trigger); 1709 } 1710 1711 /** 1712 * Check if all templates trigger belongs to are linked to same hosts. 1713 * 1714 * @param array $mt_triggers 1715 * @param string $mt_triggers[]['description'] 1716 * @param array $mt_triggers[]['templateids'] 1717 * 1718 * @throws APIException 1719 */ 1720 protected function validateTriggersWithMultipleTemplates(array $mt_triggers) { 1721 switch (get_class($this)) { 1722 case 'CTrigger': 1723 $error_different_linkages = _('Trigger "%1$s" belongs to templates with different linkages.'); 1724 break; 1725 1726 case 'CTriggerPrototype': 1727 $error_different_linkages = _('Trigger prototype "%1$s" belongs to templates with different linkages.'); 1728 break; 1729 1730 default: 1731 self::exception(ZBX_API_ERROR_INTERNAL, _('Internal error.')); 1732 } 1733 1734 $templateids = []; 1735 1736 foreach ($mt_triggers as $mt_trigger) { 1737 foreach ($mt_trigger['templateids'] as $templateid) { 1738 $templateids[$templateid] = true; 1739 } 1740 } 1741 1742 $templates = API::Template()->get([ 1743 'output' => [], 1744 'selectHosts' => ['hostid'], 1745 'selectTemplates' => ['templateid'], 1746 'templateids' => array_keys($templateids), 1747 'nopermissions' => true, 1748 'preservekeys' => true 1749 ]); 1750 1751 foreach ($templates as &$template) { 1752 $template = array_merge( 1753 zbx_objectValues($template['hosts'], 'hostid'), 1754 zbx_objectValues($template['templates'], 'templateid') 1755 ); 1756 } 1757 unset($template); 1758 1759 foreach ($mt_triggers as $mt_trigger) { 1760 $compare_links = null; 1761 1762 foreach ($mt_trigger['templateids'] as $templateid) { 1763 if ($compare_links === null) { 1764 $compare_links = $templates[$templateid]; 1765 continue; 1766 } 1767 1768 $linked_to = $templates[$templateid]; 1769 1770 if (array_diff($compare_links, $linked_to) || array_diff($linked_to, $compare_links)) { 1771 self::exception(ZBX_API_ERROR_PARAMETERS, 1772 _params($error_different_linkages, [$mt_trigger['description']]) 1773 ); 1774 } 1775 } 1776 } 1777 } 1778 1779 /** 1780 * Check if moved triggers does not have children. 1781 * 1782 * @param array $moved_triggers 1783 * @param string $moved_triggers[<triggerid>]['description'] 1784 * 1785 * @throws APIException 1786 */ 1787 protected function validateMovedTriggers(array $moved_triggers) { 1788 $_db_triggers = DBselect( 1789 'SELECT t.templateid'. 1790 ' FROM triggers t'. 1791 ' WHERE '.dbConditionInt('t.templateid', array_keys($moved_triggers)), 1792 1 1793 ); 1794 1795 if ($_db_trigger = DBfetch($_db_triggers)) { 1796 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Cannot update trigger "%1$s": %2$s.', 1797 $moved_triggers[$_db_trigger['templateid']]['description'], 1798 _('trigger with linkages cannot be moved to another template or host') 1799 )); 1800 } 1801 } 1802 1803 /** 1804 * Adds triggers and trigger prototypes from template to hosts. 1805 * 1806 * @param array $data 1807 */ 1808 public function syncTemplates(array $data) { 1809 $data['templateids'] = zbx_toArray($data['templateids']); 1810 $data['hostids'] = zbx_toArray($data['hostids']); 1811 1812 $output = ['triggerid', 'description', 'expression', 'recovery_mode', 'recovery_expression', 'url', 'status', 1813 'priority', 'comments', 'type', 'correlation_mode', 'correlation_tag', 'manual_close', 'opdata' 1814 ]; 1815 if ($this instanceof CTriggerPrototype) { 1816 $output[] = 'discover'; 1817 } 1818 1819 $triggers = $this->get([ 1820 'output' => $output, 1821 'selectTags' => ['tag', 'value'], 1822 'hostids' => $data['templateids'], 1823 'preservekeys' => true 1824 ]); 1825 1826 $triggers = CMacrosResolverHelper::resolveTriggerExpressions($triggers, 1827 ['sources' => ['expression', 'recovery_expression']] 1828 ); 1829 1830 $this->inherit($triggers, $data['hostids']); 1831 } 1832} 1833