1<?php
2/**
3 * @package     Joomla.Administrator
4 * @subpackage  com_menus
5 *
6 * @copyright   Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
7 * @license     GNU General Public License version 2 or later; see LICENSE.txt
8 */
9
10defined('_JEXEC') or die;
11
12/**
13 * Menu Item List Model for Menus.
14 *
15 * @since  1.6
16 */
17class MenusModelItems extends JModelList
18{
19	/**
20	 * Constructor.
21	 *
22	 * @param   array  $config  An optional associative array of configuration settings.
23	 *
24	 * @see     JController
25	 * @since   1.6
26	 */
27	public function __construct($config = array())
28	{
29		if (empty($config['filter_fields']))
30		{
31			$config['filter_fields'] = array(
32				'id', 'a.id',
33				'menutype', 'a.menutype', 'menutype_title',
34				'title', 'a.title',
35				'alias', 'a.alias',
36				'published', 'a.published',
37				'access', 'a.access', 'access_level',
38				'language', 'a.language',
39				'checked_out', 'a.checked_out',
40				'checked_out_time', 'a.checked_out_time',
41				'lft', 'a.lft',
42				'rgt', 'a.rgt',
43				'level', 'a.level',
44				'path', 'a.path',
45				'client_id', 'a.client_id',
46				'home', 'a.home',
47				'parent_id', 'a.parent_id',
48				'a.ordering'
49			);
50
51			$app = JFactory::getApplication();
52			$assoc = JLanguageAssociations::isEnabled();
53
54			if ($assoc)
55			{
56				$config['filter_fields'][] = 'association';
57			}
58		}
59
60		parent::__construct($config);
61	}
62
63	/**
64	 * Method to auto-populate the model state.
65	 *
66	 * Note. Calling getState in this method will result in recursion.
67	 *
68	 * @param   string  $ordering   An optional ordering field.
69	 * @param   string  $direction  An optional direction (asc|desc).
70	 *
71	 * @return  void
72	 *
73	 * @since   1.6
74	 */
75	protected function populateState($ordering = 'a.lft', $direction = 'asc')
76	{
77		$app = JFactory::getApplication('administrator');
78		$user = JFactory::getUser();
79
80		$forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd');
81
82		// Adjust the context to support modal layouts.
83		if ($layout = $app->input->get('layout'))
84		{
85			$this->context .= '.' . $layout;
86		}
87
88		// Adjust the context to support forced languages.
89		if ($forcedLanguage)
90		{
91			$this->context .= '.' . $forcedLanguage;
92		}
93
94		$search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search');
95		$this->setState('filter.search', $search);
96
97		$published = $this->getUserStateFromRequest($this->context . '.published', 'filter_published', '');
98		$this->setState('filter.published', $published);
99
100		$access = $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access');
101		$this->setState('filter.access', $access);
102
103		$parentId = $this->getUserStateFromRequest($this->context . '.filter.parent_id', 'filter_parent_id');
104		$this->setState('filter.parent_id', $parentId);
105
106		$level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level');
107		$this->setState('filter.level', $level);
108
109		// Watch changes in client_id and menutype and keep sync whenever needed.
110		$currentClientId = $app->getUserState($this->context . '.client_id', 0);
111		$clientId        = $app->input->getInt('client_id', $currentClientId);
112
113		// Load mod_menu.ini file when client is administrator
114		if ($clientId == 1)
115		{
116			JFactory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR, null, false, true);
117		}
118
119		$currentMenuType = $app->getUserState($this->context . '.menutype', '');
120		$menuType        = $app->input->getString('menutype', $currentMenuType);
121
122		// If client_id changed clear menutype and reset pagination
123		if ($clientId != $currentClientId)
124		{
125			$menuType = '';
126
127			$app->input->set('limitstart', 0);
128			$app->input->set('menutype', '');
129		}
130
131		// If menutype changed reset pagination.
132		if ($menuType != $currentMenuType)
133		{
134			$app->input->set('limitstart', 0);
135		}
136
137		if (!$menuType)
138		{
139			$app->setUserState($this->context . '.menutype', '');
140			$this->setState('menutypetitle', '');
141			$this->setState('menutypeid', '');
142		}
143		// Special menu types, if selected explicitly, will be allowed as a filter
144		elseif ($menuType == 'main')
145		{
146			// Adjust client_id to match the menutype. This is safe as client_id was not changed in this request.
147			$app->input->set('client_id', 1);
148
149			$app->setUserState($this->context . '.menutype', $menuType);
150			$this->setState('menutypetitle', ucfirst($menuType));
151			$this->setState('menutypeid', -1);
152		}
153		// Get the menutype object with appropriate checks.
154		elseif ($cMenu = $this->getMenu($menuType, true))
155		{
156			// Adjust client_id to match the menutype. This is safe as client_id was not changed in this request.
157			$app->input->set('client_id', $cMenu->client_id);
158
159			$app->setUserState($this->context . '.menutype', $menuType);
160			$this->setState('menutypetitle', $cMenu->title);
161			$this->setState('menutypeid', $cMenu->id);
162		}
163		// This menutype does not exist, leave client id unchanged but reset menutype and pagination
164		else
165		{
166			$menuType = '';
167
168			$app->input->set('limitstart', 0);
169			$app->input->set('menutype', $menuType);
170
171			$app->setUserState($this->context . '.menutype', $menuType);
172			$this->setState('menutypetitle', '');
173			$this->setState('menutypeid', '');
174		}
175
176		// Client id filter
177		$clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int');
178		$this->setState('filter.client_id', $clientId);
179
180		// Use a different filter file when client is administrator
181		if ($clientId == 1)
182		{
183			$this->filterFormName = 'filter_itemsadmin';
184		}
185
186		$this->setState('filter.menutype', $menuType);
187
188		$language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '');
189		$this->setState('filter.language', $language);
190
191		// Component parameters.
192		$params = JComponentHelper::getParams('com_menus');
193		$this->setState('params', $params);
194
195		// List state information.
196		parent::populateState($ordering, $direction);
197
198		// Force a language.
199		if (!empty($forcedLanguage))
200		{
201			$this->setState('filter.language', $forcedLanguage);
202		}
203	}
204
205	/**
206	 * Method to get a store id based on model configuration state.
207	 *
208	 * This is necessary because the model is used by the component and
209	 * different modules that might need different sets of data or different
210	 * ordering requirements.
211	 *
212	 * @param   string  $id  A prefix for the store id.
213	 *
214	 * @return  string  A store id.
215	 *
216	 * @since   1.6
217	 */
218	protected function getStoreId($id = '')
219	{
220		// Compile the store id.
221		$id .= ':' . $this->getState('filter.access');
222		$id .= ':' . $this->getState('filter.published');
223		$id .= ':' . $this->getState('filter.language');
224		$id .= ':' . $this->getState('filter.search');
225		$id .= ':' . $this->getState('filter.parent_id');
226		$id .= ':' . $this->getState('filter.menutype');
227		$id .= ':' . $this->getState('filter.client_id');
228
229		return parent::getStoreId($id);
230	}
231
232	/**
233	 * Builds an SQL query to load the list data.
234	 *
235	 * @return  JDatabaseQuery    A query object.
236	 *
237	 * @since   1.6
238	 */
239	protected function getListQuery()
240	{
241		// Create a new query object.
242		$db = $this->getDbo();
243		$query = $db->getQuery(true);
244		$user = JFactory::getUser();
245		$app = JFactory::getApplication();
246
247		// Select all fields from the table.
248		$query->select(
249			$this->getState(
250				'list.select',
251				$db->quoteName(
252					array(
253						'a.id', 'a.menutype', 'a.title', 'a.alias', 'a.note', 'a.path', 'a.link', 'a.type', 'a.parent_id',
254						'a.level', 'a.published', 'a.component_id', 'a.checked_out', 'a.checked_out_time', 'a.browserNav',
255						'a.access', 'a.img', 'a.template_style_id', 'a.params', 'a.lft', 'a.rgt', 'a.home', 'a.language', 'a.client_id'
256					),
257					array(
258						null, null, null, null, null, null, null, null, null,
259						null, 'a.published', null, null, null, null,
260						null, null, null, null, null, null, null, null, null
261					)
262				)
263			)
264		);
265		$query->select(
266			'CASE ' .
267				' WHEN a.type = ' . $db->quote('component') . ' THEN a.published+2*(e.enabled-1) ' .
268				' WHEN a.type = ' . $db->quote('url') . ' AND a.published != -2 THEN a.published+2 ' .
269				' WHEN a.type = ' . $db->quote('url') . ' AND a.published = -2 THEN a.published-1 ' .
270				' WHEN a.type = ' . $db->quote('alias') . ' AND a.published != -2 THEN a.published+4 ' .
271				' WHEN a.type = ' . $db->quote('alias') . ' AND a.published = -2 THEN a.published-1 ' .
272				' WHEN a.type = ' . $db->quote('separator') . ' AND a.published != -2 THEN a.published+6 ' .
273				' WHEN a.type = ' . $db->quote('separator') . ' AND a.published = -2 THEN a.published-1 ' .
274				' WHEN a.type = ' . $db->quote('heading') . ' AND a.published != -2 THEN a.published+8 ' .
275				' WHEN a.type = ' . $db->quote('heading') . ' AND a.published = -2 THEN a.published-1 ' .
276				' WHEN a.type = ' . $db->quote('container') . ' AND a.published != -2 THEN a.published+8 ' .
277				' WHEN a.type = ' . $db->quote('container') . ' AND a.published = -2 THEN a.published-1 ' .
278			' END AS published '
279		);
280		$query->from($db->quoteName('#__menu') . ' AS a');
281
282		// Join over the language
283		$query->select('l.title AS language_title, l.image AS language_image, l.sef AS language_sef')
284			->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language');
285
286		// Join over the users.
287		$query->select('u.name AS editor')
288			->join('LEFT', $db->quoteName('#__users') . ' AS u ON u.id = a.checked_out');
289
290		// Join over components
291		$query->select('c.element AS componentname')
292			->join('LEFT', $db->quoteName('#__extensions') . ' AS c ON c.extension_id = a.component_id');
293
294		// Join over the asset groups.
295		$query->select('ag.title AS access_level')
296			->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access');
297
298		// Join over the menu types.
299		$query->select($db->quoteName(array('mt.id', 'mt.title'), array('menutype_id', 'menutype_title')))
300			->join('LEFT', $db->quoteName('#__menu_types', 'mt') . ' ON ' . $db->qn('mt.menutype') . ' = ' . $db->qn('a.menutype'));
301
302		// Join over the associations.
303		$assoc = JLanguageAssociations::isEnabled();
304
305		if ($assoc)
306		{
307			$subQuery = $db->getQuery(true)
308				->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1')
309				->from($db->quoteName('#__associations', 'asso1'))
310				->join('INNER', $db->quoteName('#__associations', 'asso2') . ' ON ' . $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key'))
311				->where(
312					array(
313						$db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'),
314						$db->quoteName('asso1.context') . ' = ' . $db->quote('com_menus.item'),
315					)
316				);
317
318			$query->select('(' . $subQuery . ') AS ' . $db->quoteName('association'));
319		}
320
321		// Join over the extensions
322		$query->select('e.name AS name')
323			->join('LEFT', '#__extensions AS e ON e.extension_id = a.component_id');
324
325		// Exclude the root category.
326		$query->where('a.id > 1')
327			->where('a.client_id = ' . (int) $this->getState('filter.client_id'));
328
329		// Filter on the published state.
330		$published = $this->getState('filter.published');
331
332		if (is_numeric($published))
333		{
334			$query->where('a.published = ' . (int) $published);
335		}
336		elseif ($published === '')
337		{
338			$query->where('a.published IN (0, 1)');
339		}
340
341		// Filter by search in title, alias or id
342		if ($search = trim($this->getState('filter.search')))
343		{
344			if (stripos($search, 'id:') === 0)
345			{
346				$query->where('a.id = ' . (int) substr($search, 3));
347			}
348			elseif (stripos($search, 'link:') === 0)
349			{
350				if ($search = substr($search, 5))
351				{
352					$search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'));
353					$query->where('a.link LIKE ' . $search);
354				}
355			}
356			else
357			{
358				$search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'));
359				$query->where('(' . 'a.title LIKE ' . $search . ' OR a.alias LIKE ' . $search . ' OR a.note LIKE ' . $search . ')');
360			}
361		}
362
363		// Filter the items over the parent id if set.
364		$parentId = $this->getState('filter.parent_id');
365
366		if (!empty($parentId))
367		{
368			$level = $this->getState('filter.level');
369
370			// Create a subquery for the sub-items list
371			$subQuery = $db->getQuery(true)
372				->select('sub.id')
373				->from('#__menu as sub')
374				->join('INNER', '#__menu as this ON sub.lft > this.lft AND sub.rgt < this.rgt')
375				->where('this.id = ' . (int) $parentId);
376
377			if ($level)
378			{
379				$subQuery->where('sub.level <= this.level + ' . (int) ($level - 1));
380			}
381
382			// Add the subquery to the main query
383			$query->where('(a.parent_id = ' . (int) $parentId . ' OR a.parent_id IN (' . (string) $subQuery . '))');
384		}
385
386		// Filter on the level.
387		elseif ($level = $this->getState('filter.level'))
388		{
389			$query->where('a.level <= ' . (int) $level);
390		}
391
392		// Filter the items over the menu id if set.
393		$menuType = $this->getState('filter.menutype');
394
395		// A value "" means all
396		if ($menuType == '')
397		{
398			// Load all menu types we have manage access
399			$query2 = $this->getDbo()->getQuery(true)
400				->select($this->getDbo()->qn(array('id', 'menutype')))
401				->from('#__menu_types')
402				->where('client_id = ' . (int) $this->getState('filter.client_id'))
403				->order('title');
404
405			// Show protected items on explicit filter only
406			$query->where('a.menutype != ' . $db->q('main'));
407
408			$menuTypes = $this->getDbo()->setQuery($query2)->loadObjectList();
409
410			if ($menuTypes)
411			{
412				$types = array();
413
414				foreach ($menuTypes as $type)
415				{
416					if ($user->authorise('core.manage', 'com_menus.menu.' . (int) $type->id))
417					{
418						$types[] = $query->q($type->menutype);
419					}
420				}
421
422				$query->where($types ? 'a.menutype IN(' . implode(',', $types) . ')' : 0);
423			}
424		}
425		// Default behavior => load all items from a specific menu
426		elseif (strlen($menuType))
427		{
428			$query->where('a.menutype = ' . $db->quote($menuType));
429		}
430		// Empty menu type => error
431		else
432		{
433			$query->where('1 != 1');
434		}
435
436		// Filter on the access level.
437		if ($access = $this->getState('filter.access'))
438		{
439			$query->where('a.access = ' . (int) $access);
440		}
441
442		// Implement View Level Access
443		if (!$user->authorise('core.admin'))
444		{
445			$groups = $user->getAuthorisedViewLevels();
446
447			if (!empty($groups))
448			{
449				$query->where('a.access IN (' . implode(',', $groups) . ')');
450			}
451		}
452
453		// Filter on the language.
454		if ($language = $this->getState('filter.language'))
455		{
456			$query->where('a.language = ' . $db->quote($language));
457		}
458
459		// Add the list ordering clause.
460		$query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')));
461
462		return $query;
463	}
464
465	/**
466	 * Method to allow derived classes to preprocess the form.
467	 *
468	 * @param   JForm   $form   A JForm object.
469	 * @param   mixed   $data   The data expected for the form.
470	 * @param   string  $group  The name of the plugin group to import (defaults to "content").
471	 *
472	 * @return  void
473	 *
474	 * @since   3.2
475	 * @throws  Exception if there is an error in the form event.
476	 */
477	protected function preprocessForm(JForm $form, $data, $group = 'content')
478	{
479		$name = $form->getName();
480
481		if ($name == 'com_menus.items.filter')
482		{
483			$clientId = $this->getState('filter.client_id');
484			$form->setFieldAttribute('menutype', 'clientid', $clientId);
485		}
486		elseif (false !== strpos($name, 'com_menus.items.modal.'))
487		{
488			$form->removeField('client_id');
489
490			$clientId = $this->getState('filter.client_id');
491			$form->setFieldAttribute('menutype', 'clientid', $clientId);
492		}
493	}
494
495	/**
496	 * Get the client id for a menu
497	 *
498	 * @param   string   $menuType  The menutype identifier for the menu
499	 * @param   boolean  $check     Flag whether to perform check against ACL as well as existence
500	 *
501	 * @return  integer
502	 *
503	 * @since   3.7.0
504	 */
505	protected function getMenu($menuType, $check = false)
506	{
507		$query = $this->_db->getQuery(true);
508
509		$query->select('a.*')
510			->from($this->_db->qn('#__menu_types', 'a'))
511			->where('menutype = ' . $this->_db->q($menuType));
512
513		$cMenu = $this->_db->setQuery($query)->loadObject();
514
515		if ($check)
516		{
517			// Check if menu type exists.
518			if (!$cMenu)
519			{
520				JLog::add(JText::_('COM_MENUS_ERROR_MENUTYPE_NOT_FOUND'), JLog::ERROR, 'jerror');
521
522				return false;
523			}
524			// Check if menu type is valid against ACL.
525			elseif (!JFactory::getUser()->authorise('core.manage', 'com_menus.menu.' . $cMenu->id))
526			{
527				JLog::add(JText::_('JERROR_ALERTNOAUTHOR'), JLog::ERROR, 'jerror');
528
529				return false;
530			}
531		}
532
533		return $cMenu;
534	}
535
536	/**
537	 * Method to get an array of data items.
538	 *
539	 * @return  mixed  An array of data items on success, false on failure.
540	 *
541	 * @since   3.0.1
542	 */
543	public function getItems()
544	{
545		$store = $this->getStoreId();
546
547		if (!isset($this->cache[$store]))
548		{
549			$items = parent::getItems();
550			$lang  = JFactory::getLanguage();
551			$client = $this->state->get('filter.client_id');
552
553			if ($items)
554			{
555				foreach ($items as $item)
556				{
557					if ($extension = $item->componentname)
558					{
559						$lang->load("$extension.sys", JPATH_ADMINISTRATOR, null, false, true)
560						|| $lang->load("$extension.sys", JPATH_ADMINISTRATOR . '/components/' . $extension, null, false, true);
561					}
562
563					// Translate component name
564					if ($client === 1)
565					{
566						$item->title = JText::_($item->title);
567					}
568				}
569			}
570
571			$this->cache[$store] = $items;
572		}
573
574		return $this->cache[$store];
575	}
576}
577