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