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 to perform low level application related actions.
24 */
25class CApplicationManager {
26
27	/**
28	 * Create new applications.
29	 *
30	 * @param array $applications
31	 *
32	 * @return array
33	 */
34	public function create(array $applications) {
35		$insertApplications = $applications;
36		foreach ($insertApplications as &$app) {
37			unset($app['applicationTemplates']);
38		}
39		unset($app);
40
41		$applicationids = DB::insertBatch('applications', $insertApplications);
42
43		$applicationTemplates = [];
44		foreach ($applications as $anum => &$application) {
45			$application['applicationid'] = $applicationids[$anum];
46
47			if (isset($application['applicationTemplates'])) {
48				foreach ($application['applicationTemplates'] as $applicationTemplate) {
49					$applicationTemplates[] = [
50						'applicationid' => $application['applicationid'],
51						'templateid' => $applicationTemplate['templateid']
52					];
53				}
54			}
55		}
56		unset($application);
57
58		// link inherited apps
59		DB::insertBatch('application_template', $applicationTemplates);
60
61		return $applications;
62	}
63
64	/**
65	 * Update applications.
66	 *
67	 * @param array $applications
68	 *
69	 * @return array
70	 */
71	public function update(array $applications) {
72		$update = [];
73		$applicationTemplates = [];
74		foreach ($applications as $application) {
75			if (isset($application['applicationTemplates'])) {
76				foreach ($application['applicationTemplates'] as $applicationTemplate) {
77					$applicationTemplates[] = $applicationTemplate;
78				}
79				unset($application['applicationTemplates']);
80			}
81
82			$update[] = [
83				'values' => $application,
84				'where' => ['applicationid' => $application['applicationid']]
85			];
86		}
87		DB::update('applications', $update);
88
89		// replace existing application templates
90		if ($applicationTemplates) {
91			$dbApplicationTemplates = DBfetchArray(DBselect(
92				'SELECT * '.
93				' FROM application_template at'.
94				' WHERE '.dbConditionInt('at.applicationid', zbx_objectValues($applications, 'applicationid'))
95			));
96			DB::replace('application_template', $dbApplicationTemplates, $applicationTemplates);
97		}
98
99		return $applications;
100	}
101
102	/**
103	 * Link applications in template to hosts.
104	 *
105	 * @param $templateId
106	 * @param $hostIds
107	 *
108	 * @return bool
109	 */
110	public function link($templateId, $hostIds) {
111		$hostIds = zbx_toArray($hostIds);
112
113		// fetch template applications
114		$applications = DBfetchArray(DBselect(
115			'SELECT a.applicationid,a.name,a.hostid'.
116			' FROM applications a'.
117			' WHERE a.hostid='.zbx_dbstr($templateId)
118		));
119
120		$this->inherit($applications, $hostIds);
121
122		return true;
123	}
124
125	/**
126	 * Inherit passed applications to hosts.
127	 * If $hostIds is empty that means that we need to inherit all $applications to hosts which are linked to templates
128	 * where $applications belong.
129	 *
130	 * Usual use case is:
131	 *   inherit is called with some $hostIds passed
132	 *   new applications are created/updated
133	 *   inherit is called again with created/updated applications but empty $hostIds
134	 *   if any of new applications belongs to template, inherit it to all hosts linked to that template
135	 *
136	 * @param array $applications
137	 * @param array $hostIds
138	 *
139	 * @return bool
140	 */
141	public function inherit(array $applications, array $hostIds = []) {
142		$hostTemplateMap = $this->getChildHostsFromApplications($applications, $hostIds);
143		if (empty($hostTemplateMap)) {
144			return true;
145		}
146
147		$hostApps = $this->getApplicationMapsByHostIds(array_keys($hostTemplateMap));
148		$preparedApps = $this->prepareInheritedApps($applications, $hostTemplateMap, $hostApps);
149		$inheritedApps = $this->save($preparedApps);
150
151		$applications = zbx_toHash($applications, 'applicationid');
152
153		// update application linkage
154		$oldApplicationTemplateIds = [];
155		$movedAppTemplateIds = [];
156		$childAppIdsPairs = [];
157		$oldChildApps = [];
158		foreach ($inheritedApps as $newChildApp) {
159			$oldChildAppsByTemplateId = $hostApps[$newChildApp['hostid']]['byTemplateId'];
160
161			foreach ($newChildApp['applicationTemplates'] as $applicationTemplate) {
162				// check if the parent of this application had a different child on the same host
163				if (isset($oldChildAppsByTemplateId[$applicationTemplate['templateid']])
164						&& $oldChildAppsByTemplateId[$applicationTemplate['templateid']]['applicationid'] != $newChildApp['applicationid']) {
165
166					// if a different child existed, find the template-application link and remove it later
167					$oldChildApp = $oldChildAppsByTemplateId[$applicationTemplate['templateid']];
168					$oldApplicationTemplates = zbx_toHash($oldChildApp['applicationTemplates'], 'templateid');
169					$oldApplicationTemplateIds[] = $oldApplicationTemplates[$applicationTemplate['templateid']]['application_templateid'];
170
171					// save the IDs of the affected templates and old
172					if (isset($applications[$applicationTemplate['templateid']])) {
173						$movedAppTemplateIds[] = $applications[$applicationTemplate['templateid']]['hostid'];
174						$childAppIdsPairs[$oldChildApp['applicationid']] =  $newChildApp['applicationid'];
175					}
176
177					$oldChildApps[] = $oldChildApp;
178				}
179			}
180		}
181
182		// move all items and web scenarios from the old app to the new
183		if ($childAppIdsPairs) {
184			$this->moveInheritedItems($movedAppTemplateIds, $childAppIdsPairs);
185			$this->moveInheritedHttpTests($movedAppTemplateIds, $childAppIdsPairs);
186		}
187
188		// delete old application links
189		if ($oldApplicationTemplateIds) {
190			DB::delete('application_template', [
191				'application_templateid' => $oldApplicationTemplateIds
192			]);
193		}
194
195		// delete old children that have only one parent
196		$delAppIds = [];
197		foreach ($oldChildApps as $app) {
198			if (count($app['applicationTemplates']) == 1) {
199				$delAppIds[] = $app['applicationid'];
200			}
201		}
202		if ($delAppIds && $emptyIds = $this->fetchEmptyIds($delAppIds)) {
203			$this->delete($emptyIds);
204		}
205
206		$this->inherit($inheritedApps);
207
208		return true;
209	}
210
211	/**
212	 * Replaces applications for all items inherited from templates $templateIds according to the map given in
213	 * $appIdPairs.
214	 *
215	 * @param array $templateIds
216	 * @param array $appIdPairs		an array of source application ID - target application ID pairs
217	 */
218	protected function moveInheritedItems(array $templateIds, array $appIdPairs) {
219		// fetch existing item application links for all items inherited from template $templateIds
220		$itemApps = DBfetchArray(DBselect(
221			'SELECT ia2.itemappid,ia2.applicationid,ia2.itemid'.
222			' FROM items i,items i2,items_applications ia2'.
223			' WHERE i.itemid=i2.templateid'.
224				' AND i2.itemid=ia2.itemid'.
225				' AND '.dbConditionInt('i.hostid', $templateIds).
226				' AND '.dbConditionInt('ia2.applicationid', array_keys($appIdPairs))
227		));
228
229		// find item application links to target applications that may already exist
230		$query = DBselect(
231			'SELECT ia.itemid,ia.applicationid'.
232			' FROM items_applications ia'.
233			' WHERE '.dbConditionInt('ia.applicationid', $appIdPairs).
234				' AND '.dbConditionInt('ia.itemid', zbx_objectValues($itemApps, 'itemid'))
235		);
236		$exItemAppIds = [];
237		while ($row = DBfetch($query)) {
238			$exItemAppIds[$row['itemid']][$row['applicationid']] = $row['applicationid'];
239		}
240
241		$newAppItems = [];
242		$delAppItemIds = [];
243		foreach ($itemApps as $itemApp) {
244			// if no link to the target app exists, add a new one
245			if (!isset($exItemAppIds[$itemApp['itemid']][$appIdPairs[$itemApp['applicationid']]])) {
246				$newAppItems[$appIdPairs[$itemApp['applicationid']]][] = $itemApp['itemappid'];
247			}
248			// if the link to the target app already exists, delete the link to the old app
249			else {
250				$delAppItemIds[] = $itemApp['itemappid'];
251			}
252		}
253
254		// link the items to the new apps
255		foreach ($newAppItems as $targetAppId => $itemAppIds) {
256			DB::updateByPk('items_applications', $itemAppIds, [
257				'applicationid' => $targetAppId
258			]);
259		}
260
261		// delete old item application links
262		if ($delAppItemIds) {
263			DB::delete('items_applications', ['itemappid' => $delAppItemIds]);
264		}
265	}
266
267	/**
268	 * Return IDs of applications that are not used by items or HTTP tests.
269	 *
270	 * @param array $applicationIds
271	 *
272	 * @return array
273	 */
274	public function fetchEmptyIds(array $applicationIds) {
275		return DBfetchColumn(DBselect(
276			'SELECT a.applicationid '.
277			' FROM applications a'.
278			' WHERE '.dbConditionInt('a.applicationid', $applicationIds).
279				' AND NOT EXISTS (SELECT NULL FROM items_applications ia WHERE a.applicationid=ia.applicationid)'.
280				' AND NOT EXISTS (SELECT NULL FROM httptest ht WHERE a.applicationid=ht.applicationid)'
281		), 'applicationid');
282	}
283
284	/**
285	 * Return IDs of applications that are children only (!) of the given parents.
286	 *
287	 * @param array $parentApplicationIds
288	 *
289	 * @return array
290	 */
291	public function fetchExclusiveChildIds(array $parentApplicationIds) {
292		return DBfetchColumn(DBselect(
293			'SELECT at.applicationid '.
294			' FROM application_template at'.
295			' WHERE '.dbConditionInt('at.templateid', $parentApplicationIds).
296				' AND NOT EXISTS (SELECT NULL FROM application_template at2 WHERE '.
297					' at.applicationid=at2.applicationid'.
298					' AND '.dbConditionInt('at2.templateid', $parentApplicationIds, true).
299				')'
300		), 'applicationid');
301	}
302
303	/**
304	 * Delete applications.
305	 *
306	 * @param array $applicationIds
307	 */
308	public function delete(array $applicationIds) {
309		// unset applications from http tests
310		DB::update('httptest', [
311			'values' => ['applicationid' => null],
312			'where' => ['applicationid' => $applicationIds]
313		]);
314
315		// remove Monitoring > Latest data toggle profile values related to given applications
316		DB::delete('profiles', ['idx' => 'web.latest.toggle', 'idx2' => $applicationIds]);
317
318		DB::delete('applications', ['applicationid' => $applicationIds]);
319	}
320
321	/**
322	 * Replaces the applications for all http tests inherited from templates $templateIds according to the map given in
323	 * $appIdPairs.
324	 *
325	 * @param array $templateIds
326	 * @param array $appIdPairs		an array of source application ID - target application ID pairs
327	 */
328	protected function moveInheritedHttpTests(array $templateIds, array $appIdPairs) {
329		// find all http tests inherited from the given templates and linked to the given applications
330		$query = DBselect(
331			'SELECT ht2.applicationid,ht2.httptestid'.
332			' FROM httptest ht,httptest ht2'.
333			' WHERE ht.httptestid=ht2.templateid'.
334				' AND '.dbConditionInt('ht.hostid', $templateIds).
335				' AND '.dbConditionInt('ht2.applicationid', array_keys($appIdPairs))
336		);
337		$targetAppHttpTestIds = [];
338		while ($row = DBfetch($query)) {
339			$targetAppHttpTestIds[$appIdPairs[$row['applicationid']]][] = $row['httptestid'];
340		}
341
342		// link the http test to the new apps
343		foreach ($targetAppHttpTestIds as $targetAppId => $httpTestIds) {
344			DB::updateByPk('httptest', $httpTestIds, [
345				'applicationid' => $targetAppId
346			]);
347		}
348	}
349
350	/**
351	 * Get array with hosts that are linked with templates which passed applications belongs to as key and
352	 * templateid that host is linked to as value. If second parameter $hostIds is not empty, result should contain
353	 * only passed host IDs.
354	 *
355	 * Example:
356	 * We have template T1 with application A1 and template T1 with application A2 both linked to hosts H1 and H2.
357	 * When we pass A1 to this function it should return array like:
358	 *     array(H1_id => array(T1_id, T2_id), H2_id => array(T1_id, T2_id));
359	 *
360	 * @param array $applications
361	 * @param array $hostIds
362	 *
363	 * @return array
364	 */
365	protected function getChildHostsFromApplications(array $applications, array $hostIds = []) {
366		$hostsTemplatesMap = [];
367
368		$dbCursor = DBselect(
369			'SELECT ht.templateid,ht.hostid'.
370			' FROM hosts_templates ht'.
371			' WHERE '.dbConditionInt('ht.templateid', zbx_objectValues($applications, 'hostid')).
372				($hostIds ? ' AND '.dbConditionInt('ht.hostid', $hostIds) : '')
373		);
374		while ($dbHost = DBfetch($dbCursor)) {
375			$hostId = $dbHost['hostid'];
376			$templateId = $dbHost['templateid'];
377
378			if (!isset($hostsTemplatesMap[$hostId])) {
379				$hostsTemplatesMap[$hostId] = [];
380			}
381			$hostsTemplatesMap[$hostId][$templateId] = $templateId;
382		}
383
384		return $hostsTemplatesMap;
385	}
386
387	/**
388	 * Generate application data for inheritance. Using passed parameters, decide if new application must be
389	 * created on host or existing application must be updated.
390	 *
391	 * @param array $applications 		applications to prepare for inheritance
392	 * @param array $hostsTemplatesMap	map of host IDs to templates they are linked to
393	 * @param array $hostApplications	array of existing applications on the child host returned by
394	 * 									self::getApplicationMapsByHostIds()
395	 *
396	 * @return array					Return array with applications. Existing applications have "applicationid" key.
397	 */
398	protected function prepareInheritedApps(array $applications, array $hostsTemplatesMap, array $hostApplications) {
399		/*
400		 * This variable holds array of working copies of results, indexed first by host ID (hence pre-filling
401		 * with host IDs from $hostApplications as keys and empty arrays as values), and then by application name.
402		 * For each host ID / application name pair, there is only one array with application data
403		 * with key "applicationTemplates" which is updated, if application with same name is inherited from
404		 * more than one template. In the end this variable gets looped through and plain result array is constructed.
405		 */
406		$newApplications = array_fill_keys(array_keys($hostApplications), []);
407
408		foreach ($applications as $application) {
409			$applicationId = $application['applicationid'];
410
411			foreach ($hostApplications as $hostId => $hostApplication) {
412				// If application template is not linked to host, skip it.
413				if (!isset($hostsTemplatesMap[$hostId][$application['hostid']])) {
414					continue;
415				}
416
417				if (!isset($newApplications[$hostId][$application['name']])) {
418					$newApplication = [
419						'name' => $application['name'],
420						'hostid' => $hostId,
421						'applicationTemplates' => []
422					];
423				}
424				else {
425					$newApplication = $newApplications[$hostId][$application['name']];
426				}
427
428				$existingApplication = null;
429
430				/*
431				 * Look for an application with the same name, if one exists - link the parent application to it.
432				 * If no application with the same name exists, look for a child application via "templateid".
433				 * Use it only if it has only one parent. Otherwise a new application must be created.
434				 */
435				if (isset($hostApplication['byName'][$application['name']])) {
436					$existingApplication = $hostApplication['byName'][$application['name']];
437				}
438				elseif (isset($hostApplication['byTemplateId'][$applicationId])
439						&& count($hostApplication['byTemplateId'][$applicationId]['applicationTemplates']) == 1) {
440					$existingApplication = $hostApplication['byTemplateId'][$applicationId];
441				}
442
443				if ($existingApplication) {
444					$newApplication['applicationid'] = $existingApplication['applicationid'];
445
446					// Add the new template link to an existing child application if it's not present yet.
447					$newApplication['applicationTemplates'] = isset($existingApplication['applicationTemplates'])
448						? $existingApplication['applicationTemplates']
449						: [];
450
451					$applicationTemplateIds = zbx_objectValues($newApplication['applicationTemplates'], 'templateid');
452
453					if (!in_array($applicationId, $applicationTemplateIds)) {
454						$newApplication['applicationTemplates'][] = [
455							'applicationid' => $newApplication['applicationid'],
456							'templateid' => $applicationId
457						];
458					}
459				}
460				else {
461					// If no matching child application exists, add a new one.
462					$newApplication['applicationTemplates'][] = ['templateid' => $applicationId];
463				}
464
465				// Store new or updated application data so it can be reused.
466				$newApplications[$hostId][$application['name']] = $newApplication;
467			}
468		}
469
470		$result = [];
471		foreach ($newApplications as $hostId => $newApplicationsPerHost) {
472			foreach ($newApplicationsPerHost as $newApplication) {
473				$result[] = $newApplication;
474			}
475		}
476
477		return $result;
478	}
479
480	/**
481	 * Get host applications for each passed host.
482	 * Each host has two hashes with applications, one with name keys other with templateid keys.
483	 *
484	 * Resulting structure is:
485	 * array(
486	 *     'hostid1' => array(
487	 *         'byName' => array(app1data, app2data, ...),
488	 *         'nyTemplateId' => array(app1data, app2data, ...)
489	 *     ), ...
490	 * );
491	 *
492	 * @param array $hostIds
493	 *
494	 * @return array
495	 */
496	protected function getApplicationMapsByHostIds(array $hostIds) {
497		$hostApps = [];
498		foreach ($hostIds as $hostid) {
499			$hostApps[$hostid] = ['byName' => [], 'byTemplateId' => []];
500		}
501
502		// fetch applications
503		$applications = DbFetchArrayAssoc(DBselect(
504			'SELECT a.applicationid,a.name,a.hostid'.
505				' FROM applications a'.
506				' WHERE '.dbConditionInt('a.hostid', $hostIds)
507		), 'applicationid');
508		$query = DBselect(
509			'SELECT *'.
510				' FROM application_template at'.
511				' WHERE '.dbConditionInt('at.applicationid', array_keys($applications))
512		);
513		while ($applicationTemplate = DbFetch($query)) {
514			$applications[$applicationTemplate['applicationid']]['applicationTemplates'][] = $applicationTemplate;
515		}
516
517		foreach ($applications as $app) {
518			$hostApps[$app['hostid']]['byName'][$app['name']] = $app;
519
520			if (isset($app['applicationTemplates'])) {
521				foreach ($app['applicationTemplates'] as $applicationTemplate) {
522					$hostApps[$app['hostid']]['byTemplateId'][$applicationTemplate['templateid']] = $app;
523				}
524			}
525		}
526
527		return $hostApps;
528	}
529
530	/**
531	 * Save applications. If application has applicationid it gets updated otherwise a new one is created.
532	 *
533	 * @param array $applications
534	 *
535	 * @return array
536	 */
537	protected function save(array $applications) {
538		$appsCreate = [];
539		$appsUpdate = [];
540
541		foreach ($applications as $key => $app) {
542			if (isset($app['applicationid'])) {
543				$appsUpdate[] = $app;
544			}
545			else {
546				$appsCreate[$key] = $app;
547			}
548		}
549
550		if (!empty($appsCreate)) {
551			$newApps = $this->create($appsCreate);
552			foreach ($newApps as $key => $newApp) {
553				$applications[$key]['applicationid'] = $newApp['applicationid'];
554			}
555		}
556		if (!empty($appsUpdate)) {
557			$this->update($appsUpdate);
558		}
559
560		return $applications;
561	}
562}
563