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