1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22/** 23 * Class containing methods for operations with template. 24 * 25 * @package API 26 */ 27class CTemplate extends CHostGeneral { 28 29 protected $sortColumns = ['hostid', 'host', 'name']; 30 31 /** 32 * Overrides the parent function so that templateids will be used instead of hostids for the template API. 33 */ 34 public function pkOption($tableName = null) { 35 if ($tableName && $tableName != $this->tableName()) { 36 return parent::pkOption($tableName); 37 } 38 else { 39 return 'templateids'; 40 } 41 } 42 43 /** 44 * Get template data. 45 * 46 * @param array $options 47 * 48 * @return array 49 */ 50 public function get($options = []) { 51 $result = []; 52 53 $sqlParts = [ 54 'select' => ['templates' => 'h.hostid'], 55 'from' => ['hosts' => 'hosts h'], 56 'where' => ['h.status='.HOST_STATUS_TEMPLATE], 57 'group' => [], 58 'order' => [], 59 'limit' => null 60 ]; 61 62 $defOptions = [ 63 'groupids' => null, 64 'templateids' => null, 65 'parentTemplateids' => null, 66 'hostids' => null, 67 'graphids' => null, 68 'itemids' => null, 69 'triggerids' => null, 70 'with_items' => null, 71 'with_triggers' => null, 72 'with_graphs' => null, 73 'with_httptests' => null, 74 'editable' => false, 75 'nopermissions' => null, 76 // filter 77 'filter' => null, 78 'search' => '', 79 'searchByAny' => null, 80 'startSearch' => null, 81 'excludeSearch' => null, 82 'searchWildcardsEnabled' => null, 83 // output 84 'output' => API_OUTPUT_EXTEND, 85 'selectGroups' => null, 86 'selectHosts' => null, 87 'selectTemplates' => null, 88 'selectParentTemplates' => null, 89 'selectItems' => null, 90 'selectDiscoveries' => null, 91 'selectTriggers' => null, 92 'selectGraphs' => null, 93 'selectApplications' => null, 94 'selectMacros' => null, 95 'selectScreens' => null, 96 'selectHttpTests' => null, 97 'countOutput' => null, 98 'groupCount' => null, 99 'preservekeys' => null, 100 'sortfield' => '', 101 'sortorder' => '', 102 'limit' => null, 103 'limitSelects' => null 104 ]; 105 $options = zbx_array_merge($defOptions, $options); 106 107 // editable + PERMISSION CHECK 108 if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) { 109 $permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ; 110 $userGroups = getUserGroupsByUserId(self::$userData['userid']); 111 112 $sqlParts['where'][] = 'EXISTS ('. 113 'SELECT NULL'. 114 ' FROM hosts_groups hgg'. 115 ' JOIN rights r'. 116 ' ON r.id=hgg.groupid'. 117 ' AND '.dbConditionInt('r.groupid', $userGroups). 118 ' WHERE h.hostid=hgg.hostid'. 119 ' GROUP BY hgg.hostid'. 120 ' HAVING MIN(r.permission)>'.PERM_DENY. 121 ' AND MAX(r.permission)>='.zbx_dbstr($permission). 122 ')'; 123 } 124 125 // groupids 126 if (!is_null($options['groupids'])) { 127 zbx_value2array($options['groupids']); 128 129 $sqlParts['from']['hosts_groups'] = 'hosts_groups hg'; 130 $sqlParts['where'][] = dbConditionInt('hg.groupid', $options['groupids']); 131 $sqlParts['where']['hgh'] = 'hg.hostid=h.hostid'; 132 133 if (!is_null($options['groupCount'])) { 134 $sqlParts['group']['hg'] = 'hg.groupid'; 135 } 136 } 137 138 // templateids 139 if (!is_null($options['templateids'])) { 140 zbx_value2array($options['templateids']); 141 142 $sqlParts['where']['templateid'] = dbConditionInt('h.hostid', $options['templateids']); 143 } 144 145 // parentTemplateids 146 if (!is_null($options['parentTemplateids'])) { 147 zbx_value2array($options['parentTemplateids']); 148 149 $sqlParts['from']['hosts_templates'] = 'hosts_templates ht'; 150 $sqlParts['where'][] = dbConditionInt('ht.templateid', $options['parentTemplateids']); 151 $sqlParts['where']['hht'] = 'h.hostid=ht.hostid'; 152 153 if (!is_null($options['groupCount'])) { 154 $sqlParts['group']['templateid'] = 'ht.templateid'; 155 } 156 } 157 158 // hostids 159 if (!is_null($options['hostids'])) { 160 zbx_value2array($options['hostids']); 161 162 $sqlParts['from']['hosts_templates'] = 'hosts_templates ht'; 163 $sqlParts['where'][] = dbConditionInt('ht.hostid', $options['hostids']); 164 $sqlParts['where']['hht'] = 'h.hostid=ht.templateid'; 165 166 if (!is_null($options['groupCount'])) { 167 $sqlParts['group']['ht'] = 'ht.hostid'; 168 } 169 } 170 171 // itemids 172 if (!is_null($options['itemids'])) { 173 zbx_value2array($options['itemids']); 174 175 $sqlParts['from']['items'] = 'items i'; 176 $sqlParts['where'][] = dbConditionInt('i.itemid', $options['itemids']); 177 $sqlParts['where']['hi'] = 'h.hostid=i.hostid'; 178 } 179 180 // triggerids 181 if (!is_null($options['triggerids'])) { 182 zbx_value2array($options['triggerids']); 183 184 $sqlParts['from']['functions'] = 'functions f'; 185 $sqlParts['from']['items'] = 'items i'; 186 $sqlParts['where'][] = dbConditionInt('f.triggerid', $options['triggerids']); 187 $sqlParts['where']['hi'] = 'h.hostid=i.hostid'; 188 $sqlParts['where']['fi'] = 'f.itemid=i.itemid'; 189 } 190 191 // graphids 192 if (!is_null($options['graphids'])) { 193 zbx_value2array($options['graphids']); 194 195 $sqlParts['from']['graphs_items'] = 'graphs_items gi'; 196 $sqlParts['from']['items'] = 'items i'; 197 $sqlParts['where'][] = dbConditionInt('gi.graphid', $options['graphids']); 198 $sqlParts['where']['igi'] = 'i.itemid=gi.itemid'; 199 $sqlParts['where']['hi'] = 'h.hostid=i.hostid'; 200 } 201 202 // with_items 203 if (!is_null($options['with_items'])) { 204 $sqlParts['where'][] = 'EXISTS ('. 205 'SELECT NULL'. 206 ' FROM items i'. 207 ' WHERE h.hostid=i.hostid'. 208 ' AND i.flags IN ('.ZBX_FLAG_DISCOVERY_NORMAL.','.ZBX_FLAG_DISCOVERY_CREATED.')'. 209 ')'; 210 } 211 212 // with_triggers 213 if (!is_null($options['with_triggers'])) { 214 $sqlParts['where'][] = 'EXISTS ('. 215 'SELECT NULL'. 216 ' FROM items i,functions f,triggers t'. 217 ' WHERE i.hostid=h.hostid'. 218 ' AND i.itemid=f.itemid'. 219 ' AND f.triggerid=t.triggerid'. 220 ' AND t.flags IN ('.ZBX_FLAG_DISCOVERY_NORMAL.','.ZBX_FLAG_DISCOVERY_CREATED.')'. 221 ')'; 222 } 223 224 // with_graphs 225 if (!is_null($options['with_graphs'])) { 226 $sqlParts['where'][] = 'EXISTS ('. 227 'SELECT NULL'. 228 ' FROM items i,graphs_items gi,graphs g'. 229 ' WHERE i.hostid=h.hostid'. 230 ' AND i.itemid=gi.itemid'. 231 ' AND gi.graphid=g.graphid'. 232 ' AND g.flags IN ('.ZBX_FLAG_DISCOVERY_NORMAL.','.ZBX_FLAG_DISCOVERY_CREATED.')'. 233 ')'; 234 } 235 236 // with_httptests 237 if (!empty($options['with_httptests'])) { 238 $sqlParts['where'][] = 'EXISTS (SELECT ht.httptestid FROM httptest ht WHERE ht.hostid=h.hostid)'; 239 } 240 241 // filter 242 if (is_array($options['filter'])) { 243 $this->dbFilter('hosts h', $options, $sqlParts); 244 } 245 246 // search 247 if (is_array($options['search'])) { 248 zbx_db_search('hosts h', $options, $sqlParts); 249 } 250 251 // limit 252 if (zbx_ctype_digit($options['limit']) && $options['limit']) { 253 $sqlParts['limit'] = $options['limit']; 254 } 255 256 $sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts); 257 $sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts); 258 $res = DBselect($this->createSelectQueryFromParts($sqlParts), $sqlParts['limit']); 259 while ($template = DBfetch($res)) { 260 if (!is_null($options['countOutput'])) { 261 if (!is_null($options['groupCount'])) 262 $result[] = $template; 263 else 264 $result = $template['rowscount']; 265 } 266 else{ 267 $template['templateid'] = $template['hostid']; 268 unset($template['hostid']); 269 270 $result[$template['templateid']] = $template; 271 } 272 273 } 274 275 if (!is_null($options['countOutput'])) { 276 return $result; 277 } 278 279 if ($result) { 280 $result = $this->addRelatedObjects($options, $result); 281 } 282 283 // removing keys (hash -> array) 284 if (is_null($options['preservekeys'])) { 285 $result = zbx_cleanHashes($result); 286 } 287 288 return $result; 289 } 290 291 /** 292 * Add template. 293 * 294 * @param array $templates 295 * 296 * @return array 297 */ 298 public function create(array $templates) { 299 $templates = zbx_toArray($templates); 300 301 $this->validateCreate($templates); 302 303 $templateIds = []; 304 305 foreach ($templates as $key => $template) { 306 $templates[$key]['groups'] = zbx_toArray($template['groups']); 307 } 308 309 foreach ($templates as $template) { 310 // if visible name is not given or empty it should be set to host name 311 if ((!isset($template['name']) || zbx_empty(trim($template['name']))) && isset($template['host'])) { 312 $template['name'] = $template['host']; 313 } 314 315 $newTemplateIds = DB::insert('hosts', [[ 316 'host' => $template['host'], 317 'name' => $template['name'], 318 'description' => isset($template['description']) ? $template['description'] : null, 319 'status' => HOST_STATUS_TEMPLATE 320 ]]); 321 322 $templateId = reset($newTemplateIds); 323 324 $templateIds[] = $templateId; 325 326 foreach ($template['groups'] as $group) { 327 $hostGroupId = get_dbid('hosts_groups', 'hostgroupid'); 328 329 $result = DBexecute( 330 'INSERT INTO hosts_groups (hostgroupid,hostid,groupid)'. 331 ' VALUES ('.zbx_dbstr($hostGroupId).','.zbx_dbstr($templateId).','.zbx_dbstr($group['groupid']).')' 332 ); 333 334 if (!$result) { 335 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot add group.')); 336 } 337 } 338 339 $template['templateid'] = $templateId; 340 341 $result = $this->massAdd([ 342 'templates' => $template, 343 'templates_link' => isset($template['templates']) ? $template['templates'] : null, 344 'macros' => isset($template['macros']) ? $template['macros'] : null, 345 'hosts' => isset($template['hosts']) ? $template['hosts'] : null 346 ]); 347 348 if (!$result) { 349 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot create template.')); 350 } 351 } 352 353 return ['templateids' => $templateIds]; 354 } 355 356 /** 357 * Validate create template. 358 * 359 * @param array $templates 360 */ 361 protected function validateCreate(array $templates) { 362 $groupIds = []; 363 364 foreach ($templates as $template) { 365 // check if hosts have at least 1 group 366 if (!isset($template['groups']) || !$template['groups']) { 367 self::exception(ZBX_API_ERROR_PARAMETERS, 368 _s('Template "%1$s" cannot be without host group.', $template['host']) 369 ); 370 } 371 372 $template['groups'] = zbx_toArray($template['groups']); 373 374 foreach ($template['groups'] as $group) { 375 $groupIds[$group['groupid']] = $group['groupid']; 376 } 377 } 378 379 $dbHostGroups = API::HostGroup()->get([ 380 'output' => ['groupid'], 381 'groupids' => $groupIds, 382 'editable' => true, 383 'preservekeys' => true 384 ]); 385 386 foreach ($groupIds as $groupId) { 387 if (!isset($dbHostGroups[$groupId])) { 388 self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); 389 } 390 } 391 392 $templateDbFields = ['host' => null]; 393 394 foreach ($templates as $template) { 395 // if visible name is not given or empty it should be set to host name 396 if ((!isset($template['name']) || zbx_empty(trim($template['name']))) && isset($template['host'])) { 397 $template['name'] = $template['host']; 398 } 399 400 if (!check_db_fields($templateDbFields, $template)) { 401 self::exception(ZBX_API_ERROR_PARAMETERS, _('Field "host" is mandatory.')); 402 } 403 404 if (!preg_match('/^'.ZBX_PREG_HOST_FORMAT.'$/', $template['host'])) { 405 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 406 'Incorrect characters used for template name "%1$s".', 407 $template['host'] 408 )); 409 } 410 411 if (isset($template['host'])) { 412 $templateExists = API::Template()->get([ 413 'output' => ['templateid'], 414 'filter' => ['host' => $template['host']], 415 'nopermissions' => true, 416 'limit' => 1 417 ]); 418 if ($templateExists) { 419 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Template "%1$s" already exists.', $template['host'])); 420 } 421 422 $hostExists = API::Host()->get([ 423 'output' => ['hostid'], 424 'filter' => ['host' => $template['host']], 425 'nopermissions' => true, 426 'limit' => 1 427 ]); 428 if ($hostExists) { 429 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host "%1$s" already exists.', $template['host'])); 430 } 431 } 432 433 if (isset($template['name'])) { 434 $templateExists = API::Template()->get([ 435 'output' => ['templateid'], 436 'filter' => ['name' => $template['name']], 437 'nopermissions' => true, 438 'limit' => 1 439 ]); 440 if ($templateExists) { 441 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 442 'Template with the same visible name "%1$s" already exists.', 443 $template['name'] 444 )); 445 } 446 447 $hostExists = API::Host()->get([ 448 'output' => ['hostid'], 449 'filter' => ['name' => $template['name']], 450 'nopermissions' => true, 451 'limit' => 1 452 ]); 453 if ($hostExists) { 454 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 455 'Host with the same visible name "%1$s" already exists.', 456 $template['name'] 457 )); 458 } 459 } 460 } 461 } 462 463 /** 464 * Update template. 465 * 466 * @param array $templates 467 * 468 * @return array 469 */ 470 public function update(array $templates) { 471 $templates = zbx_toArray($templates); 472 473 $this->validateUpdate($templates); 474 475 $macros = []; 476 foreach ($templates as &$template) { 477 if (isset($template['macros'])) { 478 $macros[$template['templateid']] = $template['macros']; 479 480 unset($template['macros']); 481 } 482 } 483 unset($template); 484 485 if ($macros) { 486 API::UserMacro()->replaceMacros($macros); 487 } 488 489 foreach ($templates as $template) { 490 // if visible name is not given or empty it should be set to host name 491 if ((!isset($template['name']) || zbx_empty(trim($template['name']))) && isset($template['host'])) { 492 $template['name'] = $template['host']; 493 } 494 495 $templateCopy = $template; 496 497 $template['templates_link'] = isset($template['templates']) ? $template['templates'] : null; 498 unset($template['templates'], $template['templateid'], $templateCopy['templates']); 499 $template['templates'] = [$templateCopy]; 500 501 if (!$this->massUpdate($template)) { 502 self::exception(ZBX_API_ERROR_PARAMETERS, _('Failed to update template.')); 503 } 504 } 505 506 return ['templateids' => zbx_objectValues($templates, 'templateid')]; 507 } 508 509 /** 510 * Validate update template. 511 * 512 * @param array $templates 513 */ 514 protected function validateUpdate(array $templates) { 515 $dbTemplates = $this->get([ 516 'output' => ['templateid'], 517 'templateids' => zbx_objectValues($templates, 'templateid'), 518 'editable' => true, 519 'preservekeys' => true 520 ]); 521 522 foreach ($templates as $template) { 523 if (!isset($dbTemplates[$template['templateid']])) { 524 self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); 525 } 526 } 527 } 528 529 /** 530 * Delete template. 531 * 532 * @param array $templateids 533 * @param array $templateids['templateids'] 534 * 535 * @return array 536 */ 537 public function delete(array $templateids) { 538 if (empty($templateids)) { 539 self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.')); 540 } 541 542 $options = [ 543 'templateids' => $templateids, 544 'editable' => true, 545 'output' => API_OUTPUT_EXTEND, 546 'preservekeys' => true 547 ]; 548 $delTemplates = $this->get($options); 549 foreach ($templateids as $templateid) { 550 if (!isset($delTemplates[$templateid])) { 551 self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); 552 } 553 } 554 555 API::Template()->unlink($templateids, null, true); 556 557 // delete the discovery rules first 558 $delRules = API::DiscoveryRule()->get([ 559 'output' => ['itemid'], 560 'hostids' => $templateids, 561 'nopermissions' => true, 562 'preservekeys' => true 563 ]); 564 if ($delRules) { 565 API::DiscoveryRule()->delete(array_keys($delRules), true); 566 } 567 568 // delete the items 569 $delItems = API::Item()->get([ 570 'templateids' => $templateids, 571 'output' => ['itemid'], 572 'nopermissions' => true, 573 'preservekeys' => true 574 ]); 575 if ($delItems) { 576 API::Item()->delete(array_keys($delItems), true); 577 } 578 579 // delete host from maps 580 if (!empty($templateids)) { 581 DB::delete('sysmaps_elements', ['elementtype' => SYSMAP_ELEMENT_TYPE_HOST, 'elementid' => $templateids]); 582 } 583 584 // disable actions 585 // actions from conditions 586 $actionids = []; 587 $sql = 'SELECT DISTINCT actionid'. 588 ' FROM conditions'. 589 ' WHERE conditiontype='.CONDITION_TYPE_TEMPLATE. 590 ' AND '.dbConditionString('value', $templateids); 591 $dbActions = DBselect($sql); 592 while ($dbAction = DBfetch($dbActions)) { 593 $actionids[$dbAction['actionid']] = $dbAction['actionid']; 594 } 595 596 // actions from operations 597 $sql = 'SELECT DISTINCT o.actionid'. 598 ' FROM operations o,optemplate ot'. 599 ' WHERE o.operationid=ot.operationid'. 600 ' AND '.dbConditionInt('ot.templateid', $templateids); 601 $dbActions = DBselect($sql); 602 while ($dbAction = DBfetch($dbActions)) { 603 $actionids[$dbAction['actionid']] = $dbAction['actionid']; 604 } 605 606 if (!empty($actionids)) { 607 DB::update('actions', [ 608 'values' => ['status' => ACTION_STATUS_DISABLED], 609 'where' => ['actionid' => $actionids] 610 ]); 611 } 612 613 // delete action conditions 614 DB::delete('conditions', [ 615 'conditiontype' => CONDITION_TYPE_TEMPLATE, 616 'value' => $templateids 617 ]); 618 619 // delete action operation commands 620 $operationids = []; 621 $sql = 'SELECT DISTINCT ot.operationid'. 622 ' FROM optemplate ot'. 623 ' WHERE '.dbConditionInt('ot.templateid', $templateids); 624 $dbOperations = DBselect($sql); 625 while ($dbOperation = DBfetch($dbOperations)) { 626 $operationids[$dbOperation['operationid']] = $dbOperation['operationid']; 627 } 628 629 DB::delete('optemplate', [ 630 'templateid'=>$templateids, 631 ]); 632 633 // delete empty operations 634 $delOperationids = []; 635 $sql = 'SELECT DISTINCT o.operationid'. 636 ' FROM operations o'. 637 ' WHERE '.dbConditionInt('o.operationid', $operationids). 638 ' AND NOT EXISTS(SELECT NULL FROM optemplate ot WHERE ot.operationid=o.operationid)'; 639 $dbOperations = DBselect($sql); 640 while ($dbOperation = DBfetch($dbOperations)) { 641 $delOperationids[$dbOperation['operationid']] = $dbOperation['operationid']; 642 } 643 644 DB::delete('operations', [ 645 'operationid'=>$delOperationids, 646 ]); 647 648 // http tests 649 $delHttpTests = API::HttpTest()->get([ 650 'templateids' => $templateids, 651 'output' => ['httptestid'], 652 'nopermissions' => 1, 653 'preservekeys' => 1 654 ]); 655 if (!empty($delHttpTests)) { 656 API::HttpTest()->delete(array_keys($delHttpTests), true); 657 } 658 659 // Applications 660 $delApplications = API::Application()->get([ 661 'templateids' => $templateids, 662 'output' => ['applicationid'], 663 'nopermissions' => 1, 664 'preservekeys' => 1 665 ]); 666 if (!empty($delApplications)) { 667 API::Application()->delete(array_keys($delApplications), true); 668 } 669 670 DB::delete('hosts', ['hostid' => $templateids]); 671 672 // TODO: remove info from API 673 foreach ($delTemplates as $template) { 674 info(_s('Deleted: Template "%1$s".', $template['name'])); 675 add_audit_ext(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_HOST, $template['templateid'], $template['host'], 'hosts', null, null); 676 } 677 678 return ['templateids' => $templateids]; 679 } 680 681 /** 682 * Additionally allows to link templates to hosts and other templates. 683 * 684 * Checks write permissions for templates. 685 * 686 * Additional supported $data parameters are: 687 * - hosts - an array of hosts or templates to link the given templates to 688 * 689 * @param array $data 690 * 691 * @return array 692 */ 693 public function massAdd(array $data) { 694 $templates = isset($data['templates']) ? zbx_toArray($data['templates']) : []; 695 $templateIds = zbx_objectValues($templates, 'templateid'); 696 697 // check permissions 698 if (!$this->isWritable($templateIds)) { 699 self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); 700 } 701 702 // link hosts to the given templates 703 if (isset($data['hosts']) && !empty($data['hosts'])) { 704 $hostIds = zbx_objectValues($data['hosts'], 'hostid'); 705 706 if (!API::Host()->isWritable($hostIds)) { 707 self::exception(ZBX_API_ERROR_PERMISSIONS, 708 _('No permissions to referred object or it does not exist!') 709 ); 710 } 711 712 // check if any of the hosts are discovered 713 $this->checkValidator($hostIds, new CHostNormalValidator([ 714 'message' => _('Cannot update templates on discovered host "%1$s".') 715 ])); 716 717 $this->link($templateIds, $hostIds); 718 } 719 720 $data['hosts'] = []; 721 722 return parent::massAdd($data); 723 } 724 725 /** 726 * Mass update. 727 * 728 * @param string $data['host'] 729 * @param string $data['name'] 730 * @param string $data['description'] 731 * @param array $data['templates'] 732 * @param array $data['templates_clear'] 733 * @param array $data['templates_link'] 734 * @param array $data['groups'] 735 * @param array $data['hosts'] 736 * @param array $data['macros'] 737 * 738 * @return array 739 */ 740 public function massUpdate(array $data) { 741 if (!array_key_exists('templates', $data) || !is_array($data['templates'])) { 742 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Field "%1$s" is mandatory.', 'templates')); 743 } 744 745 $this->validateMassUpdate($data); 746 747 $templates = zbx_toArray($data['templates']); 748 $templateIds = zbx_objectValues($templates, 'templateid'); 749 750 $fieldsToUpdate = []; 751 752 if (isset($data['host'])) { 753 $fieldsToUpdate[] = 'host='.zbx_dbstr($data['host']); 754 } 755 756 if (isset($data['name'])) { 757 // if visible name is empty replace it with host name 758 if (zbx_empty(trim($data['name'])) && isset($data['host'])) { 759 $fieldsToUpdate[] = 'name='.zbx_dbstr($data['host']); 760 } 761 // we cannot have empty visible name 762 elseif (zbx_empty(trim($data['name'])) && !isset($data['host'])) { 763 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot have empty visible template name.')); 764 } 765 else { 766 $fieldsToUpdate[] = 'name='.zbx_dbstr($data['name']); 767 } 768 } 769 770 if (isset($data['description'])) { 771 $fieldsToUpdate[] = 'description='.zbx_dbstr($data['description']); 772 } 773 774 if ($fieldsToUpdate) { 775 DBexecute('UPDATE hosts SET '.implode(', ', $fieldsToUpdate).' WHERE '.dbConditionInt('hostid', $templateIds)); 776 } 777 778 $data['templates_clear'] = isset($data['templates_clear']) ? zbx_toArray($data['templates_clear']) : []; 779 $templateIdsClear = zbx_objectValues($data['templates_clear'], 'templateid'); 780 781 if ($data['templates_clear']) { 782 $this->massRemove([ 783 'templateids' => $templateIds, 784 'templateids_clear' => $templateIdsClear 785 ]); 786 } 787 788 // update template linkage 789 // firstly need to unlink all things, to correctly check circulars 790 if (isset($data['hosts']) && $data['hosts'] !== null) { 791 /* 792 * Get all currently linked hosts and templates (skip discovered hosts) to these templates 793 * that user has read permissions. 794 */ 795 $templateHosts = API::Host()->get([ 796 'output' => ['hostid'], 797 'templateids' => $templateIds, 798 'templated_hosts' => true, 799 'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL] 800 ]); 801 $templateHostIds = zbx_objectValues($templateHosts, 'hostid'); 802 $newHostIds = zbx_objectValues($data['hosts'], 'hostid'); 803 804 $hostsToDelete = array_diff($templateHostIds, $newHostIds); 805 $hostIdsToDelete = array_diff($hostsToDelete, $templateIdsClear); 806 $hostIdsToAdd = array_diff($newHostIds, $templateHostIds); 807 808 if ($hostIdsToDelete) { 809 $result = $this->massRemove([ 810 'hostids' => $hostIdsToDelete, 811 'templateids' => $templateIds 812 ]); 813 814 if (!$result) { 815 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot unlink template.')); 816 } 817 } 818 } 819 820 if (isset($data['templates_link']) && $data['templates_link'] !== null) { 821 $templateTemplates = API::Template()->get([ 822 'output' => ['templateid'], 823 'hostids' => $templateIds 824 ]); 825 $templateTemplateIds = zbx_objectValues($templateTemplates, 'templateid'); 826 $newTemplateIds = zbx_objectValues($data['templates_link'], 'templateid'); 827 828 $templatesToDelete = array_diff($templateTemplateIds, $newTemplateIds); 829 $templateIdsToDelete = array_diff($templatesToDelete, $templateIdsClear); 830 831 if ($templateIdsToDelete) { 832 $result = $this->massRemove([ 833 'templateids' => $templateIds, 834 'templateids_link' => $templateIdsToDelete 835 ]); 836 837 if (!$result) { 838 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot unlink template.')); 839 } 840 } 841 } 842 843 if (isset($data['hosts']) && $data['hosts'] !== null && $hostIdsToAdd) { 844 $result = $this->massAdd([ 845 'templates' => $templates, 846 'hosts' => $hostIdsToAdd 847 ]); 848 849 if (!$result) { 850 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot link template.')); 851 } 852 } 853 854 if (isset($data['templates_link']) && $data['templates_link'] !== null) { 855 $templatesToAdd = array_diff($newTemplateIds, $templateTemplateIds); 856 857 if ($templatesToAdd) { 858 $result = $this->massAdd([ 859 'templates' => $templates, 860 'templates_link' => $templatesToAdd 861 ]); 862 863 if (!$result) { 864 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot link template.')); 865 } 866 } 867 } 868 869 // macros 870 if (isset($data['macros'])) { 871 DB::delete('hostmacro', ['hostid' => $templateIds]); 872 873 $this->massAdd([ 874 'hosts' => $templates, 875 'macros' => $data['macros'] 876 ]); 877 } 878 879 /* 880 * Update template and host group linkage. This procedure should be done the last because user can unlink 881 * him self from a group with write permissions leaving only read premissions. Thus other procedures, like 882 * host-template linking, macros update, must be done before this. 883 */ 884 if (isset($data['groups']) && $data['groups'] !== null && is_array($data['groups'])) { 885 $updateGroups = zbx_toArray($data['groups']); 886 887 $templateGroups = API::HostGroup()->get([ 888 'output' => ['groupid'], 889 'templateids' => $templateIds 890 ]); 891 $templateGroupIds = zbx_objectValues($templateGroups, 'groupid'); 892 $newGroupIds = zbx_objectValues($updateGroups, 'groupid'); 893 894 $groupsToAdd = array_diff($newGroupIds, $templateGroupIds); 895 if ($groupsToAdd) { 896 $this->massAdd([ 897 'templates' => $templates, 898 'groups' => zbx_toObject($groupsToAdd, 'groupid') 899 ]); 900 } 901 902 $groupIdsToDelete = array_diff($templateGroupIds, $newGroupIds); 903 if ($groupIdsToDelete) { 904 $this->massRemove([ 905 'templateids' => $templateIds, 906 'groupids' => $groupIdsToDelete 907 ]); 908 } 909 } 910 911 return ['templateids' => $templateIds]; 912 } 913 914 /** 915 * Validate mass update. 916 * 917 * @param string $data['host'] 918 * @param string $data['name'] 919 * @param array $data['templates'] 920 * @param array $data['groups'] 921 * @param array $data['hosts'] 922 * 923 * @return array 924 */ 925 protected function validateMassUpdate(array $data) { 926 $templates = zbx_toArray($data['templates']); 927 928 $dbTemplates = $this->get([ 929 'output' => ['templateid', 'host'], 930 'templateids' => zbx_objectValues($templates, 'templateid'), 931 'editable' => true, 932 'preservekeys' => true 933 ]); 934 935 // check permissions 936 foreach ($templates as $template) { 937 if (!isset($dbTemplates[$template['templateid']])) { 938 self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); 939 } 940 } 941 942 if (array_key_exists('groups', $data) && !$data['groups'] && $dbTemplates) { 943 $template = reset($dbTemplates); 944 945 self::exception(ZBX_API_ERROR_PARAMETERS, 946 _s('Template "%1$s" cannot be without host group.', $template['host']) 947 ); 948 } 949 950 // check name 951 if (isset($data['name'])) { 952 if (count($templates) > 1) { 953 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot mass update visible template name.')); 954 } 955 956 $template = reset($templates); 957 958 $templateExists = $this->get([ 959 'output' => ['templateid'], 960 'filter' => ['name' => $data['name']], 961 'nopermissions' => true 962 ]); 963 $templateExist = reset($templateExists); 964 if ($templateExist && bccomp($templateExist['templateid'], $template['templateid']) != 0) { 965 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 966 'Template with the same visible name "%1$s" already exists.', 967 $data['name'] 968 )); 969 } 970 971 // can't set the same name as existing host 972 $hostExists = API::Host()->get([ 973 'output' => ['hostid'], 974 'filter' => ['name' => $data['name']], 975 'nopermissions' => true 976 ]); 977 if ($hostExists) { 978 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 979 'Host with the same visible name "%1$s" already exists.', 980 $data['name'] 981 )); 982 } 983 } 984 985 // check host 986 if (isset($data['host'])) { 987 if (count($templates) > 1) { 988 self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot mass update template name.')); 989 } 990 991 $template = reset($templates); 992 993 $templateExists = $this->get([ 994 'output' => ['templateid'], 995 'filter' => ['host' => $data['host']], 996 'nopermissions' => true 997 ]); 998 $templateExist = reset($templateExists); 999 if ($templateExist && bccomp($templateExist['templateid'], $template['templateid']) != 0) { 1000 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 1001 'Template with the same name "%1$s" already exists.', 1002 $template['host'] 1003 )); 1004 } 1005 1006 // can't set the same name as existing host 1007 $hostExists = API::Host()->get([ 1008 'output' => ['hostid'], 1009 'filter' => ['host' => $template['host']], 1010 'nopermissions' => true 1011 ]); 1012 if ($hostExists) { 1013 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 1014 'Host with the same name "%1$s" already exists.', 1015 $template['host'] 1016 )); 1017 } 1018 } 1019 1020 if (isset($data['host']) && !preg_match('/^'.ZBX_PREG_HOST_FORMAT.'$/', $data['host'])) { 1021 self::exception(ZBX_API_ERROR_PARAMETERS, _s( 1022 'Incorrect characters used for template name "%1$s".', 1023 $data['host'] 1024 )); 1025 } 1026 } 1027 1028 /** 1029 * Additionally allows to unlink templates from hosts and other templates. 1030 * 1031 * Checks write permissions for templates. 1032 * 1033 * Additional supported $data parameters are: 1034 * - hostids - an array of host or template IDs to unlink the given templates from 1035 * 1036 * @param array $data 1037 * 1038 * @return array 1039 */ 1040 public function massRemove(array $data) { 1041 $templateids = zbx_toArray($data['templateids']); 1042 1043 // check permissions 1044 if (!$this->isWritable($templateids)) { 1045 self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); 1046 } 1047 1048 if (isset($data['hostids'])) { 1049 // check if any of the hosts are discovered 1050 $this->checkValidator($data['hostids'], new CHostNormalValidator([ 1051 'message' => _('Cannot update templates on discovered host "%1$s".') 1052 ])); 1053 1054 API::Template()->unlink($templateids, zbx_toArray($data['hostids'])); 1055 } 1056 1057 $data['hostids'] = []; 1058 1059 return parent::massRemove($data); 1060 } 1061 1062 /** 1063 * Check if user has read permissions for templates. 1064 * 1065 * @param array $ids 1066 * 1067 * @return bool 1068 */ 1069 public function isReadable(array $ids) { 1070 if (!is_array($ids)) { 1071 return false; 1072 } 1073 if (empty($ids)) { 1074 return true; 1075 } 1076 1077 $ids = array_unique($ids); 1078 1079 $count = $this->get([ 1080 'templateids' => $ids, 1081 'countOutput' => true 1082 ]); 1083 1084 return (count($ids) == $count); 1085 } 1086 1087 /** 1088 * Check if user has write permissions for templates. 1089 * 1090 * @param array $ids 1091 * 1092 * @return bool 1093 */ 1094 public function isWritable(array $ids) { 1095 if (!is_array($ids)) { 1096 return false; 1097 } 1098 if (empty($ids)) { 1099 return true; 1100 } 1101 1102 $ids = array_unique($ids); 1103 1104 $count = $this->get([ 1105 'templateids' => $ids, 1106 'editable' => true, 1107 'countOutput' => true 1108 ]); 1109 1110 return (count($ids) == $count); 1111 } 1112 1113 protected function addRelatedObjects(array $options, array $result) { 1114 $result = parent::addRelatedObjects($options, $result); 1115 1116 $templateids = array_keys($result); 1117 1118 // Adding Templates 1119 if ($options['selectTemplates'] !== null) { 1120 if ($options['selectTemplates'] != API_OUTPUT_COUNT) { 1121 $relationMap = $this->createRelationMap($result, 'templateid', 'hostid', 'hosts_templates'); 1122 $templates = API::Template()->get([ 1123 'output' => $options['selectTemplates'], 1124 'templateids' => $relationMap->getRelatedIds(), 1125 'preservekeys' => true 1126 ]); 1127 if (!is_null($options['limitSelects'])) { 1128 order_result($templates, 'host'); 1129 } 1130 $result = $relationMap->mapMany($result, $templates, 'templates', $options['limitSelects']); 1131 } 1132 else { 1133 $templates = API::Template()->get([ 1134 'parentTemplateids' => $templateids, 1135 'countOutput' => true, 1136 'groupCount' => true 1137 ]); 1138 $templates = zbx_toHash($templates, 'templateid'); 1139 foreach ($result as $templateid => $template) { 1140 if (isset($templates[$templateid])) 1141 $result[$templateid]['templates'] = $templates[$templateid]['rowscount']; 1142 else 1143 $result[$templateid]['templates'] = 0; 1144 } 1145 } 1146 } 1147 1148 // Adding Hosts 1149 if ($options['selectHosts'] !== null) { 1150 if ($options['selectHosts'] != API_OUTPUT_COUNT) { 1151 $relationMap = $this->createRelationMap($result, 'templateid', 'hostid', 'hosts_templates'); 1152 $hosts = API::Host()->get([ 1153 'output' => $options['selectHosts'], 1154 'hostids' => $relationMap->getRelatedIds(), 1155 'preservekeys' => true 1156 ]); 1157 if (!is_null($options['limitSelects'])) { 1158 order_result($hosts, 'host'); 1159 } 1160 $result = $relationMap->mapMany($result, $hosts, 'hosts', $options['limitSelects']); 1161 } 1162 else { 1163 $hosts = API::Host()->get([ 1164 'templateids' => $templateids, 1165 'countOutput' => true, 1166 'groupCount' => true 1167 ]); 1168 $hosts = zbx_toHash($hosts, 'templateid'); 1169 foreach ($result as $templateid => $template) { 1170 if (isset($hosts[$templateid])) 1171 $result[$templateid]['hosts'] = $hosts[$templateid]['rowscount']; 1172 else 1173 $result[$templateid]['hosts'] = 0; 1174 } 1175 } 1176 } 1177 1178 // Adding screens 1179 if ($options['selectScreens'] !== null) { 1180 if ($options['selectScreens'] != API_OUTPUT_COUNT) { 1181 $screens = API::TemplateScreen()->get([ 1182 'output' => $this->outputExtend($options['selectScreens'], ['templateid']), 1183 'templateids' => $templateids, 1184 'nopermissions' => true 1185 ]); 1186 if (!is_null($options['limitSelects'])) { 1187 order_result($screens, 'name'); 1188 } 1189 1190 // preservekeys is not supported by templatescreen.get, so we're building a map using array keys 1191 $relationMap = new CRelationMap(); 1192 foreach ($screens as $key => $screen) { 1193 $relationMap->addRelation($screen['templateid'], $key); 1194 } 1195 1196 $screens = $this->unsetExtraFields($screens, ['templateid'], $options['selectScreens']); 1197 $result = $relationMap->mapMany($result, $screens, 'screens', $options['limitSelects']); 1198 } 1199 else { 1200 $screens = API::TemplateScreen()->get([ 1201 'templateids' => $templateids, 1202 'nopermissions' => true, 1203 'countOutput' => true, 1204 'groupCount' => true 1205 ]); 1206 $screens = zbx_toHash($screens, 'templateid'); 1207 foreach ($result as $templateid => $template) { 1208 if (isset($screens[$templateid])) 1209 $result[$templateid]['screens'] = $screens[$templateid]['rowscount']; 1210 else 1211 $result[$templateid]['screens'] = 0; 1212 } 1213 } 1214 } 1215 1216 return $result; 1217 } 1218} 1219