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 */
25class CTemplate extends CHostGeneral {
26
27	protected $sortColumns = ['hostid', 'host', 'name'];
28
29	/**
30	 * Overrides the parent function so that templateids will be used instead of hostids for the template API.
31	 */
32	public function pkOption($tableName = null) {
33		if ($tableName && $tableName != $this->tableName()) {
34			return parent::pkOption($tableName);
35		}
36		else {
37			return 'templateids';
38		}
39	}
40
41	/**
42	 * Get template data.
43	 *
44	 * @param array $options
45	 *
46	 * @return array
47	 */
48	public function get($options = []) {
49		$result = [];
50
51		$sqlParts = [
52			'select'	=> ['templates' => 'h.hostid'],
53			'from'		=> ['hosts' => 'hosts h'],
54			'where'		=> ['h.status='.HOST_STATUS_TEMPLATE],
55			'group'		=> [],
56			'order'		=> [],
57			'limit'		=> null
58		];
59
60		$defOptions = [
61			'groupids'					=> null,
62			'templateids'				=> null,
63			'parentTemplateids'			=> null,
64			'hostids'					=> null,
65			'graphids'					=> null,
66			'itemids'					=> null,
67			'triggerids'				=> null,
68			'with_items'				=> null,
69			'with_triggers'				=> null,
70			'with_graphs'				=> null,
71			'with_httptests'			=> null,
72			'editable'					=> false,
73			'nopermissions'				=> null,
74			// filter
75			'evaltype'					=> TAG_EVAL_TYPE_AND_OR,
76			'tags'						=> null,
77			'filter'					=> null,
78			'search'					=> '',
79			'searchByAny'				=> null,
80			'startSearch'				=> false,
81			'excludeSearch'				=> false,
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			'selectMacros'				=> null,
94			'selectDashboards'			=> null,
95			'selectHttpTests'			=> null,
96			'selectTags'				=> null,
97			'selectValueMaps'			=> null,
98			'countOutput'				=> false,
99			'groupCount'				=> false,
100			'preservekeys'				=> false,
101			'sortfield'					=> '',
102			'sortorder'					=> '',
103			'limit'						=> null,
104			'limitSelects'				=> null
105		];
106		$options = zbx_array_merge($defOptions, $options);
107		$this->validateGet($options);
108
109		// editable + PERMISSION CHECK
110		if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
111			$permission = $options['editable'] ? PERM_READ_WRITE : PERM_READ;
112			$userGroups = getUserGroupsByUserId(self::$userData['userid']);
113
114			$sqlParts['where'][] = 'EXISTS ('.
115					'SELECT NULL'.
116					' FROM hosts_groups hgg'.
117						' JOIN rights r'.
118							' ON r.id=hgg.groupid'.
119								' AND '.dbConditionInt('r.groupid', $userGroups).
120					' WHERE h.hostid=hgg.hostid'.
121					' GROUP BY hgg.hostid'.
122					' HAVING MIN(r.permission)>'.PERM_DENY.
123						' AND MAX(r.permission)>='.zbx_dbstr($permission).
124					')';
125		}
126
127		// groupids
128		if (!is_null($options['groupids'])) {
129			zbx_value2array($options['groupids']);
130
131			$sqlParts['from']['hosts_groups'] = 'hosts_groups hg';
132			$sqlParts['where'][] = dbConditionInt('hg.groupid', $options['groupids']);
133			$sqlParts['where']['hgh'] = 'hg.hostid=h.hostid';
134
135			if ($options['groupCount']) {
136				$sqlParts['group']['hg'] = 'hg.groupid';
137			}
138		}
139
140		// templateids
141		if (!is_null($options['templateids'])) {
142			zbx_value2array($options['templateids']);
143
144			$sqlParts['where']['templateid'] = dbConditionInt('h.hostid', $options['templateids']);
145		}
146
147		// parentTemplateids
148		if (!is_null($options['parentTemplateids'])) {
149			zbx_value2array($options['parentTemplateids']);
150
151			$sqlParts['from']['hosts_templates'] = 'hosts_templates ht';
152			$sqlParts['where'][] = dbConditionInt('ht.templateid', $options['parentTemplateids']);
153			$sqlParts['where']['hht'] = 'h.hostid=ht.hostid';
154
155			if ($options['groupCount']) {
156				$sqlParts['group']['templateid'] = 'ht.templateid';
157			}
158		}
159
160		// hostids
161		if (!is_null($options['hostids'])) {
162			zbx_value2array($options['hostids']);
163
164			$sqlParts['from']['hosts_templates'] = 'hosts_templates ht';
165			$sqlParts['where'][] = dbConditionInt('ht.hostid', $options['hostids']);
166			$sqlParts['where']['hht'] = 'h.hostid=ht.templateid';
167
168			if ($options['groupCount']) {
169				$sqlParts['group']['ht'] = 'ht.hostid';
170			}
171		}
172
173		// itemids
174		if (!is_null($options['itemids'])) {
175			zbx_value2array($options['itemids']);
176
177			$sqlParts['from']['items'] = 'items i';
178			$sqlParts['where'][] = dbConditionInt('i.itemid', $options['itemids']);
179			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
180		}
181
182		// triggerids
183		if (!is_null($options['triggerids'])) {
184			zbx_value2array($options['triggerids']);
185
186			$sqlParts['from']['functions'] = 'functions f';
187			$sqlParts['from']['items'] = 'items i';
188			$sqlParts['where'][] = dbConditionInt('f.triggerid', $options['triggerids']);
189			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
190			$sqlParts['where']['fi'] = 'f.itemid=i.itemid';
191		}
192
193		// graphids
194		if (!is_null($options['graphids'])) {
195			zbx_value2array($options['graphids']);
196
197			$sqlParts['from']['graphs_items'] = 'graphs_items gi';
198			$sqlParts['from']['items'] = 'items i';
199			$sqlParts['where'][] = dbConditionInt('gi.graphid', $options['graphids']);
200			$sqlParts['where']['igi'] = 'i.itemid=gi.itemid';
201			$sqlParts['where']['hi'] = 'h.hostid=i.hostid';
202		}
203
204		// with_items
205		if (!is_null($options['with_items'])) {
206			$sqlParts['where'][] = 'EXISTS ('.
207				'SELECT NULL'.
208				' FROM items i'.
209				' WHERE h.hostid=i.hostid'.
210					' AND i.flags IN ('.ZBX_FLAG_DISCOVERY_NORMAL.','.ZBX_FLAG_DISCOVERY_CREATED.')'.
211				')';
212		}
213
214		// with_triggers
215		if (!is_null($options['with_triggers'])) {
216			$sqlParts['where'][] = 'EXISTS ('.
217				'SELECT NULL'.
218				' FROM items i,functions f,triggers t'.
219				' WHERE i.hostid=h.hostid'.
220					' AND i.itemid=f.itemid'.
221					' AND f.triggerid=t.triggerid'.
222					' AND t.flags IN ('.ZBX_FLAG_DISCOVERY_NORMAL.','.ZBX_FLAG_DISCOVERY_CREATED.')'.
223				')';
224		}
225
226		// with_graphs
227		if (!is_null($options['with_graphs'])) {
228			$sqlParts['where'][] = 'EXISTS ('.
229				'SELECT NULL'.
230				' FROM items i,graphs_items gi,graphs g'.
231				' WHERE i.hostid=h.hostid'.
232					' AND i.itemid=gi.itemid'.
233					' AND gi.graphid=g.graphid'.
234					' AND g.flags IN ('.ZBX_FLAG_DISCOVERY_NORMAL.','.ZBX_FLAG_DISCOVERY_CREATED.')'.
235				')';
236		}
237
238		// with_httptests
239		if (!empty($options['with_httptests'])) {
240			$sqlParts['where'][] = 'EXISTS (SELECT ht.httptestid FROM httptest ht WHERE ht.hostid=h.hostid)';
241		}
242
243		// tags
244		if ($options['tags'] !== null && $options['tags']) {
245			$sqlParts['where'][] = CApiTagHelper::addWhereCondition($options['tags'], $options['evaltype'], 'h',
246				'host_tag', 'hostid'
247			);
248		}
249
250		// filter
251		if (is_array($options['filter'])) {
252			$this->dbFilter('hosts h', $options, $sqlParts);
253		}
254
255		// search
256		if (is_array($options['search'])) {
257			zbx_db_search('hosts h', $options, $sqlParts);
258		}
259
260		// limit
261		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
262			$sqlParts['limit'] = $options['limit'];
263		}
264
265		$sqlParts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
266		$sqlParts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sqlParts);
267		$res = DBselect(self::createSelectQueryFromParts($sqlParts), $sqlParts['limit']);
268		while ($template = DBfetch($res)) {
269			if ($options['countOutput']) {
270				if ($options['groupCount']) {
271					$result[] = $template;
272				}
273				else {
274					$result = $template['rowscount'];
275				}
276			}
277			else {
278				$template['templateid'] = $template['hostid'];
279				// Templates share table with hosts and host prototypes. Therefore remove template unrelated fields.
280				unset($template['hostid'], $template['discover']);
281
282				$result[$template['templateid']] = $template;
283			}
284
285		}
286
287		if ($options['countOutput']) {
288			return $result;
289		}
290
291		if ($result) {
292			$result = $this->addRelatedObjects($options, $result);
293		}
294
295		// removing keys (hash -> array)
296		if (!$options['preservekeys']) {
297			$result = zbx_cleanHashes($result);
298		}
299
300		return $result;
301	}
302
303	/**
304	 * Validates the input parameters for the get() method.
305	 *
306	 * @param array $options
307	 *
308	 * @throws APIException if the input is invalid
309	 */
310	protected function validateGet(array $options) {
311		// Validate input parameters.
312		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
313			'selectValueMaps' =>			['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => 'valuemapid,name,mappings,uuid']
314		]];
315		$options_filter = array_intersect_key($options, $api_input_rules['fields']);
316		if (!CApiInputValidator::validate($api_input_rules, $options_filter, '/', $error)) {
317			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
318		}
319	}
320
321	/**
322	 * Add template.
323	 *
324	 * @param array $templates
325	 *
326	 * @return array
327	 */
328	public function create(array $templates) {
329		$this->validateCreate($templates);
330
331		$ins_templates = [];
332
333		foreach ($templates as &$template) {
334			// If visible name is not given or empty it should be set to host name.
335			if (!array_key_exists('name', $template) || trim($template['name']) === '') {
336				$template['name'] = $template['host'];
337			}
338
339			$ins_templates[] = array_intersect_key($template, array_flip(['uuid', 'host', 'name', 'description']))
340				+ ['status' => HOST_STATUS_TEMPLATE];
341		}
342		unset($template);
343
344		$hosts_groups = [];
345		$hosts_tags = [];
346		$templates_hostids = [];
347		$hostids = [];
348
349		$templateids = DB::insert('hosts', $ins_templates);
350
351		foreach ($templates as $index => &$template) {
352			$template['templateid'] = $templateids[$index];
353
354			foreach (zbx_toArray($template['groups']) as $group) {
355				$hosts_groups[] = [
356					'hostid' => $template['templateid'],
357					'groupid' => $group['groupid']
358				];
359			}
360
361			if (array_key_exists('tags', $template)) {
362				foreach (zbx_toArray($template['tags']) as $tag) {
363					$hosts_tags[] = ['hostid' => $template['templateid']] + $tag;
364				}
365			}
366
367			if (array_key_exists('templates', $template)) {
368				foreach (zbx_toArray($template['templates']) as $link_template) {
369					$templates_hostids[$link_template['templateid']][] = $template['templateid'];
370				}
371			}
372
373			if (array_key_exists('hosts', $template)) {
374				foreach (zbx_toArray($template['hosts']) as $host) {
375					$templates_hostids[$template['templateid']][] = $host['hostid'];
376					$hostids[$host['hostid']] = true;
377				}
378			}
379		}
380		unset($template);
381
382		DB::insertBatch('hosts_groups', $hosts_groups);
383
384		if ($hosts_tags) {
385			DB::insert('host_tag', $hosts_tags);
386		}
387
388		$this->createHostMacros($templates);
389
390		if ($hostids) {
391			$this->checkHostPermissions(array_keys($hostids));
392		}
393
394		while ($templates_hostids) {
395			$templateid = key($templates_hostids);
396			$link_hostids = reset($templates_hostids);
397			$link_templateids = [$templateid];
398			unset($templates_hostids[$templateid]);
399
400			foreach ($templates_hostids as $templateid => $hostids) {
401				if ($link_hostids === $hostids) {
402					$link_templateids[] = $templateid;
403					unset($templates_hostids[$templateid]);
404				}
405			}
406
407			$this->link($link_templateids, $link_hostids);
408		}
409
410		$this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_TEMPLATE, $templates);
411
412		return ['templateids' => array_column($templates, 'templateid')];
413	}
414
415	/**
416	 * Validate create template.
417	 *
418	 * @param array $templates
419	 *
420	 * @throws APIException if the input is invalid.
421	 */
422	protected function validateCreate(array &$templates) {
423		$templates = zbx_toArray($templates);
424
425		$macro_rules = ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['macro']], 'fields' => [
426			'macro' =>			['type' => API_USER_MACRO, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('hostmacro', 'macro')],
427			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT]), 'default' => ZBX_MACRO_TYPE_TEXT],
428			'value' =>			['type' => API_MULTIPLE, 'flags' => API_REQUIRED, 'rules' => [
429									['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET])], 'type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'value')],
430									['if' => ['field' => 'type', 'in' => implode(',', [ZBX_MACRO_TYPE_VAULT])], 'type' => API_VAULT_SECRET, 'length' => DB::getFieldLength('hostmacro', 'value')]
431			]],
432			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'description')]
433		]];
434
435		$groupIds = [];
436
437		foreach ($templates as $index => &$template) {
438			// check if hosts have at least 1 group
439			if (!isset($template['groups']) || !$template['groups']) {
440				self::exception(ZBX_API_ERROR_PARAMETERS,
441					_s('Template "%1$s" cannot be without host group.', $template['host'])
442				);
443			}
444
445			$template['groups'] = zbx_toArray($template['groups']);
446
447			foreach ($template['groups'] as $group) {
448				if (!is_array($group) || (is_array($group) && !array_key_exists('groupid', $group))) {
449					self::exception(ZBX_API_ERROR_PARAMETERS,
450						_s('Incorrect value for field "%1$s": %2$s.', 'groups',
451							_s('the parameter "%1$s" is missing', 'groupid')
452						)
453					);
454				}
455
456				$groupIds[$group['groupid']] = $group['groupid'];
457			}
458
459			if (array_key_exists('macros', $template)) {
460				if (!CApiInputValidator::validate($macro_rules, $template['macros'], '/'.($index + 1).'/macros',
461						$error)) {
462					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
463				}
464			}
465		}
466		unset($template);
467
468		$dbHostGroups = API::HostGroup()->get([
469			'output' => ['groupid'],
470			'groupids' => $groupIds,
471			'editable' => true,
472			'preservekeys' => true
473		]);
474
475		foreach ($groupIds as $groupId) {
476			if (!isset($dbHostGroups[$groupId])) {
477				self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
478			}
479		}
480
481		$templateDbFields = ['host' => null];
482
483		$host_name_parser = new CHostNameParser();
484
485		foreach ($templates as $template) {
486			// if visible name is not given or empty it should be set to host name
487			if ((!isset($template['name']) || zbx_empty(trim($template['name']))) && isset($template['host'])) {
488				$template['name'] = $template['host'];
489			}
490
491			if (!check_db_fields($templateDbFields, $template)) {
492				self::exception(ZBX_API_ERROR_PARAMETERS, _('Field "host" is mandatory.'));
493			}
494
495			// Property 'auto_compress' is not supported for templates.
496			if (array_key_exists('auto_compress', $template)) {
497				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect input parameters.'));
498			}
499
500			if ($host_name_parser->parse($template['host']) != CParser::PARSE_SUCCESS) {
501				self::exception(ZBX_API_ERROR_PARAMETERS,
502					_s('Incorrect characters used for template name "%1$s".', $template['host'])
503				);
504			}
505
506			if (isset($template['host'])) {
507				$templateExists = API::Template()->get([
508					'output' => ['templateid'],
509					'filter' => ['host' => $template['host']],
510					'nopermissions' => true,
511					'limit' => 1
512				]);
513				if ($templateExists) {
514					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Template "%1$s" already exists.', $template['host']));
515				}
516
517				$hostExists = API::Host()->get([
518					'output' => ['hostid'],
519					'filter' => ['host' => $template['host']],
520					'nopermissions' => true,
521					'limit' => 1
522				]);
523				if ($hostExists) {
524					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host "%1$s" already exists.', $template['host']));
525				}
526			}
527
528			if (isset($template['name'])) {
529				$templateExists = API::Template()->get([
530					'output' => ['templateid'],
531					'filter' => ['name' => $template['name']],
532					'nopermissions' => true,
533					'limit' => 1
534				]);
535				if ($templateExists) {
536					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
537						'Template with the same visible name "%1$s" already exists.',
538						$template['name']
539					));
540				}
541
542				$hostExists = API::Host()->get([
543					'output' => ['hostid'],
544					'filter' => ['name' => $template['name']],
545					'nopermissions' => true,
546					'limit' => 1
547				]);
548				if ($hostExists) {
549					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
550						'Host with the same visible name "%1$s" already exists.',
551						$template['name']
552					));
553				}
554			}
555
556			// Validate tags.
557			if (array_key_exists('tags', $template)) {
558				$this->validateTags($template);
559			}
560		}
561
562		$this->checkAndAddUuid($templates);
563	}
564
565	/**
566	 * Check that no duplicate UUID is being added. Add UUID to all templates, if it doesn't exist.
567	 *
568	 * @param array $templates_to_create
569	 *
570	 * @throws APIException
571	 */
572	protected function checkAndAddUuid(array &$templates_to_create): void {
573		foreach ($templates_to_create as &$template) {
574			if (!array_key_exists('uuid', $template)) {
575				$template['uuid'] = generateUuidV4();
576			}
577		}
578		unset($template);
579
580		$db_uuid = DB::select('hosts', [
581			'output' => ['uuid'],
582			'filter' => ['uuid' => array_column($templates_to_create, 'uuid')],
583			'limit' => 1
584		]);
585
586		if ($db_uuid) {
587			self::exception(ZBX_API_ERROR_PARAMETERS,
588				_s('Entry with UUID "%1$s" already exists.', $db_uuid[0]['uuid'])
589			);
590		}
591	}
592
593	/**
594	 * Update template.
595	 *
596	 * @param array $templates
597	 *
598	 * @return array
599	 */
600	public function update(array $templates) {
601		$this->validateUpdate($templates, $db_templates);
602
603		$this->updateHostMacros($templates, $db_templates);
604
605		foreach ($templates as &$template) {
606			unset($template['macros']);
607		}
608		unset($template);
609
610		foreach ($templates as $template) {
611			// if visible name is not given or empty it should be set to host name
612			if ((!isset($template['name']) || zbx_empty(trim($template['name']))) && isset($template['host'])) {
613				$template['name'] = $template['host'];
614			}
615
616			$templateCopy = $template;
617
618			$template['templates_link'] = array_key_exists('templates', $template) ? $template['templates'] : null;
619
620			unset($template['templates'], $template['templateid'], $templateCopy['templates']);
621			$template['templates'] = [$templateCopy];
622
623			if (!$this->massUpdate($template)) {
624				self::exception(ZBX_API_ERROR_PARAMETERS, _('Failed to update template.'));
625			}
626		}
627
628		$this->updateTags(array_column($templates, 'tags', 'templateid'));
629
630		return ['templateids' => array_column($templates, 'templateid')];
631	}
632
633	/**
634	 * Validate update template.
635	 *
636	 * @param array $templates
637	 *
638	 * @throws APIException if the input is invalid.
639	 */
640	protected function validateUpdate(array &$templates, array &$db_templates = null) {
641		$templates = zbx_toArray($templates);
642
643		$macro_rules = ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'uniq' => [['hostmacroid']], 'fields' => [
644			'hostmacroid' =>	['type' => API_ID],
645			'macro' =>			['type' => API_USER_MACRO, 'length' => DB::getFieldLength('hostmacro', 'macro')],
646			'type' =>			['type' => API_INT32, 'in' => implode(',', [ZBX_MACRO_TYPE_TEXT, ZBX_MACRO_TYPE_SECRET, ZBX_MACRO_TYPE_VAULT])],
647			'value' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'value')],
648			'description' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('hostmacro', 'description')]
649		]];
650
651		$db_templates = $this->get([
652			'output' => ['templateid'],
653			'templateids' => zbx_objectValues($templates, 'templateid'),
654			'editable' => true,
655			'preservekeys' => true
656		]);
657
658		foreach ($templates as $index => &$template) {
659			if (!array_key_exists($template['templateid'], $db_templates)) {
660				self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
661			}
662
663			if (array_key_exists('uuid', $template)) {
664				self::exception(ZBX_API_ERROR_PARAMETERS,
665					_s('Invalid parameter "%1$s": %2$s.', '/' . ($index + 1), _s('unexpected parameter "%1$s"', 'uuid'))
666				);
667			}
668
669			// Property 'auto_compress' is not supported for templates.
670			if (array_key_exists('auto_compress', $template)) {
671				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect input parameters.'));
672			}
673
674			// Validate tags.
675			if (array_key_exists('tags', $template)) {
676				$this->validateTags($template);
677			}
678
679			if (array_key_exists('macros', $template)) {
680				if (!CApiInputValidator::validate($macro_rules, $template['macros'], '/'.($index + 1).'/macros',
681						$error)) {
682					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
683				}
684			}
685		}
686		unset($template);
687
688		if (array_column($templates, 'macros')) {
689			$db_templates = $this->getHostMacros($db_templates);
690			$templates = $this->validateHostMacros($templates, $db_templates);
691		}
692	}
693
694	/**
695	 * Delete template.
696	 *
697	 * @param array $templateids
698	 * @param array $templateids['templateids']
699	 *
700	 * @return array
701	 */
702	public function delete(array $templateids) {
703		if (empty($templateids)) {
704			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
705		}
706
707		$db_templates = $this->get([
708			'output' => ['templateid', 'name'],
709			'templateids' => $templateids,
710			'editable' => true,
711			'preservekeys' => true
712		]);
713
714		if (array_diff_key(array_flip($templateids), $db_templates)) {
715			self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
716		}
717
718		API::Template()->unlink($templateids, null, true);
719
720		// delete the discovery rules first
721		$del_rules = API::DiscoveryRule()->get([
722			'output' => [],
723			'hostids' => $templateids,
724			'nopermissions' => true,
725			'preservekeys' => true
726		]);
727		if ($del_rules) {
728			CDiscoveryRuleManager::delete(array_keys($del_rules));
729		}
730
731		// delete the items
732		$del_items = API::Item()->get([
733			'output' => [],
734			'templateids' => $templateids,
735			'nopermissions' => true,
736			'preservekeys' => true
737		]);
738		if ($del_items) {
739			CItemManager::delete(array_keys($del_items));
740		}
741
742		// delete host from maps
743		if (!empty($templateids)) {
744			DB::delete('sysmaps_elements', ['elementtype' => SYSMAP_ELEMENT_TYPE_HOST, 'elementid' => $templateids]);
745		}
746
747		// disable actions
748		// actions from conditions
749		$actionids = [];
750		$sql = 'SELECT DISTINCT actionid'.
751			' FROM conditions'.
752			' WHERE conditiontype='.CONDITION_TYPE_TEMPLATE.
753			' AND '.dbConditionString('value', $templateids);
754		$dbActions = DBselect($sql);
755		while ($dbAction = DBfetch($dbActions)) {
756			$actionids[$dbAction['actionid']] = $dbAction['actionid'];
757		}
758
759		// actions from operations
760		$sql = 'SELECT DISTINCT o.actionid'.
761			' FROM operations o,optemplate ot'.
762			' WHERE o.operationid=ot.operationid'.
763			' AND '.dbConditionInt('ot.templateid', $templateids);
764		$dbActions = DBselect($sql);
765		while ($dbAction = DBfetch($dbActions)) {
766			$actionids[$dbAction['actionid']] = $dbAction['actionid'];
767		}
768
769		if (!empty($actionids)) {
770			DB::update('actions', [
771				'values' => ['status' => ACTION_STATUS_DISABLED],
772				'where' => ['actionid' => $actionids]
773			]);
774		}
775
776		// delete action conditions
777		DB::delete('conditions', [
778			'conditiontype' => CONDITION_TYPE_TEMPLATE,
779			'value' => $templateids
780		]);
781
782		// delete action operation commands
783		$operationids = [];
784		$sql = 'SELECT DISTINCT ot.operationid'.
785			' FROM optemplate ot'.
786			' WHERE '.dbConditionInt('ot.templateid', $templateids);
787		$dbOperations = DBselect($sql);
788		while ($dbOperation = DBfetch($dbOperations)) {
789			$operationids[$dbOperation['operationid']] = $dbOperation['operationid'];
790		}
791
792		DB::delete('optemplate', [
793			'templateid'=>$templateids
794		]);
795
796		// delete empty operations
797		$delOperationids = [];
798		$sql = 'SELECT DISTINCT o.operationid'.
799			' FROM operations o'.
800			' WHERE '.dbConditionInt('o.operationid', $operationids).
801			' AND NOT EXISTS(SELECT NULL FROM optemplate ot WHERE ot.operationid=o.operationid)';
802		$dbOperations = DBselect($sql);
803		while ($dbOperation = DBfetch($dbOperations)) {
804			$delOperationids[$dbOperation['operationid']] = $dbOperation['operationid'];
805		}
806
807		DB::delete('operations', [
808			'operationid'=>$delOperationids
809		]);
810
811		// http tests
812		$delHttpTests = API::HttpTest()->get([
813			'templateids' => $templateids,
814			'output' => ['httptestid'],
815			'nopermissions' => 1,
816			'preservekeys' => true
817		]);
818		if (!empty($delHttpTests)) {
819			API::HttpTest()->delete(array_keys($delHttpTests), true);
820		}
821
822		// Get host prototype operations from LLD overrides where this template is linked.
823		$lld_override_operationids = [];
824
825		$db_lld_override_operationids = DBselect(
826			'SELECT loo.lld_override_operationid'.
827			' FROM lld_override_operation loo'.
828			' WHERE EXISTS('.
829				'SELECT NULL'.
830				' FROM lld_override_optemplate lot'.
831				' WHERE lot.lld_override_operationid=loo.lld_override_operationid'.
832				' AND '.dbConditionId('lot.templateid', $templateids).
833			')'
834		);
835		while ($db_lld_override_operationid = DBfetch($db_lld_override_operationids)) {
836			$lld_override_operationids[] = $db_lld_override_operationid['lld_override_operationid'];
837		}
838
839		if ($lld_override_operationids) {
840			DB::delete('lld_override_optemplate', ['templateid' => $templateids]);
841
842			// Make sure there no other operations left to safely delete the operation.
843			$delete_lld_override_operationids = [];
844
845			$db_delete_lld_override_operationids = DBselect(
846				'SELECT loo.lld_override_operationid'.
847				' FROM lld_override_operation loo'.
848				' WHERE NOT EXISTS ('.
849						'SELECT NULL'.
850						' FROM lld_override_opstatus los'.
851						' WHERE los.lld_override_operationid=loo.lld_override_operationid'.
852					')'.
853					' AND NOT EXISTS ('.
854						'SELECT NULL'.
855						' FROM lld_override_opdiscover lod'.
856						' WHERE lod.lld_override_operationid=loo.lld_override_operationid'.
857					')'.
858					' AND NOT EXISTS ('.
859						'SELECT NULL'.
860						' FROM lld_override_opinventory loi'.
861						' WHERE loi.lld_override_operationid=loo.lld_override_operationid'.
862					')'.
863					' AND NOT EXISTS ('.
864						'SELECT NULL'.
865						' FROM lld_override_optemplate lot'.
866						' WHERE lot.lld_override_operationid=loo.lld_override_operationid'.
867					')'.
868					' AND '.dbConditionId('loo.lld_override_operationid', $lld_override_operationids)
869			);
870
871			while ($db_delete_lld_override_operationid = DBfetch($db_delete_lld_override_operationids)) {
872				$delete_lld_override_operationids[] = $db_delete_lld_override_operationid['lld_override_operationid'];
873			}
874
875			if ($delete_lld_override_operationids) {
876				DB::delete('lld_override_operation', ['lld_override_operationid' => $delete_lld_override_operationids]);
877			}
878		}
879
880		// Finally delete the template.
881		DB::delete('hosts', ['hostid' => $templateids]);
882
883		// TODO: remove info from API
884		foreach ($db_templates as $db_template) {
885			info(_s('Deleted: Template "%1$s".', $db_template['name']));
886		}
887
888		$this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_TEMPLATE, $db_templates);
889
890		return ['templateids' => $templateids];
891	}
892
893	/**
894	 * Additionally allows to link templates to hosts and other templates.
895	 *
896	 * Checks write permissions for templates.
897	 *
898	 * Additional supported $data parameters are:
899	 * - hosts  - an array of hosts or templates to link the given templates to
900	 *
901	 * @param array $data
902	 *
903	 * @return array
904	 */
905	public function massAdd(array $data) {
906		$templates = isset($data['templates']) ? zbx_toArray($data['templates']) : [];
907		$templateIds = zbx_objectValues($templates, 'templateid');
908
909		$this->checkPermissions($templateIds, _('No permissions to referred object or it does not exist!'));
910
911		// link hosts to the given templates
912		if (isset($data['hosts']) && !empty($data['hosts'])) {
913			$hostIds = zbx_objectValues($data['hosts'], 'hostid');
914
915			$this->checkHostPermissions($hostIds);
916
917			// check if any of the hosts are discovered
918			$this->checkValidator($hostIds, new CHostNormalValidator([
919				'message' => _('Cannot update templates on discovered host "%1$s".')
920			]));
921
922			$this->link($templateIds, $hostIds);
923		}
924
925		$data['hosts'] = [];
926
927		return parent::massAdd($data);
928	}
929
930	/**
931	 * Mass update.
932	 *
933	 * @param string $data['host']
934	 * @param string $data['name']
935	 * @param string $data['description']
936	 * @param array  $data['templates']
937	 * @param array  $data['templates_clear']
938	 * @param array  $data['templates_link']
939	 * @param array  $data['groups']
940	 * @param array  $data['hosts']
941	 * @param array  $data['macros']
942	 *
943	 * @return array
944	 */
945	public function massUpdate(array $data) {
946		if (!array_key_exists('templates', $data) || !is_array($data['templates'])) {
947			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Field "%1$s" is mandatory.', 'templates'));
948		}
949
950		$this->validateMassUpdate($data);
951
952		$templates = zbx_toArray($data['templates']);
953		$templateIds = zbx_objectValues($templates, 'templateid');
954
955		$fieldsToUpdate = [];
956
957		if (isset($data['host'])) {
958			$fieldsToUpdate[] = 'host='.zbx_dbstr($data['host']);
959		}
960
961		if (isset($data['name'])) {
962			// if visible name is empty replace it with host name
963			if (zbx_empty(trim($data['name'])) && isset($data['host'])) {
964				$fieldsToUpdate[] = 'name='.zbx_dbstr($data['host']);
965			}
966			// we cannot have empty visible name
967			elseif (zbx_empty(trim($data['name'])) && !isset($data['host'])) {
968				self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot have empty visible template name.'));
969			}
970			else {
971				$fieldsToUpdate[] = 'name='.zbx_dbstr($data['name']);
972			}
973		}
974
975		if (isset($data['description'])) {
976			$fieldsToUpdate[] = 'description='.zbx_dbstr($data['description']);
977		}
978
979		if ($fieldsToUpdate) {
980			DBexecute('UPDATE hosts SET '.implode(', ', $fieldsToUpdate).' WHERE '.dbConditionInt('hostid', $templateIds));
981		}
982
983		$data['templates_clear'] = isset($data['templates_clear']) ? zbx_toArray($data['templates_clear']) : [];
984		$templateIdsClear = zbx_objectValues($data['templates_clear'], 'templateid');
985
986		if ($data['templates_clear']) {
987			$this->massRemove([
988				'templateids' => $templateIds,
989				'templateids_clear' => $templateIdsClear
990			]);
991		}
992
993		// update template linkage
994		// firstly need to unlink all things, to correctly check circulars
995		if (isset($data['hosts']) && $data['hosts'] !== null) {
996			/*
997			 * Get all currently linked hosts and templates (skip discovered hosts) to these templates
998			 * that user has read permissions.
999			 */
1000			$templateHosts = API::Host()->get([
1001				'output' => ['hostid'],
1002				'templateids' => $templateIds,
1003				'templated_hosts' => true,
1004				'filter' => ['flags' => ZBX_FLAG_DISCOVERY_NORMAL]
1005			]);
1006			$templateHostIds = zbx_objectValues($templateHosts, 'hostid');
1007			$newHostIds = zbx_objectValues($data['hosts'], 'hostid');
1008
1009			$hostsToDelete = array_diff($templateHostIds, $newHostIds);
1010			$hostIdsToDelete = array_diff($hostsToDelete, $templateIdsClear);
1011			$hostIdsToAdd = array_diff($newHostIds, $templateHostIds);
1012
1013			if ($hostIdsToDelete) {
1014				$result = $this->massRemove([
1015					'hostids' => $hostIdsToDelete,
1016					'templateids' => $templateIds
1017				]);
1018
1019				if (!$result) {
1020					self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot unlink template.'));
1021				}
1022			}
1023		}
1024
1025		if (isset($data['templates_link']) && $data['templates_link'] !== null) {
1026			$templateTemplates = API::Template()->get([
1027				'output' => ['templateid'],
1028				'hostids' => $templateIds
1029			]);
1030			$templateTemplateIds = zbx_objectValues($templateTemplates, 'templateid');
1031			$newTemplateIds = zbx_objectValues($data['templates_link'], 'templateid');
1032
1033			$templatesToDelete = array_diff($templateTemplateIds, $newTemplateIds);
1034			$templateIdsToDelete = array_diff($templatesToDelete, $templateIdsClear);
1035
1036			if ($templateIdsToDelete) {
1037				$result = $this->massRemove([
1038					'templateids' => $templateIds,
1039					'templateids_link' => $templateIdsToDelete
1040				]);
1041
1042				if (!$result) {
1043					self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot unlink template.'));
1044				}
1045			}
1046		}
1047
1048		if (isset($data['hosts']) && $data['hosts'] !== null && $hostIdsToAdd) {
1049			$result = $this->massAdd([
1050				'templates' => $templates,
1051				'hosts' => $hostIdsToAdd
1052			]);
1053
1054			if (!$result) {
1055				self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot link template.'));
1056			}
1057		}
1058
1059		if (isset($data['templates_link']) && $data['templates_link'] !== null) {
1060			$templatesToAdd = array_diff($newTemplateIds, $templateTemplateIds);
1061
1062			if ($templatesToAdd) {
1063				$result = $this->massAdd([
1064					'templates' => $templates,
1065					'templates_link' => $templatesToAdd
1066				]);
1067
1068				if (!$result) {
1069					self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot link template.'));
1070				}
1071			}
1072		}
1073
1074		// macros
1075		if (isset($data['macros'])) {
1076			DB::delete('hostmacro', ['hostid' => $templateIds]);
1077
1078			$this->massAdd([
1079				'templates' => $templates,
1080				'macros' => $data['macros']
1081			]);
1082		}
1083
1084		/*
1085		 * Update template and host group linkage. This procedure should be done the last because user can unlink
1086		 * him self from a group with write permissions leaving only read premissions. Thus other procedures, like
1087		 * host-template linking, macros update, must be done before this.
1088		 */
1089		if (isset($data['groups']) && $data['groups'] !== null && is_array($data['groups'])) {
1090			$updateGroups = zbx_toArray($data['groups']);
1091
1092			$templateGroups = API::HostGroup()->get([
1093				'output' => ['groupid'],
1094				'templateids' => $templateIds
1095			]);
1096			$templateGroupIds = zbx_objectValues($templateGroups, 'groupid');
1097			$newGroupIds = zbx_objectValues($updateGroups, 'groupid');
1098
1099			$groupsToAdd = array_diff($newGroupIds, $templateGroupIds);
1100			if ($groupsToAdd) {
1101				$this->massAdd([
1102					'templates' => $templates,
1103					'groups' => zbx_toObject($groupsToAdd, 'groupid')
1104				]);
1105			}
1106
1107			$groupIdsToDelete = array_diff($templateGroupIds, $newGroupIds);
1108			if ($groupIdsToDelete) {
1109				$this->massRemove([
1110					'templateids' => $templateIds,
1111					'groupids' => $groupIdsToDelete
1112				]);
1113			}
1114		}
1115
1116		return ['templateids' => $templateIds];
1117	}
1118
1119	/**
1120	 * Validate mass update.
1121	 *
1122	 * @param string $data['host']
1123	 * @param string $data['name']
1124	 * @param array  $data['templates']
1125	 * @param array  $data['groups']
1126	 * @param array  $data['hosts']
1127	 *
1128	 * @return array
1129	 */
1130	protected function validateMassUpdate(array $data) {
1131		$templates = zbx_toArray($data['templates']);
1132
1133		$dbTemplates = $this->get([
1134			'output' => ['templateid', 'host'],
1135			'templateids' => zbx_objectValues($templates, 'templateid'),
1136			'editable' => true,
1137			'preservekeys' => true
1138		]);
1139
1140		// check permissions
1141		foreach ($templates as $template) {
1142			if (!isset($dbTemplates[$template['templateid']])) {
1143				self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.'));
1144			}
1145		}
1146
1147		if (array_key_exists('groups', $data) && !$data['groups'] && $dbTemplates) {
1148			$template = reset($dbTemplates);
1149
1150			self::exception(ZBX_API_ERROR_PARAMETERS,
1151				_s('Template "%1$s" cannot be without host group.', $template['host'])
1152			);
1153		}
1154
1155		// check name
1156		if (isset($data['name'])) {
1157			if (count($templates) > 1) {
1158				self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot mass update visible template name.'));
1159			}
1160
1161			$template = reset($templates);
1162
1163			$templateExists = $this->get([
1164				'output' => ['templateid'],
1165				'filter' => ['name' => $data['name']],
1166				'nopermissions' => true
1167			]);
1168			$templateExist = reset($templateExists);
1169			if ($templateExist && bccomp($templateExist['templateid'], $template['templateid']) != 0) {
1170				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1171					'Template with the same visible name "%1$s" already exists.',
1172					$data['name']
1173				));
1174			}
1175
1176			// can't set the same name as existing host
1177			$hostExists = API::Host()->get([
1178				'output' => ['hostid'],
1179				'filter' => ['name' => $data['name']],
1180				'nopermissions' => true
1181			]);
1182			if ($hostExists) {
1183				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1184					'Host with the same visible name "%1$s" already exists.',
1185					$data['name']
1186				));
1187			}
1188		}
1189
1190		// check host
1191		if (isset($data['host'])) {
1192			if (count($templates) > 1) {
1193				self::exception(ZBX_API_ERROR_PARAMETERS, _('Cannot mass update template name.'));
1194			}
1195
1196			$template = reset($templates);
1197
1198			$templateExists = $this->get([
1199				'output' => ['templateid'],
1200				'filter' => ['host' => $data['host']],
1201				'nopermissions' => true
1202			]);
1203			$templateExist = reset($templateExists);
1204			if ($templateExist && bccomp($templateExist['templateid'], $template['templateid']) != 0) {
1205				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1206					'Template with the same name "%1$s" already exists.',
1207					$template['host']
1208				));
1209			}
1210
1211			// can't set the same name as existing host
1212			$hostExists = API::Host()->get([
1213				'output' => ['hostid'],
1214				'filter' => ['host' => $template['host']],
1215				'nopermissions' => true
1216			]);
1217			if ($hostExists) {
1218				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1219					'Host with the same name "%1$s" already exists.',
1220					$template['host']
1221				));
1222			}
1223		}
1224
1225		$host_name_parser = new CHostNameParser();
1226
1227		if (array_key_exists('host', $data) && $host_name_parser->parse($data['host']) != CParser::PARSE_SUCCESS) {
1228			self::exception(ZBX_API_ERROR_PARAMETERS,
1229				_s('Incorrect characters used for template name "%1$s".', $data['host'])
1230			);
1231		}
1232	}
1233
1234	/**
1235	 * Additionally allows to unlink templates from hosts and other templates.
1236	 *
1237	 * Checks write permissions for templates.
1238	 *
1239	 * Additional supported $data parameters are:
1240	 * - hostids  - an array of host or template IDs to unlink the given templates from
1241	 *
1242	 * @param array $data
1243	 *
1244	 * @return array
1245	 */
1246	public function massRemove(array $data) {
1247		$templateids = zbx_toArray($data['templateids']);
1248
1249		$this->checkPermissions($templateids, _('You do not have permission to perform this operation.'));
1250
1251		if (isset($data['hostids'])) {
1252			// check if any of the hosts are discovered
1253			$this->checkValidator($data['hostids'], new CHostNormalValidator([
1254				'message' => _('Cannot update templates on discovered host "%1$s".')
1255			]));
1256
1257			API::Template()->unlink($templateids, zbx_toArray($data['hostids']));
1258		}
1259
1260		$data['hostids'] = [];
1261
1262		return parent::massRemove($data);
1263	}
1264
1265	/**
1266	 * Check if user has write permissions for templates.
1267	 *
1268	 * @param array  $templateids
1269	 * @param string $error
1270	 *
1271	 * @return bool
1272	 */
1273	private function checkPermissions(array $templateids, $error) {
1274		if ($templateids) {
1275			$templateids = array_unique($templateids);
1276
1277			$count = $this->get([
1278				'countOutput' => true,
1279				'templateids' => $templateids,
1280				'editable' => true
1281			]);
1282
1283			if ($count != count($templateids)) {
1284				self::exception(ZBX_API_ERROR_PERMISSIONS, $error);
1285			}
1286		}
1287	}
1288
1289	protected function addRelatedObjects(array $options, array $result) {
1290		$result = parent::addRelatedObjects($options, $result);
1291
1292		$templateids = array_keys($result);
1293
1294		// Adding Templates
1295		if ($options['selectTemplates'] !== null) {
1296			if ($options['selectTemplates'] != API_OUTPUT_COUNT) {
1297				$templates = [];
1298				$relationMap = $this->createRelationMap($result, 'templateid', 'hostid', 'hosts_templates');
1299				$related_ids = $relationMap->getRelatedIds();
1300
1301				if ($related_ids) {
1302					$templates = API::Template()->get([
1303						'output' => $options['selectTemplates'],
1304						'templateids' => $related_ids,
1305						'preservekeys' => true
1306					]);
1307					if (!is_null($options['limitSelects'])) {
1308						order_result($templates, 'host');
1309					}
1310				}
1311
1312				$result = $relationMap->mapMany($result, $templates, 'templates', $options['limitSelects']);
1313			}
1314			else {
1315				$templates = API::Template()->get([
1316					'parentTemplateids' => $templateids,
1317					'countOutput' => true,
1318					'groupCount' => true
1319				]);
1320				$templates = zbx_toHash($templates, 'templateid');
1321				foreach ($result as $templateid => $template) {
1322					$result[$templateid]['templates'] = array_key_exists($templateid, $templates)
1323						? $templates[$templateid]['rowscount']
1324						: '0';
1325				}
1326			}
1327		}
1328
1329		// Adding Hosts
1330		if ($options['selectHosts'] !== null) {
1331			if ($options['selectHosts'] != API_OUTPUT_COUNT) {
1332				$hosts = [];
1333				$relationMap = $this->createRelationMap($result, 'templateid', 'hostid', 'hosts_templates');
1334				$related_ids = $relationMap->getRelatedIds();
1335
1336				if ($related_ids) {
1337					$hosts = API::Host()->get([
1338						'output' => $options['selectHosts'],
1339						'hostids' => $related_ids,
1340						'preservekeys' => true
1341					]);
1342					if (!is_null($options['limitSelects'])) {
1343						order_result($hosts, 'host');
1344					}
1345				}
1346
1347				$result = $relationMap->mapMany($result, $hosts, 'hosts', $options['limitSelects']);
1348			}
1349			else {
1350				$hosts = API::Host()->get([
1351					'templateids' => $templateids,
1352					'countOutput' => true,
1353					'groupCount' => true
1354				]);
1355				$hosts = zbx_toHash($hosts, 'templateid');
1356				foreach ($result as $templateid => $template) {
1357					$result[$templateid]['hosts'] = array_key_exists($templateid, $hosts)
1358						? $hosts[$templateid]['rowscount']
1359						: '0';
1360				}
1361			}
1362		}
1363
1364		// Adding dashboards.
1365		if ($options['selectDashboards'] !== null) {
1366			if ($options['selectDashboards'] != API_OUTPUT_COUNT) {
1367				$dashboards = API::TemplateDashboard()->get([
1368					'output' => $this->outputExtend($options['selectDashboards'], ['templateid']),
1369					'templateids' => $templateids
1370				]);
1371				if (!is_null($options['limitSelects'])) {
1372					order_result($dashboards, 'name');
1373				}
1374
1375				// Build relation map.
1376				$relationMap = new CRelationMap();
1377				foreach ($dashboards as $key => $dashboard) {
1378					$relationMap->addRelation($dashboard['templateid'], $key);
1379				}
1380
1381				$dashboards = $this->unsetExtraFields($dashboards, ['templateid'], $options['selectDashboards']);
1382				$result = $relationMap->mapMany($result, $dashboards, 'dashboards', $options['limitSelects']);
1383			}
1384			else {
1385				$dashboards = API::TemplateDashboard()->get([
1386					'templateids' => $templateids,
1387					'countOutput' => true,
1388					'groupCount' => true
1389				]);
1390				$dashboards = zbx_toHash($dashboards, 'templateid');
1391				foreach ($result as $templateid => $template) {
1392					$result[$templateid]['dashboards'] = array_key_exists($templateid, $dashboards)
1393						? $dashboards[$templateid]['rowscount']
1394						: '0';
1395				}
1396			}
1397		}
1398
1399		return $result;
1400	}
1401}
1402