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