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 maps.
24 */
25class CMap extends CMapElement {
26
27	public const ACCESS_RULES = [
28		'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER],
29		'create' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_EDIT_MAPS],
30		'update' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_EDIT_MAPS],
31		'delete' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_EDIT_MAPS]
32	];
33
34	protected $tableName = 'sysmaps';
35	protected $tableAlias = 's';
36	protected $sortColumns = ['name', 'width', 'height'];
37
38	private $defOptions = [
39		'sysmapids'					=> null,
40		'userids'					=> null,
41		'editable'					=> false,
42		'nopermissions'				=> null,
43		// filter
44		'filter'					=> null,
45		'search'					=> null,
46		'searchByAny'				=> null,
47		'startSearch'				=> false,
48		'excludeSearch'				=> false,
49		'searchWildcardsEnabled'	=> null,
50		// output
51		'output'					=> API_OUTPUT_EXTEND,
52		'selectSelements'			=> null,
53		'selectShapes'				=> null,
54		'selectLines'				=> null,
55		'selectLinks'				=> null,
56		'selectIconMap'				=> null,
57		'selectUrls'				=> null,
58		'selectUsers'				=> null,
59		'selectUserGroups'			=> null,
60		'countOutput'				=> false,
61		'expandUrls' 				=> null,
62		'preservekeys'				=> false,
63		'sortfield'					=> '',
64		'sortorder'					=> '',
65		'limit'						=> null
66	];
67
68	/**
69	 * Get map data.
70	 *
71	 * @param array  $options
72	 * @param array  $options['sysmapids']					Map IDs.
73	 * @param bool	 $options['output']						List of map parameters to return.
74	 * @param array  $options['selectSelements']			List of map element properties to return.
75	 * @param array  $options['selectShapes']				List of map shape properties to return.
76	 * @param array  $options['selectLines']				List of map line properties to return.
77	 * @param array  $options['selectLinks']				List of map link properties to return.
78	 * @param array  $options['selectIconMap']				List of map icon map properties to return.
79	 * @param array  $options['selectUrls']					List of map URL properties to return.
80	 * @param array  $options['selectUsers']				List of users that the map is shared with.
81	 * @param array  $options['selectUserGroups']			List of user groups that the map is shared with.
82	 * @param bool	 $options['countOutput']				Return the count of records, instead of actual results.
83	 * @param array  $options['userids']					Map owner user IDs.
84	 * @param bool   $options['editable']					Return with read-write permission only. Ignored for
85	 *														SuperAdmins.
86	 * @param bool	 $options['nopermissions']				Return requested maps even if user has no permissions to
87	 *														them.
88	 * @param array  $options['filter']						List of field and exactly matched value pairs by which maps
89	 *														need to be filtered.
90	 * @param array  $options['search']						List of field-value pairs by which maps need to be searched.
91	 * @param array  $options['expandUrls']					Adds global map URLs to the corresponding map elements and
92	 *														expands macros in all map element URLs.
93	 * @param bool	 $options['searchByAny']
94	 * @param bool	 $options['startSearch']
95	 * @param bool	 $options['excludeSearch']
96	 * @param bool	 $options['searchWildcardsEnabled']
97	 * @param array  $options['preservekeys']				Use IDs as keys in the resulting array.
98	 * @param int    $options['limit']						Limit selection.
99	 * @param string $options['sortorder']
100	 * @param string $options['sortfield']
101	 *
102	 * @return array|integer Requested map data as array or the count of retrieved objects, if the countOutput
103	 *						 parameter has been used.
104	 */
105	public function get(array $options = []) {
106		$options = zbx_array_merge($this->defOptions, $options);
107
108		$limit = $options['limit'];
109		$options['limit'] = null;
110
111		if ($options['countOutput']) {
112			$count_output = true;
113			$options['output'] = ['sysmapid'];
114			$options['countOutput'] = false;
115		}
116		else {
117			$count_output = false;
118		}
119
120		$result = $this->getMaps($options);
121
122		if ($result && self::$userData['type'] != USER_TYPE_SUPER_ADMIN && !$options['nopermissions']) {
123			$sysmapids = array_flip($this->checkPermissions(array_keys($result), (bool) $options['editable']));
124
125			foreach ($result as $sysmapid => $foo) {
126				if (!array_key_exists($sysmapid, $sysmapids)) {
127					unset($result[$sysmapid]);
128				}
129			}
130		}
131
132		if ($count_output) {
133			return (string) count($result);
134		}
135
136		if ($limit !== null) {
137			$result = array_slice($result, 0, $limit, true);
138		}
139
140		if ($result) {
141			$result = $this->addRelatedObjects($options, $result);
142		}
143
144		// removing keys (hash -> array)
145		if (!$options['preservekeys']) {
146			$result = zbx_cleanHashes($result);
147		}
148
149		return $result;
150	}
151
152	/**
153	 * Returns maps without checking permissions to the elements.
154	 */
155	private function getMaps(array $options) {
156		$sql_parts = [
157			'select'	=> ['sysmaps' => 's.sysmapid'],
158			'from'		=> ['sysmaps' => 'sysmaps s'],
159			'where'		=> [],
160			'order'		=> [],
161			'limit'		=> null
162		];
163
164		// Editable + permission check.
165		if (self::$userData['type'] < USER_TYPE_ZABBIX_ADMIN && !$options['nopermissions']) {
166			$public_maps = '';
167
168			if ($options['editable']) {
169				$permission = PERM_READ_WRITE;
170			}
171			else {
172				$permission = PERM_READ;
173				$public_maps = ' OR s.private='.PUBLIC_SHARING;
174			}
175
176			$user_groups = getUserGroupsByUserId(self::$userData['userid']);
177
178			$sql_parts['where'][] = '(EXISTS ('.
179					'SELECT NULL'.
180					' FROM sysmap_user su'.
181					' WHERE s.sysmapid=su.sysmapid'.
182						' AND su.userid='.self::$userData['userid'].
183						' AND su.permission>='.$permission.
184				')'.
185				' OR EXISTS ('.
186					'SELECT NULL'.
187					' FROM sysmap_usrgrp sg'.
188					' WHERE s.sysmapid=sg.sysmapid'.
189						' AND '.dbConditionInt('sg.usrgrpid', $user_groups).
190						' AND sg.permission>='.$permission.
191				')'.
192				' OR s.userid='.self::$userData['userid'].
193				$public_maps.
194			')';
195		}
196
197		// sysmapids
198		if ($options['sysmapids'] !== null) {
199			zbx_value2array($options['sysmapids']);
200			$sql_parts['where']['sysmapid'] = dbConditionInt('s.sysmapid', $options['sysmapids']);
201		}
202
203		// userids
204		if ($options['userids'] !== null) {
205			zbx_value2array($options['userids']);
206
207			$sql_parts['where'][] = dbConditionInt('s.userid', $options['userids']);
208		}
209
210		// search
211		if ($options['search'] !== null) {
212			zbx_db_search('sysmaps s', $options, $sql_parts);
213		}
214
215		// filter
216		if ($options['filter'] !== null) {
217			$this->dbFilter('sysmaps s', $options, $sql_parts);
218		}
219
220		// limit
221		if (zbx_ctype_digit($options['limit']) && $options['limit']) {
222			$sql_parts['limit'] = $options['limit'];
223		}
224
225		$result = [];
226
227		$sql_parts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
228		$sql_parts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts);
229		$sysmaps = DBselect(self::createSelectQueryFromParts($sql_parts), $sql_parts['limit']);
230
231		while ($sysmap = DBfetch($sysmaps)) {
232			$result[$sysmap['sysmapid']] = $sysmap;
233		}
234
235		return $result;
236	}
237
238	/**
239	 * Returns maps with selected permission level.
240	 *
241	 * @param array $sysmapids
242	 * @param bool  $editable
243	 *
244	 * @return array
245	 */
246	private function checkPermissions(array $sysmapids, $editable) {
247		$sysmaps_r = [];
248		foreach ($sysmapids as $sysmapid) {
249			$sysmaps_r[$sysmapid] = true;
250		}
251
252		$selement_maps = [];
253
254		// Populating the map tree $selement_maps and list of shared maps $sysmaps_r.
255		do {
256			$selements = self::getSelements($sysmapids, SYSMAP_ELEMENT_TYPE_MAP);
257
258			$sysmapids = [];
259
260			foreach ($selements as $sysmapid => $selement) {
261				if (!array_key_exists($sysmapid, $sysmaps_r)) {
262					$sysmapids[$sysmapid] = true;
263				}
264			}
265
266			$sysmapids = array_keys($sysmapids);
267			$selement_maps += $selements;
268
269			if ($sysmapids) {
270				$db_sysmaps = $this->getMaps([
271					'output' => [],
272					'sysmapids' => $sysmapids,
273					'preservekeys' => true
274				] + $this->defOptions);
275
276				foreach ($sysmapids as $i => $sysmapid) {
277					if (array_key_exists($sysmapid, $db_sysmaps)) {
278						$sysmaps_r[$sysmapid] = true;
279					}
280					else {
281						unset($sysmapids[$i]);
282					}
283				}
284			}
285		}
286		while ($sysmapids);
287
288		$sysmaps_rw = $editable ? $sysmaps_r : [];
289
290		foreach ($sysmaps_r as &$sysmap_r) {
291			$sysmap_r = ['permission' => PERM_NONE, 'has_elements' => false];
292		}
293		unset($sysmap_r);
294
295		self::setHasElements($sysmaps_r, $selement_maps);
296
297		// Setting PERM_READ permission for maps with at least one image.
298		$selement_images = self::getSelements(array_keys($sysmaps_r), SYSMAP_ELEMENT_TYPE_IMAGE);
299		self::setMapPermissions($sysmaps_r, $selement_images, [0 => []], $selement_maps);
300		self::setHasElements($sysmaps_r, $selement_images);
301
302		$sysmapids = self::getSysmapIds($sysmaps_r, $sysmaps_rw);
303
304		// Check permissions to the host groups.
305		if ($sysmapids) {
306			$selement_groups = self::getSelements($sysmapids, SYSMAP_ELEMENT_TYPE_HOST_GROUP);
307
308			$db_groups = API::HostGroup()->get([
309				'output' => [],
310				'groupids' => array_keys($selement_groups),
311				'preservekeys' => true
312			]);
313
314			if ($editable) {
315				self::unsetMapsByElements($sysmaps_rw, $selement_groups, $db_groups);
316			}
317			self::setMapPermissions($sysmaps_r, $selement_groups, $db_groups, $selement_maps);
318			self::setHasElements($sysmaps_r, $selement_groups);
319
320			$sysmapids = self::getSysmapIds($sysmaps_r, $sysmaps_rw);
321		}
322
323		// Check permissions to the hosts.
324		if ($sysmapids) {
325			$selement_hosts = self::getSelements($sysmapids, SYSMAP_ELEMENT_TYPE_HOST);
326
327			$db_hosts = API::Host()->get([
328				'output' => [],
329				'hostids' => array_keys($selement_hosts),
330				'preservekeys' => true
331			]);
332
333			if ($editable) {
334				self::unsetMapsByElements($sysmaps_rw, $selement_hosts, $db_hosts);
335			}
336			self::setMapPermissions($sysmaps_r, $selement_hosts, $db_hosts, $selement_maps);
337			self::setHasElements($sysmaps_r, $selement_hosts);
338
339			$sysmapids = self::getSysmapIds($sysmaps_r, $sysmaps_rw);
340		}
341
342		// Check permissions to the triggers.
343		if ($sysmapids) {
344			$selement_triggers = self::getSelements($sysmapids, SYSMAP_ELEMENT_TYPE_TRIGGER);
345			$link_triggers = self::getLinkTriggers($sysmapids);
346
347			$db_triggers = API::Trigger()->get([
348				'output' => [],
349				'triggerids' => array_keys($selement_triggers + $link_triggers),
350				'preservekeys' => true
351			]);
352
353			if ($editable) {
354				self::unsetMapsByElements($sysmaps_rw, $selement_triggers, $db_triggers);
355				self::unsetMapsByElements($sysmaps_rw, $link_triggers, $db_triggers);
356			}
357			self::setMapPermissions($sysmaps_r, $selement_triggers, $db_triggers, $selement_maps);
358			self::setMapPermissions($sysmaps_r, $link_triggers, $db_triggers, $selement_maps);
359			self::setHasElements($sysmaps_r, $selement_triggers);
360			self::setHasElements($sysmaps_r, $link_triggers);
361		}
362
363		foreach ($sysmaps_r as $sysmapid => $sysmap_r) {
364			if (!$sysmap_r['has_elements']) {
365				self::setMapPermission($sysmaps_r, $selement_maps, $sysmapid);
366			}
367		}
368
369		foreach ($sysmaps_r as $sysmapid => $sysmap_r) {
370			if ($sysmap_r['permission'] == PERM_NONE) {
371				unset($sysmaps_r[$sysmapid]);
372			}
373		}
374
375		if ($editable) {
376			self::unsetMapsByTree($sysmaps_rw, $sysmaps_r, $selement_maps);
377		}
378
379		return array_keys($editable ? $sysmaps_rw : $sysmaps_r);
380	}
381
382	/**
383	 * Returns map elements for selected maps.
384	 */
385	private static function getSelements(array $sysmapids, $elementtype) {
386		$selements = [];
387
388		switch ($elementtype) {
389			case SYSMAP_ELEMENT_TYPE_IMAGE:
390				$sql = 'SELECT se.sysmapid,0 AS elementid'.
391					' FROM sysmaps_elements se'.
392					' WHERE '.dbConditionInt('se.sysmapid', $sysmapids).
393						' AND '.dbConditionInt('se.elementtype', [$elementtype]);
394				break;
395
396			case SYSMAP_ELEMENT_TYPE_HOST_GROUP:
397			case SYSMAP_ELEMENT_TYPE_HOST:
398			case SYSMAP_ELEMENT_TYPE_MAP:
399				$sql = 'SELECT se.sysmapid,se.elementid'.
400					' FROM sysmaps_elements se'.
401					' WHERE '.dbConditionInt('se.sysmapid', $sysmapids).
402						' AND '.dbConditionInt('se.elementtype', [$elementtype]);
403				break;
404
405			case SYSMAP_ELEMENT_TYPE_TRIGGER:
406				$sql = 'SELECT se.sysmapid,st.triggerid AS elementid'.
407					' FROM sysmaps_elements se,sysmap_element_trigger st'.
408					' WHERE se.selementid=st.selementid'.
409						' AND '.dbConditionInt('se.sysmapid', $sysmapids).
410						' AND '.dbConditionInt('se.elementtype', [$elementtype]);
411				break;
412		}
413		$db_selements = DBSelect($sql);
414
415		while ($db_selement = DBfetch($db_selements)) {
416			$selements[$db_selement['elementid']][] = ['sysmapid' => $db_selement['sysmapid']];
417		}
418
419		return $selements;
420	}
421
422	/**
423	 * Returns map links for selected maps.
424	 */
425	private static function getLinkTriggers(array $sysmapids) {
426		$link_triggers = [];
427
428		$db_links = DBSelect(
429			'SELECT sl.sysmapid,slt.triggerid'.
430			' FROM sysmaps_links sl,sysmaps_link_triggers slt'.
431			' WHERE sl.linkid=slt.linkid'.
432				' AND '.dbConditionInt('sl.sysmapid', $sysmapids)
433		);
434
435		while ($db_link = DBfetch($db_links)) {
436			$link_triggers[$db_link['triggerid']][] = ['sysmapid' => $db_link['sysmapid']];
437		}
438
439		return $link_triggers;
440	}
441
442	/**
443	 * Removes all inaccessible maps by map tree.
444	 *
445	 * @param array $sysmaps_rw[<sysmapids>]                  The list of writable maps.
446	 * @param array $sysmaps_r[<sysmapids>]                   The list of readable maps.
447	 * @param array $selement_maps                            The map tree.
448	 * @param array $selement_maps[<sysmapid>][]['sysmapid']  Parent map ID.
449	 */
450	private static function unsetMapsByTree(array &$sysmaps_rw, array $sysmaps_r, array $selement_maps) {
451		foreach ($selement_maps as $child_sysmapid => $selements) {
452			if (!array_key_exists($child_sysmapid, $sysmaps_r)) {
453				foreach ($selements as $selement) {
454					unset($sysmaps_rw[$selement['sysmapid']]);
455				}
456			}
457		}
458	}
459
460	/**
461	 * Removes all inaccessible maps by inacessible elements.
462	 *
463	 * @param array $sysmaps_rw[<sysmapids>]              The list of writable maps.
464	 * @param array $elements                             The map elements.
465	 * @param array $elements[<elementid>][]['sysmapid']  Map ID.
466	 * @param array $db_elements                          The list of readable elements.
467	 * @param array $db_elements[<elementid>]
468	 */
469	private static function unsetMapsByElements(array &$sysmaps_rw, array $elements, array $db_elements) {
470		foreach ($elements as $elementid => $selements) {
471			if (!array_key_exists($elementid, $db_elements)) {
472				foreach ($selements as $selement) {
473					unset($sysmaps_rw[$selement['sysmapid']]);
474				}
475			}
476		}
477	}
478
479	/**
480	 * Set PERM_READ permission for map and all parent maps.
481	 *
482	 * @param array  $sysmaps_r[<sysmapids>]                   The list of readable maps.
483	 * @param array  $selement_maps                            The map elements.
484	 * @param array  $selement_maps[<sysmapid>][]['sysmapid']  Map ID.
485	 * @param string $sysmapid
486	 */
487	private static function setMapPermission(array &$sysmaps_r, array $selement_maps, $sysmapid) {
488		if (array_key_exists($sysmapid, $selement_maps)) {
489			foreach ($selement_maps[$sysmapid] as $selement) {
490				self::setMapPermission($sysmaps_r, $selement_maps, $selement['sysmapid']);
491			}
492		}
493		$sysmaps_r[$sysmapid]['permission'] = PERM_READ;
494	}
495
496	/**
497	 * Setting PERM_READ permissions for maps with at least one available element.
498	 *
499	 * @param array $sysmaps_r[<sysmapids>]                   The list of readable maps.
500	 * @param array $elements                                 The map elements.
501	 * @param array $elements[<elementid>][]['sysmapid']      Map ID.
502	 * @param array $db_elements                              The list of readable elements.
503	 * @param array $db_elements[<elementid>]
504	 * @param array $selement_maps                            The map elements.
505	 * @param array $selement_maps[<sysmapid>][]['sysmapid']  Map ID.
506	 */
507	private static function setMapPermissions(array &$sysmaps_r, array $elements, array $db_elements,
508			array $selement_maps) {
509		foreach ($elements as $elementid => $selements) {
510			if (array_key_exists($elementid, $db_elements)) {
511				foreach ($selements as $selement) {
512					self::setMapPermission($sysmaps_r, $selement_maps, $selement['sysmapid']);
513				}
514			}
515		}
516	}
517
518	/**
519	 * Setting "has_elements" flag for maps.
520	 *
521	 * @param array $sysmaps_r[<sysmapids>]                   The list of readable maps.
522	 * @param array $elements                                 The map elements.
523	 * @param array $elements[<elementid>]
524	 */
525	private static function setHasElements(array &$sysmaps_r, array $elements) {
526		foreach ($elements as $elementid => $selements) {
527			foreach ($selements as $selement) {
528				$sysmaps_r[$selement['sysmapid']]['has_elements'] = true;
529			}
530		}
531	}
532
533	/**
534	 * Returns map ids which will be checked for permissions.
535	 *
536	 * @param array $sysmaps_r
537	 * @param array $sysmaps_r[<sysmapid>]['permission']
538	 * @param array $sysmaps_rw
539	 * @param array $sysmaps_rw[<sysmapid>]
540	 */
541	private static function getSysmapIds(array $sysmaps_r, array $sysmaps_rw) {
542		$sysmapids = $sysmaps_rw;
543
544		foreach ($sysmaps_r as $sysmapid => $sysmap) {
545			if ($sysmap['permission'] == PERM_NONE) {
546				$sysmapids[$sysmapid] = true;
547			}
548		}
549
550		return array_keys($sysmapids);
551	}
552
553	/**
554	 * Validates the input parameters for the delete() method.
555	 *
556	 * @param array $sysmapids
557	 *
558	 * @throws APIException if the input is invalid.
559	 */
560	protected function validateDelete(array $sysmapids) {
561		if (!$sysmapids) {
562			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
563		}
564
565		$db_maps = $this->get([
566			'output' => ['sysmapid'],
567			'sysmapids' => $sysmapids,
568			'editable' => true,
569			'preservekeys' => true
570		]);
571
572		foreach ($sysmapids as $sysmapid) {
573			if (!array_key_exists($sysmapid, $db_maps)) {
574				self::exception(ZBX_API_ERROR_PARAMETERS, _('No permissions to referred object or it does not exist!'));
575			}
576		}
577	}
578
579	/**
580	 * Validate the input parameters for the create() method.
581	 *
582	 * @param array $maps		maps data array
583	 *
584	 * @throws APIException if the input is invalid.
585	 */
586	protected function validateCreate(array $maps) {
587		if (!$maps) {
588			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
589		}
590
591		$user_data = self::$userData;
592
593		$map_db_fields = [
594			'name' => null,
595			'width' => null,
596			'height' => null,
597			'urls' => [],
598			'selements' => [],
599			'links' => []
600		];
601
602		// Validate mandatory fields and map name.
603		foreach ($maps as $map) {
604			if (!check_db_fields($map_db_fields, $map)) {
605				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect fields for sysmap.'));
606			}
607		}
608
609		// Check for duplicate names.
610		$duplicate = CArrayHelper::findDuplicate($maps, 'name');
611		if ($duplicate) {
612			self::exception(ZBX_API_ERROR_PARAMETERS,
613				_s('Duplicate "name" value "%1$s" for map.', $duplicate['name'])
614			);
615		}
616
617		// Check if map already exists.
618		$db_maps = $this->get([
619			'output' => ['name'],
620			'filter' => ['name' => zbx_objectValues($maps, 'name')],
621			'nopermissions' => true,
622			'limit' => 1
623		]);
624
625		if ($db_maps) {
626			self::exception(ZBX_API_ERROR_PARAMETERS, _s('Map "%1$s" already exists.', $db_maps[0]['name']));
627		}
628
629		$private_validator = new CLimitedSetValidator([
630			'values' => [PUBLIC_SHARING, PRIVATE_SHARING]
631		]);
632
633		$permission_validator = new CLimitedSetValidator([
634			'values' => [PERM_READ, PERM_READ_WRITE]
635		]);
636
637		$show_suppressed_types = [ZBX_PROBLEM_SUPPRESSED_FALSE, ZBX_PROBLEM_SUPPRESSED_TRUE];
638		$show_suppressed_validator = new CLimitedSetValidator(['values' => $show_suppressed_types]);
639
640		$expandproblem_types = [SYSMAP_PROBLEMS_NUMBER, SYSMAP_SINGLE_PROBLEM, SYSMAP_PROBLEMS_NUMBER_CRITICAL];
641		$expandproblem_validator = new CLimitedSetValidator(['values' => $expandproblem_types]);
642
643		// Continue to check 2 more mandatory fields and other optional fields.
644		foreach ($maps as $map_index => $map) {
645			// Check mandatory fields "width" and "height".
646			if ($map['width'] > 65535 || $map['width'] < 1) {
647				self::exception(ZBX_API_ERROR_PARAMETERS,
648					_s('Incorrect "width" value for map "%1$s".', $map['name'])
649				);
650			}
651
652			if ($map['height'] > 65535 || $map['height'] < 1) {
653				self::exception(ZBX_API_ERROR_PARAMETERS,
654					_s('Incorrect "height" value for map "%1$s".', $map['name'])
655				);
656			}
657
658			// Check if owner can be set.
659			if (array_key_exists('userid', $map)) {
660				if ($map['userid'] === '' || $map['userid'] === null || $map['userid'] === false) {
661					self::exception(ZBX_API_ERROR_PARAMETERS, _('Map owner cannot be empty.'));
662				}
663				elseif ($map['userid'] != $user_data['userid'] && $user_data['type'] != USER_TYPE_SUPER_ADMIN
664						&& $user_data['type'] != USER_TYPE_ZABBIX_ADMIN) {
665					self::exception(ZBX_API_ERROR_PARAMETERS, _('Only administrators can set map owner.'));
666				}
667			}
668
669			// Check for invalid "private" values.
670			if (array_key_exists('private', $map)) {
671				if (!$private_validator->validate($map['private'])) {
672					self::exception(ZBX_API_ERROR_PARAMETERS,
673						_s('Incorrect "private" value "%1$s" for map "%2$s".', $map['private'], $map['name'])
674					);
675				}
676			}
677
678			// Check for invalid "show_suppressed" values.
679			if (array_key_exists('show_suppressed', $map)
680					&& !$show_suppressed_validator->validate($map['show_suppressed'])) {
681				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
682					'show_suppressed', _s('value must be one of %1$s', implode(', ', $show_suppressed_types))
683				));
684			}
685
686			if (array_key_exists('expandproblem', $map) && !$expandproblem_validator->validate($map['expandproblem'])) {
687				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.', 'expandproblem',
688					_s('value must be one of %1$s', implode(', ', $expandproblem_types))
689				));
690			}
691
692			$userids = [];
693
694			// Map user shares.
695			if (array_key_exists('users', $map)) {
696				if (!is_array($map['users'])) {
697					self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
698				}
699
700				$required_fields = ['userid', 'permission'];
701
702				foreach ($map['users'] as $share) {
703					// Check required parameters.
704					$missing_keys = array_diff($required_fields, array_keys($share));
705
706					if ($missing_keys) {
707						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
708							'User sharing is missing parameters: %1$s for map "%2$s".',
709							implode(', ', $missing_keys),
710							$map['name']
711						));
712					}
713					else {
714						foreach ($required_fields as $field) {
715							if ($share[$field] === '' || $share[$field] === null) {
716								self::exception(ZBX_API_ERROR_PARAMETERS, _s(
717									'Sharing option "%1$s" is missing a value for map "%2$s".',
718									$field,
719									$map['name']
720								));
721							}
722						}
723					}
724
725					if (!$permission_validator->validate($share['permission'])) {
726						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
727							'Incorrect "permission" value "%1$s" in users for map "%2$s".',
728							$share['permission'],
729							$map['name']
730						));
731					}
732
733					if (array_key_exists('private', $map) && $map['private'] == PUBLIC_SHARING
734							&& $share['permission'] == PERM_READ) {
735						self::exception(ZBX_API_ERROR_PARAMETERS,
736							_s('Map "%1$s" is public and read-only sharing is disallowed.', $map['name'])
737						);
738					}
739
740					if (array_key_exists($share['userid'], $userids)) {
741						self::exception(ZBX_API_ERROR_PARAMETERS,
742							_s('Duplicate userid "%1$s" in users for map "%2$s".', $share['userid'], $map['name'])
743						);
744					}
745
746					$userids[$share['userid']] = $share['userid'];
747				}
748			}
749
750			if (array_key_exists('userid', $map) && $map['userid']) {
751				$userids[$map['userid']] = $map['userid'];
752			}
753
754			// Users validation.
755			if ($userids) {
756				$db_users = API::User()->get([
757					'userids' => $userids,
758					'countOutput' => true
759				]);
760
761				if (count($userids) != $db_users) {
762					self::exception(ZBX_API_ERROR_PARAMETERS,
763						_s('Incorrect user ID specified for map "%1$s".', $map['name'])
764					);
765				}
766			}
767
768			// Map user group shares.
769			if (array_key_exists('userGroups', $map)) {
770				if (!is_array($map['userGroups'])) {
771					self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
772				}
773
774				$shared_user_groupids = [];
775				$required_fields = ['usrgrpid', 'permission'];
776
777				foreach ($map['userGroups'] as $share) {
778					// Check required parameters.
779					$missing_keys = array_diff($required_fields, array_keys($share));
780
781					if ($missing_keys) {
782						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
783							'User group sharing is missing parameters: %1$s for map "%2$s".',
784							implode(', ', $missing_keys),
785							$map['name']
786						));
787					}
788					else {
789						foreach ($required_fields as $field) {
790							if ($share[$field] === '' || $share[$field] === null) {
791								self::exception(ZBX_API_ERROR_PARAMETERS, _s(
792									'Field "%1$s" is missing a value for map "%2$s".',
793									$field,
794									$map['name']
795								));
796							}
797						}
798					}
799
800					if (!$permission_validator->validate($share['permission'])) {
801						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
802							'Incorrect "permission" value "%1$s" in user groups for map "%2$s".',
803							$share['permission'],
804							$map['name']
805						));
806					}
807
808					if (array_key_exists('private', $map) && $map['private'] == PUBLIC_SHARING
809							&& $share['permission'] == PERM_READ) {
810						self::exception(ZBX_API_ERROR_PARAMETERS,
811							_s('Map "%1$s" is public and read-only sharing is disallowed.', $map['name'])
812						);
813					}
814
815					if (array_key_exists($share['usrgrpid'], $shared_user_groupids)) {
816						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
817							'Duplicate usrgrpid "%1$s" in user groups for map "%2$s".',
818							$share['usrgrpid'],
819							$map['name']
820						));
821					}
822
823					$shared_user_groupids[$share['usrgrpid']] = $share['usrgrpid'];
824				}
825
826				if ($shared_user_groupids) {
827					$db_user_groups = API::UserGroup()->get([
828						'usrgrpids' => $shared_user_groupids,
829						'countOutput' => true
830					]);
831
832					if (count($shared_user_groupids) != $db_user_groups) {
833						self::exception(ZBX_API_ERROR_PARAMETERS,
834							_s('Incorrect user group ID specified for map "%1$s".', $map['name'])
835						);
836					}
837				}
838
839				unset($shared_user_groupids);
840			}
841
842			// Map labels.
843			$map_labels = ['label_type' => ['typeName' => _('icon')]];
844
845			if (array_key_exists('label_format', $map) && $map['label_format'] == SYSMAP_LABEL_ADVANCED_ON) {
846				$map_labels['label_type_hostgroup'] = [
847					'string' => 'label_string_hostgroup',
848					'typeName' => _('host group')
849				];
850				$map_labels['label_type_host'] = [
851					'string' => 'label_string_host',
852					'typeName' => _('host')
853				];
854				$map_labels['label_type_trigger'] = [
855					'string' => 'label_string_trigger',
856					'typeName' => _('trigger')
857				];
858				$map_labels['label_type_map'] = [
859					'string' => 'label_string_map',
860					'typeName' => _('map')
861				];
862				$map_labels['label_type_image'] = [
863					'string' => 'label_string_image',
864					'typeName' => _('image')
865				];
866			}
867
868			foreach ($map_labels as $label_name => $label_data) {
869				if (!array_key_exists($label_name, $map)) {
870					continue;
871				}
872
873				if (sysmapElementLabel($map[$label_name]) === false) {
874					self::exception(ZBX_API_ERROR_PARAMETERS,
875						_s('Incorrect %1$s label type value for map "%2$s".', $label_data['typeName'], $map['name'])
876					);
877				}
878
879				if ($map[$label_name] == MAP_LABEL_TYPE_CUSTOM) {
880					if (!array_key_exists('string', $label_data)) {
881						self::exception(ZBX_API_ERROR_PARAMETERS,
882							_s('Incorrect %1$s label type value for map "%2$s".', $label_data['typeName'], $map['name'])
883						);
884					}
885
886					if (!array_key_exists($label_data['string'], $map) || zbx_empty($map[$label_data['string']])) {
887						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
888								'Custom label for map "%2$s" elements of type "%1$s" may not be empty.',
889								$label_data['typeName'],
890								$map['name']
891							)
892						);
893					}
894				}
895
896				if ($label_name == 'label_type_image' && $map[$label_name] == MAP_LABEL_TYPE_STATUS) {
897					self::exception(ZBX_API_ERROR_PARAMETERS,
898						_s('Incorrect %1$s label type value for map "%2$s".', $label_data['typeName'], $map['name'])
899					);
900				}
901
902				if ($label_name === 'label_type' || $label_name === 'label_type_host') {
903					continue;
904				}
905
906				if ($map[$label_name] == MAP_LABEL_TYPE_IP) {
907					self::exception(ZBX_API_ERROR_PARAMETERS,
908						_s('Incorrect %1$s label type value for map "%2$s".', $label_data['typeName'], $map['name'])
909					);
910				}
911			}
912
913			// Validating grid options.
914			$possibleGridSizes = [20, 40, 50, 75, 100];
915
916			// Grid size.
917			if (array_key_exists('grid_size', $map) && !in_array($map['grid_size'], $possibleGridSizes)) {
918				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
919					'Value "%1$s" is invalid for parameter "grid_show". Choices are: "%2$s".',
920					$map['grid_size'],
921					implode('", "', $possibleGridSizes)
922				));
923			}
924
925			// Grid auto align.
926			if (array_key_exists('grid_align', $map) && $map['grid_align'] != SYSMAP_GRID_ALIGN_ON
927					&& $map['grid_align'] != SYSMAP_GRID_ALIGN_OFF) {
928				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
929					'Value "%1$s" is invalid for parameter "grid_align". Choices are: "%2$s" and "%3$s"',
930					$map['grid_align'],
931					SYSMAP_GRID_ALIGN_ON,
932					SYSMAP_GRID_ALIGN_OFF
933				));
934			}
935
936			// Grid show.
937			if (array_key_exists('grid_show', $map) && $map['grid_show'] != SYSMAP_GRID_SHOW_ON
938					&& $map['grid_show'] != SYSMAP_GRID_SHOW_OFF) {
939				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
940					'Value "%1$s" is invalid for parameter "grid_show". Choices are: "%2$s" and "%3$s".',
941					$map['grid_show'],
942					SYSMAP_GRID_SHOW_ON,
943					SYSMAP_GRID_SHOW_OFF
944				));
945			}
946
947			// Urls.
948			if (array_key_exists('urls', $map) && $map['urls']) {
949				$url_names = zbx_toHash($map['urls'], 'name');
950
951				foreach ($map['urls'] as $url) {
952					if ($url['name'] === '' || $url['url'] === '') {
953						self::exception(ZBX_API_ERROR_PARAMETERS,
954							_s('URL should have both "name" and "url" fields for map "%1$s".', $map['name'])
955						);
956					}
957
958					if (!array_key_exists($url['name'], $url_names)) {
959						self::exception(ZBX_API_ERROR_PARAMETERS,
960							_s('URL name should be unique for map "%1$s".', $map['name'])
961						);
962					}
963
964					$url_validate_options = ['allow_user_macro' => false];
965					if ($url['elementtype'] == SYSMAP_ELEMENT_TYPE_HOST) {
966						$url_validate_options['allow_inventory_macro'] = INVENTORY_URL_MACRO_HOST;
967					}
968					elseif ($url['elementtype'] == SYSMAP_ELEMENT_TYPE_TRIGGER) {
969						$url_validate_options['allow_inventory_macro'] = INVENTORY_URL_MACRO_TRIGGER;
970					}
971					else {
972						$url_validate_options['allow_inventory_macro'] = INVENTORY_URL_MACRO_NONE;
973					}
974
975					if (!CHtmlUrlValidator::validate($url['url'], $url_validate_options)) {
976						self::exception(ZBX_API_ERROR_PARAMETERS, _('Wrong value for url field.'));
977					}
978
979					unset($url_names[$url['name']]);
980				}
981			}
982
983			if (array_key_exists('selements', $map)) {
984				if (!is_array($map['selements'])) {
985					self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
986				}
987				elseif (!CMapHelper::checkSelementPermissions($map['selements'])) {
988					self::exception(ZBX_API_ERROR_PERMISSIONS,
989						_('No permissions to referred object or it does not exist!')
990					);
991				}
992
993				foreach (array_values($map['selements']) as $selement_index => $selement) {
994					$this->validateSelementTags($selement, '/'.($map_index + 1).'/selements/'.($selement_index + 1));
995				}
996			}
997
998			// Map selement links.
999			if (array_key_exists('links', $map) && $map['links']) {
1000				$selementids = zbx_objectValues($map['selements'], 'selementid');
1001
1002				foreach ($map['links'] as $link) {
1003					if (!in_array($link['selementid1'], $selementids)) {
1004						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1005							'Link selementid1 field is pointing to a nonexistent map selement ID "%1$s" for map "%2$s".',
1006							$link['selementid1'],
1007							$map['name']
1008						));
1009					}
1010
1011					if (!in_array($link['selementid2'], $selementids)) {
1012						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1013							'Link selementid2 field is pointing to a nonexistent map selement ID "%1$s" for map "%2$s".',
1014							$link['selementid2'],
1015							$map['name']
1016						));
1017					}
1018				}
1019			}
1020		}
1021	}
1022
1023	/**
1024	 * Validate the input parameters for the update() method.
1025	 *
1026	 * @param array $maps			maps data array
1027	 * @param array $db_maps		db maps data array
1028	 *
1029	 * @throws APIException if the input is invalid.
1030	 */
1031	protected function validateUpdate(array $maps, array $db_maps) {
1032		if (!$maps) {
1033			self::exception(ZBX_API_ERROR_PARAMETERS, _('Empty input parameter.'));
1034		}
1035
1036		$user_data = self::$userData;
1037
1038		// Validate given IDs.
1039		$this->checkObjectIds($maps, 'sysmapid',
1040			_('No "%1$s" given for map.'),
1041			_('Empty map ID.'),
1042			_('Incorrect map ID.')
1043		);
1044
1045		$check_names = [];
1046
1047		foreach ($maps as $map) {
1048			// Check if this map exists and user has write permissions.
1049			if (!array_key_exists($map['sysmapid'], $db_maps)) {
1050				self::exception(ZBX_API_ERROR_PERMISSIONS,
1051					_('No permissions to referred object or it does not exist!')
1052				);
1053			}
1054
1055			// Validate "name" field.
1056			if (array_key_exists('name', $map)) {
1057				if (is_array($map['name'])) {
1058					self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1059				}
1060				elseif ($map['name'] === '' || $map['name'] === null || $map['name'] === false) {
1061					self::exception(ZBX_API_ERROR_PARAMETERS, _('Map name cannot be empty.'));
1062				}
1063
1064				if ($db_maps[$map['sysmapid']]['name'] !== $map['name']) {
1065					$check_names[] = $map;
1066				}
1067			}
1068		}
1069
1070		if ($check_names) {
1071			// Check for duplicate names.
1072			$duplicate = CArrayHelper::findDuplicate($check_names, 'name');
1073			if ($duplicate) {
1074				self::exception(ZBX_API_ERROR_PARAMETERS,
1075					_s('Duplicate "name" value "%1$s" for map.', $duplicate['name'])
1076				);
1077			}
1078
1079			$db_map_names = $this->get([
1080				'output' => ['sysmapid', 'name'],
1081				'filter' => ['name' => zbx_objectValues($check_names, 'name')],
1082				'nopermissions' => true
1083			]);
1084			$db_map_names = zbx_toHash($db_map_names, 'name');
1085
1086			// Check for existing names.
1087			foreach ($check_names as $map) {
1088				if (array_key_exists($map['name'], $db_map_names)
1089						&& bccomp($db_map_names[$map['name']]['sysmapid'], $map['sysmapid']) != 0) {
1090					self::exception(ZBX_API_ERROR_PARAMETERS,
1091						_s('Map "%1$s" already exists.', $map['name'])
1092					);
1093				}
1094			}
1095		}
1096
1097		$private_validator = new CLimitedSetValidator([
1098			'values' => [PUBLIC_SHARING, PRIVATE_SHARING]
1099		]);
1100
1101		$permission_validator = new CLimitedSetValidator([
1102			'values' => [PERM_READ, PERM_READ_WRITE]
1103		]);
1104
1105		$show_suppressed_types = [ZBX_PROBLEM_SUPPRESSED_FALSE, ZBX_PROBLEM_SUPPRESSED_TRUE];
1106		$show_suppressed_validator = new CLimitedSetValidator(['values' => $show_suppressed_types]);
1107
1108		$expandproblem_types = [SYSMAP_PROBLEMS_NUMBER, SYSMAP_SINGLE_PROBLEM, SYSMAP_PROBLEMS_NUMBER_CRITICAL];
1109		$expandproblem_validator = new CLimitedSetValidator(['values' => $expandproblem_types]);
1110
1111		foreach ($maps as $map_index => $map) {
1112			// Check if owner can be set.
1113			if (array_key_exists('userid', $map)) {
1114				if ($map['userid'] === '' || $map['userid'] === null || $map['userid'] === false) {
1115					self::exception(ZBX_API_ERROR_PARAMETERS, _('Map owner cannot be empty.'));
1116				}
1117				elseif ($map['userid'] != $user_data['userid'] && $user_data['type'] != USER_TYPE_SUPER_ADMIN
1118						&& $user_data['type'] != USER_TYPE_ZABBIX_ADMIN) {
1119					self::exception(ZBX_API_ERROR_PARAMETERS, _('Only administrators can set map owner.'));
1120				}
1121			}
1122
1123			// Unset extra field.
1124			unset($db_maps[$map['sysmapid']]['userid']);
1125
1126			$map = array_merge($db_maps[$map['sysmapid']], $map);
1127
1128			// Check "width" and "height" fields.
1129			if ($map['width'] > 65535 || $map['width'] < 1) {
1130				self::exception(ZBX_API_ERROR_PARAMETERS,
1131					_s('Incorrect "width" value for map "%1$s".', $map['name'])
1132				);
1133			}
1134
1135			if ($map['height'] > 65535 || $map['height'] < 1) {
1136				self::exception(ZBX_API_ERROR_PARAMETERS,
1137					_s('Incorrect "height" value for map "%1$s".', $map['name'])
1138				);
1139			}
1140
1141			if (!$private_validator->validate($map['private'])) {
1142				self::exception(ZBX_API_ERROR_PARAMETERS,
1143					_s('Incorrect "private" value "%1$s" for map "%2$s".', $map['private'], $map['name'])
1144				);
1145			}
1146
1147			// Check for invalid "show_suppressed" values.
1148			if (array_key_exists('show_suppressed', $map)
1149					&& !$show_suppressed_validator->validate($map['show_suppressed'])) {
1150				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.',
1151					'show_suppressed', _s('value must be one of %1$s', implode(', ', $show_suppressed_types))
1152				));
1153			}
1154
1155			if (array_key_exists('expandproblem', $map) && !$expandproblem_validator->validate($map['expandproblem'])) {
1156				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Incorrect value for field "%1$s": %2$s.', 'expandproblem',
1157					_s('value must be one of %1$s', implode(', ', $expandproblem_types))
1158				));
1159			}
1160
1161			$userids = [];
1162
1163			// Map user shares.
1164			if (array_key_exists('users', $map)) {
1165				if (!is_array($map['users'])) {
1166					self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1167				}
1168
1169				$required_fields = ['userid', 'permission'];
1170
1171				foreach ($map['users'] as $share) {
1172					// Check required parameters.
1173					$missing_keys = array_diff($required_fields, array_keys($share));
1174
1175					if ($missing_keys) {
1176						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1177							'User sharing is missing parameters: %1$s for map "%2$s".',
1178							implode(', ', $missing_keys),
1179							$map['name']
1180						));
1181					}
1182					else {
1183						foreach ($required_fields as $field) {
1184							if ($share[$field] === '' || $share[$field] === null) {
1185								self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1186									'Sharing option "%1$s" is missing a value for map "%2$s".',
1187									$field,
1188									$map['name']
1189								));
1190							}
1191						}
1192					}
1193
1194					if (!$permission_validator->validate($share['permission'])) {
1195						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1196							'Incorrect "permission" value "%1$s" in users for map "%2$s".',
1197							$share['permission'],
1198							$map['name']
1199						));
1200					}
1201
1202					if ($map['private'] == PUBLIC_SHARING && $share['permission'] == PERM_READ) {
1203						self::exception(ZBX_API_ERROR_PARAMETERS,
1204							_s('Map "%1$s" is public and read-only sharing is disallowed.', $map['name'])
1205						);
1206					}
1207
1208					if (array_key_exists($share['userid'], $userids)) {
1209						self::exception(ZBX_API_ERROR_PARAMETERS,
1210							_s('Duplicate userid "%1$s" in users for map "%2$s".', $share['userid'], $map['name'])
1211						);
1212					}
1213
1214					$userids[$share['userid']] = $share['userid'];
1215				}
1216			}
1217
1218			if (array_key_exists('userid', $map) && $map['userid']) {
1219				$userids[$map['userid']] = $map['userid'];
1220			}
1221
1222			// Users validation.
1223			if ($userids) {
1224				$db_users = API::User()->get([
1225					'userids' => $userids,
1226					'countOutput' => true
1227				]);
1228
1229				if (count($userids) != $db_users) {
1230					self::exception(ZBX_API_ERROR_PARAMETERS,
1231						_s('Incorrect user ID specified for map "%1$s".', $map['name'])
1232					);
1233				}
1234			}
1235
1236			// Map user group shares.
1237			if (array_key_exists('userGroups', $map)) {
1238				if (!is_array($map['userGroups'])) {
1239					self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1240				}
1241
1242				$shared_user_groupids = [];
1243				$required_fields = ['usrgrpid', 'permission'];
1244
1245				foreach ($map['userGroups'] as $share) {
1246					// Check required parameters.
1247					$missing_keys = array_diff($required_fields, array_keys($share));
1248
1249					if ($missing_keys) {
1250						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1251							'User group sharing is missing parameters: %1$s for map "%2$s".',
1252							implode(', ', $missing_keys),
1253							$map['name'])
1254						);
1255					}
1256					else {
1257						foreach ($required_fields as $field) {
1258							if ($share[$field] === '' || $share[$field] === null) {
1259								self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1260									'Sharing option "%1$s" is missing a value for map "%2$s".',
1261									$field,
1262									$map['name']
1263								));
1264							}
1265						}
1266					}
1267
1268					if (!$permission_validator->validate($share['permission'])) {
1269						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1270							'Incorrect "permission" value "%1$s" in user groups for map "%2$s".',
1271							$share['permission'],
1272							$map['name']
1273						));
1274					}
1275
1276					if ($map['private'] == PUBLIC_SHARING && $share['permission'] == PERM_READ) {
1277						self::exception(ZBX_API_ERROR_PARAMETERS,
1278							_s('Map "%1$s" is public and read-only sharing is disallowed.', $map['name'])
1279						);
1280					}
1281
1282					if (array_key_exists($share['usrgrpid'], $shared_user_groupids)) {
1283						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1284							'Duplicate usrgrpid "%1$s" in user groups for map "%2$s".',
1285							$share['usrgrpid'],
1286							$map['name']
1287						));
1288					}
1289
1290					$shared_user_groupids[$share['usrgrpid']] = $share['usrgrpid'];
1291				}
1292
1293				if ($shared_user_groupids) {
1294					$db_user_groups = API::UserGroup()->get([
1295						'usrgrpids' => $shared_user_groupids,
1296						'countOutput' => true
1297					]);
1298
1299					if (count($shared_user_groupids) != $db_user_groups) {
1300						self::exception(ZBX_API_ERROR_PARAMETERS,
1301							_s('Incorrect user group ID specified for map "%1$s".', $map['name'])
1302						);
1303					}
1304				}
1305
1306				unset($shared_user_groupids);
1307			}
1308
1309			// Map labels.
1310			$map_labels = ['label_type' => ['typeName' => _('icon')]];
1311
1312			if (array_key_exists('label_format', $map)
1313					&& $map['label_format'] == SYSMAP_LABEL_ADVANCED_ON) {
1314				$map_labels['label_type_hostgroup'] = [
1315					'string' => 'label_string_hostgroup',
1316					'typeName' => _('host group')
1317				];
1318				$map_labels['label_type_host'] = [
1319					'string' => 'label_string_host',
1320					'typeName' => _('host')
1321				];
1322				$map_labels['label_type_trigger'] = [
1323					'string' => 'label_string_trigger',
1324					'typeName' => _('trigger')
1325				];
1326				$map_labels['label_type_map'] = [
1327					'string' => 'label_string_map',
1328					'typeName' => _('map')
1329				];
1330				$map_labels['label_type_image'] = [
1331					'string' => 'label_string_image',
1332					'typeName' => _('image')
1333				];
1334			}
1335
1336			foreach ($map_labels as $label_name => $labelData) {
1337				if (!array_key_exists($label_name, $map)) {
1338					continue;
1339				}
1340
1341				if (sysmapElementLabel($map[$label_name]) === false) {
1342					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1343						'Incorrect %1$s label type value for map "%2$s".',
1344						$labelData['typeName'],
1345						$map['name']
1346					));
1347				}
1348
1349				if ($map[$label_name] == MAP_LABEL_TYPE_CUSTOM) {
1350					if (!array_key_exists('string', $labelData)) {
1351						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1352							'Incorrect %1$s label type value for map "%2$s".',
1353							$labelData['typeName'],
1354							$map['name']
1355						));
1356					}
1357
1358					if (!array_key_exists($labelData['string'], $map) || zbx_empty($map[$labelData['string']])) {
1359						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1360							'Custom label for map "%2$s" elements of type "%1$s" may not be empty.',
1361							$labelData['typeName'],
1362							$map['name']
1363						));
1364					}
1365				}
1366
1367				if ($label_name === 'label_type_image' && $map[$label_name] == MAP_LABEL_TYPE_STATUS) {
1368					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1369						'Incorrect %1$s label type value for map "%2$s".',
1370						$labelData['typeName'],
1371						$map['name']
1372					));
1373				}
1374
1375				if ($label_name === 'label_type' || $label_name === 'label_type_host') {
1376					continue;
1377				}
1378
1379				if ($map[$label_name] == MAP_LABEL_TYPE_IP) {
1380					self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1381						'Incorrect %1$s label type value for map "%2$s".',
1382						$labelData['typeName'],
1383						$map['name']
1384					));
1385				}
1386			}
1387
1388			// Validating grid options.
1389			$possibleGridSizes = [20, 40, 50, 75, 100];
1390
1391			// Grid size.
1392			if (array_key_exists('grid_size', $map) && !in_array($map['grid_size'], $possibleGridSizes)) {
1393				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1394					'Value "%1$s" is invalid for parameter "grid_show". Choices are: "%2$s".',
1395					$map['grid_size'],
1396					implode('", "', $possibleGridSizes)
1397				));
1398			}
1399
1400			// Grid auto align.
1401			if (array_key_exists('grid_align', $map) && $map['grid_align'] != SYSMAP_GRID_ALIGN_ON
1402					&& $map['grid_align'] != SYSMAP_GRID_ALIGN_OFF) {
1403				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1404					'Value "%1$s" is invalid for parameter "grid_align". Choices are: "%2$s" and "%3$s"',
1405					$map['grid_align'],
1406					SYSMAP_GRID_ALIGN_ON,
1407					SYSMAP_GRID_ALIGN_OFF
1408				));
1409			}
1410
1411			// Grid show.
1412			if (array_key_exists('grid_show', $map) && $map['grid_show'] != SYSMAP_GRID_SHOW_ON
1413					&& $map['grid_show'] != SYSMAP_GRID_SHOW_OFF) {
1414				self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1415					'Value "%1$s" is invalid for parameter "grid_show". Choices are: "%2$s" and "%3$s".',
1416					$map['grid_show'],
1417					SYSMAP_GRID_SHOW_ON,
1418					SYSMAP_GRID_SHOW_OFF
1419				));
1420			}
1421
1422			// Urls.
1423			if (array_key_exists('urls', $map) && !empty($map['urls'])) {
1424				$urlNames = zbx_toHash($map['urls'], 'name');
1425
1426				foreach ($map['urls'] as $url) {
1427					if ($url['name'] === '' || $url['url'] === '') {
1428						self::exception(ZBX_API_ERROR_PARAMETERS,
1429							_s('URL should have both "name" and "url" fields for map "%1$s".', $map['name'])
1430						);
1431					}
1432
1433					if (!array_key_exists($url['name'], $urlNames)) {
1434						self::exception(ZBX_API_ERROR_PARAMETERS,
1435							_s('URL name should be unique for map "%1$s".', $map['name'])
1436						);
1437					}
1438
1439					$url_validate_options = ['allow_user_macro' => false];
1440					if ($url['elementtype'] == SYSMAP_ELEMENT_TYPE_HOST) {
1441						$url_validate_options['allow_inventory_macro'] = INVENTORY_URL_MACRO_HOST;
1442					}
1443					elseif ($url['elementtype'] == SYSMAP_ELEMENT_TYPE_TRIGGER) {
1444						$url_validate_options['allow_inventory_macro'] = INVENTORY_URL_MACRO_TRIGGER;
1445					}
1446					else {
1447						$url_validate_options['allow_inventory_macro'] = INVENTORY_URL_MACRO_NONE;
1448					}
1449
1450					if (!CHtmlUrlValidator::validate($url['url'], $url_validate_options)) {
1451						self::exception(ZBX_API_ERROR_PARAMETERS, _('Wrong value for url field.'));
1452					}
1453
1454					unset($urlNames[$url['name']]);
1455				}
1456			}
1457
1458			if (array_key_exists('selements', $map) && !is_array($map['selements'])) {
1459				self::exception(ZBX_API_ERROR_PARAMETERS, _('Incorrect arguments passed to function.'));
1460			}
1461
1462			if (array_key_exists('selements', $map)) {
1463				foreach (array_values($map['selements']) as $selement_index => $selement) {
1464					$this->validateSelementTags($selement, '/'.($map_index + 1).'/selements/'.($selement_index + 1));
1465				}
1466			}
1467
1468			// Map selement links.
1469			if (array_key_exists('links', $map) && $map['links']) {
1470				$selementids = zbx_objectValues($map['selements'], 'selementid');
1471
1472				foreach ($map['links'] as $link) {
1473					if (!in_array($link['selementid1'], $selementids)) {
1474						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1475							'Link selementid1 field is pointing to a nonexistent map selement ID "%1$s" for map "%2$s".',
1476							$link['selementid1'],
1477							$map['name']
1478						));
1479					}
1480
1481					if (!in_array($link['selementid2'], $selementids)) {
1482						self::exception(ZBX_API_ERROR_PARAMETERS, _s(
1483							'Link selementid2 field is pointing to a nonexistent map selement ID "%1$s" for map "%2$s".',
1484							$link['selementid2'],
1485							$map['name']
1486						));
1487					}
1488				}
1489			}
1490		}
1491
1492		// Validate circular reference.
1493		foreach ($maps as &$map) {
1494			$map = array_merge($db_maps[$map['sysmapid']], $map);
1495			$this->cref_maps[$map['sysmapid']] = $map;
1496		}
1497		unset($map);
1498
1499		$this->validateCircularReference($maps);
1500	}
1501
1502	/**
1503	 * Validate Map element tag properties.
1504	 *
1505	 * @param array  $selement['evaltype']
1506	 * @param array  $selement['tags']
1507	 * @param string $selement['tags'][]['tag']
1508	 * @param string $selement['tags'][]['value']
1509	 * @param int    $selement['tags'][]['operator']
1510	 * @param string $path
1511	 *
1512	 * @throws APIException if input is invalid.
1513	 */
1514	protected function validateSelementTags(array $selement, string $path): void {
1515		if (!array_key_exists('tags', $selement)) {
1516			return;
1517		}
1518
1519		$api_input_rules = ['type' => API_OBJECT, 'fields' => [
1520			'evaltype'	=>		['type' => API_INT32, 'in' => implode(',', [CONDITION_EVAL_TYPE_AND_OR, CONDITION_EVAL_TYPE_OR]), 'default' => DB::getDefault('sysmaps_elements', 'evaltype')],
1521			'tags'		=>		['type' => API_OBJECTS, 'uniq' => [['tag', 'value']], 'fields' => [
1522				'tag'		=>		['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('sysmaps_element_tag', 'tag')],
1523				'value'		=>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmaps_element_tag', 'value'), 'default' => DB::getDefault('sysmaps_element_tag', 'value')],
1524				'operator'	=>		['type' => API_STRING_UTF8, 'in' => implode(',', [TAG_OPERATOR_LIKE, TAG_OPERATOR_EQUAL, TAG_OPERATOR_NOT_LIKE, TAG_OPERATOR_NOT_EQUAL, TAG_OPERATOR_EXISTS, TAG_OPERATOR_NOT_EXISTS]), 'default' => DB::getDefault('sysmaps_element_tag', 'operator')]
1525			]]
1526		]];
1527
1528		$data = array_intersect_key($selement, $api_input_rules['fields']);
1529		if (!CApiInputValidator::validate($api_input_rules, $data, $path, $error)) {
1530			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1531		}
1532	}
1533
1534	/**
1535	 * Hash of maps data for circular reference validation. Map id is used as key.
1536	 *
1537	 * @var array
1538	 */
1539	protected $cref_maps;
1540
1541	/**
1542	 * Validate maps for circular reference.
1543	 *
1544	 * @param array $maps   Array of maps to be validated for circular reference.
1545	 *
1546	 * @throws APIException if input is invalid.
1547	 */
1548	protected function validateCircularReference(array $maps) {
1549		foreach ($maps as $map) {
1550			if (!array_key_exists('selements', $map) || !$map['selements']) {
1551				continue;
1552			}
1553			$cref_mapids = array_key_exists('sysmapid', $map) ? [$map['sysmapid']] : [];
1554
1555			foreach ($map['selements'] as $selement) {
1556				if (!$this->validateCircularReferenceRecursive($selement, $cref_mapids)) {
1557					self::exception(ZBX_API_ERROR_PARAMETERS,
1558						_s('Cannot add map element of the map "%1$s" due to circular reference.', $map['name'])
1559					);
1560				}
1561			}
1562		}
1563	}
1564
1565	/**
1566	 * Recursive map element circular reference validation.
1567	 *
1568	 * @param array $selement       Map selement data array.
1569	 * @param array $cref_mapids    Array of map ids for current recursion step.
1570	 *
1571	 * @return bool
1572	 */
1573	protected function validateCircularReferenceRecursive(array $selement, &$cref_mapids) {
1574		if ($selement['elementtype'] != SYSMAP_ELEMENT_TYPE_MAP) {
1575			return true;
1576		}
1577
1578		$sysmapid = $selement['elements'][0]['sysmapid'];
1579
1580		if ($sysmapid !== null && !array_key_exists($sysmapid, $this->cref_maps)) {
1581			$db_maps = DB::select($this->tableName, [
1582				'output' => ['name'],
1583				'filter' => ['sysmapid' => $sysmapid]
1584			]);
1585
1586			if ($db_maps) {
1587				$db_map = $db_maps[0];
1588				$db_map['selements'] = [];
1589				$selements = DB::select('sysmaps_elements', [
1590					'output' => ['elementid'],
1591					'filter' => [
1592						'sysmapid' => $sysmapid,
1593						'elementtype' => SYSMAP_ELEMENT_TYPE_MAP
1594					]
1595				]);
1596
1597				foreach ($selements as $selement) {
1598					$db_map['selements'][] = [
1599						'elementtype' => SYSMAP_ELEMENT_TYPE_MAP,
1600						'elements' => [
1601							['sysmapid' => $selement['elementid']]
1602						]
1603					];
1604				}
1605
1606				$this->cref_maps[$sysmapid] = $db_map;
1607				unset($selements);
1608			}
1609			else {
1610				self::exception(ZBX_API_ERROR_PARAMETERS, _('No permissions to referred object or it does not exist!'));
1611			}
1612		}
1613
1614		if (in_array($sysmapid, $cref_mapids)) {
1615			$cref_mapids[] = $sysmapid;
1616			return false;
1617		}
1618
1619		// Find maps that reference the current element, and if one has selements, check all of them recursively.
1620		if ($sysmapid !== null && array_key_exists('selements', $this->cref_maps[$sysmapid])
1621				&& is_array($this->cref_maps[$sysmapid]['selements'])) {
1622			$cref_mapids[] = $sysmapid;
1623
1624			foreach ($this->cref_maps[$sysmapid]['selements'] as $selement) {
1625				if (!$this->validateCircularReferenceRecursive($selement, $cref_mapids)) {
1626					return false;
1627				}
1628			}
1629
1630			array_pop($cref_mapids);
1631		}
1632
1633		return true;
1634	}
1635
1636	/**
1637	 * Add map.
1638	 *
1639	 * @param array  $maps
1640	 * @param string $maps['name']
1641	 * @param array  $maps['width']
1642	 * @param int    $maps['height']
1643	 * @param string $maps['backgroundid']
1644	 * @param string $maps['highlight']
1645	 * @param array  $maps['label_type']
1646	 * @param int    $maps['label_location']
1647	 * @param int    $maps['grid_size']			size of one grid cell. 100 refers to 100x100 and so on.
1648	 * @param int    $maps['grid_show']			does grid need to be shown. Constants: SYSMAP_GRID_SHOW_ON / SYSMAP_GRID_SHOW_OFF
1649	 * @param int    $maps['grid_align']		do elements need to be aligned to the grid. Constants: SYSMAP_GRID_ALIGN_ON / SYSMAP_GRID_ALIGN_OFF
1650	 *
1651	 * @return array
1652	 */
1653	public function create($maps) {
1654		$maps = zbx_toArray($maps);
1655
1656		$this->validateCreate($maps);
1657
1658		foreach ($maps as &$map) {
1659			$map['userid'] = array_key_exists('userid', $map) ? $map['userid'] : self::$userData['userid'];
1660		}
1661		unset($map);
1662
1663		$sysmapids = DB::insert('sysmaps', $maps);
1664
1665		$shared_users = [];
1666		$shared_user_groups = [];
1667		$urls = [];
1668		$shapes = [];
1669		$selements = [];
1670		$links = [];
1671		$api_shape_rules = ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [
1672			'type' =>				['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [SYSMAP_SHAPE_TYPE_RECTANGLE, SYSMAP_SHAPE_TYPE_ELLIPSE])],
1673			'x' =>					['type' => API_INT32],
1674			'y' =>					['type' => API_INT32],
1675			'width' =>				['type' => API_INT32],
1676			'height' =>				['type' => API_INT32],
1677			'font' =>				['type' => API_INT32, 'in' => '0:12'],
1678			'font_size' =>			['type' => API_INT32, 'in' => '1:250'],
1679			'text_halign' =>		['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_LABEL_HALIGN_CENTER, SYSMAP_SHAPE_LABEL_HALIGN_LEFT, SYSMAP_SHAPE_LABEL_HALIGN_RIGHT])],
1680			'text_valign' =>		['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_LABEL_VALIGN_MIDDLE, SYSMAP_SHAPE_LABEL_VALIGN_TOP, SYSMAP_SHAPE_LABEL_VALIGN_BOTTOM])],
1681			'border_type' =>		['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_BORDER_TYPE_NONE, SYSMAP_SHAPE_BORDER_TYPE_SOLID, SYSMAP_SHAPE_BORDER_TYPE_DOTTED, SYSMAP_SHAPE_BORDER_TYPE_DASHED])],
1682			'border_width' =>		['type' => API_INT32, 'in' => '0:50'],
1683			'border_color' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'border_color')],
1684			'background_color' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'background_color')],
1685			'font_color' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'font_color')],
1686			'text' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'text')],
1687			'zindex' =>				['type' => API_INT32]
1688		]];
1689		$api_line_rules = ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [
1690			'x1' =>					['type' => API_INT32],
1691			'y1' =>					['type' => API_INT32],
1692			'x2' =>					['type' => API_INT32],
1693			'y2' =>					['type' => API_INT32],
1694			'line_type' =>			['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_BORDER_TYPE_NONE, SYSMAP_SHAPE_BORDER_TYPE_SOLID, SYSMAP_SHAPE_BORDER_TYPE_DOTTED, SYSMAP_SHAPE_BORDER_TYPE_DASHED])],
1695			'line_width' =>			['type' => API_INT32, 'in' => '0:50'],
1696			'line_color' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'border_color')],
1697			'zindex' =>				['type' => API_INT32]
1698		]];
1699		$default_shape_width = DB::getDefault('sysmap_shape', 'width');
1700		$default_shape_height = DB::getDefault('sysmap_shape', 'height');
1701
1702		foreach ($sysmapids as $key => $sysmapid) {
1703			// Map user shares.
1704			if (array_key_exists('users', $maps[$key])) {
1705				foreach ($maps[$key]['users'] as $user) {
1706					$shared_users[] = [
1707						'sysmapid' => $sysmapid,
1708						'userid' => $user['userid'],
1709						'permission' => $user['permission']
1710					];
1711				}
1712			}
1713
1714			// Map user group shares.
1715			if (array_key_exists('userGroups', $maps[$key])) {
1716				foreach ($maps[$key]['userGroups'] as $user_group) {
1717					$shared_user_groups[] = [
1718						'sysmapid' => $sysmapid,
1719						'usrgrpid' => $user_group['usrgrpid'],
1720						'permission' => $user_group['permission']
1721					];
1722				}
1723			}
1724
1725			if (array_key_exists('urls', $maps[$key])) {
1726				foreach ($maps[$key]['urls'] as $url) {
1727					$url['sysmapid'] = $sysmapid;
1728					$urls[] = $url;
1729				}
1730			}
1731
1732			if (array_key_exists('selements', $maps[$key])) {
1733				foreach ($maps[$key]['selements'] as $snum => $selement) {
1734					$maps[$key]['selements'][$snum]['sysmapid'] = $sysmapid;
1735				}
1736
1737				$selements = array_merge($selements, $maps[$key]['selements']);
1738			}
1739
1740			if (array_key_exists('shapes', $maps[$key])) {
1741				$path = '/'.($key + 1).'/shape';
1742				$api_shape_rules['fields']['x']['in'] = '0:'.$maps[$key]['width'];
1743				$api_shape_rules['fields']['y']['in'] = '0:'.$maps[$key]['height'];
1744				$api_shape_rules['fields']['width']['in'] = '1:'.$maps[$key]['width'];
1745				$api_shape_rules['fields']['height']['in'] = '1:'.$maps[$key]['height'];
1746
1747				foreach ($maps[$key]['shapes'] as &$shape) {
1748					$shape['width'] = array_key_exists('width', $shape) ? $shape['width'] : $default_shape_width;
1749					$shape['height'] = array_key_exists('height', $shape) ? $shape['height'] : $default_shape_height;
1750				}
1751				unset($shape);
1752
1753				if (!CApiInputValidator::validate($api_shape_rules, $maps[$key]['shapes'], $path, $error)) {
1754					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1755				}
1756
1757				foreach ($maps[$key]['shapes'] as $snum => $shape) {
1758					$maps[$key]['shapes'][$snum]['sysmapid'] = $sysmapid;
1759				}
1760
1761				$shapes = array_merge($shapes, $maps[$key]['shapes']);
1762			}
1763
1764			if (array_key_exists('lines', $maps[$key])) {
1765				$path = '/'.($key + 1).'/line';
1766				$api_line_rules['fields']['x1']['in'] = '0:'.$maps[$key]['width'];
1767				$api_line_rules['fields']['y1']['in'] = '0:'.$maps[$key]['height'];
1768				$api_line_rules['fields']['x2']['in'] = '0:'.$maps[$key]['width'];
1769				$api_line_rules['fields']['y2']['in'] = '0:'.$maps[$key]['height'];
1770
1771				foreach ($maps[$key]['lines'] as &$line) {
1772					$line['x2'] = array_key_exists('x2', $line) ? $line['x2'] : $default_shape_width;
1773					$line['y2'] = array_key_exists('y2', $line) ? $line['y2'] : $default_shape_height;
1774				}
1775				unset($line);
1776
1777				if (!CApiInputValidator::validate($api_line_rules, $maps[$key]['lines'], $path, $error)) {
1778					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
1779				}
1780
1781				foreach ($maps[$key]['lines'] as $line) {
1782					$shape = CMapHelper::convertLineToShape($line);
1783					$shape['sysmapid'] = $sysmapid;
1784					$shapes[] = $shape;
1785				}
1786			}
1787
1788			if (array_key_exists('links', $maps[$key])) {
1789				foreach ($maps[$key]['links'] as $lnum => $link) {
1790					$maps[$key]['links'][$lnum]['sysmapid'] = $sysmapid;
1791				}
1792
1793				$links = array_merge($links, $maps[$key]['links']);
1794			}
1795		}
1796
1797		DB::insert('sysmap_user', $shared_users);
1798		DB::insert('sysmap_usrgrp', $shared_user_groups);
1799		DB::insert('sysmap_url', $urls);
1800
1801		if ($selements) {
1802			$selementids = $this->createSelements($selements);
1803
1804			if ($links) {
1805				$map_virt_selements = [];
1806				foreach ($selementids['selementids'] as $key => $selementid) {
1807					$map_virt_selements[$selements[$key]['selementid']] = $selementid;
1808				}
1809
1810				foreach ($links as $key => $link) {
1811					$links[$key]['selementid1'] = $map_virt_selements[$link['selementid1']];
1812					$links[$key]['selementid2'] = $map_virt_selements[$link['selementid2']];
1813				}
1814				unset($map_virt_selements);
1815
1816				$linkids = $this->createLinks($links);
1817
1818				$link_triggers = [];
1819				foreach ($linkids['linkids'] as $key => $linkId) {
1820					if (!array_key_exists('linktriggers', $links[$key])) {
1821						continue;
1822					}
1823
1824					foreach ($links[$key]['linktriggers'] as $link_trigger) {
1825						$link_trigger['linkid'] = $linkId;
1826						$link_triggers[] = $link_trigger;
1827					}
1828				}
1829
1830				if ($link_triggers) {
1831					$this->createLinkTriggers($link_triggers);
1832				}
1833			}
1834		}
1835
1836		if ($shapes) {
1837			$this->createShapes($shapes);
1838		}
1839
1840		return ['sysmapids' => $sysmapids];
1841	}
1842
1843	/**
1844	 * Update map.
1845	 *
1846	 * @param array  $maps						multidimensional array with Hosts data
1847	 * @param string $maps['sysmapid']
1848	 * @param string $maps['name']
1849	 * @param array  $maps['width']
1850	 * @param int    $maps['height']
1851	 * @param string $maps['backgroundid']
1852	 * @param array  $maps['label_type']
1853	 * @param int    $maps['label_location']
1854	 * @param int    $maps['grid_size']			size of one grid cell. 100 refers to 100x100 and so on.
1855	 * @param int    $maps['grid_show']			does grid need to be shown. Constants: SYSMAP_GRID_SHOW_ON / SYSMAP_GRID_SHOW_OFF
1856	 * @param int    $maps['grid_align']		do elements need to be aligned to the grid. Constants: SYSMAP_GRID_ALIGN_ON / SYSMAP_GRID_ALIGN_OFF
1857	 *
1858	 * @return array
1859	 */
1860	public function update(array $maps) {
1861		$maps = zbx_toArray($maps);
1862		$sysmapids = zbx_objectValues($maps, 'sysmapid');
1863
1864		$db_maps = $this->get([
1865			'output' => API_OUTPUT_EXTEND,
1866			'sysmapids' => zbx_objectValues($maps, 'sysmapid'),
1867			'selectLinks' => API_OUTPUT_EXTEND,
1868			'selectSelements' => API_OUTPUT_EXTEND,
1869			'selectShapes' => ['sysmap_shapeid', 'type', 'x', 'y', 'width', 'height', 'text', 'font', 'font_size',
1870				'font_color', 'text_halign', 'text_valign', 'border_type', 'border_width', 'border_color',
1871				'background_color', 'zindex'
1872			],
1873			'selectLines' => ['sysmap_shapeid', 'x1', 'y1', 'x2', 'y2', 'line_type', 'line_width', 'line_color',
1874				'zindex'
1875			],
1876			'selectUrls' => ['sysmapid', 'sysmapurlid', 'name', 'url', 'elementtype'],
1877			'selectUsers' => ['sysmapuserid', 'sysmapid', 'userid', 'permission'],
1878			'selectUserGroups' => ['sysmapusrgrpid', 'sysmapid', 'usrgrpid', 'permission'],
1879			'editable' => true,
1880			'preservekeys' => true
1881		]);
1882
1883		$this->validateUpdate($maps, $db_maps);
1884
1885		$update_maps = [];
1886		$url_ids_to_delete = [];
1887		$urls_to_update = [];
1888		$urls_to_add = [];
1889		$selements_to_delete = [];
1890		$selements_to_update = [];
1891		$selements_to_add = [];
1892		$shapes_to_delete = [];
1893		$shapes_to_update = [];
1894		$shapes_to_add = [];
1895		$links_to_delete = [];
1896		$links_to_update = [];
1897		$links_to_add = [];
1898		$shared_userids_to_delete = [];
1899		$shared_users_to_update = [];
1900		$shared_users_to_add = [];
1901		$shared_user_groupids_to_delete = [];
1902		$shared_user_groups_to_update = [];
1903		$shared_user_groups_to_add = [];
1904		$api_shape_rules = ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [
1905			'sysmap_shapeid' =>		['type' => API_ID],
1906			'type' =>				['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_TYPE_RECTANGLE, SYSMAP_SHAPE_TYPE_ELLIPSE])],
1907			'x' =>					['type' => API_INT32],
1908			'y' =>					['type' => API_INT32],
1909			'width' =>				['type' => API_INT32],
1910			'height' =>				['type' => API_INT32],
1911			'font' =>				['type' => API_INT32, 'in' => '0:12'],
1912			'font_size' =>			['type' => API_INT32, 'in' => '1:250'],
1913			'text_halign' =>		['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_LABEL_HALIGN_CENTER, SYSMAP_SHAPE_LABEL_HALIGN_LEFT, SYSMAP_SHAPE_LABEL_HALIGN_RIGHT])],
1914			'text_valign' =>		['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_LABEL_VALIGN_MIDDLE, SYSMAP_SHAPE_LABEL_VALIGN_TOP, SYSMAP_SHAPE_LABEL_VALIGN_BOTTOM])],
1915			'border_type' =>		['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_BORDER_TYPE_NONE, SYSMAP_SHAPE_BORDER_TYPE_SOLID, SYSMAP_SHAPE_BORDER_TYPE_DOTTED, SYSMAP_SHAPE_BORDER_TYPE_DASHED])],
1916			'border_width' =>		['type' => API_INT32, 'in' => '0:50'],
1917			'border_color' =>		['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'border_color')],
1918			'background_color' =>	['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'background_color')],
1919			'font_color' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'font_color')],
1920			'text' =>				['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'text')],
1921			'zindex' =>				['type' => API_INT32]
1922		]];
1923		$api_line_rules = ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [
1924			'sysmap_shapeid' =>		['type' => API_ID],
1925			'x1' =>					['type' => API_INT32],
1926			'y1' =>					['type' => API_INT32],
1927			'x2' =>					['type' => API_INT32],
1928			'y2' =>					['type' => API_INT32],
1929			'line_type' =>			['type' => API_INT32, 'in' => implode(',', [SYSMAP_SHAPE_BORDER_TYPE_NONE, SYSMAP_SHAPE_BORDER_TYPE_SOLID, SYSMAP_SHAPE_BORDER_TYPE_DOTTED, SYSMAP_SHAPE_BORDER_TYPE_DASHED])],
1930			'line_width' =>			['type' => API_INT32, 'in' => '0:50'],
1931			'line_color' =>			['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('sysmap_shape', 'border_color')],
1932			'zindex' =>				['type' => API_INT32]
1933		]];
1934		$default_shape_width = DB::getDefault('sysmap_shape', 'width');
1935		$default_shape_height = DB::getDefault('sysmap_shape', 'height');
1936
1937		foreach ($maps as $index => $map) {
1938			$update_maps[] = [
1939				'values' => $map,
1940				'where' => ['sysmapid' => $map['sysmapid']]
1941			];
1942
1943			$db_map = $db_maps[$map['sysmapid']];
1944
1945			// Map user shares.
1946			if (array_key_exists('users', $map)) {
1947				$user_shares_diff = zbx_array_diff($map['users'], $db_map['users'], 'userid');
1948
1949				foreach ($user_shares_diff['both'] as $update_user_share) {
1950					$shared_users_to_update[] = [
1951						'values' => $update_user_share,
1952						'where' => ['userid' => $update_user_share['userid'], 'sysmapid' => $map['sysmapid']]
1953					];
1954				}
1955
1956				foreach ($user_shares_diff['first'] as $new_shared_user) {
1957					$new_shared_user['sysmapid'] = $map['sysmapid'];
1958					$shared_users_to_add[] = $new_shared_user;
1959				}
1960
1961				$shared_userids_to_delete = array_merge($shared_userids_to_delete,
1962					zbx_objectValues($user_shares_diff['second'], 'sysmapuserid')
1963				);
1964			}
1965
1966			// Map user group shares.
1967			if (array_key_exists('userGroups', $map)) {
1968				$user_group_shares_diff = zbx_array_diff($map['userGroups'], $db_map['userGroups'],
1969					'usrgrpid'
1970				);
1971
1972				foreach ($user_group_shares_diff['both'] as $update_user_share) {
1973					$shared_user_groups_to_update[] = [
1974						'values' => $update_user_share,
1975						'where' => ['usrgrpid' => $update_user_share['usrgrpid'], 'sysmapid' => $map['sysmapid']]
1976					];
1977				}
1978
1979				foreach ($user_group_shares_diff['first'] as $new_shared_user_group) {
1980					$new_shared_user_group['sysmapid'] = $map['sysmapid'];
1981					$shared_user_groups_to_add[] = $new_shared_user_group;
1982				}
1983
1984				$shared_user_groupids_to_delete = array_merge($shared_user_groupids_to_delete,
1985					zbx_objectValues($user_group_shares_diff['second'], 'sysmapusrgrpid')
1986				);
1987			}
1988
1989			// Urls.
1990			if (array_key_exists('urls', $map)) {
1991				$url_diff = zbx_array_diff($map['urls'], $db_map['urls'], 'name');
1992
1993				foreach ($url_diff['both'] as $updateUrl) {
1994					$urls_to_update[] = [
1995						'values' => $updateUrl,
1996						'where' => ['name' => $updateUrl['name'], 'sysmapid' => $map['sysmapid']]
1997					];
1998				}
1999
2000				foreach ($url_diff['first'] as $new_url) {
2001					$new_url['sysmapid'] = $map['sysmapid'];
2002					$urls_to_add[] = $new_url;
2003				}
2004
2005				$url_ids_to_delete = array_merge($url_ids_to_delete,
2006					zbx_objectValues($url_diff['second'], 'sysmapurlid')
2007				);
2008			}
2009
2010			// Map elements.
2011			if (array_key_exists('selements', $map)) {
2012				$selement_diff = zbx_array_diff($map['selements'], $db_map['selements'], 'selementid');
2013
2014				// We need sysmapid for add operations.
2015				foreach ($selement_diff['first'] as $new_selement) {
2016					$new_selement['sysmapid'] = $map['sysmapid'];
2017					$selements_to_add[] = $new_selement;
2018				}
2019
2020				foreach ($selement_diff['both'] as &$selement) {
2021					$selement['sysmapid'] = $map['sysmapid'];
2022				}
2023				unset($selement);
2024
2025				$selements_to_update = array_merge($selements_to_update, $selement_diff['both']);
2026				$selements_to_delete = array_merge($selements_to_delete, $selement_diff['second']);
2027			}
2028
2029			$map_width = array_key_exists('width', $map) ? $map['width'] : $db_map['width'];
2030			$map_height = array_key_exists('height', $map) ? $map['height'] : $db_map['height'];
2031
2032			// Map shapes.
2033			if (array_key_exists('shapes', $map)) {
2034				$map['shapes'] = array_values($map['shapes']);
2035
2036				foreach ($map['shapes'] as &$shape) {
2037					$shape['width'] = array_key_exists('width', $shape) ? $shape['width'] : $default_shape_width;
2038					$shape['height'] = array_key_exists('height', $shape) ? $shape['height'] : $default_shape_height;
2039				}
2040				unset($shape);
2041
2042				$shape_diff = zbx_array_diff($map['shapes'], $db_map['shapes'], 'sysmap_shapeid');
2043
2044				$path = '/'.($index + 1).'/shape';
2045				foreach ($shape_diff['first'] as $new_shape) {
2046					if (array_key_exists('sysmap_shapeid', $new_shape)) {
2047						self::exception(ZBX_API_ERROR_PARAMETERS,
2048							_('No permissions to referred object or it does not exist!')
2049						);
2050					}
2051				}
2052
2053				unset($api_shape_rules['fields']['sysmap_shapeid']);
2054				$api_shape_rules['fields']['type']['flags'] = API_REQUIRED;
2055				$api_shape_rules['fields']['x']['in'] = '0:'.$map_width;
2056				$api_shape_rules['fields']['y']['in'] = '0:'.$map_height;
2057				$api_shape_rules['fields']['width']['in'] = '1:'.$map_width;
2058				$api_shape_rules['fields']['height']['in'] = '1:'.$map_height;
2059
2060				if (!CApiInputValidator::validate($api_shape_rules, $shape_diff['first'], $path, $error)) {
2061					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
2062				}
2063
2064				$api_shape_rules['fields']['sysmap_shapeid'] = ['type' => API_ID, 'flags' => API_REQUIRED];
2065				$api_shape_rules['fields']['type']['flags'] = 0;
2066				if (!CApiInputValidator::validate($api_shape_rules, $shape_diff['both'], $path, $error)) {
2067					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
2068				}
2069
2070				$shapes_to_update = array_merge($shapes_to_update, $shape_diff['both']);
2071				$shapes_to_delete = array_merge($shapes_to_delete, $shape_diff['second']);
2072
2073				// We need sysmapid for add operations.
2074				foreach ($shape_diff['first'] as $new_shape) {
2075					$new_shape['sysmapid'] = $map['sysmapid'];
2076					$shapes_to_add[] = $new_shape;
2077				}
2078			}
2079
2080			if (array_key_exists('lines', $map)) {
2081				$map['lines'] = array_values($map['lines']);
2082				$shapes = [];
2083
2084				$api_line_rules['fields']['x1']['in'] = '0:'.$map_width;
2085				$api_line_rules['fields']['y1']['in'] = '0:'.$map_height;
2086				$api_line_rules['fields']['x2']['in'] = '0:'.$map_width;
2087				$api_line_rules['fields']['y2']['in'] = '0:'.$map_height;
2088
2089				$path = '/'.($index + 1).'/line';
2090
2091				foreach ($map['lines'] as &$line) {
2092					$line['x2'] = array_key_exists('x2', $line) ? $line['x2'] : $default_shape_width;
2093					$line['y2'] = array_key_exists('y2', $line) ? $line['y2'] : $default_shape_height;
2094				}
2095				unset($line);
2096
2097				if (!CApiInputValidator::validate($api_line_rules, $map['lines'], $path, $error)) {
2098					self::exception(ZBX_API_ERROR_PARAMETERS, $error);
2099				}
2100
2101				foreach ($map['lines'] as $line) {
2102					$shapes[] = CMapHelper::convertLineToShape($line);
2103				}
2104
2105				$line_diff = zbx_array_diff($shapes, $db_map['lines'], 'sysmap_shapeid');
2106
2107				foreach ($line_diff['first'] as $new_line) {
2108					if (array_key_exists('sysmap_shapeid', $new_line)) {
2109						self::exception(ZBX_API_ERROR_PARAMETERS,
2110							_('No permissions to referred object or it does not exist!')
2111						);
2112					}
2113				}
2114
2115				$shapes_to_update = array_merge($shapes_to_update, $line_diff['both']);
2116				$shapes_to_delete = array_merge($shapes_to_delete, $line_diff['second']);
2117
2118				// We need sysmapid for add operations.
2119				foreach ($line_diff['first'] as $new_shape) {
2120					$new_shape['sysmapid'] = $map['sysmapid'];
2121					$shapes_to_add[] = $new_shape;
2122				}
2123			}
2124
2125			// Links.
2126			if (array_key_exists('links', $map)) {
2127				$link_diff = zbx_array_diff($map['links'], $db_map['links'], 'linkid');
2128
2129				// We need sysmapId for add operations.
2130				foreach ($link_diff['first'] as $newLink) {
2131					$newLink['sysmapid'] = $map['sysmapid'];
2132
2133					$links_to_add[] = $newLink;
2134				}
2135
2136				$links_to_update = array_merge($links_to_update, $link_diff['both']);
2137				$links_to_delete = array_merge($links_to_delete, $link_diff['second']);
2138			}
2139		}
2140
2141		DB::update('sysmaps', $update_maps);
2142
2143		// User shares.
2144		DB::insert('sysmap_user', $shared_users_to_add);
2145		DB::update('sysmap_user', $shared_users_to_update);
2146
2147		if ($shared_userids_to_delete) {
2148			DB::delete('sysmap_user', ['sysmapuserid' => $shared_userids_to_delete]);
2149		}
2150
2151		// User group shares.
2152		DB::insert('sysmap_usrgrp', $shared_user_groups_to_add);
2153		DB::update('sysmap_usrgrp', $shared_user_groups_to_update);
2154
2155		if ($shared_user_groupids_to_delete) {
2156			DB::delete('sysmap_usrgrp', ['sysmapusrgrpid' => $shared_user_groupids_to_delete]);
2157		}
2158
2159		// Urls.
2160		DB::insert('sysmap_url', $urls_to_add);
2161		DB::update('sysmap_url', $urls_to_update);
2162
2163		if ($url_ids_to_delete) {
2164			DB::delete('sysmap_url', ['sysmapurlid' => $url_ids_to_delete]);
2165		}
2166
2167		// Selements.
2168		$new_selementids = ['selementids' => []];
2169		if ($selements_to_add) {
2170			$new_selementids = $this->createSelements($selements_to_add);
2171		}
2172
2173		if ($selements_to_update) {
2174			$this->updateSelements($selements_to_update);
2175		}
2176
2177		if ($selements_to_delete) {
2178			$this->deleteSelements($selements_to_delete);
2179		}
2180
2181		if ($shapes_to_add) {
2182			$this->createShapes($shapes_to_add);
2183		}
2184
2185		if ($shapes_to_update) {
2186			$this->updateShapes($shapes_to_update);
2187		}
2188
2189		if ($shapes_to_delete) {
2190			$this->deleteShapes($shapes_to_delete);
2191		}
2192
2193		// Links.
2194		if ($links_to_add || $links_to_update) {
2195			$selements_names = [];
2196			foreach ($new_selementids['selementids'] as $key => $selementId) {
2197				$selements_names[$selements_to_add[$key]['selementid']] = $selementId;
2198			}
2199
2200			foreach ($selements_to_update as $selement) {
2201				$selements_names[$selement['selementid']] = $selement['selementid'];
2202			}
2203
2204			foreach ($links_to_add as $key => $link) {
2205				if (array_key_exists($link['selementid1'], $selements_names)) {
2206					$links_to_add[$key]['selementid1'] = $selements_names[$link['selementid1']];
2207				}
2208				if (array_key_exists($link['selementid2'], $selements_names)) {
2209					$links_to_add[$key]['selementid2'] = $selements_names[$link['selementid2']];
2210				}
2211			}
2212
2213			foreach ($links_to_update as $key => $link) {
2214				if (array_key_exists($link['selementid1'], $selements_names)) {
2215					$links_to_update[$key]['selementid1'] = $selements_names[$link['selementid1']];
2216				}
2217				if (array_key_exists($link['selementid2'], $selements_names)) {
2218					$links_to_update[$key]['selementid2'] = $selements_names[$link['selementid2']];
2219				}
2220			}
2221
2222			unset($selements_names);
2223		}
2224
2225		$new_linkids = ['linkids' => []];
2226		$update_linkids = ['linkids' => []];
2227
2228		if ($links_to_add) {
2229			$new_linkids = $this->createLinks($links_to_add);
2230		}
2231
2232		if ($links_to_update) {
2233			$update_linkids = $this->updateLinks($links_to_update);
2234		}
2235
2236		if ($links_to_delete) {
2237			$this->deleteLinks($links_to_delete);
2238		}
2239
2240		// Link triggers.
2241		$link_triggers_to_delete = [];
2242		$link_triggers_to_update = [];
2243		$link_triggers_to_add = [];
2244
2245		foreach ($new_linkids['linkids'] as $key => $linkid) {
2246			if (!array_key_exists('linktriggers', $links_to_add[$key])) {
2247				continue;
2248			}
2249
2250			foreach ($links_to_add[$key]['linktriggers'] as $link_trigger) {
2251				$link_trigger['linkid'] = $linkid;
2252				$link_triggers_to_add[] = $link_trigger;
2253			}
2254		}
2255
2256		$db_links = [];
2257
2258		$link_trigger_resource = DBselect(
2259			'SELECT slt.* FROM sysmaps_link_triggers slt WHERE '.dbConditionInt('slt.linkid', $update_linkids['linkids'])
2260		);
2261		while ($db_link_trigger = DBfetch($link_trigger_resource)) {
2262			zbx_subarray_push($db_links, $db_link_trigger['linkid'], $db_link_trigger);
2263		}
2264
2265		foreach ($update_linkids['linkids'] as $key => $linkid) {
2266			if (!array_key_exists('linktriggers', $links_to_update[$key])) {
2267				continue;
2268			}
2269
2270			$db_link_triggers = array_key_exists($linkid, $db_links) ? $db_links[$linkid] : [];
2271			$db_link_triggers_diff = zbx_array_diff($links_to_update[$key]['linktriggers'],
2272				$db_link_triggers, 'linktriggerid'
2273			);
2274
2275			foreach ($db_link_triggers_diff['first'] as $new_link_trigger) {
2276				$new_link_trigger['linkid'] = $linkid;
2277				$link_triggers_to_add[] = $new_link_trigger;
2278			}
2279
2280			$link_triggers_to_update = array_merge($link_triggers_to_update, $db_link_triggers_diff['both']);
2281			$link_triggers_to_delete = array_merge($link_triggers_to_delete, $db_link_triggers_diff['second']);
2282		}
2283
2284		if ($link_triggers_to_delete) {
2285			$this->deleteLinkTriggers($link_triggers_to_delete);
2286		}
2287
2288		if ($link_triggers_to_add) {
2289			$this->createLinkTriggers($link_triggers_to_add);
2290		}
2291
2292		if ($link_triggers_to_update) {
2293			$this->updateLinkTriggers($link_triggers_to_update);
2294		}
2295
2296		return ['sysmapids' => $sysmapids];
2297	}
2298
2299	/**
2300	 * Delete Map.
2301	 *
2302	 * @param array $sysmapids
2303	 *
2304	 * @return array
2305	 */
2306	public function delete(array $sysmapids) {
2307		$this->validateDelete($sysmapids);
2308
2309		DB::delete('sysmaps_elements', [
2310			'elementid' => $sysmapids,
2311			'elementtype' => SYSMAP_ELEMENT_TYPE_MAP
2312		]);
2313		DB::delete('profiles', [
2314			'idx' => 'web.maps.sysmapid',
2315			'value_id' => $sysmapids
2316		]);
2317		DB::delete('profiles', [
2318			'idx' => 'web.favorite.sysmapids',
2319			'source' => 'sysmapid',
2320			'value_id' => $sysmapids
2321		]);
2322		DB::delete('sysmaps', ['sysmapid' => $sysmapids]);
2323
2324		return ['sysmapids' => $sysmapids];
2325	}
2326
2327	protected function addRelatedObjects(array $options, array $result) {
2328		$result = parent::addRelatedObjects($options, $result);
2329
2330		$sysmapIds = array_keys($result);
2331
2332		// adding elements
2333		if ($options['selectSelements'] !== null && $options['selectSelements'] != API_OUTPUT_COUNT) {
2334			$selements = API::getApiService()->select('sysmaps_elements', [
2335				'output' => $this->outputExtend($options['selectSelements'], ['selementid', 'sysmapid', 'elementtype',
2336					'elementid', 'elementsubtype'
2337				]),
2338				'filter' => ['sysmapid' => $sysmapIds],
2339				'preservekeys' => true
2340			]);
2341			$relation_map = $this->createRelationMap($selements, 'sysmapid', 'selementid');
2342
2343			if ($this->outputIsRequested('elements', $options['selectSelements']) && $selements) {
2344				foreach ($selements as &$selement) {
2345					$selement['elements'] = [];
2346				}
2347				unset($selement);
2348
2349				$selement_triggers = DBselect(
2350					'SELECT st.selementid,st.triggerid,st.selement_triggerid'.
2351					' FROM sysmap_element_trigger st,triggers tr'.
2352					' WHERE '.dbConditionInt('st.selementid', array_keys($selements)).
2353						' AND st.triggerid=tr.triggerid'.
2354					' ORDER BY tr.priority DESC,st.selement_triggerid'
2355				);
2356				while ($selement_trigger = DBfetch($selement_triggers)) {
2357					$selements[$selement_trigger['selementid']]['elements'][] = [
2358						'triggerid' => $selement_trigger['triggerid']
2359					];
2360					if ($selements[$selement_trigger['selementid']]['elementid'] == 0) {
2361						$selements[$selement_trigger['selementid']]['elementid'] = $selement_trigger['triggerid'];
2362					}
2363				}
2364
2365				$single_element_types = [SYSMAP_ELEMENT_TYPE_HOST, SYSMAP_ELEMENT_TYPE_MAP,
2366					SYSMAP_ELEMENT_TYPE_HOST_GROUP
2367				];
2368
2369				foreach ($selements as &$selement) {
2370					if (in_array($selement['elementtype'], $single_element_types)) {
2371						switch ($selement['elementtype']) {
2372							case SYSMAP_ELEMENT_TYPE_HOST_GROUP:
2373								$field = 'groupid';
2374								break;
2375
2376							case SYSMAP_ELEMENT_TYPE_HOST:
2377								$field = 'hostid';
2378								break;
2379
2380							case SYSMAP_ELEMENT_TYPE_MAP:
2381								$field = 'sysmapid';
2382								break;
2383						}
2384						$selement['elements'][] = [$field => $selement['elementid']];
2385					}
2386				}
2387				unset($selement);
2388			}
2389
2390			// add selement URLs
2391			if ($this->outputIsRequested('urls', $options['selectSelements'])) {
2392				foreach ($selements as &$selement) {
2393					$selement['urls'] = [];
2394				}
2395				unset($selement);
2396
2397				if (!is_null($options['expandUrls'])) {
2398					$dbMapUrls = DBselect(
2399						'SELECT su.sysmapurlid,su.sysmapid,su.name,su.url,su.elementtype'.
2400						' FROM sysmap_url su'.
2401						' WHERE '.dbConditionInt('su.sysmapid', $sysmapIds)
2402					);
2403					while ($mapUrl = DBfetch($dbMapUrls)) {
2404						foreach ($selements as $snum => $selement) {
2405							if (bccomp($selement['sysmapid'], $mapUrl['sysmapid']) == 0
2406									&& (($selement['elementtype'] == $mapUrl['elementtype']
2407											&& $selement['elementsubtype'] == SYSMAP_ELEMENT_SUBTYPE_HOST_GROUP
2408											)
2409											|| ($selement['elementsubtype'] == SYSMAP_ELEMENT_SUBTYPE_HOST_GROUP_ELEMENTS
2410													&& $mapUrl['elementtype'] == SYSMAP_ELEMENT_TYPE_HOST)
2411										)) {
2412								$selements[$snum]['urls'][] = $mapUrl;
2413							}
2414						}
2415					}
2416				}
2417
2418				$dbSelementUrls = DBselect(
2419					'SELECT seu.sysmapelementurlid,seu.selementid,seu.name,seu.url'.
2420					' FROM sysmap_element_url seu'.
2421					' WHERE '.dbConditionInt('seu.selementid', array_keys($selements))
2422				);
2423				while ($selementUrl = DBfetch($dbSelementUrls)) {
2424					$selements[$selementUrl['selementid']]['urls'][] = $selementUrl;
2425				}
2426
2427				if (!is_null($options['expandUrls'])) {
2428					$resolve_opt = ['resolve_element_urls' => true];
2429					$selements = CMacrosResolverHelper::resolveMacrosInMapElements($selements, $resolve_opt);
2430				}
2431			}
2432
2433			if ($this->outputIsRequested('tags', $options['selectSelements']) && $selements) {
2434				$db_tags = DBselect(
2435					'SELECT selementid, tag, value, operator'.
2436					' FROM sysmaps_element_tag'.
2437					' WHERE '.dbConditionInt('selementid', array_keys($selements))
2438				);
2439
2440				array_walk($selements, function (&$selement) {
2441					$selement['tags'] = [];
2442				});
2443
2444				while ($db_tag = DBfetch($db_tags)) {
2445					$selements[$db_tag['selementid']]['tags'][] = [
2446						'tag' => $db_tag['tag'],
2447						'value' => $db_tag['value'],
2448						'operator' => $db_tag['operator']
2449					];
2450				}
2451			}
2452
2453			if ($this->outputIsRequested('permission', $options['selectSelements']) && $selements) {
2454				if ($options['editable']) {
2455					foreach ($selements as &$selement) {
2456						$selement['permission'] = PERM_READ_WRITE;
2457					}
2458					unset($selement);
2459				}
2460				elseif (self::$userData['type'] == USER_TYPE_SUPER_ADMIN) {
2461					foreach ($selements as &$selement) {
2462						$selement['permission'] = PERM_READ;
2463					}
2464					unset($selement);
2465				}
2466				else {
2467					$ids = [
2468						SYSMAP_ELEMENT_TYPE_HOST_GROUP => [],
2469						SYSMAP_ELEMENT_TYPE_HOST => [],
2470						SYSMAP_ELEMENT_TYPE_TRIGGER => [],
2471						SYSMAP_ELEMENT_TYPE_MAP => []
2472					];
2473					$trigger_selementids = [];
2474
2475					foreach ($selements as &$selement) {
2476						switch ($selement['elementtype']) {
2477							case SYSMAP_ELEMENT_TYPE_HOST_GROUP:
2478							case SYSMAP_ELEMENT_TYPE_HOST:
2479							case SYSMAP_ELEMENT_TYPE_MAP:
2480								$ids[$selement['elementtype']][$selement['elementid']][] = $selement['selementid'];
2481								$selement['permission'] = PERM_NONE;
2482								break;
2483
2484							case SYSMAP_ELEMENT_TYPE_TRIGGER:
2485								$trigger_selementids[$selement['selementid']] = true;
2486								$selement['permission'] = PERM_NONE;
2487								break;
2488
2489							case SYSMAP_ELEMENT_TYPE_IMAGE:
2490								$selement['permission'] = PERM_READ;
2491								break;
2492						}
2493					}
2494					unset($selement);
2495
2496					$db[SYSMAP_ELEMENT_TYPE_HOST_GROUP] = $ids[SYSMAP_ELEMENT_TYPE_HOST_GROUP]
2497						? API::HostGroup()->get([
2498							'output' => [],
2499							'groupids' => array_keys($ids[SYSMAP_ELEMENT_TYPE_HOST_GROUP]),
2500							'preservekeys' => true
2501						])
2502						: [];
2503
2504					$db[SYSMAP_ELEMENT_TYPE_HOST] = $ids[SYSMAP_ELEMENT_TYPE_HOST]
2505						? API::Host()->get([
2506							'output' => [],
2507							'hostids' => array_keys($ids[SYSMAP_ELEMENT_TYPE_HOST]),
2508							'preservekeys' => true
2509						])
2510					: [];
2511
2512					$db[SYSMAP_ELEMENT_TYPE_MAP] = $ids[SYSMAP_ELEMENT_TYPE_MAP]
2513						? API::Map()->get([
2514							'output' => [],
2515							'sysmapids' => array_keys($ids[SYSMAP_ELEMENT_TYPE_MAP]),
2516							'preservekeys' => true
2517						])
2518						: [];
2519
2520					if ($trigger_selementids) {
2521						$db_selement_triggers = DBselect(
2522							'SELECT st.selementid,st.triggerid'.
2523							' FROM sysmap_element_trigger st'.
2524							' WHERE '.dbConditionInt('st.selementid', array_keys($trigger_selementids))
2525						);
2526
2527						while ($db_selement_trigger = DBfetch($db_selement_triggers)) {
2528							$ids[SYSMAP_ELEMENT_TYPE_TRIGGER][$db_selement_trigger['triggerid']][] =
2529								$db_selement_trigger['selementid'];
2530						}
2531					}
2532
2533					$db[SYSMAP_ELEMENT_TYPE_TRIGGER] = $ids[SYSMAP_ELEMENT_TYPE_TRIGGER]
2534						? API::Trigger()->get([
2535							'output' => [],
2536							'triggerids' => array_keys($ids[SYSMAP_ELEMENT_TYPE_TRIGGER]),
2537							'preservekeys' => true
2538						])
2539						: [];
2540
2541					foreach ($ids as $elementtype => $elementids) {
2542						foreach ($elementids as $elementid => $selementids) {
2543							if (array_key_exists($elementid, $db[$elementtype])) {
2544								foreach ($selementids as $selementid) {
2545									$selements[$selementid]['permission'] = PERM_READ;
2546								}
2547							}
2548						}
2549					}
2550				}
2551			}
2552
2553			foreach ($selements as &$selement) {
2554				unset($selement['elementid']);
2555			}
2556			unset($selement);
2557
2558			$selements = $this->unsetExtraFields($selements,
2559				['sysmapid', 'selementid', 'elementtype', 'elementsubtype'],
2560				$options['selectSelements']
2561			);
2562			$result = $relation_map->mapMany($result, $selements, 'selements');
2563		}
2564
2565		$shape_types = [];
2566		if ($options['selectShapes'] !== null && $options['selectShapes'] != API_OUTPUT_COUNT) {
2567			$shape_types = [SYSMAP_SHAPE_TYPE_RECTANGLE, SYSMAP_SHAPE_TYPE_ELLIPSE];
2568		}
2569
2570		if ($options['selectLines'] !== null && $options['selectLines'] != API_OUTPUT_COUNT) {
2571			$shape_types[] = SYSMAP_SHAPE_TYPE_LINE;
2572		}
2573
2574		// Adding shapes.
2575		if ($shape_types) {
2576			$fields = API_OUTPUT_EXTEND;
2577
2578			if ($options['selectShapes'] != API_OUTPUT_EXTEND && $options['selectLines'] != API_OUTPUT_EXTEND) {
2579				$fields = ['sysmap_shapeid', 'sysmapid', 'type'];
2580				$mapping = [
2581					'x1' => 'x',
2582					'y1' => 'y',
2583					'x2' => 'width',
2584					'y2' =>	'height',
2585					'line_type' => 'border_type',
2586					'line_width' => 'border_width',
2587					'line_color' => 'border_color'
2588				];
2589
2590				if (is_array($options['selectLines'])) {
2591					foreach ($mapping as $source_field => $target_field) {
2592						if (in_array($source_field, $options['selectLines'])) {
2593							$fields[] = $target_field;
2594						}
2595					}
2596				}
2597
2598				if (is_array($options['selectShapes'])) {
2599					$fields = array_merge($fields, $options['selectShapes']);
2600				}
2601			}
2602
2603			$db_shapes = API::getApiService()->select('sysmap_shape', [
2604				'output' => $fields,
2605				'filter' => ['sysmapid' => $sysmapIds, 'type' => $shape_types],
2606				'preservekeys' => true
2607			]);
2608
2609			$shapes = [];
2610			$lines = [];
2611			foreach ($db_shapes as $key => $db_shape) {
2612				if ($db_shape['type'] == SYSMAP_SHAPE_TYPE_LINE) {
2613					$lines[$key] = CMapHelper::convertShapeToLine($db_shape);
2614				}
2615				else {
2616					$shapes[$key] = $db_shape;
2617				}
2618			}
2619
2620			$relation_map = $this->createRelationMap($db_shapes, 'sysmapid', 'sysmap_shapeid');
2621
2622			if ($options['selectShapes'] !== null && $options['selectShapes'] != API_OUTPUT_COUNT) {
2623				$shapes = $this->unsetExtraFields($shapes, ['sysmap_shapeid', 'type', 'x', 'y', 'width', 'height',
2624					'text', 'font', 'font_size', 'font_color', 'text_halign', 'text_valign', 'border_type',
2625					'border_width', 'border_color', 'background_color', 'zindex'
2626				], $options['selectShapes']);
2627				$shapes = $this->unsetExtraFields($shapes, ['sysmapid'], null);
2628
2629				$result = $relation_map->mapMany($result, $shapes, 'shapes');
2630			}
2631
2632			if ($options['selectLines'] !== null && $options['selectLines'] != API_OUTPUT_COUNT) {
2633				$lines = $this->unsetExtraFields($lines, ['sysmap_shapeid', 'x1', 'x2', 'y1', 'y2', 'line_type',
2634					'line_width', 'line_color', 'zindex'
2635				], $options['selectLines']);
2636				$lines = $this->unsetExtraFields($lines, ['sysmapid', 'type'], null);
2637
2638				$result = $relation_map->mapMany($result, $lines, 'lines');
2639			}
2640		}
2641
2642		// adding icon maps
2643		if ($options['selectIconMap'] !== null && $options['selectIconMap'] != API_OUTPUT_COUNT) {
2644			$iconMaps = API::getApiService()->select($this->tableName(), [
2645				'output' => ['sysmapid', 'iconmapid'],
2646				'filter' => ['sysmapid' => $sysmapIds]
2647			]);
2648
2649			$relation_map = $this->createRelationMap($iconMaps, 'sysmapid', 'iconmapid');
2650
2651			$iconMaps = API::IconMap()->get([
2652				'output' => $this->outputExtend($options['selectIconMap'], ['iconmapid']),
2653				'iconmapids' => zbx_objectValues($iconMaps, 'iconmapid'),
2654				'preservekeys' => true
2655			]);
2656
2657			$iconMaps = $this->unsetExtraFields($iconMaps, ['iconmapid'], $options['selectIconMap']);
2658
2659			$result = $relation_map->mapOne($result, $iconMaps, 'iconmap');
2660		}
2661
2662		// adding links
2663		if ($options['selectLinks'] !== null && $options['selectLinks'] != API_OUTPUT_COUNT) {
2664			$links = API::getApiService()->select('sysmaps_links', [
2665				'output' => $this->outputExtend($options['selectLinks'], ['sysmapid', 'linkid']),
2666				'filter' => ['sysmapid' => $sysmapIds],
2667				'preservekeys' => true
2668			]);
2669			$relation_map = $this->createRelationMap($links, 'sysmapid', 'linkid');
2670
2671			// add link triggers
2672			if ($this->outputIsRequested('linktriggers', $options['selectLinks'])) {
2673				$linkTriggers = DBFetchArrayAssoc(DBselect(
2674					'SELECT DISTINCT slt.*'.
2675					' FROM sysmaps_link_triggers slt'.
2676					' WHERE '.dbConditionInt('slt.linkid', $relation_map->getRelatedIds())
2677				), 'linktriggerid');
2678				$linkTriggerRelationMap = $this->createRelationMap($linkTriggers, 'linkid', 'linktriggerid');
2679				$links = $linkTriggerRelationMap->mapMany($links, $linkTriggers, 'linktriggers');
2680			}
2681
2682			if ($this->outputIsRequested('permission', $options['selectLinks']) && $links) {
2683				if ($options['editable']) {
2684					foreach ($links as &$link) {
2685						$link['permission'] = PERM_READ_WRITE;
2686					}
2687					unset($link);
2688				}
2689				elseif (self::$userData['type'] == USER_TYPE_SUPER_ADMIN) {
2690					foreach ($links as &$link) {
2691						$link['permission'] = PERM_READ;
2692					}
2693					unset($link);
2694				}
2695				else {
2696					$db_link_triggers = DBselect(
2697						'SELECT slt.linkid,slt.triggerid'.
2698						' FROM sysmaps_link_triggers slt'.
2699						' WHERE '.dbConditionInt('slt.linkid', array_keys($links))
2700					);
2701
2702					$triggerids = [];
2703					$has_triggers = [];
2704
2705					while ($db_link_trigger = DBfetch($db_link_triggers)) {
2706						$triggerids[$db_link_trigger['triggerid']][] = $db_link_trigger['linkid'];
2707						$has_triggers[$db_link_trigger['linkid']] = true;
2708					}
2709
2710					foreach ($links as &$link) {
2711						$link['permission'] = array_key_exists($link['linkid'], $has_triggers) ? PERM_NONE : PERM_READ;
2712					}
2713					unset($link);
2714
2715					$db_triggers = $triggerids
2716						? API::Trigger()->get([
2717							'output' => [],
2718							'triggerids' => array_keys($triggerids),
2719							'preservekeys' => true
2720						])
2721						: [];
2722
2723					foreach ($triggerids as $triggerid => $linkids) {
2724						if (array_key_exists($triggerid, $db_triggers)) {
2725							foreach ($linkids as $linkid) {
2726								$links[$linkid]['permission'] = PERM_READ;
2727							}
2728						}
2729					}
2730				}
2731			}
2732
2733			$links = $this->unsetExtraFields($links, ['sysmapid', 'linkid'], $options['selectLinks']);
2734			$result = $relation_map->mapMany($result, $links, 'links');
2735		}
2736
2737		// adding urls
2738		if ($options['selectUrls'] !== null && $options['selectUrls'] != API_OUTPUT_COUNT) {
2739			$links = API::getApiService()->select('sysmap_url', [
2740				'output' => $this->outputExtend($options['selectUrls'], ['sysmapid', 'sysmapurlid']),
2741				'filter' => ['sysmapid' => $sysmapIds],
2742				'preservekeys' => true
2743			]);
2744			$relation_map = $this->createRelationMap($links, 'sysmapid', 'sysmapurlid');
2745
2746			$links = $this->unsetExtraFields($links, ['sysmapid', 'sysmapurlid'], $options['selectUrls']);
2747			$result = $relation_map->mapMany($result, $links, 'urls');
2748		}
2749
2750		// Adding user shares.
2751		if ($options['selectUsers'] !== null && $options['selectUsers'] != API_OUTPUT_COUNT) {
2752			$userids = [];
2753			$relation_map = $this->createRelationMap($result, 'sysmapid', 'userid', 'sysmap_user');
2754			$related_ids = $relation_map->getRelatedIds();
2755
2756			if ($related_ids) {
2757				// Get all allowed users.
2758				$users = API::User()->get([
2759					'output' => ['userid'],
2760					'userids' => $related_ids,
2761					'preservekeys' => true
2762				]);
2763
2764				$userids = zbx_objectValues($users, 'userid');
2765			}
2766
2767			if ($userids) {
2768				$users = API::getApiService()->select('sysmap_user', [
2769					'output' => $this->outputExtend($options['selectUsers'], ['sysmapid', 'userid']),
2770					'filter' => ['sysmapid' => $sysmapIds, 'userid' => $userids],
2771					'preservekeys' => true
2772				]);
2773
2774				$relation_map = $this->createRelationMap($users, 'sysmapid', 'sysmapuserid');
2775
2776				$users = $this->unsetExtraFields($users, ['sysmapuserid', 'userid', 'permission'],
2777					$options['selectUsers']
2778				);
2779
2780				foreach ($users as &$user) {
2781					unset($user['sysmapid']);
2782				}
2783				unset($user);
2784
2785				$result = $relation_map->mapMany($result, $users, 'users');
2786			}
2787			else {
2788				foreach ($result as &$row) {
2789					$row['users'] = [];
2790				}
2791				unset($row);
2792			}
2793		}
2794
2795		// Adding user group shares.
2796		if ($options['selectUserGroups'] !== null && $options['selectUserGroups'] != API_OUTPUT_COUNT) {
2797			$groupids = [];
2798			$relation_map = $this->createRelationMap($result, 'sysmapid', 'usrgrpid', 'sysmap_usrgrp');
2799			$related_ids = $relation_map->getRelatedIds();
2800
2801			if ($related_ids) {
2802				// Get all allowed groups.
2803				$groups = API::UserGroup()->get([
2804					'output' => ['usrgrpid'],
2805					'usrgrpids' => $related_ids,
2806					'preservekeys' => true
2807				]);
2808
2809				$groupids = zbx_objectValues($groups, 'usrgrpid');
2810			}
2811
2812			if ($groupids) {
2813				$user_groups = API::getApiService()->select('sysmap_usrgrp', [
2814					'output' => $this->outputExtend($options['selectUserGroups'], ['sysmapid', 'usrgrpid']),
2815					'filter' => ['sysmapid' => $sysmapIds, 'usrgrpid' => $groupids],
2816					'preservekeys' => true
2817				]);
2818
2819				$relation_map = $this->createRelationMap($user_groups, 'sysmapid', 'sysmapusrgrpid');
2820
2821				$user_groups = $this->unsetExtraFields($user_groups, ['sysmapusrgrpid', 'usrgrpid', 'permission'],
2822					$options['selectUserGroups']
2823				);
2824
2825				foreach ($user_groups as &$user_group) {
2826					unset($user_group['sysmapid']);
2827				}
2828				unset($user_group);
2829
2830				$result = $relation_map->mapMany($result, $user_groups, 'userGroups');
2831			}
2832			else {
2833				foreach ($result as &$row) {
2834					$row['userGroups'] = [];
2835				}
2836				unset($row);
2837			}
2838		}
2839
2840		return $result;
2841	}
2842}
2843