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 * Get trigger severity full line height css style name. 24 * 25 * @param int $severity Trigger severity. 26 * 27 * @return string|null 28 */ 29function getSeverityFlhStyle($severity) { 30 switch ($severity) { 31 case TRIGGER_SEVERITY_DISASTER: 32 return ZBX_STYLE_FLH_DISASTER_BG; 33 case TRIGGER_SEVERITY_HIGH: 34 return ZBX_STYLE_FLH_HIGH_BG; 35 case TRIGGER_SEVERITY_AVERAGE: 36 return ZBX_STYLE_FLH_AVERAGE_BG; 37 case TRIGGER_SEVERITY_WARNING: 38 return ZBX_STYLE_FLH_WARNING_BG; 39 case TRIGGER_SEVERITY_INFORMATION: 40 return ZBX_STYLE_FLH_INFO_BG; 41 case TRIGGER_SEVERITY_NOT_CLASSIFIED: 42 return ZBX_STYLE_FLH_NA_BG; 43 default: 44 return null; 45 } 46} 47 48/** 49 * Get trigger severity status css style name. 50 * 51 * @param int $severity Trigger severity. 52 * 53 * @return string|null 54 */ 55function getSeverityStatusStyle($severity) { 56 switch ($severity) { 57 case TRIGGER_SEVERITY_DISASTER: 58 return ZBX_STYLE_STATUS_DISASTER_BG; 59 case TRIGGER_SEVERITY_HIGH: 60 return ZBX_STYLE_STATUS_HIGH_BG; 61 case TRIGGER_SEVERITY_AVERAGE: 62 return ZBX_STYLE_STATUS_AVERAGE_BG; 63 case TRIGGER_SEVERITY_WARNING: 64 return ZBX_STYLE_STATUS_WARNING_BG; 65 case TRIGGER_SEVERITY_INFORMATION: 66 return ZBX_STYLE_STATUS_INFO_BG; 67 case TRIGGER_SEVERITY_NOT_CLASSIFIED: 68 return ZBX_STYLE_STATUS_NA_BG; 69 default: 70 return null; 71 } 72} 73 74function getSeverityStyle($severity, $type = true) { 75 if (!$type) { 76 return ZBX_STYLE_NORMAL_BG; 77 } 78 79 switch ($severity) { 80 case TRIGGER_SEVERITY_DISASTER: 81 return ZBX_STYLE_DISASTER_BG; 82 case TRIGGER_SEVERITY_HIGH: 83 return ZBX_STYLE_HIGH_BG; 84 case TRIGGER_SEVERITY_AVERAGE: 85 return ZBX_STYLE_AVERAGE_BG; 86 case TRIGGER_SEVERITY_WARNING: 87 return ZBX_STYLE_WARNING_BG; 88 case TRIGGER_SEVERITY_INFORMATION: 89 return ZBX_STYLE_INFO_BG; 90 case TRIGGER_SEVERITY_NOT_CLASSIFIED: 91 return ZBX_STYLE_NA_BG; 92 default: 93 return null; 94 } 95} 96 97/** 98 * Get trigger severity name by given state and configuration. 99 * 100 * @param int $severity Trigger severity. 101 * 102 * @return string 103 */ 104function getSeverityName($severity) { 105 switch ($severity) { 106 case TRIGGER_SEVERITY_NOT_CLASSIFIED: 107 return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_0)); 108 case TRIGGER_SEVERITY_INFORMATION: 109 return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_1)); 110 case TRIGGER_SEVERITY_WARNING: 111 return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_2)); 112 case TRIGGER_SEVERITY_AVERAGE: 113 return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_3)); 114 case TRIGGER_SEVERITY_HIGH: 115 return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_4)); 116 case TRIGGER_SEVERITY_DISASTER: 117 return _(CSettingsHelper::get(CSettingsHelper::SEVERITY_NAME_5)); 118 default: 119 return _('Unknown'); 120 } 121} 122 123function getSeverityColor($severity, $value = TRIGGER_VALUE_TRUE) { 124 if ($value == TRIGGER_VALUE_FALSE) { 125 return 'AAFFAA'; 126 } 127 128 switch ($severity) { 129 case TRIGGER_SEVERITY_DISASTER: 130 $color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_5); 131 break; 132 case TRIGGER_SEVERITY_HIGH: 133 $color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_4); 134 break; 135 case TRIGGER_SEVERITY_AVERAGE: 136 $color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_3); 137 break; 138 case TRIGGER_SEVERITY_WARNING: 139 $color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_2); 140 break; 141 case TRIGGER_SEVERITY_INFORMATION: 142 $color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_1); 143 break; 144 case TRIGGER_SEVERITY_NOT_CLASSIFIED: 145 $color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_0); 146 break; 147 default: 148 $color = CSettingsHelper::get(CSettingsHelper::SEVERITY_COLOR_0); 149 } 150 151 return $color; 152} 153 154/** 155 * Generate array with severities options. 156 * 157 * @param int $min Minimal severity. 158 * @param int $max Maximum severity. 159 * 160 * @return array 161 */ 162function getSeverities($min = TRIGGER_SEVERITY_NOT_CLASSIFIED, $max = TRIGGER_SEVERITY_COUNT - 1) { 163 $severities = []; 164 165 foreach (range($min, $max) as $severity) { 166 $severities[] = [ 167 'name' => getSeverityName($severity), 168 'value' => $severity, 169 'style' => getSeverityStyle($severity) 170 ]; 171 } 172 173 return $severities; 174} 175 176/** 177 * Returns HTML representation of trigger severity cell containing severity name and color. 178 * 179 * @param int $severity Trigger, Event or Problem severity. 180 * @param string|null $text Trigger severity name. 181 * @param bool $force_normal True to return 'normal' class, false to return corresponding severity class. 182 * @param bool $return_as_div True to return severity cell as DIV element. 183 * 184 * @return CDiv|CCol 185 */ 186function getSeverityCell($severity, $text = null, $force_normal = false, $return_as_div = false) { 187 if ($text === null) { 188 $text = CHtml::encode(getSeverityName($severity)); 189 } 190 191 if ($force_normal) { 192 return new CCol($text); 193 } 194 195 $return = $return_as_div ? new CDiv($text) : new CCol($text); 196 return $return->addClass(getSeverityStyle($severity)); 197} 198 199/** 200 * Add color style and blinking to an object like CSpan or CDiv depending on trigger status. 201 * Settings and colors are kept in 'config' database table. 202 * 203 * @param mixed $object object like CSpan, CDiv, etc. 204 * @param int $triggerValue TRIGGER_VALUE_FALSE or TRIGGER_VALUE_TRUE 205 * @param int $triggerLastChange 206 * @param bool $isAcknowledged 207 */ 208function addTriggerValueStyle($object, $triggerValue, $triggerLastChange, $isAcknowledged) { 209 $color_class = null; 210 $blinks = null; 211 212 // Color class for text and blinking depends on trigger value and whether event is acknowledged. 213 if ($triggerValue == TRIGGER_VALUE_TRUE && !$isAcknowledged) { 214 $color_class = ZBX_STYLE_PROBLEM_UNACK_FG; 215 $blinks = CSettingsHelper::get(CSettingsHelper::PROBLEM_UNACK_STYLE); 216 } 217 elseif ($triggerValue == TRIGGER_VALUE_TRUE && $isAcknowledged) { 218 $color_class = ZBX_STYLE_PROBLEM_ACK_FG; 219 $blinks = CSettingsHelper::get(CSettingsHelper::PROBLEM_ACK_STYLE); 220 } 221 elseif ($triggerValue == TRIGGER_VALUE_FALSE && !$isAcknowledged) { 222 $color_class = ZBX_STYLE_OK_UNACK_FG; 223 $blinks = CSettingsHelper::get(CSettingsHelper::OK_UNACK_STYLE); 224 } 225 elseif ($triggerValue == TRIGGER_VALUE_FALSE && $isAcknowledged) { 226 $color_class = ZBX_STYLE_OK_ACK_FG; 227 $blinks = CSettingsHelper::get(CSettingsHelper::OK_ACK_STYLE); 228 } 229 230 if ($color_class != null && $blinks != null) { 231 $object->addClass($color_class); 232 233 // blinking 234 $timeSinceLastChange = time() - $triggerLastChange; 235 $blink_period = timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::BLINK_PERIOD)); 236 237 if ($blinks && $timeSinceLastChange < $blink_period) { 238 $object->addClass('blink'); // elements with this class will blink 239 $object->setAttribute('data-time-to-blink', $blink_period - $timeSinceLastChange); 240 } 241 } 242 else { 243 $object->addClass(ZBX_STYLE_GREY); 244 } 245} 246 247function trigger_value2str($value = null) { 248 $triggerValues = [ 249 TRIGGER_VALUE_FALSE => _('OK'), 250 TRIGGER_VALUE_TRUE => _('PROBLEM') 251 ]; 252 253 if ($value === null) { 254 return $triggerValues; 255 } 256 elseif (isset($triggerValues[$value])) { 257 return $triggerValues[$value]; 258 } 259 260 return _('Unknown'); 261} 262 263function get_trigger_by_triggerid($triggerid) { 264 $db_trigger = DBfetch(DBselect('SELECT t.* FROM triggers t WHERE t.triggerid='.zbx_dbstr($triggerid))); 265 if (!empty($db_trigger)) { 266 return $db_trigger; 267 } 268 error(_s('No trigger with triggerid "%1$s".', $triggerid)); 269 270 return false; 271} 272 273function get_hosts_by_triggerid($triggerids) { 274 zbx_value2array($triggerids); 275 276 return DBselect( 277 'SELECT DISTINCT h.*'. 278 ' FROM hosts h,functions f,items i'. 279 ' WHERE i.itemid=f.itemid'. 280 ' AND h.hostid=i.hostid'. 281 ' AND '.dbConditionInt('f.triggerid', $triggerids) 282 ); 283} 284 285function get_triggers_by_hostid($hostid) { 286 return DBselect( 287 'SELECT DISTINCT t.*'. 288 ' FROM triggers t,functions f,items i'. 289 ' WHERE i.hostid='.zbx_dbstr($hostid). 290 ' AND f.itemid=i.itemid'. 291 ' AND f.triggerid=t.triggerid' 292 ); 293} 294 295// unescape Raw URL 296function utf8RawUrlDecode($source) { 297 $decodedStr = ''; 298 $pos = 0; 299 $len = strlen($source); 300 while ($pos < $len) { 301 $charAt = substr($source, $pos, 1); 302 if ($charAt == '%') { 303 $pos++; 304 $charAt = substr($source, $pos, 1); 305 if ($charAt == 'u') { 306 // we got a unicode character 307 $pos++; 308 $unicodeHexVal = substr($source, $pos, 4); 309 $unicode = hexdec($unicodeHexVal); 310 $entity = "&#".$unicode.';'; 311 $decodedStr .= html_entity_decode(utf8_encode($entity), ENT_COMPAT, 'UTF-8'); 312 $pos += 4; 313 } 314 else { 315 $decodedStr .= substr($source, $pos-1, 1); 316 } 317 } 318 else { 319 $decodedStr .= $charAt; 320 $pos++; 321 } 322 } 323 324 return $decodedStr; 325} 326 327/** 328 * Copies the given triggers to the given hosts or templates. 329 * 330 * Without the $src_hostid parameter it will only be able to copy triggers that belong to only one host. If the 331 * $src_hostid parameter is not passed, and a trigger has multiple hosts, it will throw an error. If the 332 * $src_hostid parameter is passed, the given host will be replaced with the destination host. 333 * 334 * This function takes care of copied trigger dependencies. 335 * If trigger is copied alongside with trigger on which it depends, then dependencies is replaced directly using new ids, 336 * If there is target host within dependency trigger, algorithm will search for potential matching trigger in target host, 337 * if matching trigger is found, then id from this trigger is used, if not rise exception, 338 * otherwise original dependency will be left. 339 * 340 * 341 * @param array $src_triggerids Triggers which will be copied to $dst_hostids 342 * @param array $dst_hostids Hosts and templates to whom add triggers. IDs not present in DB (host table) 343 * will be ignored. 344 * @param int $src_hostid Host ID in which context trigger with multiple hosts will be treated. 345 * 346 * @return bool 347 */ 348function copyTriggersToHosts($src_triggerids, $dst_hostids, $src_hostid = null) { 349 $options = [ 350 'output' => ['triggerid', 'expression', 'description', 'url', 'status', 'priority', 'comments', 'type', 351 'recovery_mode', 'recovery_expression', 'correlation_mode', 'correlation_tag', 'manual_close', 'opdata', 352 'event_name' 353 ], 354 'selectDependencies' => ['triggerid'], 355 'selectTags' => ['tag', 'value'], 356 'triggerids' => $src_triggerids 357 ]; 358 359 if ($src_hostid) { 360 $srcHost = API::Host()->get([ 361 'output' => ['host'], 362 'hostids' => $src_hostid, 363 'preservekeys' => true, 364 'templated_hosts' => true 365 ]); 366 367 if (!$srcHost = reset($srcHost)) { 368 return false; 369 } 370 } 371 else { 372 // Select source trigger first host 'host'. 373 $options['selectHosts'] = ['host']; 374 } 375 376 $dbSrcTriggers = API::Trigger()->get($options); 377 378 $dbSrcTriggers = CMacrosResolverHelper::resolveTriggerExpressions($dbSrcTriggers, 379 ['sources' => ['expression', 'recovery_expression']] 380 ); 381 382 $dbDstHosts = API::Host()->get([ 383 'output' => ['hostid', 'host'], 384 'hostids' => $dst_hostids, 385 'preservekeys' => true, 386 'templated_hosts' => true 387 ]); 388 389 $newTriggers = []; 390 391 foreach ($dbDstHosts as $dstHost) { 392 // Create each trigger for each host. 393 394 foreach ($dbSrcTriggers as $srcTrigger) { 395 if ($src_hostid) { 396 // Get host 'host' for triggerExpressionReplaceHost(). 397 398 $host = $srcHost['host']; 399 $srcTriggerContextHostId = $src_hostid; 400 } 401 else { 402 if (count($srcTrigger['hosts']) > 1) { 403 error(_s('Cannot copy trigger "%1$s:%2$s", because it has multiple hosts in the expression.', 404 $srcTrigger['description'], $srcTrigger['expression'] 405 )); 406 407 return false; 408 } 409 410 // Use source trigger first host 'host'. 411 $host = $srcTrigger['hosts'][0]['host']; 412 $srcTriggerContextHostId = $srcTrigger['hosts'][0]['hostid']; 413 } 414 415 $srcTrigger['expression'] = triggerExpressionReplaceHost($srcTrigger['expression'], $host, 416 $dstHost['host'] 417 ); 418 419 if ($srcTrigger['recovery_mode'] == ZBX_RECOVERY_MODE_RECOVERY_EXPRESSION) { 420 $srcTrigger['recovery_expression'] = triggerExpressionReplaceHost($srcTrigger['recovery_expression'], 421 $host, $dstHost['host'] 422 ); 423 } 424 425 // The dependencies must be added after all triggers are created. 426 $result = API::Trigger()->create([[ 427 'description' => $srcTrigger['description'], 428 'event_name' => $srcTrigger['event_name'], 429 'opdata' => $srcTrigger['opdata'], 430 'expression' => $srcTrigger['expression'], 431 'url' => $srcTrigger['url'], 432 'status' => $srcTrigger['status'], 433 'priority' => $srcTrigger['priority'], 434 'comments' => $srcTrigger['comments'], 435 'type' => $srcTrigger['type'], 436 'recovery_mode' => $srcTrigger['recovery_mode'], 437 'recovery_expression' => $srcTrigger['recovery_expression'], 438 'correlation_mode' => $srcTrigger['correlation_mode'], 439 'correlation_tag' => $srcTrigger['correlation_tag'], 440 'tags' => $srcTrigger['tags'], 441 'manual_close' => $srcTrigger['manual_close'] 442 ]]); 443 444 if (!$result) { 445 return false; 446 } 447 448 $newTriggers[$srcTrigger['triggerid']][] = [ 449 'newTriggerId' => reset($result['triggerids']), 450 'newTriggerExpression' => $srcTrigger['expression'], 451 'newTriggerHostId' => $dstHost['hostid'], 452 'newTriggerHost' => $dstHost['host'], 453 'srcTriggerContextHostId' => $srcTriggerContextHostId, 454 'srcTriggerContextHost' => $host 455 ]; 456 } 457 } 458 459 $depids = []; 460 foreach ($dbSrcTriggers as $srcTrigger) { 461 foreach ($srcTrigger['dependencies'] as $depTrigger) { 462 $depids[] = $depTrigger['triggerid']; 463 } 464 } 465 $depTriggers = API::Trigger()->get([ 466 'triggerids' => $depids, 467 'output' => ['description', 'expression', 'recovery_mode', 'recovery_expression'], 468 'selectHosts' => ['hostid'], 469 'preservekeys' => true 470 ]); 471 472 $depTriggers = CMacrosResolverHelper::resolveTriggerExpressions($depTriggers, 473 ['sources' => ['expression', 'recovery_expression']] 474 ); 475 476 if ($newTriggers) { 477 // Map dependencies to the new trigger IDs and save. 478 479 $dependencies = []; 480 481 foreach ($dbSrcTriggers as $srcTrigger) { 482 if ($srcTrigger['dependencies']) { 483 // Get corresponding created triggers. 484 $dst_triggers = $newTriggers[$srcTrigger['triggerid']]; 485 486 foreach ($dst_triggers as $dst_trigger) { 487 foreach ($srcTrigger['dependencies'] as $depTrigger) { 488 /* 489 * We have added $depTrigger trigger and we know corresponding trigger ID for newly 490 * created trigger. 491 */ 492 if (array_key_exists($depTrigger['triggerid'], $newTriggers)) { 493 $dst_dep_triggers = $newTriggers[$depTrigger['triggerid']]; 494 495 foreach ($dst_dep_triggers as $dst_dep_trigger) { 496 /* 497 * Dependency is within same host according to $src_hostid parameter or dep trigger has 498 * single host. 499 */ 500 if ($dst_trigger['srcTriggerContextHostId'] == $dst_dep_trigger['srcTriggerContextHostId'] 501 && $dst_dep_trigger['newTriggerHostId'] == $dst_trigger['newTriggerHostId']) { 502 $depTriggerId = $dst_dep_trigger['newTriggerId']; 503 break; 504 } 505 // Dependency is to trigger from another host. 506 else { 507 $depTriggerId = $depTrigger['triggerid']; 508 } 509 } 510 } 511 // We need to search for $depTrigger trigger if target host is within dependency hosts. 512 elseif (in_array(['hostid' => $dst_trigger['srcTriggerContextHostId']], 513 $depTriggers[$depTrigger['triggerid']]['hosts'])) { 514 // Get all possible $depTrigger matching triggers by description. 515 $targetHostTriggersByDescription = API::Trigger()->get([ 516 'hostids' => $dst_trigger['newTriggerHostId'], 517 'output' => ['hosts', 'triggerid', 'expression'], 518 'filter' => ['description' => $depTriggers[$depTrigger['triggerid']]['description']], 519 'preservekeys' => true 520 ]); 521 522 $targetHostTriggersByDescription = 523 CMacrosResolverHelper::resolveTriggerExpressions($targetHostTriggersByDescription); 524 525 // Compare exploded expressions for exact match. 526 $expr1 = $depTriggers[$depTrigger['triggerid']]['expression']; 527 $depTriggerId = null; 528 529 foreach ($targetHostTriggersByDescription as $potentialTargetTrigger) { 530 $expr2 = triggerExpressionReplaceHost($potentialTargetTrigger['expression'], 531 $dst_trigger['newTriggerHost'], $dst_trigger['srcTriggerContextHost'] 532 ); 533 534 if ($expr2 == $expr1) { 535 // Matching trigger has been found. 536 $depTriggerId = $potentialTargetTrigger['triggerid']; 537 break; 538 } 539 } 540 541 // If matching trigger wasn't found raise exception. 542 if ($depTriggerId === null) { 543 $expr2 = triggerExpressionReplaceHost($expr1, $dst_trigger['srcTriggerContextHost'], 544 $dst_trigger['newTriggerHost'] 545 ); 546 547 error(_s( 548 'Cannot add dependency from trigger "%1$s:%2$s" to non existing trigger "%3$s:%4$s".', 549 $srcTrigger['description'], $dst_trigger['newTriggerExpression'], 550 $depTriggers[$depTrigger['triggerid']]['description'], $expr2 551 )); 552 553 return false; 554 } 555 } 556 else { 557 // Leave original dependency. 558 559 $depTriggerId = $depTrigger['triggerid']; 560 } 561 562 $dependencies[] = [ 563 'triggerid' => $dst_trigger['newTriggerId'], 564 'dependsOnTriggerid' => $depTriggerId 565 ]; 566 } 567 } 568 } 569 } 570 571 if ($dependencies) { 572 if (!API::Trigger()->addDependencies($dependencies)) { 573 return false; 574 } 575 } 576 } 577 578 return true; 579} 580 581/** 582 * Purpose: Replaces host in trigger expression. 583 * nodata(/localhost/agent.ping, 5m) => nodata(/localhost6/agent.ping, 5m) 584 * 585 * @param string $expression full expression with host names and item keys 586 * @param string $src_host 587 * @param string $dst_host 588 * 589 * @return string 590 */ 591function triggerExpressionReplaceHost(string $expression, string $src_host, string $dst_host): string { 592 $expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]); 593 594 if ($expression_parser->parse($expression) == CParser::PARSE_SUCCESS) { 595 $hist_functions = $expression_parser->getResult()->getTokensOfTypes( 596 [CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION] 597 ); 598 $hist_function = end($hist_functions); 599 do { 600 $query_parameter = $hist_function['data']['parameters'][0]; 601 if ($query_parameter['data']['host'] === $src_host) { 602 $expression = substr_replace($expression, '/'.$dst_host.'/'.$query_parameter['data']['item'], 603 $query_parameter['pos'], $query_parameter['length'] 604 ); 605 } 606 } 607 while ($hist_function = prev($hist_functions)); 608 } 609 610 return $expression; 611} 612 613function replace_template_dependencies($deps, $hostid) { 614 foreach ($deps as $id => $val) { 615 $sql = 'SELECT t.triggerid'. 616 ' FROM triggers t,functions f,items i'. 617 ' WHERE t.triggerid=f.triggerid'. 618 ' AND f.itemid=i.itemid'. 619 ' AND t.templateid='.zbx_dbstr($val). 620 ' AND i.hostid='.zbx_dbstr($hostid); 621 if ($db_new_dep = DBfetch(DBselect($sql))) { 622 $deps[$id] = $db_new_dep['triggerid']; 623 } 624 } 625 626 return $deps; 627} 628 629/** 630 * Prepare arrays containing only hosts and triggers that will be shown results table. 631 * 632 * @param array $db_hosts 633 * @param array $db_triggers 634 * 635 * @return array 636 */ 637function getTriggersOverviewTableData(array $db_hosts, array $db_triggers): array { 638 // Prepare triggers to show in results table. 639 $triggers_by_name = []; 640 foreach ($db_triggers as $trigger) { 641 foreach ($trigger['hosts'] as $host) { 642 if (!array_key_exists($host['hostid'], $db_hosts)) { 643 continue; 644 } 645 646 $triggers_by_name[$trigger['description']][$host['hostid']] = $trigger['triggerid']; 647 } 648 } 649 650 $limit = (int) CSettingsHelper::get(CSettingsHelper::MAX_OVERVIEW_TABLE_SIZE); 651 $exceeded_trigs = (count($triggers_by_name) > $limit); 652 $triggers_by_name = array_slice($triggers_by_name, 0, $limit, true); 653 foreach ($triggers_by_name as $name => $triggers) { 654 $triggers_by_name[$name] = array_slice($triggers, 0, $limit, true); 655 } 656 657 // Prepare hosts to show in results table. 658 $exceeded_hosts = false; 659 $hosts_by_name = []; 660 foreach ($db_hosts as $host) { 661 if (count($hosts_by_name) >= $limit) { 662 $exceeded_hosts = true; 663 break; 664 } 665 else { 666 $hosts_by_name[$host['name']] = $host['hostid']; 667 } 668 } 669 670 return [$triggers_by_name, $hosts_by_name, ($exceeded_hosts || $exceeded_trigs)]; 671} 672 673/** 674 * @param array $groupids 675 * @param array $host_options 676 * @param array $trigger_options 677 * @param array $problem_options 678 * @param int $problem_options['min_severity'] (optional) Minimal problem severity. 679 * @param int $problem_options['show_suppressed'] (optional) Whether to show triggers with suppressed problems. 680 * @param int $problem_options['time_from'] (optional) The time starting from which the problems were created. 681 * @param array $problem_options['tags'] (optional) 682 * @param string $problem_options['tags'][]['tag'] (optional) 683 * @param int $problem_options['tags'][]['operation'] (optional) 684 * @param string $problem_options['tags'][]['value'] (optional) 685 * @param int $problem_options['evaltype'] (optional) 686 * 687 * @return array 688 */ 689function getTriggersOverviewData(array $groupids, array $host_options = [], array $trigger_options = [], 690 array $problem_options = []): array { 691 692 $host_options = [ 693 'output' => ['hostid', 'name'], 694 'groupids' => $groupids ? $groupids : null, 695 'with_monitored_triggers' => true, 696 'preservekeys' => true 697 ] + $host_options; 698 699 $trigger_options = [ 700 'output' => ['triggerid', 'expression', 'description', 'value', 'priority', 'lastchange', 'flags', 'comments', 701 'manual_close' 702 ], 703 'selectHosts' => ['hostid', 'name'], 704 'selectDependencies' => ['triggerid'], 705 'monitored' => true 706 ] + $trigger_options; 707 708 $problem_options += [ 709 'show_suppressed' => ZBX_PROBLEM_SUPPRESSED_FALSE 710 ]; 711 712 $limit = (int) CSettingsHelper::get(CSettingsHelper::MAX_OVERVIEW_TABLE_SIZE); 713 714 do { 715 $db_hosts = API::Host()->get(['limit' => $limit + 1] + $host_options); 716 $fetch_hosts = (count($db_hosts) > $limit); 717 718 $db_triggers = getTriggersWithActualSeverity([ 719 'hostids' => array_keys($db_hosts) 720 ] + $trigger_options, $problem_options); 721 722 if (!$db_triggers) { 723 $db_hosts = []; 724 } 725 726 // Unset hosts without having matching triggers. 727 $represented_hosts = []; 728 foreach ($db_triggers as $trigger) { 729 $hostids = array_column($trigger['hosts'], 'hostid'); 730 $represented_hosts += array_combine($hostids, $hostids); 731 } 732 733 $db_hosts = array_intersect_key($db_hosts, $represented_hosts); 734 735 $fetch_hosts &= (count($db_hosts) < $limit); 736 $limit += (int) CSettingsHelper::get(CSettingsHelper::MAX_OVERVIEW_TABLE_SIZE); 737 738 } while ($fetch_hosts); 739 740 CArrayHelper::sort($db_hosts, [ 741 ['field' => 'name', 'order' => ZBX_SORT_UP] 742 ]); 743 744 $db_triggers = CMacrosResolverHelper::resolveTriggerNames($db_triggers, true); 745 $dependencies = $db_triggers ? getTriggerDependencies($db_triggers) : []; 746 747 CArrayHelper::sort($db_triggers, [ 748 ['field' => 'description', 'order' => ZBX_SORT_UP] 749 ]); 750 751 [$triggers_by_name, $hosts_by_name, $exceeded_limit] = getTriggersOverviewTableData($db_hosts, $db_triggers); 752 753 return [$db_hosts, $db_triggers, $dependencies, $triggers_by_name, $hosts_by_name, $exceeded_limit]; 754} 755 756/** 757 * Get triggers data with priority set to highest priority of unresolved problems generated by this trigger. 758 * 759 * @param array $trigger_options API options. Array 'output' should contain 'value', option 760 * 'preservekeys' should be set to true. 761 * @param array $problem_options 762 * @param int $problem_options['show_suppressed'] Whether to show triggers with suppressed problems. 763 * @param int $problem_options['min_severity'] (optional) Minimal problem severity. 764 * @param int $problem_options['time_from'] (optional) The time starting from which the problems were 765 * created. 766 * @param bool $problem_options['acknowledged'] (optional) Whether to show triggers with acknowledged 767 * problems. 768 * @param array $problem_options['tags'] (optional) 769 * @param string $problem_options['tags'][]['tag'] (optional) 770 * @param int $problem_options['tags'][]['operation'] (optional) 771 * @param string $problem_options['tags'][]['value'] (optional) 772 * @param int $problem_options['evaltype'] (optional) 773 * 774 * @return array 775 */ 776function getTriggersWithActualSeverity(array $trigger_options, array $problem_options) { 777 $problem_options += [ 778 'min_severity' => TRIGGER_SEVERITY_NOT_CLASSIFIED, 779 'show_suppressed' => null, 780 'show_recent' => null, 781 'time_from' => null, 782 'acknowledged' => null 783 ]; 784 785 $triggers = API::Trigger()->get(['preservekeys' => true] + $trigger_options); 786 787 $nondependent_trigger_options = [ 788 'output' => [], 789 'triggerids' => array_keys($triggers), 790 'skipDependent' => true, 791 'preservekeys' => true 792 ]; 793 794 $nondependent_triggers = API::Trigger()->get($nondependent_trigger_options); 795 796 CArrayHelper::sort($triggers, ['description']); 797 798 if ($triggers) { 799 $problem_stats = []; 800 801 foreach ($triggers as $triggerid => &$trigger) { 802 $trigger['priority'] = TRIGGER_SEVERITY_NOT_CLASSIFIED; 803 $trigger['resolved'] = true; 804 805 $problem_stats[$triggerid] = [ 806 'has_resolved' => false, 807 'has_unresolved' => false, 808 'has_resolved_unacknowledged' => false, 809 'has_unresolved_unacknowledged' => false 810 ]; 811 812 if ($trigger['value'] == TRIGGER_VALUE_TRUE && !array_key_exists($triggerid, $nondependent_triggers)) { 813 $trigger['value'] = TRIGGER_VALUE_FALSE; 814 } 815 } 816 unset($trigger); 817 818 $problems = API::Problem()->get([ 819 'output' => ['eventid', 'acknowledged', 'objectid', 'severity', 'r_eventid'], 820 'objectids' => array_keys($triggers), 821 'suppressed' => ($problem_options['show_suppressed'] == ZBX_PROBLEM_SUPPRESSED_FALSE) ? false : null, 822 'recent' => $problem_options['show_recent'], 823 'acknowledged' => $problem_options['acknowledged'], 824 'time_from' => $problem_options['time_from'], 825 'tags' => array_key_exists('tags', $problem_options) ? $problem_options['tags'] : null, 826 'evaltype' => array_key_exists('evaltype', $problem_options) 827 ? $problem_options['evaltype'] 828 : TAG_EVAL_TYPE_AND_OR 829 ]); 830 831 foreach ($problems as $problem) { 832 $triggerid = $problem['objectid']; 833 834 if ($problem['r_eventid'] == 0 && array_key_exists($triggerid, $nondependent_triggers)) { 835 $triggers[$triggerid]['resolved'] = false; 836 } 837 838 $triggers[$triggerid]['problem']['eventid'] = $problem['eventid']; 839 840 if ($triggers[$triggerid]['priority'] < $problem['severity']) { 841 $triggers[$triggerid]['priority'] = $problem['severity']; 842 } 843 844 if ($problem['r_eventid'] == 0) { 845 $problem_stats[$triggerid]['has_unresolved'] = true; 846 if ($problem['acknowledged'] == 0 && $problem['severity'] >= $problem_options['min_severity']) { 847 $problem_stats[$triggerid]['has_unresolved_unacknowledged'] = true; 848 } 849 } 850 else { 851 $problem_stats[$triggerid]['has_resolved'] = true; 852 if ($problem['acknowledged'] == 0 && $problem['severity'] >= $problem_options['min_severity']) { 853 $problem_stats[$triggerid]['has_resolved_unacknowledged'] = true; 854 } 855 } 856 } 857 858 foreach ($triggers as $triggerid => &$trigger) { 859 $stats = $problem_stats[$triggerid]; 860 861 $trigger['problem']['acknowledged'] = ( 862 // Trigger has only resolved problems, all acknowledged. 863 ($stats['has_resolved'] && !$stats['has_resolved_unacknowledged'] && !$stats['has_unresolved']) 864 // Trigger has unresolved problems, all acknowledged. 865 || ($stats['has_unresolved'] && !$stats['has_unresolved_unacknowledged']) 866 ) ? 1 : 0; 867 868 $trigger['value'] = ($triggers[$triggerid]['resolved'] === true) 869 ? TRIGGER_VALUE_FALSE 870 : TRIGGER_VALUE_TRUE; 871 872 if (($stats['has_resolved'] || $stats['has_unresolved']) 873 && $trigger['priority'] >= $problem_options['min_severity']) { 874 continue; 875 } 876 877 if (!array_key_exists('only_true', $trigger_options) 878 || ($trigger_options['only_true'] === null && $trigger_options['filter']['value'] === null)) { 879 // Overview type = 'Data', Maps, Dasboard or Overview 'show any' mode. 880 $trigger['value'] = TRIGGER_VALUE_FALSE; 881 } 882 else { 883 unset($triggers[$triggerid]); 884 } 885 } 886 unset($trigger); 887 } 888 889 return $triggers; 890} 891 892/** 893 * Creates and returns a trigger status cell for the trigger overview table. 894 * 895 * @param array $trigger 896 * @param array $dependencies The list of trigger dependencies, prepared by getTriggerDependencies() function. 897 * 898 * @return CCol 899 */ 900function getTriggerOverviewCell(array $trigger, array $dependencies): CCol { 901 $ack = $trigger['problem']['acknowledged'] == 1 ? (new CSpan())->addClass(ZBX_STYLE_ICON_ACKN) : null; 902 $desc = array_key_exists($trigger['triggerid'], $dependencies) 903 ? makeTriggerDependencies($dependencies[$trigger['triggerid']], false) 904 : []; 905 906 $column = (new CCol([$desc, $ack])) 907 ->addClass(getSeverityStyle($trigger['priority'], $trigger['value'] == TRIGGER_VALUE_TRUE)) 908 ->addClass(ZBX_STYLE_CURSOR_POINTER); 909 910 $eventid = 0; 911 $blink_period = timeUnitToSeconds(CSettingsHelper::get(CSettingsHelper::BLINK_PERIOD)); 912 $duration = time() - $trigger['lastchange']; 913 914 if ($blink_period > 0 && $duration < $blink_period) { 915 $column->addClass('blink'); 916 $column->setAttribute('data-time-to-blink', $blink_period - $duration); 917 $column->setAttribute('data-toggle-class', ZBX_STYLE_BLINK_HIDDEN); 918 } 919 920 if ($trigger['value'] == TRIGGER_VALUE_TRUE) { 921 $eventid = $trigger['problem']['eventid']; 922 $acknowledge = true; 923 } 924 else { 925 $acknowledge = false; 926 } 927 928 $column->setMenuPopup(CMenuPopupHelper::getTrigger($trigger['triggerid'], $eventid, $acknowledge)); 929 930 return $column; 931} 932 933/** 934 * Calculate trigger availability. 935 * 936 * @param int $triggerId trigger id 937 * @param int $startTime begin period 938 * @param int $endTime end period 939 * 940 * @return array 941 */ 942function calculateAvailability($triggerId, $startTime, $endTime) { 943 $startValue = TRIGGER_VALUE_FALSE; 944 945 if ($startTime > 0 && $startTime <= time()) { 946 $sql = 'SELECT e.eventid,e.value'. 947 ' FROM events e'. 948 ' WHERE e.objectid='.zbx_dbstr($triggerId). 949 ' AND e.source='.EVENT_SOURCE_TRIGGERS. 950 ' AND e.object='.EVENT_OBJECT_TRIGGER. 951 ' AND e.clock<'.zbx_dbstr($startTime). 952 ' ORDER BY e.eventid DESC'; 953 if ($row = DBfetch(DBselect($sql, 1))) { 954 $startValue = $row['value']; 955 } 956 957 $min = $startTime; 958 } 959 960 $sql = 'SELECT COUNT(e.eventid) AS cnt,MIN(e.clock) AS min_clock,MAX(e.clock) AS max_clock'. 961 ' FROM events e'. 962 ' WHERE e.objectid='.zbx_dbstr($triggerId). 963 ' AND e.source='.EVENT_SOURCE_TRIGGERS. 964 ' AND e.object='.EVENT_OBJECT_TRIGGER; 965 if ($startTime) { 966 $sql .= ' AND e.clock>='.zbx_dbstr($startTime); 967 } 968 if ($endTime) { 969 $sql .= ' AND e.clock<='.zbx_dbstr($endTime); 970 } 971 972 $dbEvents = DBfetch(DBselect($sql)); 973 if ($dbEvents['cnt'] > 0) { 974 if (!isset($min)) { 975 $min = $dbEvents['min_clock']; 976 } 977 $max = $dbEvents['max_clock']; 978 } 979 else { 980 if ($startTime == 0 && $endTime == 0) { 981 $max = time(); 982 $min = $max - SEC_PER_DAY; 983 } 984 else { 985 $ret['true_time'] = 0; 986 $ret['false_time'] = 0; 987 $ret['true'] = (TRIGGER_VALUE_TRUE == $startValue) ? 100 : 0; 988 $ret['false'] = (TRIGGER_VALUE_FALSE == $startValue) ? 100 : 0; 989 return $ret; 990 } 991 } 992 993 $state = $startValue; 994 $true_time = 0; 995 $false_time = 0; 996 $time = $min; 997 if ($startTime == 0 && $endTime == 0) { 998 $max = time(); 999 } 1000 if ($endTime == 0) { 1001 $endTime = $max; 1002 } 1003 1004 $rows = 0; 1005 $dbEvents = DBselect( 1006 'SELECT e.eventid,e.clock,e.value'. 1007 ' FROM events e'. 1008 ' WHERE e.objectid='.zbx_dbstr($triggerId). 1009 ' AND e.source='.EVENT_SOURCE_TRIGGERS. 1010 ' AND e.object='.EVENT_OBJECT_TRIGGER. 1011 ' AND e.clock BETWEEN '.$min.' AND '.$max. 1012 ' ORDER BY e.eventid' 1013 ); 1014 while ($row = DBfetch($dbEvents)) { 1015 $clock = $row['clock']; 1016 $value = $row['value']; 1017 1018 $diff = max($clock - $time, 0); 1019 $time = $clock; 1020 1021 if ($state == 0) { 1022 $false_time += $diff; 1023 $state = $value; 1024 } 1025 elseif ($state == 1) { 1026 $true_time += $diff; 1027 $state = $value; 1028 } 1029 $rows++; 1030 } 1031 1032 if ($rows == 0) { 1033 $trigger = get_trigger_by_triggerid($triggerId); 1034 $state = $trigger['value']; 1035 } 1036 1037 if ($state == TRIGGER_VALUE_FALSE) { 1038 $false_time = $false_time + $endTime - $time; 1039 } 1040 elseif ($state == TRIGGER_VALUE_TRUE) { 1041 $true_time = $true_time + $endTime - $time; 1042 } 1043 $total_time = $true_time + $false_time; 1044 1045 if ($total_time == 0) { 1046 $ret['true_time'] = 0; 1047 $ret['false_time'] = 0; 1048 $ret['true'] = 0; 1049 $ret['false'] = 0; 1050 } 1051 else { 1052 $ret['true_time'] = $true_time; 1053 $ret['false_time'] = $false_time; 1054 $ret['true'] = (100 * $true_time) / $total_time; 1055 $ret['false'] = (100 * $false_time) / $total_time; 1056 } 1057 1058 return $ret; 1059} 1060 1061function get_triggers_unacknowledged($db_element, $count_problems = null, $ack = false) { 1062 $elements = [ 1063 'hosts' => [], 1064 'hosts_groups' => [], 1065 'triggers' => [] 1066 ]; 1067 1068 get_map_elements($db_element, $elements); 1069 if (empty($elements['hosts_groups']) && empty($elements['hosts']) && empty($elements['triggers'])) { 1070 return 0; 1071 } 1072 1073 $options = [ 1074 'monitored' => true, 1075 'countOutput' => true, 1076 'filter' => [], 1077 'limit' => CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT) + 1 1078 ]; 1079 1080 if ($ack) { 1081 $options['withAcknowledgedEvents'] = 1; 1082 } 1083 else { 1084 $options['withUnacknowledgedEvents'] = 1; 1085 } 1086 1087 if ($count_problems) { 1088 $options['filter']['value'] = TRIGGER_VALUE_TRUE; 1089 } 1090 if (!empty($elements['hosts_groups'])) { 1091 $options['groupids'] = array_unique($elements['hosts_groups']); 1092 } 1093 if (!empty($elements['hosts'])) { 1094 $options['hostids'] = array_unique($elements['hosts']); 1095 } 1096 if (!empty($elements['triggers'])) { 1097 $options['triggerids'] = array_unique($elements['triggers']); 1098 } 1099 1100 return API::Trigger()->get($options); 1101} 1102 1103/** 1104 * Make trigger info block. 1105 * 1106 * @param array $trigger Trigger described in info block. 1107 * @param array $eventid Associated eventid. 1108 * 1109 * @return object 1110 */ 1111function make_trigger_details($trigger, $eventid) { 1112 $hostNames = []; 1113 1114 $hostIds = zbx_objectValues($trigger['hosts'], 'hostid'); 1115 1116 $hosts = API::Host()->get([ 1117 'output' => ['name', 'hostid', 'status'], 1118 'hostids' => $hostIds 1119 ]); 1120 1121 if (count($hosts) > 1) { 1122 order_result($hosts, 'name'); 1123 } 1124 1125 foreach ($hosts as $host) { 1126 $hostNames[] = (new CLinkAction($host['name']))->setMenuPopup(CMenuPopupHelper::getHost($host['hostid'])); 1127 $hostNames[] = ', '; 1128 } 1129 array_pop($hostNames); 1130 1131 $table = (new CTableInfo()) 1132 ->addRow([ 1133 new CCol(_n('Host', 'Hosts', count($hosts))), 1134 new CCol($hostNames) 1135 ]) 1136 ->addRow([ 1137 new CCol(_('Trigger')), 1138 new CCol((new CLinkAction(CMacrosResolverHelper::resolveTriggerName($trigger))) 1139 ->addClass(ZBX_STYLE_WORDWRAP) 1140 ->setMenuPopup(CMenuPopupHelper::getTrigger($trigger['triggerid'], $eventid)) 1141 ) 1142 ]) 1143 ->addRow([ 1144 _('Severity'), 1145 getSeverityCell($trigger['priority']) 1146 ]); 1147 1148 $trigger = CMacrosResolverHelper::resolveTriggerExpressions(zbx_toHash($trigger, 'triggerid'), [ 1149 'html' => true, 1150 'resolve_usermacros' => true, 1151 'resolve_macros' => true, 1152 'sources' => ['expression', 'recovery_expression'] 1153 ]); 1154 1155 $trigger = reset($trigger); 1156 1157 $table 1158 ->addRow([ 1159 new CCol(_('Problem expression')), 1160 new CCol((new CDiv($trigger['expression']))->addClass(ZBX_STYLE_WORDWRAP)) 1161 ]) 1162 ->addRow([ 1163 new CCol(_('Recovery expression')), 1164 new CCol((new CDiv($trigger['recovery_expression']))->addClass(ZBX_STYLE_WORDWRAP)) 1165 ]) 1166 ->addRow([_('Event generation'), _('Normal').((TRIGGER_MULT_EVENT_ENABLED == $trigger['type']) 1167 ? SPACE.'+'.SPACE._('Multiple PROBLEM events') 1168 : '') 1169 ]); 1170 1171 $table->addRow([_('Allow manual close'), ($trigger['manual_close'] == ZBX_TRIGGER_MANUAL_CLOSE_ALLOWED) 1172 ? (new CCol(_('Yes')))->addClass(ZBX_STYLE_GREEN) 1173 : (new CCol(_('No')))->addClass(ZBX_STYLE_RED) 1174 ]); 1175 1176 $table->addRow([_('Enabled'), ($trigger['status'] == TRIGGER_STATUS_ENABLED) 1177 ? (new CCol(_('Yes')))->addClass(ZBX_STYLE_GREEN) 1178 : (new CCol(_('No')))->addClass(ZBX_STYLE_RED) 1179 ]); 1180 1181 return $table; 1182} 1183 1184/** 1185 * Analyze an expression and returns expression html tree. 1186 * 1187 * @param string $expression Trigger expression or recovery expression string. 1188 * @param int $type Type can be either TRIGGER_EXPRESSION or TRIGGER_RECOVERY_EXPRESSION. 1189 * @param string $error [OUT] An error message. 1190 * 1191 * @return array|bool 1192 */ 1193function analyzeExpression(string $expression, int $type, string &$error = null) { 1194 if ($expression === '') { 1195 return ['', null]; 1196 } 1197 1198 $expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]); 1199 1200 if ($expression_parser->parse($expression) != CParser::PARSE_SUCCESS) { 1201 $error = $expression_parser->getError(); 1202 1203 return false; 1204 } 1205 1206 $expression_tree[] = getExpressionTree($expression_parser, 0, $expression_parser->getLength() - 1); 1207 1208 $next = []; 1209 $letter_num = 0; 1210 1211 return buildExpressionHtmlTree($expression_tree, $next, $letter_num, 0, null, $type); 1212} 1213 1214/** 1215 * Builds expression HTML tree. 1216 * 1217 * @param array $expressionTree Output of getExpressionTree() function. 1218 * @param array $next Parameter only for recursive call; should be empty array. 1219 * @param int $letterNum Parameter only for recursive call; should be 0. 1220 * @param int $level Parameter only for recursive call. 1221 * @param string $operator Parameter only for recursive call. 1222 * @param int $type Type can be either TRIGGER_EXPRESSION or TRIGGER_RECOVERY_EXPRESSION. 1223 * 1224 * @return array Array containing the trigger expression formula as the first element and an array describing the 1225 * expression tree as the second. 1226 */ 1227function buildExpressionHtmlTree(array $expressionTree, array &$next, &$letterNum, $level = 0, $operator = null, 1228 $type) { 1229 $treeList = []; 1230 $outline = ''; 1231 1232 end($expressionTree); 1233 $lastKey = key($expressionTree); 1234 1235 foreach ($expressionTree as $key => $element) { 1236 switch ($element['type']) { 1237 case 'operator': 1238 $next[$level] = ($key != $lastKey); 1239 $expr = expressionLevelDraw($next, $level); 1240 $expr[] = SPACE; 1241 $expr[] = ($element['operator'] === 'and') ? _('And') : _('Or'); 1242 $levelDetails = [ 1243 'list' => $expr, 1244 'id' => $element['id'], 1245 'expression' => [ 1246 'value' => $element['expression'] 1247 ] 1248 ]; 1249 1250 $levelErrors = expressionHighLevelErrors($element['expression']); 1251 if ($levelErrors) { 1252 $levelDetails['expression']['levelErrors'] = $levelErrors; 1253 } 1254 $treeList[] = $levelDetails; 1255 1256 list($subOutline, $subTreeList) = buildExpressionHtmlTree($element['elements'], $next, $letterNum, 1257 $level + 1, $element['operator'], $type 1258 ); 1259 $treeList = array_merge($treeList, $subTreeList); 1260 1261 $outline .= ($level == 0) ? $subOutline : '('.$subOutline.')'; 1262 if ($operator !== null && $next[$level]) { 1263 $outline .= ' '.$operator.' '; 1264 } 1265 break; 1266 1267 case 'expression': 1268 $next[$level] = ($key != $lastKey); 1269 1270 $letter = num2letter($letterNum++); 1271 $outline .= $letter; 1272 if ($operator !== null && $next[$level]) { 1273 $outline .= ' '.$operator.' '; 1274 } 1275 1276 if (defined('NO_LINK_IN_TESTING')) { 1277 $url = $element['expression']; 1278 } 1279 else { 1280 if ($type == TRIGGER_EXPRESSION) { 1281 $expressionId = 'expr_'.$element['id']; 1282 } 1283 else { 1284 $expressionId = 'recovery_expr_'.$element['id']; 1285 } 1286 1287 $url = (new CLinkAction($element['expression'])) 1288 ->setId($expressionId) 1289 ->onClick('javascript: copy_expression("'.$expressionId.'", '.$type.');'); 1290 } 1291 $expr = expressionLevelDraw($next, $level); 1292 $expr[] = SPACE; 1293 $expr[] = bold($letter); 1294 $expr[] = SPACE; 1295 $expr[] = $url; 1296 1297 $levelDetails = [ 1298 'list' => $expr, 1299 'id' => $element['id'], 1300 'expression' => [ 1301 'value' => $element['expression'] 1302 ] 1303 ]; 1304 1305 $levelErrors = expressionHighLevelErrors($element['expression']); 1306 if ($levelErrors) { 1307 $levelDetails['expression']['levelErrors'] = $levelErrors; 1308 } 1309 $treeList[] = $levelDetails; 1310 break; 1311 } 1312 } 1313 1314 return [$outline, $treeList]; 1315} 1316 1317function expressionHighLevelErrors($expression) { 1318 static $errors, $definedErrorPhrases; 1319 1320 if (!isset($errors)) { 1321 $definedErrorPhrases = [ 1322 EXPRESSION_HOST_UNKNOWN => _('Unknown host, no such host present in system'), 1323 EXPRESSION_HOST_ITEM_UNKNOWN => _('Unknown host item, no such item in selected host'), 1324 EXPRESSION_NOT_A_MACRO_ERROR => _('Given expression is not a macro'), 1325 EXPRESSION_FUNCTION_UNKNOWN => _('Incorrect function is used'), 1326 EXPRESSION_UNSUPPORTED_VALUE_TYPE => _('Incorrect item value type') 1327 ]; 1328 $errors = []; 1329 } 1330 1331 if (!isset($errors[$expression])) { 1332 $errors[$expression] = []; 1333 $expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]); 1334 if ($expression_parser->parse($expression) == CParser::PARSE_SUCCESS) { 1335 $tokens = $expression_parser->getResult()->getTokensOfTypes([ 1336 CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION, 1337 CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION 1338 ]); 1339 foreach ($tokens as $token) { 1340 $info = get_item_function_info($token['match']); 1341 1342 if (!is_array($info) && isset($definedErrorPhrases[$info])) { 1343 if (!isset($errors[$expression][$token['match']])) { 1344 $errors[$expression][$token['match']] = $definedErrorPhrases[$info]; 1345 } 1346 } 1347 } 1348 } 1349 } 1350 1351 $ret = []; 1352 if (!$errors[$expression]) { 1353 return $ret; 1354 } 1355 1356 $expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]); 1357 if ($expression_parser->parse($expression) == CParser::PARSE_SUCCESS) { 1358 $tokens = $expression_parser->getResult()->getTokensOfTypes([ 1359 CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION, 1360 CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION 1361 ]); 1362 foreach ($tokens as $token) { 1363 if (isset($errors[$expression][$token['match']])) { 1364 $ret[$token['match']] = $errors[$expression][$token['match']]; 1365 } 1366 } 1367 } 1368 1369 return $ret; 1370} 1371 1372/** 1373 * Draw level for trigger expression builder tree. 1374 * 1375 * @param array $next 1376 * @param int $level 1377 * 1378 * @return array 1379 */ 1380function expressionLevelDraw(array $next, $level) { 1381 $expr = []; 1382 for ($i = 1; $i <= $level; $i++) { 1383 if ($i == $level) { 1384 $class_name = $next[$i] ? 'icon-tree-top-bottom-right' : 'icon-tree-top-right'; 1385 } 1386 else { 1387 $class_name = $next[$i] ? 'icon-tree-top-bottom' : 'icon-tree-empty'; 1388 } 1389 1390 $expr[] = (new CSpan(''))->addClass($class_name); 1391 } 1392 return $expr; 1393} 1394 1395/** 1396 * Makes tree of expression elements 1397 * 1398 * Expression: 1399 * "last(/host1/system.cpu.util[,iowait], 0) > 50 and last(/host2/system.cpu.util[,iowait], 0) > 50" 1400 * Result: 1401 * [ 1402 * [0] => [ 1403 * 'id' => '0_94', 1404 * 'type' => 'operator', 1405 * 'operator' => 'and', 1406 * 'elements' => [ 1407 * [0] => [ 1408 * 'id' => '0_44', 1409 * 'type' => 'expression', 1410 * 'expression' => 'last(/host1/system.cpu.util[,iowait], 0) > 50' 1411 * ], 1412 * [1] => [ 1413 * 'id' => '50_94', 1414 * 'type' => 'expression', 1415 * 'expression' => 'last(/host2/system.cpu.util[,iowait], 0) > 50' 1416 * ] 1417 * ] 1418 * ] 1419 * ] 1420 * 1421 * @param CExpressionParser $expression_parser 1422 * @param int $start 1423 * @param int $end 1424 * 1425 * @return array 1426 */ 1427function getExpressionTree(CExpressionParser $expression_parser, int $start, int $end) { 1428 $tokens = array_column($expression_parser->getResult()->getTokens(), null, 'pos'); 1429 $expression = $expression_parser->getMatch(); 1430 1431 $expressionTree = []; 1432 foreach (['or', 'and'] as $operator) { 1433 $operatorFound = false; 1434 $lParentheses = -1; 1435 $rParentheses = -1; 1436 $expressions = []; 1437 $openSymbolNum = $start; 1438 1439 for ($i = $start, $level = 0; $i <= $end; $i++) { 1440 switch ($expression[$i]) { 1441 case ' ': 1442 case "\r": 1443 case "\n": 1444 case "\t": 1445 if ($openSymbolNum == $i) { 1446 $openSymbolNum++; 1447 } 1448 break; 1449 1450 case '(': 1451 if ($level == 0) { 1452 $lParentheses = $i; 1453 } 1454 $level++; 1455 break; 1456 1457 case ')': 1458 $level--; 1459 if ($level == 0) { 1460 $rParentheses = $i; 1461 } 1462 break; 1463 1464 default: 1465 /* 1466 * Once reached the end of a complete expression, parse the expression on the left side of the 1467 * operator. 1468 */ 1469 if ($level == 0 && array_key_exists($i, $tokens) 1470 && $tokens[$i]['type'] == CExpressionParserResult::TOKEN_TYPE_OPERATOR 1471 && $tokens[$i]['match'] === $operator) { 1472 // Find the last symbol of the expression before the operator. 1473 $closeSymbolNum = $i - 1; 1474 1475 // Trim blank symbols after the expression. 1476 while (strpos(CExpressionParser::WHITESPACES, $expression[$closeSymbolNum]) !== false) { 1477 $closeSymbolNum--; 1478 } 1479 1480 $expressions[] = getExpressionTree($expression_parser, $openSymbolNum, $closeSymbolNum); 1481 $openSymbolNum = $i + $tokens[$i]['length']; 1482 $operatorFound = true; 1483 } 1484 } 1485 } 1486 1487 // Trim blank symbols in the end of the trigger expression. 1488 $closeSymbolNum = $end; 1489 while (strpos(CExpressionParser::WHITESPACES, $expression[$closeSymbolNum]) !== false) { 1490 $closeSymbolNum--; 1491 } 1492 1493 /* 1494 * Once found a whole expression and parsed the expression on the left side of the operator, parse the 1495 * expression on the right. 1496 */ 1497 if ($operatorFound) { 1498 $expressions[] = getExpressionTree($expression_parser, $openSymbolNum, $closeSymbolNum); 1499 1500 // Trim blank symbols in the beginning of the trigger expression. 1501 $openSymbolNum = $start; 1502 while (strpos(CExpressionParser::WHITESPACES, $expression[$openSymbolNum]) !== false) { 1503 $openSymbolNum++; 1504 } 1505 1506 // Trim blank symbols in the end of the trigger expression. 1507 $closeSymbolNum = $end; 1508 while (strpos(CExpressionParser::WHITESPACES, $expression[$closeSymbolNum]) !== false) { 1509 $closeSymbolNum--; 1510 } 1511 1512 $expressionTree = [ 1513 'id' => $openSymbolNum.'_'.$closeSymbolNum, 1514 'expression' => substr($expression, $openSymbolNum, $closeSymbolNum - $openSymbolNum + 1), 1515 'type' => 'operator', 1516 'operator' => $operator, 1517 'elements' => $expressions 1518 ]; 1519 break; 1520 } 1521 // If finding both operators failed, it means there's only one expression return the result. 1522 elseif ($operator === 'and') { 1523 // Trim extra parentheses. 1524 if ($openSymbolNum == $lParentheses && $closeSymbolNum == $rParentheses) { 1525 $openSymbolNum++; 1526 $closeSymbolNum--; 1527 1528 $expressionTree = getExpressionTree($expression_parser, $openSymbolNum, $closeSymbolNum); 1529 } 1530 // No extra parentheses remain, return the result. 1531 else { 1532 $expressionTree = [ 1533 'id' => $openSymbolNum.'_'.$closeSymbolNum, 1534 'expression' => substr($expression, $openSymbolNum, $closeSymbolNum - $openSymbolNum + 1), 1535 'type' => 'expression' 1536 ]; 1537 } 1538 } 1539 } 1540 1541 return $expressionTree; 1542} 1543 1544/** 1545 * Recreate an expression depending on action. 1546 * 1547 * Supported action values: 1548 * - and - add an expression using "and"; 1549 * - or - add an expression using "or"; 1550 * - r - replace; 1551 * - R - remove. 1552 * 1553 * @param string $expression 1554 * @param string $expression_id Element identifier like "0_55". 1555 * @param string $action Action to perform. 1556 * @param string $new_expression Expression for AND, OR or replace actions. 1557 * @param string $error [OUT] An error message. 1558 * 1559 * @return bool|string Returns new expression or false if expression is incorrect. 1560 */ 1561function remakeExpression($expression, $expression_id, $action, $new_expression, string &$error = null) { 1562 if ($expression === '') { 1563 return false; 1564 } 1565 1566 $expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]); 1567 if ($action !== 'R' && $expression_parser->parse($new_expression) != CParser::PARSE_SUCCESS) { 1568 $error = $expression_parser->getError(); 1569 return false; 1570 } 1571 1572 if ($expression_parser->parse($expression) != CParser::PARSE_SUCCESS) { 1573 $error = $expression_parser->getError(); 1574 return false; 1575 } 1576 1577 $expression_tree[] = getExpressionTree($expression_parser, 0, $expression_parser->getLength() - 1); 1578 1579 if (rebuildExpressionTree($expression_tree, $expression_id, $action, $new_expression)) { 1580 $expression = makeExpression($expression_tree); 1581 } 1582 1583 return $expression; 1584} 1585 1586/** 1587 * Rebuild expression depending on action. 1588 * 1589 * Supported action values: 1590 * - and - add an expression using "and"; 1591 * - or - add an expression using "or"; 1592 * - r - replace; 1593 * - R - remove. 1594 * 1595 * Example: 1596 * $expressionTree = array( 1597 * [0] => array( 1598 * 'id' => '0_94', 1599 * 'type' => 'operator', 1600 * 'operator' => 'and', 1601 * 'elements' => array( 1602 * [0] => array( 1603 * 'id' => '0_44', 1604 * 'type' => 'expression', 1605 * 'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50' 1606 * ), 1607 * [1] => array( 1608 * 'id' => '50_94', 1609 * 'type' => 'expression', 1610 * 'expression' => '{host2:system.cpu.util[,iowait].last(0)} > 50' 1611 * ) 1612 * ) 1613 * ) 1614 * ) 1615 * $action = 'R' 1616 * $expressionId = '50_94' 1617 * 1618 * Result: 1619 * $expressionTree = array( 1620 * [0] => array( 1621 * 'id' => '0_44', 1622 * 'type' => 'expression', 1623 * 'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50' 1624 * ) 1625 * ) 1626 * 1627 * @param array $expressionTree 1628 * @param string $expressionId element identifier like "0_55" 1629 * @param string $action action to perform 1630 * @param string $newExpression expression for AND, OR or replace actions 1631 * @param string $operator parameter only for recursive call 1632 * 1633 * @return bool returns true if element is found, false - otherwise 1634 */ 1635function rebuildExpressionTree(array &$expressionTree, $expressionId, $action, $newExpression, $operator = null) { 1636 foreach ($expressionTree as $key => $expression) { 1637 if ($expressionId == $expressionTree[$key]['id']) { 1638 switch ($action) { 1639 case 'and': 1640 case 'or': 1641 switch ($expressionTree[$key]['type']) { 1642 case 'operator': 1643 if ($expressionTree[$key]['operator'] == $action) { 1644 $expressionTree[$key]['elements'][] = [ 1645 'expression' => $newExpression, 1646 'type' => 'expression' 1647 ]; 1648 } 1649 else { 1650 $element = [ 1651 'type' => 'operator', 1652 'operator' => $action, 1653 'elements' => [ 1654 $expressionTree[$key], 1655 [ 1656 'expression' => $newExpression, 1657 'type' => 'expression' 1658 ] 1659 ] 1660 ]; 1661 $expressionTree[$key] = $element; 1662 } 1663 break; 1664 case 'expression': 1665 if (!$operator || $operator != $action) { 1666 $element = [ 1667 'type' => 'operator', 1668 'operator' => $action, 1669 'elements' => [ 1670 $expressionTree[$key], 1671 [ 1672 'expression' => $newExpression, 1673 'type' => 'expression' 1674 ] 1675 ] 1676 ]; 1677 $expressionTree[$key] = $element; 1678 } 1679 else { 1680 $expressionTree[] = [ 1681 'expression' => $newExpression, 1682 'type' => 'expression' 1683 ]; 1684 } 1685 break; 1686 } 1687 break; 1688 // replace 1689 case 'r': 1690 $expressionTree[$key]['expression'] = $newExpression; 1691 if ($expressionTree[$key]['type'] == 'operator') { 1692 $expressionTree[$key]['type'] = 'expression'; 1693 unset($expressionTree[$key]['operator'], $expressionTree[$key]['elements']); 1694 } 1695 break; 1696 // remove 1697 case 'R': 1698 unset($expressionTree[$key]); 1699 break; 1700 } 1701 return true; 1702 } 1703 1704 if ($expressionTree[$key]['type'] == 'operator') { 1705 if (rebuildExpressionTree($expressionTree[$key]['elements'], $expressionId, $action, $newExpression, 1706 $expressionTree[$key]['operator'])) { 1707 return true; 1708 } 1709 } 1710 } 1711 1712 return false; 1713} 1714 1715/** 1716 * Makes expression by expression tree 1717 * 1718 * Example: 1719 * $expressionTree = array( 1720 * [0] => array( 1721 * 'type' => 'operator', 1722 * 'operator' => 'and', 1723 * 'elements' => array( 1724 * [0] => array( 1725 * 'type' => 'expression', 1726 * 'expression' => '{host1:system.cpu.util[,iowait].last(0)} > 50' 1727 * ), 1728 * [1] => array( 1729 * 'type' => 'expression', 1730 * 'expression' => '{host2:system.cpu.util[,iowait].last(0)} > 50' 1731 * ) 1732 * ) 1733 * ) 1734 * ) 1735 * 1736 * Result: 1737 * "{host1:system.cpu.util[,iowait].last(0)} > 50 and {host2:system.cpu.util[,iowait].last(0)} > 50" 1738 * 1739 * @param array $expressionTree 1740 * @param int $level parameter only for recursive call 1741 * @param string $operator parameter only for recursive call 1742 * 1743 * @return string 1744 */ 1745function makeExpression(array $expressionTree, $level = 0, $operator = null) { 1746 $expression = ''; 1747 1748 end($expressionTree); 1749 $lastKey = key($expressionTree); 1750 1751 foreach ($expressionTree as $key => $element) { 1752 switch ($element['type']) { 1753 case 'operator': 1754 $subExpression = makeExpression($element['elements'], $level + 1, $element['operator']); 1755 1756 $expression .= ($level == 0) ? $subExpression : '('.$subExpression.')'; 1757 break; 1758 case 'expression': 1759 $expression .= $element['expression']; 1760 break; 1761 } 1762 if ($operator !== null && $key != $lastKey) { 1763 $expression .= ' '.$operator.' '; 1764 } 1765 } 1766 1767 return $expression; 1768} 1769 1770function get_item_function_info(string $expr) { 1771 $rule_float = ['value_type' => _('Numeric (float)'), 'values' => null]; 1772 $rule_int = ['value_type' => _('Numeric (integer)'), 'values' => null]; 1773 $rule_str = ['value_type' => _('String'), 'values' => null]; 1774 $rule_any = ['value_type' => _('Any'), 'values' => null]; 1775 $rule_0or1 = ['value_type' => _('0 or 1'), 'values' => [0 => 0, 1 => 1]]; 1776 $rules = [ 1777 // Every nested array should have two elements: label, values. 1778 'integer' => [ 1779 ITEM_VALUE_TYPE_UINT64 => $rule_int 1780 ], 1781 'numeric' => [ 1782 ITEM_VALUE_TYPE_UINT64 => $rule_int, 1783 ITEM_VALUE_TYPE_FLOAT => $rule_float 1784 ], 1785 'numeric_as_float' => [ 1786 ITEM_VALUE_TYPE_UINT64 => $rule_float, 1787 ITEM_VALUE_TYPE_FLOAT => $rule_float 1788 ], 1789 'numeric_as_uint' => [ 1790 ITEM_VALUE_TYPE_UINT64 => $rule_int, 1791 ITEM_VALUE_TYPE_FLOAT => $rule_int 1792 ], 1793 'numeric_as_0or1' => [ 1794 ITEM_VALUE_TYPE_UINT64 => $rule_0or1, 1795 ITEM_VALUE_TYPE_FLOAT => $rule_0or1 1796 ], 1797 'string_as_0or1' => [ 1798 ITEM_VALUE_TYPE_TEXT => $rule_0or1, 1799 ITEM_VALUE_TYPE_STR => $rule_0or1, 1800 ITEM_VALUE_TYPE_LOG => $rule_0or1 1801 ], 1802 'string_as_uint' => [ 1803 ITEM_VALUE_TYPE_TEXT => $rule_int, 1804 ITEM_VALUE_TYPE_STR => $rule_int, 1805 ITEM_VALUE_TYPE_LOG => $rule_int 1806 ], 1807 'string' => [ 1808 ITEM_VALUE_TYPE_TEXT => $rule_str, 1809 ITEM_VALUE_TYPE_STR => $rule_str, 1810 ITEM_VALUE_TYPE_LOG => $rule_str 1811 ], 1812 'log_as_uint' => [ 1813 ITEM_VALUE_TYPE_LOG => $rule_int 1814 ], 1815 'log_as_0or1' => [ 1816 ITEM_VALUE_TYPE_LOG => $rule_0or1 1817 ] 1818 ]; 1819 1820 $hist_functions = [ 1821 'avg' => $rules['numeric_as_float'], 1822 'change' => $rules['numeric'] + $rules['string_as_0or1'], 1823 'count' => $rules['numeric_as_uint'] + $rules['string_as_uint'], 1824 'countunique' => $rules['numeric_as_uint'] + $rules['string_as_uint'], 1825 'find' => $rules['numeric_as_0or1'] + $rules['string_as_0or1'], 1826 'first' => $rules['numeric'] + $rules['string'], 1827 'forecast' => $rules['numeric_as_float'], 1828 'fuzzytime' => $rules['numeric_as_0or1'], 1829 'kurtosis' => $rules['numeric_as_float'], 1830 'last' => $rules['numeric'] + $rules['string'], 1831 'logeventid' => $rules['log_as_0or1'], 1832 'logseverity' => $rules['log_as_uint'], 1833 'logsource' => $rules['log_as_0or1'], 1834 'mad' => $rules['numeric_as_float'], 1835 'max' => $rules['numeric'], 1836 'min' => $rules['numeric'], 1837 'nodata' => $rules['numeric_as_0or1'] + $rules['string_as_0or1'], 1838 'percentile' => $rules['numeric'], 1839 'skewness' => $rules['numeric_as_float'], 1840 'stddevpop' => $rules['numeric_as_float'], 1841 'stddevsamp' => $rules['numeric_as_float'], 1842 'sum' => $rules['numeric'], 1843 'sumofsquares' => $rules['numeric_as_float'], 1844 'timeleft' => $rules['numeric_as_float'], 1845 'trendavg' => $rules['numeric'], 1846 'trendcount' => $rules['numeric'], 1847 'trendmax' => $rules['numeric'], 1848 'trendmin' => $rules['numeric'], 1849 'trendsum' => $rules['numeric'], 1850 'varpop' => $rules['numeric_as_float'], 1851 'varsamp' => $rules['numeric_as_float'] 1852 ]; 1853 1854 $math_functions = [ 1855 'abs' => ['any' => $rule_float], 1856 'acos' => ['any' => $rule_float], 1857 'ascii' => ['any' => $rule_int], 1858 'asin' => ['any' => $rule_float], 1859 'atan' => ['any' => $rule_float], 1860 'atan2' => ['any' => $rule_float], 1861 'avg' => ['any' => $rule_float], 1862 'between' => ['any' => $rule_0or1], 1863 'bitand' => ['any' => $rule_int], 1864 'bitlength' => ['any' => $rule_int], 1865 'bitlshift' => ['any' => $rule_int], 1866 'bitnot' => ['any' => $rule_int], 1867 'bitor' => ['any' => $rule_int], 1868 'bitrshift' => ['any' => $rule_int], 1869 'bitxor' => ['any' => $rule_int], 1870 'bytelength' => ['any' => $rule_int], 1871 'cbrt' => ['any' => $rule_float], 1872 'ceil' => ['any' => $rule_int], 1873 'char' => ['any' => $rule_str], 1874 'concat' => ['any' => $rule_str], 1875 'cos' => ['any' => $rule_float], 1876 'cosh' => ['any' => $rule_float], 1877 'cot' => ['any' => $rule_float], 1878 'date' => [ 1879 'any' => ['value_type' => 'YYYYMMDD', 'values' => null] 1880 ], 1881 'dayofmonth' => [ 1882 'any' => ['value_type' => '1-31', 'values' => null] 1883 ], 1884 'dayofweek' => [ 1885 'any' => ['value_type' => '1-7', 'values' => [1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, 7 => 7]] 1886 ], 1887 'degrees' => ['any' => $rule_float], 1888 'e' => ['any' => $rule_float], 1889 'exp' => ['any' => $rule_float], 1890 'expm1' => ['any' => $rule_float], 1891 'floor' => ['any' => $rule_int], 1892 'in' => ['any' => $rule_0or1], 1893 'insert' => ['any' => $rule_str], 1894 'left' => ['any' => $rule_str], 1895 'length' => ['any' => $rule_int], 1896 'log' => ['any' => $rule_float], 1897 'log10' => ['any' => $rule_float], 1898 'ltrim' => ['any' => $rule_str], 1899 'max' => ['any' => $rule_float], 1900 'mid' => ['any' => $rule_str], 1901 'min' => ['any' => $rule_float], 1902 'mod' => ['any' => $rule_float], 1903 'now' => ['any' => $rule_int], 1904 'pi' => ['any' => $rule_float], 1905 'power' => ['any' => $rule_float], 1906 'radians' => ['any' => $rule_float], 1907 'rand' => ['any' => $rule_int], 1908 'repeat' => ['any' => $rule_str], 1909 'replace' => ['any' => $rule_str], 1910 'right' => ['any' => $rule_str], 1911 'round' => ['any' => $rule_float], 1912 'rtrim' => ['any' => $rule_str], 1913 'signum' => ['any' => $rule_int], 1914 'sin' => ['any' => $rule_float], 1915 'sinh' => ['any' => $rule_float], 1916 'sqrt' => ['any' => $rule_float], 1917 'sum' => ['any' => $rule_float], 1918 'tan' => ['any' => $rule_float], 1919 'time' => [ 1920 'any' => ['value_type' => 'HHMMSS', 'values' => null] 1921 ], 1922 'trim' => ['any' => $rule_str], 1923 'truncate' => ['any' => $rule_float] 1924 ]; 1925 1926 $expression_parser = new CExpressionParser(['usermacros' => true, 'lldmacros' => true]); 1927 $expression_parser->parse($expr); 1928 $token = $expression_parser->getResult()->getTokens()[0]; 1929 1930 switch ($token['type']) { 1931 case CExpressionParserResult::TOKEN_TYPE_MACRO: 1932 $result = $rule_0or1; 1933 break; 1934 1935 case CExpressionParserResult::TOKEN_TYPE_USER_MACRO: 1936 case CExpressionParserResult::TOKEN_TYPE_LLD_MACRO: 1937 $result = $rule_any; 1938 break; 1939 1940 case CExpressionParserResult::TOKEN_TYPE_HIST_FUNCTION: 1941 if (!array_key_exists($token['data']['function'], $hist_functions)) { 1942 $result = EXPRESSION_FUNCTION_UNKNOWN; 1943 break; 1944 } 1945 1946 $hosts = API::Host()->get([ 1947 'output' => ['hostid'], 1948 'filter' => [ 1949 'host' => $token['data']['parameters'][0]['data']['host'] 1950 ], 1951 'templated_hosts' => true 1952 ]); 1953 1954 if (!$hosts) { 1955 $result = EXPRESSION_HOST_UNKNOWN; 1956 break; 1957 } 1958 1959 $items = API::Item()->get([ 1960 'output' => ['value_type'], 1961 'hostids' => $hosts[0]['hostid'], 1962 'filter' => [ 1963 'key_' => $token['data']['parameters'][0]['data']['item'] 1964 ], 1965 'webitems' => true 1966 ]); 1967 1968 if (!$items) { 1969 $items = API::ItemPrototype()->get([ 1970 'output' => ['value_type'], 1971 'hostids' => $hosts[0]['hostid'], 1972 'filter' => [ 1973 'key_' => $token['data']['parameters'][0]['data']['item'] 1974 ] 1975 ]); 1976 } 1977 1978 if (!$items) { 1979 $result = EXPRESSION_HOST_ITEM_UNKNOWN; 1980 break; 1981 } 1982 1983 $hist_function = $hist_functions[$token['data']['function']]; 1984 $value_type = $items[0]['value_type']; 1985 1986 if (array_key_exists('any', $hist_function)) { 1987 $value_type = 'any'; 1988 } 1989 elseif (!array_key_exists($value_type, $hist_function)) { 1990 $result = EXPRESSION_UNSUPPORTED_VALUE_TYPE; 1991 break; 1992 } 1993 1994 $result = $hist_function[$value_type]; 1995 break; 1996 1997 case CExpressionParserResult::TOKEN_TYPE_MATH_FUNCTION: 1998 if (!array_key_exists($token['data']['function'], $math_functions)) { 1999 $result = EXPRESSION_FUNCTION_UNKNOWN; 2000 break; 2001 } 2002 2003 $result = $math_functions[$token['data']['function']]['any']; 2004 break; 2005 2006 default: 2007 $result = EXPRESSION_NOT_A_MACRO_ERROR; 2008 } 2009 2010 return $result; 2011} 2012 2013/** 2014 * Quoting $param if it contains special characters. 2015 * 2016 * @param string $param 2017 * @param bool $forced 2018 * 2019 * @return string 2020 */ 2021function quoteFunctionParam($param, $forced = false) { 2022 if (!$forced) { 2023 if (!isset($param[0]) || ($param[0] != '"' && false === strpbrk($param, ',)'))) { 2024 return $param; 2025 } 2026 } 2027 2028 return '"'.str_replace('"', '\\"', $param).'"'; 2029} 2030 2031/** 2032 * Returns the text indicating the trigger's status and state. If the $state parameter is not given, only the status of 2033 * the trigger will be taken into account. 2034 * 2035 * @param int $status 2036 * @param int $state 2037 * 2038 * @return string 2039 */ 2040function triggerIndicator($status, $state = null) { 2041 if ($status == TRIGGER_STATUS_ENABLED) { 2042 return ($state == TRIGGER_STATE_UNKNOWN) ? _('Unknown') : _('Enabled'); 2043 } 2044 2045 return _('Disabled'); 2046} 2047 2048/** 2049 * Returns the CSS class for the trigger's status and state indicator. If the $state parameter is not given, only the 2050 * status of the trigger will be taken into account. 2051 * 2052 * @param int $status 2053 * @param int $state 2054 * 2055 * @return string 2056 */ 2057function triggerIndicatorStyle($status, $state = null) { 2058 if ($status == TRIGGER_STATUS_ENABLED) { 2059 return ($state == TRIGGER_STATE_UNKNOWN) ? 2060 ZBX_STYLE_GREY : 2061 ZBX_STYLE_GREEN; 2062 } 2063 2064 return ZBX_STYLE_RED; 2065} 2066 2067/** 2068 * Orders triggers by both status and state. Triggers are sorted in the following order: enabled, disabled, unknown. 2069 * 2070 * Keep in sync with orderItemsByStatus(). 2071 * 2072 * @param array $triggers 2073 * @param string $sortorder 2074 */ 2075function orderTriggersByStatus(array &$triggers, $sortorder = ZBX_SORT_UP) { 2076 $sort = []; 2077 2078 foreach ($triggers as $key => $trigger) { 2079 if ($trigger['status'] == TRIGGER_STATUS_ENABLED) { 2080 $sort[$key] = ($trigger['state'] == TRIGGER_STATE_UNKNOWN) ? 2 : 0; 2081 } 2082 else { 2083 $sort[$key] = 1; 2084 } 2085 } 2086 2087 if ($sortorder == ZBX_SORT_UP) { 2088 asort($sort); 2089 } 2090 else { 2091 arsort($sort); 2092 } 2093 2094 $sortedTriggers = []; 2095 foreach ($sort as $key => $val) { 2096 $sortedTriggers[$key] = $triggers[$key]; 2097 } 2098 $triggers = $sortedTriggers; 2099} 2100 2101/** 2102 * Create the list of hosts for each trigger. 2103 * 2104 * @param array $triggers 2105 * @param string $triggers[]['triggerid'] 2106 * @param array $triggers[]['hosts'] 2107 * @param string $triggers[]['hosts'][]['hostid'] 2108 * 2109 * @return array 2110 */ 2111function getTriggersHostsList(array $triggers) { 2112 $hostids = []; 2113 2114 foreach ($triggers as $trigger) { 2115 foreach ($trigger['hosts'] as $host) { 2116 $hostids[$host['hostid']] = true; 2117 } 2118 } 2119 2120 $db_hosts = $hostids 2121 ? API::Host()->get([ 2122 'output' => ['hostid', 'name', 'maintenanceid', 'maintenance_status', 'maintenance_type'], 2123 'hostids' => array_keys($hostids), 2124 'preservekeys' => true 2125 ]) 2126 : []; 2127 2128 $triggers_hosts = []; 2129 foreach ($triggers as $trigger) { 2130 $triggers_hosts[$trigger['triggerid']] = []; 2131 2132 foreach ($trigger['hosts'] as $host) { 2133 if (!array_key_exists($host['hostid'], $db_hosts)) { 2134 continue; 2135 } 2136 2137 $triggers_hosts[$trigger['triggerid']][] = $db_hosts[$host['hostid']]; 2138 } 2139 order_result($triggers_hosts[$trigger['triggerid']], 'name'); 2140 } 2141 2142 return $triggers_hosts; 2143} 2144 2145/** 2146 * Make the list of hosts for each trigger. 2147 * 2148 * @param array $triggers_hosts 2149 * @param string $triggers_hosts[<triggerid>][]['hostid'] 2150 * @param string $triggers_hosts[<triggerid>][]['name'] 2151 * @param string $triggers_hosts[<triggerid>][]['maintenanceid'] 2152 * @param int $triggers_hosts[<triggerid>][]['maintenance_status'] 2153 * @param int $triggers_hosts[<triggerid>][]['maintenance_type'] 2154 * 2155 * @return array 2156 */ 2157function makeTriggersHostsList(array $triggers_hosts) { 2158 $db_maintenances = []; 2159 2160 $hostids = []; 2161 $maintenanceids = []; 2162 2163 foreach ($triggers_hosts as $hosts) { 2164 foreach ($hosts as $host) { 2165 $hostids[$host['hostid']] = true; 2166 if ($host['maintenance_status'] == HOST_MAINTENANCE_STATUS_ON) { 2167 $maintenanceids[$host['maintenanceid']] = true; 2168 } 2169 } 2170 } 2171 2172 if ($hostids) { 2173 if ($maintenanceids) { 2174 $db_maintenances = API::Maintenance()->get([ 2175 'output' => ['name', 'description'], 2176 'maintenanceids' => array_keys($maintenanceids), 2177 'preservekeys' => true 2178 ]); 2179 } 2180 } 2181 2182 foreach ($triggers_hosts as &$hosts) { 2183 $trigger_hosts = []; 2184 2185 foreach ($hosts as $host) { 2186 $host_name = (new CLinkAction($host['name'])) 2187 ->setMenuPopup(CMenuPopupHelper::getHost($host['hostid'])); 2188 2189 if ($host['maintenance_status'] == HOST_MAINTENANCE_STATUS_ON) { 2190 if (array_key_exists($host['maintenanceid'], $db_maintenances)) { 2191 $maintenance = $db_maintenances[$host['maintenanceid']]; 2192 $maintenance_icon = makeMaintenanceIcon($host['maintenance_type'], $maintenance['name'], 2193 $maintenance['description'] 2194 ); 2195 } 2196 else { 2197 $maintenance_icon = makeMaintenanceIcon($host['maintenance_type'], _('Inaccessible maintenance'), 2198 '' 2199 ); 2200 } 2201 2202 $host_name = (new CSpan([$host_name, $maintenance_icon]))->addClass(ZBX_STYLE_REL_CONTAINER); 2203 } 2204 2205 if ($trigger_hosts) { 2206 $trigger_hosts[] = (new CSpan(','))->addClass('separator'); 2207 } 2208 $trigger_hosts[] = $host_name; 2209 } 2210 2211 $hosts = $trigger_hosts; 2212 } 2213 unset($hosts); 2214 2215 return $triggers_hosts; 2216} 2217 2218/** 2219 * Get parent templates for each given trigger. 2220 * 2221 * @param $array $triggers An array of triggers. 2222 * @param string $triggers[]['triggerid'] ID of a trigger. 2223 * @param string $triggers[]['templateid'] ID of parent template trigger. 2224 * @param int $flag Origin of the trigger (ZBX_FLAG_DISCOVERY_NORMAL or 2225 * ZBX_FLAG_DISCOVERY_PROTOTYPE). 2226 * 2227 * @return array 2228 */ 2229function getTriggerParentTemplates(array $triggers, $flag) { 2230 $parent_triggerids = []; 2231 $data = [ 2232 'links' => [], 2233 'templates' => [] 2234 ]; 2235 2236 foreach ($triggers as $trigger) { 2237 if ($trigger['templateid'] != 0) { 2238 $parent_triggerids[$trigger['templateid']] = true; 2239 $data['links'][$trigger['triggerid']] = ['triggerid' => $trigger['templateid']]; 2240 } 2241 } 2242 2243 if (!$parent_triggerids) { 2244 return $data; 2245 } 2246 2247 $all_parent_triggerids = []; 2248 $hostids = []; 2249 if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) { 2250 $lld_ruleids = []; 2251 } 2252 2253 do { 2254 if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) { 2255 $db_triggers = API::TriggerPrototype()->get([ 2256 'output' => ['triggerid', 'templateid'], 2257 'selectHosts' => ['hostid'], 2258 'selectDiscoveryRule' => ['itemid'], 2259 'triggerids' => array_keys($parent_triggerids) 2260 ]); 2261 } 2262 // ZBX_FLAG_DISCOVERY_NORMAL 2263 else { 2264 $db_triggers = API::Trigger()->get([ 2265 'output' => ['triggerid', 'templateid'], 2266 'selectHosts' => ['hostid'], 2267 'triggerids' => array_keys($parent_triggerids) 2268 ]); 2269 } 2270 2271 $all_parent_triggerids += $parent_triggerids; 2272 $parent_triggerids = []; 2273 2274 foreach ($db_triggers as $db_trigger) { 2275 foreach ($db_trigger['hosts'] as $host) { 2276 $data['templates'][$host['hostid']] = []; 2277 $hostids[$db_trigger['triggerid']][] = $host['hostid']; 2278 } 2279 2280 if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) { 2281 $lld_ruleids[$db_trigger['triggerid']] = $db_trigger['discoveryRule']['itemid']; 2282 } 2283 2284 if ($db_trigger['templateid'] != 0) { 2285 if (!array_key_exists($db_trigger['templateid'], $all_parent_triggerids)) { 2286 $parent_triggerids[$db_trigger['templateid']] = true; 2287 } 2288 2289 $data['links'][$db_trigger['triggerid']] = ['triggerid' => $db_trigger['templateid']]; 2290 } 2291 } 2292 } 2293 while ($parent_triggerids); 2294 2295 foreach ($data['links'] as &$parent_trigger) { 2296 $parent_trigger['hostids'] = array_key_exists($parent_trigger['triggerid'], $hostids) 2297 ? $hostids[$parent_trigger['triggerid']] 2298 : [0]; 2299 2300 if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) { 2301 $parent_trigger['lld_ruleid'] = array_key_exists($parent_trigger['triggerid'], $lld_ruleids) 2302 ? $lld_ruleids[$parent_trigger['triggerid']] 2303 : 0; 2304 } 2305 } 2306 unset($parent_trigger); 2307 2308 $db_templates = $data['templates'] 2309 ? API::Template()->get([ 2310 'output' => ['name'], 2311 'templateids' => array_keys($data['templates']), 2312 'preservekeys' => true 2313 ]) 2314 : []; 2315 2316 $rw_templates = $db_templates 2317 ? API::Template()->get([ 2318 'output' => [], 2319 'templateids' => array_keys($db_templates), 2320 'editable' => true, 2321 'preservekeys' => true 2322 ]) 2323 : []; 2324 2325 $data['templates'][0] = []; 2326 2327 foreach ($data['templates'] as $hostid => &$template) { 2328 $template = array_key_exists($hostid, $db_templates) 2329 ? [ 2330 'hostid' => $hostid, 2331 'name' => $db_templates[$hostid]['name'], 2332 'permission' => array_key_exists($hostid, $rw_templates) ? PERM_READ_WRITE : PERM_READ 2333 ] 2334 : [ 2335 'hostid' => $hostid, 2336 'name' => _('Inaccessible template'), 2337 'permission' => PERM_DENY 2338 ]; 2339 } 2340 unset($template); 2341 2342 return $data; 2343} 2344 2345/** 2346 * Returns a template prefix for selected trigger. 2347 * 2348 * @param string $triggerid 2349 * @param array $parent_templates The list of the templates, prepared by getTriggerParentTemplates() function. 2350 * @param int $flag Origin of the trigger (ZBX_FLAG_DISCOVERY_NORMAL or ZBX_FLAG_DISCOVERY_PROTOTYPE). 2351 * @param bool $provide_links If this parameter is false, prefix will not contain links. 2352 * 2353 * @return array|null 2354 */ 2355function makeTriggerTemplatePrefix($triggerid, array $parent_templates, $flag, bool $provide_links) { 2356 if (!array_key_exists($triggerid, $parent_templates['links'])) { 2357 return null; 2358 } 2359 2360 while (array_key_exists($parent_templates['links'][$triggerid]['triggerid'], $parent_templates['links'])) { 2361 $triggerid = $parent_templates['links'][$triggerid]['triggerid']; 2362 } 2363 2364 $templates = []; 2365 foreach ($parent_templates['links'][$triggerid]['hostids'] as $hostid) { 2366 $templates[] = $parent_templates['templates'][$hostid]; 2367 } 2368 2369 CArrayHelper::sort($templates, ['name']); 2370 2371 $list = []; 2372 2373 foreach ($templates as $template) { 2374 if ($provide_links && $template['permission'] == PERM_READ_WRITE) { 2375 if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) { 2376 $url = (new CUrl('trigger_prototypes.php')) 2377 ->setArgument('parent_discoveryid', $parent_templates['links'][$triggerid]['lld_ruleid']) 2378 ->setArgument('context', 'template'); 2379 } 2380 // ZBX_FLAG_DISCOVERY_NORMAL 2381 else { 2382 $url = (new CUrl('triggers.php')) 2383 ->setArgument('filter_hostids', [$template['hostid']]) 2384 ->setArgument('filter_set', 1) 2385 ->setArgument('context', 'template'); 2386 } 2387 2388 $name = (new CLink(CHtml::encode($template['name']), $url))->addClass(ZBX_STYLE_LINK_ALT); 2389 } 2390 else { 2391 $name = new CSpan(CHtml::encode($template['name'])); 2392 } 2393 2394 $list[] = $name->addClass(ZBX_STYLE_GREY); 2395 $list[] = ', '; 2396 } 2397 2398 array_pop($list); 2399 $list[] = NAME_DELIMITER; 2400 2401 return $list; 2402} 2403 2404/** 2405 * Returns a list of trigger templates. 2406 * 2407 * @param string $triggerid 2408 * @param array $parent_templates The list of the templates, prepared by getTriggerParentTemplates() function. 2409 * @param int $flag Origin of the trigger (ZBX_FLAG_DISCOVERY_NORMAL or ZBX_FLAG_DISCOVERY_PROTOTYPE). 2410 * @param bool $provide_links If this parameter is false, prefix will not contain links. 2411 * 2412 * @return array 2413 */ 2414function makeTriggerTemplatesHtml($triggerid, array $parent_templates, $flag, bool $provide_links) { 2415 $list = []; 2416 2417 while (array_key_exists($triggerid, $parent_templates['links'])) { 2418 $list_item = []; 2419 $templates = []; 2420 2421 foreach ($parent_templates['links'][$triggerid]['hostids'] as $hostid) { 2422 $templates[] = $parent_templates['templates'][$hostid]; 2423 } 2424 2425 $show_parentheses = (count($templates) > 1 && $list); 2426 2427 if ($show_parentheses) { 2428 CArrayHelper::sort($templates, ['name']); 2429 $list_item[] = '('; 2430 } 2431 2432 foreach ($templates as $template) { 2433 if ($provide_links && $template['permission'] == PERM_READ_WRITE) { 2434 if ($flag == ZBX_FLAG_DISCOVERY_PROTOTYPE) { 2435 $url = (new CUrl('trigger_prototypes.php')) 2436 ->setArgument('form', 'update') 2437 ->setArgument('triggerid', $parent_templates['links'][$triggerid]['triggerid']) 2438 ->setArgument('parent_discoveryid', $parent_templates['links'][$triggerid]['lld_ruleid']) 2439 ->setArgument('context', 'template'); 2440 } 2441 // ZBX_FLAG_DISCOVERY_NORMAL 2442 else { 2443 $url = (new CUrl('triggers.php')) 2444 ->setArgument('form', 'update') 2445 ->setArgument('triggerid', $parent_templates['links'][$triggerid]['triggerid']) 2446 ->setArgument('hostid', $template['hostid']) 2447 ->setArgument('context', 'template'); 2448 } 2449 2450 $name = new CLink(CHtml::encode($template['name']), $url); 2451 } 2452 else { 2453 $name = (new CSpan(CHtml::encode($template['name'])))->addClass(ZBX_STYLE_GREY); 2454 } 2455 2456 $list_item[] = $name; 2457 $list_item[] = ', '; 2458 } 2459 array_pop($list_item); 2460 2461 if ($show_parentheses) { 2462 $list_item[] = ')'; 2463 } 2464 2465 array_unshift($list, $list_item, ' ⇒ '); 2466 2467 $triggerid = $parent_templates['links'][$triggerid]['triggerid']; 2468 } 2469 2470 if ($list) { 2471 array_pop($list); 2472 } 2473 2474 return $list; 2475} 2476 2477/** 2478 * Check if user has read permissions for triggers. 2479 * 2480 * @param $triggerids 2481 * 2482 * @return bool 2483 */ 2484function isReadableTriggers(array $triggerids) { 2485 return count($triggerids) == API::Trigger()->get([ 2486 'triggerids' => $triggerids, 2487 'countOutput' => true 2488 ]); 2489} 2490 2491/** 2492 * Returns a list of the trigger dependencies. 2493 * 2494 * @param array $triggers 2495 * @param array $triggers[<triggerid>]['dependencies'] 2496 * @param string $triggers[<triggerid>]['dependencies'][]['triggerid'] 2497 * 2498 * @return array 2499 */ 2500function getTriggerDependencies(array $triggers) { 2501 $triggerids = []; 2502 $triggerids_up = []; 2503 $triggerids_down = []; 2504 2505 // "Depends on" triggers. 2506 foreach ($triggers as $triggerid => $trigger) { 2507 foreach ($trigger['dependencies'] as $dependency) { 2508 $triggerids[$dependency['triggerid']] = true; 2509 $triggerids_up[$triggerid][] = $dependency['triggerid']; 2510 } 2511 } 2512 2513 // "Dependent" triggers. 2514 $db_trigger_depends = DBselect( 2515 'SELECT triggerid_down,triggerid_up'. 2516 ' FROM trigger_depends'. 2517 ' WHERE '.dbConditionInt('triggerid_up', array_keys($triggers)) 2518 ); 2519 2520 while ($row = DBfetch($db_trigger_depends)) { 2521 $triggerids[$row['triggerid_down']] = true; 2522 $triggerids_down[$row['triggerid_up']][] = $row['triggerid_down']; 2523 } 2524 2525 $dependencies = []; 2526 2527 if (!$triggerids) { 2528 return $dependencies; 2529 } 2530 2531 $db_triggers = API::Trigger()->get([ 2532 'output' => ['expression', 'description'], 2533 'triggerids' => array_keys($triggerids), 2534 'preservekeys' => true 2535 ]); 2536 $db_triggers = CMacrosResolverHelper::resolveTriggerNames($db_triggers); 2537 2538 foreach ($triggerids_up as $triggerid_up => $triggerids) { 2539 foreach ($triggerids as $triggerid) { 2540 $dependencies[$triggerid_up]['down'][] = array_key_exists($triggerid, $db_triggers) 2541 ? $db_triggers[$triggerid]['description'] 2542 : _('Inaccessible trigger'); 2543 } 2544 } 2545 2546 foreach ($triggerids_down as $triggerid_down => $triggerids) { 2547 foreach ($triggerids as $triggerid) { 2548 $dependencies[$triggerid_down]['up'][] = array_key_exists($triggerid, $db_triggers) 2549 ? $db_triggers[$triggerid]['description'] 2550 : _('Inaccessible trigger'); 2551 } 2552 } 2553 2554 return $dependencies; 2555} 2556 2557/** 2558 * Returns icons with tooltips for triggers with dependencies. 2559 * 2560 * @param array $dependencies 2561 * @param array $dependencies['up'] (optional) The list of "Dependent" triggers. 2562 * @param array $dependencies['down'] (optional) The list of "Depeneds on" triggers. 2563 * @param bool $freeze_on_click 2564 * 2565 * @return array 2566 */ 2567function makeTriggerDependencies(array $dependencies, $freeze_on_click = true) { 2568 $result = []; 2569 2570 foreach (['down', 'up'] as $type) { 2571 if (array_key_exists($type, $dependencies)) { 2572 $header = ($type === 'down') ? _('Depends on') : _('Dependent'); 2573 $class = ($type === 'down') ? ZBX_STYLE_ICON_DEPEND_DOWN : ZBX_STYLE_ICON_DEPEND_UP; 2574 2575 $table = (new CTableInfo()) 2576 ->setAttribute('style', 'max-width: '.ZBX_TEXTAREA_STANDARD_WIDTH.'px;') 2577 ->setHeader([$header]); 2578 2579 foreach ($dependencies[$type] as $description) { 2580 $table->addRow($description); 2581 } 2582 2583 $result[] = (new CSpan()) 2584 ->addClass($class) 2585 ->addClass(ZBX_STYLE_CURSOR_POINTER) 2586 ->setHint($table, '', $freeze_on_click); 2587 } 2588 } 2589 2590 return $result; 2591} 2592 2593/** 2594 * Return list of functions that can be used without /host/key reference. 2595 * 2596 * @return array 2597 */ 2598function getStandaloneFunctions(): array { 2599 return ['date', 'dayofmonth', 'dayofweek', 'time', 'now']; 2600} 2601 2602/** 2603 * Returns a list of functions that return a constant or random number. 2604 * 2605 * @return array 2606 */ 2607function getFunctionsConstants(): array { 2608 return ['e', 'pi', 'rand']; 2609} 2610