1<?php declare(strict_types = 1);
2/*
3** Zabbix
4** Copyright (C) 2001-2021 Zabbix SIA
5**
6** This program is free software; you can redistribute it and/or modify
7** it under the terms of the GNU General Public License as published by
8** the Free Software Foundation; either version 2 of the License, or
9** (at your option) any later version.
10**
11** This program is distributed in the hope that it will be useful,
12** but WITHOUT ANY WARRANTY; without even the implied warranty of
13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14** GNU General Public License for more details.
15**
16** You should have received a copy of the GNU General Public License
17** along with this program; if not, write to the Free Software
18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19**/
20
21
22class CMenuItem extends CTag {
23
24	/**
25	 * @var string
26	 */
27	private $action;
28
29	/**
30	 * @var array
31	 */
32	private $aliases = [];
33
34	/**
35	 * @var string
36	 */
37	private $icon_class;
38
39	/**
40	 * @var string
41	 */
42	private $label;
43
44	/**
45	 * @var CMenu
46	 */
47	private $sub_menu;
48
49	/**
50	 * @var string
51	 */
52	private $title;
53
54	/**
55	 * @var string
56	 */
57	private $target;
58
59	/**
60	 * @var CUrl
61	 */
62	private $url;
63
64	/**
65	 * @var bool
66	 */
67	private $is_selected = false;
68
69	/**
70	 * Create menu item.
71	 *
72	 * @param string $label  Menu item visual label.
73	 */
74	public function __construct(string $label) {
75		parent::__construct('li', true);
76
77		$this->label = $label;
78	}
79
80	/**
81	 * Get action name.
82	 *
83	 * @return string|null
84	 */
85	public function getAction(): ?string {
86		return $this->action;
87	}
88
89	/**
90	 * Set action name and derive a corresponding URL for menu item link.
91	 *
92	 * @param string $action_name  Action name.
93	 *
94	 * @return CMenuItem
95	 */
96	public function setAction(string $action_name): self {
97		return $this->setUrl((new CUrl('zabbix.php'))->setArgument('action', $action_name), $action_name);
98	}
99
100	/**
101	 * Get action name aliases.
102	 *
103	 * @return array
104	 */
105	public function getAliases(): array {
106		return $this->aliases;
107	}
108
109	/**
110	 * Set action name aliases.
111	 *
112	 * @param array $aliases  The aliases of menu item. Is able to specify the alias in following formats:
113	 *                        - {action_name} - The alias is applicable to page with specified action name with any GET
114	 *                          parameters in URL or without them;
115	 *                        - {action_name}?{param}={value} - The alias is applicable to page with specified action
116	 *                          when specified GET parameter exists in URL and have the same value;
117	 *                        - {action_name}?{param}=* - The alias is applicable to page with specified action
118	 *                          when specified GET parameter exists in URL and have any value;
119	 *                        - {action_name}?!{param}={value} - The alias is applicable to page with specified action
120	 *                          when specified GET parameter not exists in URL or have different value;
121	 *                        - {action_name}?!{param}=* - The alias is applicable to page with specified action
122	 *                          when specified GET parameter not exists in URL.
123	 *
124	 * @return CMenuItem
125	 */
126	public function setAliases(array $aliases): self {
127		foreach ($aliases as $alias) {
128			['path' => $action_name, 'query' => $query_string] = parse_url($alias) + ['query' => ''];
129			parse_str($query_string, $query_params);
130			$this->aliases[$action_name][] = $query_params;
131		}
132
133		return $this;
134	}
135
136	/**
137	 * Set icon CSS class for menu item link.
138	 *
139	 * @param string $icon_class
140	 *
141	 * @return CMenuItem
142	 */
143	public function setIcon(string $icon_class): self {
144		$this->icon_class = $icon_class;
145
146		return $this;
147	}
148
149	/**
150	 * Get visual label of menu item.
151	 *
152	 * @return string
153	 */
154	public function getLabel(): string {
155		return $this->label;
156	}
157
158	/**
159	 * Check if menu item is marked as selected.
160	 *
161	 * @return bool
162	 */
163	public function isSelected(): bool {
164		return $this->is_selected;
165	}
166
167	/**
168	 * Mark menu item as selected.
169	 *
170	 * @return CMenuItem
171	 */
172	public function setSelected(): self {
173		$this->is_selected = true;
174		$this->addClass('is-selected');
175
176		return $this;
177	}
178
179	/**
180	 * Deep find menu item (including this one) by action name and mark the whole chain as selected.
181	 *
182	 * @param string $action_name     Action name to search for.
183	 * @param array  $request_params  Parameters of current HTTP request to compare in search process.
184	 * @param bool   $expand          Add 'is-expanded' class for selected submenus.
185	 *
186	 * @return bool  True, if menu item was selected.
187	 */
188	public function setSelectedByAction(string $action_name, array $request_params, bool $expand = true): bool {
189		if (array_key_exists($action_name, $this->aliases)) {
190			foreach ($this->aliases[$action_name] as $alias_params) {
191				$no_unacceptable_params = true;
192				$unacceptable_params = [];
193				foreach ($alias_params as $name => $value) {
194					if ($name[0] === '!') {
195						$unacceptable_params[substr($name, 1)] = $value;
196						unset($alias_params[$name]);
197					}
198				}
199
200				if ($unacceptable_params) {
201					$unacceptable_params_existing = array_intersect_assoc($unacceptable_params, $request_params);
202					foreach ($unacceptable_params as $name => $value) {
203						if ($value === '*' && array_key_exists($name, $request_params)) {
204							$unacceptable_params_existing[$name] = '*';
205						}
206					}
207
208					$no_unacceptable_params = array_diff_assoc($unacceptable_params, $unacceptable_params_existing)
209						? true
210						: false;
211				}
212
213				$alias_params_diff = array_diff_assoc($alias_params, $request_params);
214				foreach ($alias_params_diff as $name => $value) {
215					if ($value === '*') {
216						unset($alias_params_diff[$name]);
217					}
218				}
219
220				if ($no_unacceptable_params && !$alias_params_diff) {
221					$this->setSelected();
222
223					return true;
224				}
225			}
226		}
227
228		if ($this->sub_menu !== null && $this->sub_menu->setSelectedByAction($action_name, $request_params, $expand)) {
229			$this->setSelected();
230
231			return true;
232		}
233
234		return false;
235	}
236
237	/**
238	 * Get submenu of menu item or create new one, if not exists.
239	 *
240	 * @return CMenu
241	 */
242	public function getSubMenu(): CMenu {
243		if ($this->sub_menu === null) {
244			$this->setSubMenu(new CMenu());
245		}
246
247		return $this->sub_menu;
248	}
249
250	/**
251	 * Set submenu for menu item.
252	 *
253	 * @param CMenu $sub_menu
254	 *
255	 * @return CMenuItem
256	 */
257	public function setSubMenu(CMenu $sub_menu): self {
258		$this->sub_menu = $sub_menu->addClass('submenu');
259		$this->addClass('has-submenu');
260
261		return $this;
262	}
263
264	/**
265	 * Check if menu item has submenu.
266	 *
267	 * @return bool
268	 */
269	public function hasSubMenu(): bool {
270		return ($this->sub_menu !== null);
271	}
272
273	/**
274	 * Set target attribute for the menu item link.
275	 *
276	 * @param string $target
277	 *
278	 * @return CMenuItem
279	 */
280	public function setTarget(string $target): self {
281		$this->target = $target;
282
283		return $this;
284	}
285
286	/**
287	 * Set title attribute for the menu item link.
288	 *
289	 * @param string $title
290	 *
291	 * @return CMenuItem
292	 */
293	public function setTitle($title): self {
294		$this->title = $title;
295
296		return $this;
297	}
298
299	/**
300	 * Get url of the menu item link.
301	 *
302	 * @return CUrl|null
303	 */
304	public function getUrl(): ?CUrl {
305		return $this->url;
306	}
307
308	/**
309	 * Set url for the menu item link.
310	 *
311	 * @param CUrl        $url
312	 * @param string|null $action_name  Associate action name to be matched by setSelected method.
313	 *
314	 * @return CMenuItem
315	 */
316	public function setUrl(CUrl $url, string $action_name = null): self {
317		$action = null;
318
319		if ($action_name !== null) {
320			$this->setAliases([$action_name]);
321			['path' => $action] = parse_url($action_name);
322		}
323
324		$this->url = $url;
325		$this->action = $action;
326
327		return $this;
328	}
329
330	public function toString($destroy = true)
331	{
332		if ($this->url !== null || $this->sub_menu !== null) {
333			$this->addItem([
334				(new CLink($this->label, $this->sub_menu !== null ? '#' : $this->url->getUrl()))
335					->addClass($this->icon_class)
336					->setTitle($this->title)
337					->setTarget($this->target),
338				$this->sub_menu
339			]);
340		}
341		else {
342			$this->addItem(
343				(new CSpan($this->label))
344					->addClass($this->icon_class)
345					->setTitle($this->title)
346			);
347		}
348
349		return parent::toString($destroy);
350	}
351}
352