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
22class CTemplateImporter extends CImporter {
23
24	/**
25	 * @var array		a list of template IDs which were created or updated
26	 */
27	protected $processedTemplateIds = [];
28
29	/**
30	 * Import templates.
31	 *
32	 * @throws Exception
33	 *
34	 * @param array $templates
35	 */
36	public function import(array $templates) {
37		$templates = zbx_toHash($templates, 'host');
38
39		$this->checkCircularTemplateReferences($templates);
40
41		if (!$this->options['templateLinkage']['createMissing']
42				&& !$this->options['templateLinkage']['deleteMissing']) {
43			foreach ($templates as $name => $template) {
44				unset($templates[$name]['templates']);
45			}
46		}
47
48		do {
49			$independentTemplates = $this->getIndependentTemplates($templates);
50
51			$templatesToCreate = [];
52			$templatesToUpdate = [];
53			$templateLinkage = [];
54			$tmpls_to_clear = [];
55
56			foreach ($independentTemplates as $name) {
57				$template = $templates[$name];
58				unset($templates[$name]);
59
60				$template = $this->resolveTemplateReferences($template);
61
62				/*
63				 * Save linked templates for 2 purposes:
64				 *  - save linkages to add in case if 'create new' linkages is checked;
65				 *  - calculate missing linkages in case if 'delete missing' is checked.
66				 */
67				if (!empty($template['templates'])) {
68					$templateLinkage[$template['host']] = $template['templates'];
69				}
70				unset($template['templates']);
71
72				if (array_key_exists('templateid', $template) && ($this->options['templates']['updateExisting']
73						|| $this->options['process_templates'])) {
74					$templatesToUpdate[] = $template;
75				}
76				else if ($this->options['templates']['createMissing']) {
77					if (array_key_exists('templateid', $template)) {
78						throw new Exception(_s('Template "%1$s" already exists.', $name));
79					}
80
81					$templatesToCreate[] = $template;
82				}
83			}
84
85			if ($this->options['templates']['createMissing'] && $templatesToCreate) {
86				$newTemplateIds = API::Template()->create($templatesToCreate);
87
88				foreach ($templatesToCreate as $num => $createdTemplate) {
89					$templateId = $newTemplateIds['templateids'][$num];
90
91					$this->referencer->addTemplateRef($createdTemplate['host'], $templateId);
92					$this->processedTemplateIds[$templateId] = $templateId;
93
94					if ($this->options['templateLinkage']['createMissing']
95							&& !empty($templateLinkage[$createdTemplate['host']])) {
96						API::Template()->massAdd([
97							'templates' => ['templateid' => $templateId],
98							'templates_link' => $templateLinkage[$createdTemplate['host']]
99						]);
100					}
101				}
102			}
103
104			if ($templatesToUpdate) {
105
106				// Get template linkages to unlink and clear.
107				if ($this->options['templateLinkage']['deleteMissing']) {
108					// Get already linked templates.
109					$db_template_links = API::Template()->get([
110						'output' => ['templateid'],
111						'selectParentTemplates' => ['templateid'],
112						'templateids' => zbx_objectValues($templatesToUpdate, 'templateid'),
113						'preservekeys' => true
114					]);
115
116					foreach ($db_template_links as &$db_template_link) {
117						$db_template_link = zbx_objectValues($db_template_link['parentTemplates'], 'templateid');
118					}
119					unset($db_template_link);
120
121					foreach ($templatesToUpdate as $tmpl) {
122						if (array_key_exists($tmpl['host'], $templateLinkage)) {
123							$tmpls_to_clear[$tmpl['templateid']] = array_diff($db_template_links[$tmpl['templateid']],
124								zbx_objectValues($templateLinkage[$tmpl['host']], 'templateid')
125							);
126						}
127						else {
128							$tmpls_to_clear[$tmpl['templateid']] = $db_template_links[$tmpl['templateid']];
129						}
130					}
131				}
132
133				if ($this->options['templates']['updateExisting']) {
134					API::Template()->update($templatesToUpdate);
135				}
136
137				foreach ($templatesToUpdate as $updatedTemplate) {
138					$this->processedTemplateIds[$updatedTemplate['templateid']] = $updatedTemplate['templateid'];
139
140					// Drop existing template linkages if 'delete missing' is selected.
141					if (array_key_exists($updatedTemplate['templateid'], $tmpls_to_clear)
142							&& $tmpls_to_clear[$updatedTemplate['templateid']]) {
143						API::Template()->massRemove([
144							'templateids' => [$updatedTemplate['templateid']],
145							'templateids_clear' => $tmpls_to_clear[$updatedTemplate['templateid']]
146						]);
147					}
148
149					// Make new template linkages.
150					if ($this->options['templateLinkage']['createMissing']
151							&& !empty($templateLinkage[$updatedTemplate['host']])) {
152						API::Template()->massAdd([
153							'templates' => $updatedTemplate,
154							'templates_link' => $templateLinkage[$updatedTemplate['host']]
155						]);
156					}
157				}
158			}
159		} while (!empty($independentTemplates));
160
161		// if there are templates left in $templates, then they have unresolved references
162		foreach ($templates as $template) {
163			$unresolvedReferences = [];
164			foreach ($template['templates'] as $linkedTemplate) {
165				if (!$this->referencer->resolveTemplate($linkedTemplate['name'])) {
166					$unresolvedReferences[] = $linkedTemplate['name'];
167				}
168			}
169			throw new Exception(_n('Cannot import template "%1$s", linked template "%2$s" does not exist.',
170				'Cannot import template "%1$s", linked templates "%2$s" do not exist.',
171				$template['host'], implode(', ', $unresolvedReferences), count($unresolvedReferences)));
172		}
173	}
174
175	/**
176	 * Get a list of created or updated template IDs.
177	 *
178	 * @return array
179	 */
180	public function getProcessedTemplateIds() {
181		return $this->processedTemplateIds;
182	}
183
184	/**
185	 * Check if templates have circular references.
186	 *
187	 * @throws Exception
188	 * @see checkCircularRecursive
189	 *
190	 * @param array $templates
191	 */
192	protected function checkCircularTemplateReferences(array $templates) {
193		foreach ($templates as $name => $template) {
194			if (empty($template['templates'])) {
195				continue;
196			}
197
198			foreach ($template['templates'] as $linkedTemplate) {
199				$checked = [$name];
200				if ($circTemplates = $this->checkCircularRecursive($linkedTemplate, $templates, $checked)) {
201					throw new Exception(_s('Circular reference in templates: %1$s.', implode(' - ', $circTemplates)));
202				}
203			}
204		}
205	}
206
207	/**
208	 * Recursive function for searching for circular template references.
209	 * If circular reference exist it return array with template names with circular reference.
210	 *
211	 * @param array $linkedTemplate template element to inspect on current recursive loop
212	 * @param array $templates      all templates where circular references should be searched
213	 * @param array $checked        template names that already were processed,
214	 *                              should contain unique values if no circular references exist
215	 *
216	 * @return array|bool
217	 */
218	protected function checkCircularRecursive(array $linkedTemplate, array $templates, array $checked) {
219		$linkedTemplateName = $linkedTemplate['name'];
220
221		// if current element map name is already in list of checked map names,
222		// circular reference exists
223		if (in_array($linkedTemplateName, $checked)) {
224			// to have nice result containing only maps that have circular reference,
225			// remove everything that was added before repeated map name
226			$checked = array_slice($checked, array_search($linkedTemplateName, $checked));
227			// add repeated name to have nice loop like m1->m2->m3->m1
228			$checked[] = $linkedTemplateName;
229			return $checked;
230		}
231		else {
232			$checked[] = $linkedTemplateName;
233		}
234
235		// we need to find map that current element reference to
236		// and if it has selements check all them recursively
237		if (!empty($templates[$linkedTemplateName]['templates'])) {
238			foreach ($templates[$linkedTemplateName]['templates'] as $tpl) {
239				return $this->checkCircularRecursive($tpl, $templates, $checked);
240			}
241		}
242
243		return false;
244	}
245
246	/**
247	 * Get templates that don't have not existing linked templates i.e. all templates that must be linked to these templates exist.
248	 * Returns array with template names (host).
249	 *
250	 * @param array $templates
251	 *
252	 * @return array
253	 */
254	protected function getIndependentTemplates(array $templates) {
255		foreach ($templates as $num => $template) {
256			if (empty($template['templates'])) {
257				continue;
258			}
259
260			foreach ($template['templates'] as $linkedTpl) {
261				if (!$this->referencer->resolveTemplate($linkedTpl['name'])) {
262					unset($templates[$num]);
263					continue 2;
264				}
265			}
266		}
267
268		return zbx_objectValues($templates, 'host');
269	}
270
271	/**
272	 * Change all references in template to database ids.
273	 *
274	 * @throws Exception
275	 *
276	 * @param array $template
277	 *
278	 * @return array
279	 */
280	protected function resolveTemplateReferences(array $template) {
281		if ($templateId = $this->referencer->resolveTemplate($template['host'])) {
282			$template['templateid'] = $templateId;
283
284			// if we update template, existing macros should have hostmacroid
285			if (array_key_exists('macros', $template)) {
286				foreach ($template['macros'] as &$macro) {
287					if ($hostMacroId = $this->referencer->resolveMacro($templateId, $macro['macro'])) {
288						$macro['hostmacroid'] = $hostMacroId;
289					}
290				}
291				unset($macro);
292			}
293		}
294
295		foreach ($template['groups'] as $gnum => $group) {
296			if (!$this->referencer->resolveGroup($group['name'])) {
297				throw new Exception(_s('Group "%1$s" does not exist.', $group['name']));
298			}
299			$template['groups'][$gnum] = ['groupid' => $this->referencer->resolveGroup($group['name'])];
300		}
301
302		if (isset($template['templates'])) {
303			foreach ($template['templates'] as $tnum => $parentTemplate) {
304				$template['templates'][$tnum] = [
305					'templateid' => $this->referencer->resolveTemplate($parentTemplate['name'])
306				];
307			}
308		}
309
310		return $template;
311	}
312}
313