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 * Common class for dashboards API and template dashboards API.
24 */
25abstract class CDashboardGeneral extends CApiService {
26
27	protected const WIDGET_FIELD_TYPE_COLUMNS_FK = [
28		ZBX_WIDGET_FIELD_TYPE_GROUP => 'value_groupid',
29		ZBX_WIDGET_FIELD_TYPE_HOST => 'value_hostid',
30		ZBX_WIDGET_FIELD_TYPE_ITEM => 'value_itemid',
31		ZBX_WIDGET_FIELD_TYPE_ITEM_PROTOTYPE => 'value_itemid',
32		ZBX_WIDGET_FIELD_TYPE_GRAPH => 'value_graphid',
33		ZBX_WIDGET_FIELD_TYPE_GRAPH_PROTOTYPE => 'value_graphid',
34		ZBX_WIDGET_FIELD_TYPE_MAP => 'value_sysmapid'
35	];
36
37	protected const WIDGET_FIELD_TYPE_COLUMNS = [
38		ZBX_WIDGET_FIELD_TYPE_INT32 => 'value_int',
39		ZBX_WIDGET_FIELD_TYPE_STR => 'value_str'
40	] + self::WIDGET_FIELD_TYPE_COLUMNS_FK;
41
42	protected $tableName = 'dashboard';
43	protected $tableAlias = 'd';
44	protected $sortColumns = ['dashboardid', 'name'];
45
46	/**
47	 * @param array $options
48	 *
49	 * @throws APIException if the input is invalid.
50	 *
51	 * @return array|int
52	 */
53	abstract public function get(array $options = []);
54
55	/**
56	 * @param array $dashboardids
57	 *
58	 * @throws APIException if the input is invalid.
59	 *
60	 * @return array
61	 */
62	public function delete(array $dashboardids): array {
63		$api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true];
64
65		if (!CApiInputValidator::validate($api_input_rules, $dashboardids, '/', $error)) {
66			self::exception(ZBX_API_ERROR_PARAMETERS, $error);
67		}
68
69		$db_dashboards = $this->get([
70			'output' => ['dashboardid', 'name'],
71			'dashboardids' => $dashboardids,
72			'editable' => true,
73			'preservekeys' => true
74		]);
75
76		if (count($db_dashboards) != count($dashboardids)) {
77			self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!'));
78		}
79
80		// Check if dashboards are used in scheduled reports.
81		if ($this instanceof CDashboard) {
82			$db_reports = DB::select('report', [
83				'output' => ['name', 'dashboardid'],
84				'filter' => ['dashboardid' => $dashboardids],
85				'limit' => 1
86			]);
87
88			if ($db_reports) {
89				self::exception(ZBX_API_ERROR_PARAMETERS, _s('Dashboard "%1$s" is used in report "%2$s".',
90					$db_dashboards[$db_reports[0]['dashboardid']]['name'], $db_reports[0]['name']
91				));
92			}
93		}
94
95		$db_dashboard_pages = DB::select('dashboard_page', [
96			'output' => [],
97			'filter' => ['dashboardid' => $dashboardids],
98			'preservekeys' => true
99		]);
100
101		if ($db_dashboard_pages) {
102			$db_widgets = DB::select('widget', [
103				'output' => [],
104				'filter' => ['dashboard_pageid' => array_keys($db_dashboard_pages)],
105				'preservekeys' => true
106			]);
107
108			if ($db_widgets) {
109				self::deleteWidgets(array_keys($db_widgets));
110			}
111
112			DB::delete('dashboard_page', ['dashboard_pageid' => array_keys($db_dashboard_pages)]);
113		}
114
115		DB::delete('dashboard', ['dashboardid' => $dashboardids]);
116
117		$this->addAuditBulk(AUDIT_ACTION_DELETE, static::AUDIT_RESOURCE, $db_dashboards);
118
119		return ['dashboardids' => $dashboardids];
120	}
121
122	/**
123	 * Add the existing pages, widgets and widget fields to $db_dashboards whether these are affected by the update.
124	 *
125	 * @param array $dashboards
126	 * @param array $db_dashboards
127	 */
128	protected function addAffectedObjects(array $dashboards, array &$db_dashboards): void {
129		// Select pages of these dashboards.
130		$dashboardids = [];
131
132		// Select widgets of these pages.
133		$dashboard_pageids = [];
134
135		// Select fields of these widgets.
136		$widgetids = [];
137
138		foreach ($dashboards as $dashboard) {
139			if (array_key_exists('pages', $dashboard)) {
140				$dashboardids[$dashboard['dashboardid']] = true;
141
142				foreach ($dashboard['pages'] as $dashboard_page) {
143					if (array_key_exists('dashboard_pageid', $dashboard_page)) {
144						if (array_key_exists('widgets', $dashboard_page)) {
145							$dashboard_pageids[$dashboard_page['dashboard_pageid']] = true;
146
147							foreach ($dashboard_page['widgets'] as $widget) {
148								if (array_key_exists('widgetid', $widget)) {
149									if (array_key_exists('fields', $widget)) {
150										$widgetids[$widget['widgetid']] = true;
151									}
152								}
153							}
154						}
155					}
156				}
157			}
158		}
159
160		foreach ($db_dashboards as &$db_dashboard) {
161			$db_dashboard['pages'] = [];
162		}
163		unset($db_dashboard);
164
165		if ($dashboardids) {
166			$db_dashboard_pages = DB::select('dashboard_page', [
167				'output' => array_keys(DB::getSchema('dashboard_page')['fields']),
168				'filter' => ['dashboardid' => array_keys($dashboardids)],
169				'preservekeys' => true
170			]);
171
172			foreach ($db_dashboard_pages as &$db_dashboard_page) {
173				$db_dashboard_page['widgets'] = [];
174			}
175			unset($db_dashboard_page);
176
177			if ($dashboard_pageids) {
178				$db_widgets = DB::select('widget', [
179					'output' => array_keys(DB::getSchema('widget')['fields']),
180					'filter' => ['dashboard_pageid' => array_keys($dashboard_pageids)],
181					'preservekeys' => true
182				]);
183
184				foreach ($db_widgets as &$db_widget) {
185					$db_widget['fields'] = [];
186				}
187				unset($db_widget);
188
189				if ($widgetids) {
190					$db_widget_fields = DB::select('widget_field', [
191						'output' => array_keys(DB::getSchema('widget_field')['fields']),
192						'filter' => ['widgetid' => array_keys($widgetids)],
193						'preservekeys' => true
194					]);
195
196					foreach ($db_widget_fields as $widget_fieldid => $db_widget_field) {
197						$db_widgets[$db_widget_field['widgetid']]['fields'][$widget_fieldid] = $db_widget_field;
198					}
199				}
200
201				foreach ($db_widgets as $widgetid => $db_widget) {
202					$db_dashboard_pages[$db_widget['dashboard_pageid']]['widgets'][$widgetid] = $db_widget;
203				}
204			}
205
206			foreach ($db_dashboard_pages as $dashboard_pageid => $db_dashboard_page) {
207				$db_dashboards[$db_dashboard_page['dashboardid']]['pages'][$dashboard_pageid] = $db_dashboard_page;
208			}
209		}
210	}
211
212	/**
213	 * Check ownership of the referenced pages and widgets.
214	 *
215	 * @param array $dashboards
216	 * @param array $db_dashboards
217	 *
218	 * @throws APIException.
219	 */
220	protected function checkReferences(array $dashboards, array $db_dashboards): void {
221		foreach ($dashboards as $dashboard) {
222			if (!array_key_exists('pages', $dashboard)) {
223				continue;
224			}
225
226			$db_dashboard_pages = $db_dashboards[$dashboard['dashboardid']]['pages'];
227
228			foreach ($dashboard['pages'] as $dashboard_page) {
229				if (array_key_exists('dashboard_pageid', $dashboard_page)
230						&& !array_key_exists($dashboard_page['dashboard_pageid'], $db_dashboard_pages)) {
231					self::exception(ZBX_API_ERROR_PERMISSIONS,
232						_('No permissions to referred object or it does not exist!')
233					);
234				}
235
236				if (!array_key_exists('widgets', $dashboard_page)) {
237					continue;
238				}
239
240				$db_widgets = array_key_exists('dashboard_pageid', $dashboard_page)
241					? $db_dashboard_pages[$dashboard_page['dashboard_pageid']]['widgets']
242					: [];
243
244				foreach ($dashboard_page['widgets'] as $widget) {
245					if (array_key_exists('widgetid', $widget) && !array_key_exists($widget['widgetid'], $db_widgets)) {
246						self::exception(ZBX_API_ERROR_PERMISSIONS,
247							_('No permissions to referred object or it does not exist!')
248						);
249					}
250				}
251			}
252		}
253	}
254
255	/**
256	 * Check widgets.
257	 *
258	 * Note: For any object with ID in $dashboards a corresponding object in $db_dashboards must exist.
259	 *
260	 * @param array      $dashboards
261	 * @param array|null $db_dashboards
262	 *
263	 * @throws APIException if the input is invalid.
264	 */
265	protected function checkWidgets(array $dashboards, array $db_dashboards = null): void {
266		$widget_defaults = DB::getDefaults('widget');
267
268		foreach ($dashboards as $dashboard) {
269			if (!array_key_exists('pages', $dashboard)) {
270				continue;
271			}
272
273			$db_dashboard_pages = ($db_dashboards !== null) ? $db_dashboards[$dashboard['dashboardid']]['pages'] : null;
274
275			foreach ($dashboard['pages'] as $index => $dashboard_page) {
276				if (!array_key_exists('widgets', $dashboard_page)) {
277					continue;
278				}
279
280				$filled = [];
281
282				foreach ($dashboard_page['widgets'] as $widget) {
283					$widget += array_key_exists('widgetid', $widget)
284						? $db_dashboard_pages[$dashboard_page['dashboard_pageid']]['widgets'][$widget['widgetid']]
285						: $widget_defaults;
286
287					for ($x = $widget['x']; $x < $widget['x'] + $widget['width']; $x++) {
288						for ($y = $widget['y']; $y < $widget['y'] + $widget['height']; $y++) {
289							if (array_key_exists($x, $filled) && array_key_exists($y, $filled[$x])) {
290								self::exception(ZBX_API_ERROR_PARAMETERS,
291									_s('Overlapping widgets at X:%3$d, Y:%4$d on page #%2$d of dashboard "%1$s".',
292										$dashboard['name'], $index + 1, $widget['x'], $widget['y']
293									)
294								);
295							}
296
297							$filled[$x][$y] = true;
298						}
299					}
300
301					if ($widget['x'] + $widget['width'] > DASHBOARD_MAX_COLUMNS
302							|| $widget['y'] + $widget['height'] > DASHBOARD_MAX_ROWS) {
303						self::exception(ZBX_API_ERROR_PARAMETERS,
304							_s('Widget at X:%3$d, Y:%4$d on page #%2$d of dashboard "%1$s" is out of bounds.',
305								$dashboard['name'], $index + 1, $widget['x'], $widget['y']
306							)
307						);
308					}
309				}
310			}
311		}
312	}
313
314	/**
315	 * Check widget fields.
316	 *
317	 * Note: For any object with ID in $dashboards a corresponding object in $db_dashboards must exist.
318	 *
319	 * @param array      $dashboards
320	 * @param array|null $db_dashboards
321	 *
322	 * @throws APIException if the input is invalid.
323	 */
324	protected function checkWidgetFields(array $dashboards, array $db_dashboards = null): void {
325		$ids = [
326			ZBX_WIDGET_FIELD_TYPE_ITEM => [],
327			ZBX_WIDGET_FIELD_TYPE_ITEM_PROTOTYPE => [],
328			ZBX_WIDGET_FIELD_TYPE_GRAPH => [],
329			ZBX_WIDGET_FIELD_TYPE_GRAPH_PROTOTYPE => [],
330			ZBX_WIDGET_FIELD_TYPE_GROUP => [],
331			ZBX_WIDGET_FIELD_TYPE_HOST => [],
332			ZBX_WIDGET_FIELD_TYPE_MAP => []
333		];
334
335		foreach ($dashboards as $dashboard) {
336			if (!array_key_exists('pages', $dashboard)) {
337				continue;
338			}
339
340			$db_dashboard_pages = ($db_dashboards !== null) ? $db_dashboards[$dashboard['dashboardid']]['pages'] : null;
341
342			foreach ($dashboard['pages'] as $dashboard_page) {
343				if (!array_key_exists('widgets', $dashboard_page)) {
344					continue;
345				}
346
347				foreach ($dashboard_page['widgets'] as $widget) {
348					if (!array_key_exists('fields', $widget)) {
349						continue;
350					}
351
352					$widgetid = array_key_exists('widgetid', $widget) ? $widget['widgetid'] : null;
353
354					// Skip testing linked object availability of already stored widget fields.
355					$stored_widget_fields = [];
356
357					if ($widgetid !== null) {
358						$db_widget = $db_dashboard_pages[$dashboard_page['dashboard_pageid']]['widgets'][$widgetid];
359
360						foreach ($db_widget['fields'] as $db_widget_field) {
361							if (array_key_exists($db_widget_field['type'], $ids)) {
362								$value = $db_widget_field[self::WIDGET_FIELD_TYPE_COLUMNS[$db_widget_field['type']]];
363								$stored_widget_fields[$db_widget_field['type']][$value] = true;
364							}
365						}
366					}
367
368					foreach ($widget['fields'] as $widget_field) {
369						if (array_key_exists($widget_field['type'], $ids)) {
370							if ($widgetid === null
371									|| !array_key_exists($widget_field['type'], $stored_widget_fields)
372									|| !array_key_exists($widget_field['value'],
373										$stored_widget_fields[$widget_field['type']]
374									)) {
375								if ($this instanceof CTemplateDashboard) {
376									$ids[$widget_field['type']][$widget_field['value']][$dashboard['templateid']] =
377										true;
378								}
379								else {
380									$ids[$widget_field['type']][$widget_field['value']] = true;
381								}
382							}
383						}
384					}
385				}
386			}
387		}
388
389		if ($ids[ZBX_WIDGET_FIELD_TYPE_ITEM]) {
390			$itemids = array_keys($ids[ZBX_WIDGET_FIELD_TYPE_ITEM]);
391
392			$db_items = API::Item()->get([
393				'output' => ($this instanceof CTemplateDashboard) ? ['hostid'] : [],
394				'itemids' => $itemids,
395				'webitems' => true,
396				'preservekeys' => true
397			]);
398
399			foreach ($itemids as $itemid) {
400				if (!array_key_exists($itemid, $db_items)) {
401					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Item with ID "%1$s" is not available.', $itemid));
402				}
403
404				if ($this instanceof CTemplateDashboard) {
405					foreach (array_keys($ids[ZBX_WIDGET_FIELD_TYPE_ITEM][$itemid]) as $templateid) {
406						if ($db_items[$itemid]['hostid'] != $templateid) {
407							self::exception(ZBX_API_ERROR_PARAMETERS,
408								_s('Item with ID "%1$s" is not available.', $itemid)
409							);
410						}
411					}
412				}
413			}
414		}
415
416		if ($ids[ZBX_WIDGET_FIELD_TYPE_ITEM_PROTOTYPE]) {
417			$item_prototypeids = array_keys($ids[ZBX_WIDGET_FIELD_TYPE_ITEM_PROTOTYPE]);
418
419			$db_item_prototypes = API::ItemPrototype()->get([
420				'output' => ($this instanceof CTemplateDashboard) ? ['hostid'] : [],
421				'itemids' => $item_prototypeids,
422				'preservekeys' => true
423			]);
424
425			foreach ($item_prototypeids as $item_prototypeid) {
426				if (!array_key_exists($item_prototypeid, $db_item_prototypes)) {
427					self::exception(ZBX_API_ERROR_PARAMETERS,
428						_s('Item prototype with ID "%1$s" is not available.', $item_prototypeid)
429					);
430				}
431
432				if ($this instanceof CTemplateDashboard) {
433					foreach (array_keys($ids[ZBX_WIDGET_FIELD_TYPE_ITEM_PROTOTYPE][$item_prototypeid]) as $templateid) {
434						if ($db_item_prototypes[$item_prototypeid]['hostid'] != $templateid) {
435							self::exception(ZBX_API_ERROR_PARAMETERS,
436								_s('Item prototype with ID "%1$s" is not available.', $item_prototypeid)
437							);
438						}
439					}
440				}
441			}
442		}
443
444		if ($ids[ZBX_WIDGET_FIELD_TYPE_GRAPH]) {
445			$graphids = array_keys($ids[ZBX_WIDGET_FIELD_TYPE_GRAPH]);
446
447			$db_graphs = API::Graph()->get([
448				'output' => [],
449				'selectHosts' => ($this instanceof CTemplateDashboard) ? ['hostid'] : null,
450				'graphids' => $graphids,
451				'preservekeys' => true
452			]);
453
454			foreach ($graphids as $graphid) {
455				if (!array_key_exists($graphid, $db_graphs)) {
456					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Graph with ID "%1$s" is not available.', $graphid));
457				}
458
459				if ($this instanceof CTemplateDashboard) {
460					foreach (array_keys($ids[ZBX_WIDGET_FIELD_TYPE_GRAPH][$graphid]) as $templateid) {
461						if (!in_array($templateid, array_column($db_graphs[$graphid]['hosts'], 'hostid'))) {
462							self::exception(ZBX_API_ERROR_PARAMETERS,
463								_s('Graph with ID "%1$s" is not available.', $graphid)
464							);
465						}
466					}
467				}
468			}
469		}
470
471		if ($ids[ZBX_WIDGET_FIELD_TYPE_GRAPH_PROTOTYPE]) {
472			$graph_prototypeids = array_keys($ids[ZBX_WIDGET_FIELD_TYPE_GRAPH_PROTOTYPE]);
473
474			$db_graph_prototypes = API::GraphPrototype()->get([
475				'output' => [],
476				'selectHosts' => ($this instanceof CTemplateDashboard) ? ['hostid'] : null,
477				'graphids' => $graph_prototypeids,
478				'preservekeys' => true
479			]);
480
481			foreach ($graph_prototypeids as $graph_prototypeid) {
482				if (!array_key_exists($graph_prototypeid, $db_graph_prototypes)) {
483					self::exception(ZBX_API_ERROR_PARAMETERS,
484						_s('Graph prototype with ID "%1$s" is not available.', $graph_prototypeid)
485					);
486				}
487
488				if ($this instanceof CTemplateDashboard) {
489					$templateids = array_keys($ids[ZBX_WIDGET_FIELD_TYPE_GRAPH_PROTOTYPE][$graph_prototypeid]);
490					foreach ($templateids as $templateid) {
491						$hostids = array_column($db_graph_prototypes[$graph_prototypeid]['hosts'], 'hostid');
492						if (!in_array($templateid, $hostids)) {
493							self::exception(ZBX_API_ERROR_PARAMETERS,
494								_s('Graph prototype with ID "%1$s" is not available.', $graph_prototypeid)
495							);
496						}
497					}
498				}
499			}
500		}
501
502		if ($ids[ZBX_WIDGET_FIELD_TYPE_GROUP]) {
503			$groupids = array_keys($ids[ZBX_WIDGET_FIELD_TYPE_GROUP]);
504
505			$db_groups = API::HostGroup()->get([
506				'output' => [],
507				'groupids' => $groupids,
508				'preservekeys' => true
509			]);
510
511			foreach ($groupids as $groupid) {
512				if (!array_key_exists($groupid, $db_groups)) {
513					self::exception(ZBX_API_ERROR_PARAMETERS,
514						_s('Host group with ID "%1$s" is not available.', $groupid)
515					);
516				}
517			}
518		}
519
520		if ($ids[ZBX_WIDGET_FIELD_TYPE_HOST]) {
521			$hostids = array_keys($ids[ZBX_WIDGET_FIELD_TYPE_HOST]);
522
523			$db_hosts = API::Host()->get([
524				'output' => [],
525				'hostids' => $hostids,
526				'preservekeys' => true
527			]);
528
529			foreach ($hostids as $hostid) {
530				if (!array_key_exists($hostid, $db_hosts)) {
531					self::exception(ZBX_API_ERROR_PARAMETERS, _s('Host with ID "%1$s" is not available.', $hostid));
532				}
533			}
534		}
535
536		if ($ids[ZBX_WIDGET_FIELD_TYPE_MAP]) {
537			$sysmapids = array_keys($ids[ZBX_WIDGET_FIELD_TYPE_MAP]);
538
539			$db_sysmaps = API::Map()->get([
540				'output' => [],
541				'sysmapids' => $sysmapids,
542				'preservekeys' => true
543			]);
544
545			foreach ($sysmapids as $sysmapid) {
546				if (!array_key_exists($sysmapid, $db_sysmaps)) {
547					self::exception(ZBX_API_ERROR_PARAMETERS,
548						_s('Map with ID "%1$s" is not available.', $sysmapid)
549					);
550				}
551			}
552		}
553	}
554
555	/**
556	 * Update table "dashboard_page".
557	 *
558	 * Note: For any object with ID in $dashboards a corresponding object in $db_dashboards must exist.
559	 *
560	 * @param array      $dashboards
561	 * @param array|null $db_dashboards
562	 */
563	protected function updatePages(array $dashboards, array $db_dashboards = null): void {
564		$db_dashboard_pages = [];
565
566		if ($db_dashboards !== null) {
567			foreach ($dashboards as $dashboard) {
568				if (array_key_exists('pages', $dashboard)) {
569					$db_dashboard_pages += $db_dashboards[$dashboard['dashboardid']]['pages'];
570				}
571			}
572		}
573
574		$ins_dashboard_pages = [];
575		$upd_dashboard_pages = [];
576
577		foreach ($dashboards as $dashboard) {
578			if (!array_key_exists('pages', $dashboard)) {
579				continue;
580			}
581
582			foreach ($dashboard['pages'] as $index => $dashboard_page) {
583				$dashboard_page['sortorder'] = $index;
584
585				if (array_key_exists('dashboard_pageid', $dashboard_page)) {
586					$upd_dashboard_page = DB::getUpdatedValues('dashboard_page', $dashboard_page,
587						$db_dashboard_pages[$dashboard_page['dashboard_pageid']]
588					);
589
590					if ($upd_dashboard_page) {
591						$upd_dashboard_pages[] = [
592							'values' => $upd_dashboard_page,
593							'where' => ['dashboard_pageid' => $dashboard_page['dashboard_pageid']]
594						];
595					}
596
597					unset($db_dashboard_pages[$dashboard_page['dashboard_pageid']]);
598				}
599				else {
600					unset($dashboard_page['widgets']);
601					$ins_dashboard_pages[] = ['dashboardid' => $dashboard['dashboardid']] + $dashboard_page;
602				}
603			}
604		}
605
606		if ($ins_dashboard_pages) {
607			$dashboard_pageids = DB::insert('dashboard_page', $ins_dashboard_pages);
608
609			foreach ($dashboards as &$dashboard) {
610				if (array_key_exists('pages', $dashboard)) {
611					foreach ($dashboard['pages'] as &$dashboard_page) {
612						if (!array_key_exists('dashboard_pageid', $dashboard_page)) {
613							$dashboard_page['dashboard_pageid'] = array_shift($dashboard_pageids);
614						}
615					}
616					unset($dashboard_page);
617				}
618			}
619			unset($dashboard);
620		}
621
622		if ($upd_dashboard_pages) {
623			DB::update('dashboard_page', $upd_dashboard_pages);
624		}
625
626		$this->updateWidgets($dashboards, $db_dashboards);
627
628		if ($db_dashboard_pages) {
629			DB::delete('dashboard_page', ['dashboard_pageid' => array_keys($db_dashboard_pages)]);
630		}
631	}
632
633	/**
634	 * Update table "widget".
635	 *
636	 * Note: For any object with ID in $dashboards a corresponding object in $db_dashboards must exist.
637	 *
638	 * @param array      $dashboards
639	 * @param array|null $db_dashboards
640	 */
641	protected function updateWidgets(array $dashboards, array $db_dashboards = null): void {
642		$db_widgets = [];
643
644		if ($db_dashboards !== null) {
645			foreach ($dashboards as $dashboard) {
646				if (!array_key_exists('pages', $dashboard)) {
647					continue;
648				}
649
650				$db_dashboard_pages = $db_dashboards[$dashboard['dashboardid']]['pages'];
651
652				foreach ($dashboard['pages'] as $dashboard_page) {
653					if (!array_key_exists('widgets', $dashboard_page)) {
654						continue;
655					}
656
657					if (array_key_exists($dashboard_page['dashboard_pageid'], $db_dashboard_pages)) {
658						$db_widgets += $db_dashboard_pages[$dashboard_page['dashboard_pageid']]['widgets'];
659					}
660				}
661			}
662		}
663
664		$ins_widgets = [];
665		$upd_widgets = [];
666
667		foreach ($dashboards as $dashboard) {
668			if (!array_key_exists('pages', $dashboard)) {
669				continue;
670			}
671
672			foreach ($dashboard['pages'] as $dashboard_page) {
673				if (!array_key_exists('widgets', $dashboard_page)) {
674					continue;
675				}
676
677				foreach ($dashboard_page['widgets'] as $widget) {
678					if (array_key_exists('widgetid', $widget)) {
679						$upd_widget = DB::getUpdatedValues('widget', $widget, $db_widgets[$widget['widgetid']]);
680
681						if ($upd_widget) {
682							$upd_widgets[] = [
683								'values' => $upd_widget,
684								'where' => ['widgetid' => $widget['widgetid']]
685							];
686						}
687
688						unset($db_widgets[$widget['widgetid']]);
689					}
690					else {
691						$ins_widgets[] = ['dashboard_pageid' => $dashboard_page['dashboard_pageid']] + $widget;
692					}
693				}
694			}
695		}
696
697		if ($ins_widgets) {
698			$widgetids = DB::insert('widget', $ins_widgets);
699
700			foreach ($dashboards as &$dashboard) {
701				if (array_key_exists('pages', $dashboard)) {
702					foreach ($dashboard['pages'] as &$dashboard_page) {
703						if (array_key_exists('widgets', $dashboard_page)) {
704							foreach ($dashboard_page['widgets'] as &$widget) {
705								if (!array_key_exists('widgetid', $widget)) {
706									$widget['widgetid'] = array_shift($widgetids);
707								}
708							}
709							unset($widget);
710						}
711					}
712					unset($dashboard_page);
713				}
714			}
715			unset($dashboard);
716		}
717
718		if ($upd_widgets) {
719			DB::update('widget', $upd_widgets);
720		}
721
722		if ($db_widgets) {
723			self::deleteWidgets(array_keys($db_widgets));
724		}
725
726		$this->updateWidgetFields($dashboards, $db_dashboards);
727	}
728
729	/**
730	 * Update table "widget_field".
731	 *
732	 * Note: For any object with ID in $dashboards a corresponding object in $db_dashboards must exist.
733	 *
734	 * @param array      $dashboards
735	 * @param array|null $db_dashboards
736	 */
737	protected function updateWidgetFields(array $dashboards, array $db_dashboards = null): void {
738		$ins_widget_fields = [];
739		$upd_widget_fields = [];
740		$del_widget_fieldids = [];
741
742		foreach ($dashboards as $dashboard) {
743			if (!array_key_exists('pages', $dashboard)) {
744				continue;
745			}
746
747			$db_dashboard_pages = ($db_dashboards !== null) ? $db_dashboards[$dashboard['dashboardid']]['pages'] : [];
748
749			foreach ($dashboard['pages'] as $dashboard_page) {
750				if (!array_key_exists('widgets', $dashboard_page)) {
751					continue;
752				}
753
754				$db_widgets = array_key_exists($dashboard_page['dashboard_pageid'], $db_dashboard_pages)
755					? $db_dashboard_pages[$dashboard_page['dashboard_pageid']]['widgets']
756					: [];
757
758				foreach ($dashboard_page['widgets'] as $widget) {
759					if (!array_key_exists('fields', $widget)) {
760						continue;
761					}
762
763					$db_widget_fields = array_key_exists($widget['widgetid'], $db_widgets)
764						? $db_widgets[$widget['widgetid']]['fields']
765						: [];
766
767					$widget_fields = [];
768
769					foreach ($widget['fields'] as $widget_field) {
770						$widget_field[self::WIDGET_FIELD_TYPE_COLUMNS[$widget_field['type']]] = $widget_field['value'];
771						$widget_fields[$widget_field['type']][$widget_field['name']][] = $widget_field;
772					}
773
774					foreach ($db_widget_fields as $db_widget_field) {
775						if (array_key_exists($db_widget_field['type'], $widget_fields)
776								&& array_key_exists($db_widget_field['name'], $widget_fields[$db_widget_field['type']])
777								&& $widget_fields[$db_widget_field['type']][$db_widget_field['name']]) {
778							$widget_field = array_shift(
779								$widget_fields[$db_widget_field['type']][$db_widget_field['name']]
780							);
781
782							$upd_widget_field = DB::getUpdatedValues('widget_field', $widget_field, $db_widget_field);
783
784							if ($upd_widget_field) {
785								$upd_widget_fields[] = [
786									'values' => $upd_widget_field,
787									'where' => ['widget_fieldid' => $db_widget_field['widget_fieldid']]
788								];
789							}
790						}
791						else {
792							$del_widget_fieldids[] = $db_widget_field['widget_fieldid'];
793						}
794					}
795
796					foreach ($widget_fields as $widget_fields) {
797						foreach ($widget_fields as $widget_fields) {
798							foreach ($widget_fields as $widget_field) {
799								$ins_widget_fields[] = ['widgetid' => $widget['widgetid']] + $widget_field;
800							}
801						}
802					}
803				}
804			}
805		}
806
807		if ($ins_widget_fields) {
808			DB::insert('widget_field', $ins_widget_fields);
809		}
810
811		if ($upd_widget_fields) {
812			DB::update('widget_field', $upd_widget_fields);
813		}
814
815		if ($del_widget_fieldids) {
816			DB::delete('widget_field', ['widget_fieldid' => $del_widget_fieldids]);
817		}
818	}
819
820	/**
821	 * Delete widgets.
822	 *
823	 * This will also delete profile keys related to the specified widgets, including the standard ones:
824	 *   - web.dashboard.widget.rf_rate
825	 *   - web.dashboard.widget.navtree.item.selected
826	 *   - web.dashboard.widget.navtree.item-*.toggle
827	 *
828	 * @static
829	 *
830	 * @param array $widgetids
831	 */
832	protected static function deleteWidgets(array $widgetids): void {
833		DBexecute(
834			'DELETE FROM profiles'.
835				' WHERE idx LIKE '.zbx_dbstr('web.dashboard.widget.%').
836					' AND '.dbConditionId('idx2', $widgetids)
837		);
838
839		DB::delete('widget', ['widgetid' => $widgetids]);
840	}
841
842	protected function addRelatedObjects(array $options, array $result) {
843		$result = parent::addRelatedObjects($options, $result);
844
845		if ($options['selectPages'] !== null) {
846			foreach ($result as &$row) {
847				$row['pages'] = [];
848			}
849			unset($row);
850
851			$widgets_requested = $this->outputIsRequested('widgets', $options['selectPages']);
852
853			if ($widgets_requested && is_array($options['selectPages'])) {
854				$options['selectPages'] = array_diff($options['selectPages'], ['widgets']);
855			}
856
857			$db_dashboard_pages = API::getApiService()->select('dashboard_page', [
858				'output' => $this->outputExtend($options['selectPages'], ['dashboardid', 'sortorder']),
859				'filter' => ['dashboardid' => array_keys($result)],
860				'preservekeys' => true
861			]);
862
863			if ($db_dashboard_pages) {
864				uasort($db_dashboard_pages, function (array $db_dashboard_page_1, array $db_dashboard_page_2): int {
865					return $db_dashboard_page_1['sortorder'] <=> $db_dashboard_page_2['sortorder'];
866				});
867
868				if ($widgets_requested) {
869					foreach ($db_dashboard_pages as &$db_dashboard_page) {
870						$db_dashboard_page['widgets'] = [];
871					}
872					unset($db_dashboard_page);
873
874					$db_widgets = DB::select('widget', [
875						'output' => ['widgetid', 'type', 'name', 'x', 'y', 'width', 'height', 'view_mode',
876							'dashboard_pageid'
877						],
878						'filter' => ['dashboard_pageid' => array_keys($db_dashboard_pages)],
879						'preservekeys' => true
880					]);
881
882					if ($db_widgets) {
883						foreach ($db_widgets as &$db_widget) {
884							$db_widget['fields'] = [];
885						}
886						unset($db_widget);
887
888						$db_widget_fields = DB::select('widget_field', [
889							'output' => ['widget_fieldid', 'widgetid', 'type', 'name', 'value_int', 'value_str',
890								'value_groupid', 'value_hostid', 'value_itemid', 'value_graphid', 'value_sysmapid'
891							],
892							'filter' => [
893								'widgetid' => array_keys($db_widgets),
894								'type' => array_keys(self::WIDGET_FIELD_TYPE_COLUMNS)
895							]
896						]);
897
898						foreach ($db_widget_fields as $db_widget_field) {
899							$db_widgets[$db_widget_field['widgetid']]['fields'][] = [
900								'type' => $db_widget_field['type'],
901								'name' => $db_widget_field['name'],
902								'value' => $db_widget_field[self::WIDGET_FIELD_TYPE_COLUMNS[$db_widget_field['type']]]
903							];
904						}
905					}
906
907					foreach ($db_widgets as $db_widget) {
908						$dashboard_pageid = $db_widget['dashboard_pageid'];
909						unset($db_widget['dashboard_pageid']);
910						$db_dashboard_pages[$dashboard_pageid]['widgets'][] = $db_widget;
911					}
912				}
913
914				$db_dashboard_pages = $this->unsetExtraFields($db_dashboard_pages, ['dashboard_pageid'],
915					$options['selectPages']
916				);
917
918				foreach ($db_dashboard_pages as $db_dashboard_page) {
919					$dashboardid = $db_dashboard_page['dashboardid'];
920					unset($db_dashboard_page['dashboardid'], $db_dashboard_page['sortorder']);
921					$result[$dashboardid]['pages'][] = $db_dashboard_page;
922				}
923			}
924		}
925
926		return $result;
927	}
928}
929