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