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