[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Joomla! Content Management System 5 * 6 * @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org> 7 * @license GNU General Public License version 2 or later; see LICENSE.txt 8 */ 9 10 namespace Joomla\CMS\MVC\Model; 11 12 use Exception; 13 use Joomla\CMS\Factory; 14 use Joomla\CMS\Filter\InputFilter; 15 use Joomla\CMS\Form\Form; 16 use Joomla\CMS\Form\FormFactoryAwareInterface; 17 use Joomla\CMS\Form\FormFactoryAwareTrait; 18 use Joomla\CMS\MVC\Factory\MVCFactoryInterface; 19 use Joomla\CMS\Pagination\Pagination; 20 use Joomla\Database\DatabaseQuery; 21 22 // phpcs:disable PSR1.Files.SideEffects 23 \defined('JPATH_PLATFORM') or die; 24 // phpcs:enable PSR1.Files.SideEffects 25 26 /** 27 * Model class for handling lists of items. 28 * 29 * @since 1.6 30 */ 31 class ListModel extends BaseDatabaseModel implements FormFactoryAwareInterface, ListModelInterface 32 { 33 use FormBehaviorTrait; 34 use FormFactoryAwareTrait; 35 36 /** 37 * Internal memory based cache array of data. 38 * 39 * @var array 40 * @since 1.6 41 */ 42 protected $cache = array(); 43 44 /** 45 * Context string for the model type. This is used to handle uniqueness 46 * when dealing with the getStoreId() method and caching data structures. 47 * 48 * @var string 49 * @since 1.6 50 */ 51 protected $context = null; 52 53 /** 54 * Valid filter fields or ordering. 55 * 56 * @var array 57 * @since 1.6 58 */ 59 protected $filter_fields = array(); 60 61 /** 62 * An internal cache for the last query used. 63 * 64 * @var DatabaseQuery[] 65 * @since 1.6 66 */ 67 protected $query = array(); 68 69 /** 70 * The cache ID used when last populating $this->query 71 * 72 * @var null|string 73 * @since 3.10.4 74 */ 75 protected $lastQueryStoreId = null; 76 77 /** 78 * Name of the filter form to load 79 * 80 * @var string 81 * @since 3.2 82 */ 83 protected $filterFormName = null; 84 85 /** 86 * Associated HTML form 87 * 88 * @var string 89 * @since 3.2 90 */ 91 protected $htmlFormName = 'adminForm'; 92 93 /** 94 * A list of filter variables to not merge into the model's state 95 * 96 * @var array 97 * @since 3.4.5 98 * @deprecated 4.0.0 use $filterForbiddenList instead 99 */ 100 protected $filterBlacklist = array(); 101 102 /** 103 * A list of forbidden filter variables to not merge into the model's state 104 * 105 * @var array 106 * @since 4.0.0 107 */ 108 protected $filterForbiddenList = array(); 109 110 /** 111 * A list of forbidden variables to not merge into the model's state 112 * 113 * @var array 114 * @since 3.4.5 115 * @deprecated 4.0.0 use $listForbiddenList instead 116 */ 117 protected $listBlacklist = array('select'); 118 119 /** 120 * A list of forbidden variables to not merge into the model's state 121 * 122 * @var array 123 * @since 4.0.0 124 */ 125 protected $listForbiddenList = array('select'); 126 127 /** 128 * Constructor 129 * 130 * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). 131 * @param MVCFactoryInterface $factory The factory. 132 * 133 * @since 1.6 134 * @throws Exception 135 */ 136 public function __construct($config = array(), MVCFactoryInterface $factory = null) 137 { 138 parent::__construct($config, $factory); 139 140 // Add the ordering filtering fields allowed list. 141 if (isset($config['filter_fields'])) { 142 $this->filter_fields = $config['filter_fields']; 143 } 144 145 // Guess the context as Option.ModelName. 146 if (empty($this->context)) { 147 $this->context = strtolower($this->option . '.' . $this->getName()); 148 } 149 150 // @deprecated in 4.0 remove in Joomla 5.0 151 if (!empty($this->filterBlacklist)) { 152 $this->filterForbiddenList = array_merge($this->filterBlacklist, $this->filterForbiddenList); 153 } 154 155 // @deprecated in 4.0 remove in Joomla 5.0 156 if (!empty($this->listBlacklist)) { 157 $this->listForbiddenList = array_merge($this->listBlacklist, $this->listForbiddenList); 158 } 159 } 160 161 /** 162 * Provide a query to be used to evaluate if this is an Empty State, can be overridden in the model to provide granular control. 163 * 164 * @return DatabaseQuery 165 * 166 * @since 4.0.0 167 */ 168 protected function getEmptyStateQuery() 169 { 170 $query = clone $this->_getListQuery(); 171 172 if ($query instanceof DatabaseQuery) { 173 $query->clear('bounded') 174 ->clear('group') 175 ->clear('having') 176 ->clear('join') 177 ->clear('values') 178 ->clear('where'); 179 } 180 181 return $query; 182 } 183 184 /** 185 * Is this an empty state, I.e: no items of this type regardless of the searched for states. 186 * 187 * @return boolean 188 * 189 * @throws Exception 190 * 191 * @since 4.0.0 192 */ 193 public function getIsEmptyState(): bool 194 { 195 return $this->_getListCount($this->getEmptyStateQuery()) === 0; 196 } 197 198 /** 199 * Method to cache the last query constructed. 200 * 201 * This method ensures that the query is constructed only once for a given state of the model. 202 * 203 * @return DatabaseQuery A DatabaseQuery object 204 * 205 * @since 1.6 206 */ 207 protected function _getListQuery() 208 { 209 // Compute the current store id. 210 $currentStoreId = $this->getStoreId(); 211 212 // If the last store id is different from the current, refresh the query. 213 if ($this->lastQueryStoreId !== $currentStoreId || empty($this->query)) { 214 $this->lastQueryStoreId = $currentStoreId; 215 $this->query = $this->getListQuery(); 216 } 217 218 return $this->query; 219 } 220 221 /** 222 * Function to get the active filters 223 * 224 * @return array Associative array in the format: array('filter_published' => 0) 225 * 226 * @since 3.2 227 */ 228 public function getActiveFilters() 229 { 230 $activeFilters = array(); 231 232 if (!empty($this->filter_fields)) { 233 foreach ($this->filter_fields as $filter) { 234 $filterName = 'filter.' . $filter; 235 236 if (property_exists($this->state, $filterName) && (!empty($this->state->{$filterName}) || is_numeric($this->state->{$filterName}))) { 237 $activeFilters[$filter] = $this->state->get($filterName); 238 } 239 } 240 } 241 242 return $activeFilters; 243 } 244 245 /** 246 * Method to get an array of data items. 247 * 248 * @return mixed An array of data items on success, false on failure. 249 * 250 * @since 1.6 251 */ 252 public function getItems() 253 { 254 // Get a storage key. 255 $store = $this->getStoreId(); 256 257 // Try to load the data from internal storage. 258 if (isset($this->cache[$store])) { 259 return $this->cache[$store]; 260 } 261 262 try { 263 // Load the list items and add the items to the internal cache. 264 $this->cache[$store] = $this->_getList($this->_getListQuery(), $this->getStart(), $this->getState('list.limit')); 265 } catch (\RuntimeException $e) { 266 $this->setError($e->getMessage()); 267 268 return false; 269 } 270 271 return $this->cache[$store]; 272 } 273 274 /** 275 * Method to get a DatabaseQuery object for retrieving the data set from a database. 276 * 277 * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. 278 * 279 * @since 1.6 280 */ 281 protected function getListQuery() 282 { 283 return $this->getDbo()->getQuery(true); 284 } 285 286 /** 287 * Method to get a \JPagination object for the data set. 288 * 289 * @return Pagination A Pagination object for the data set. 290 * 291 * @since 1.6 292 */ 293 public function getPagination() 294 { 295 // Get a storage key. 296 $store = $this->getStoreId('getPagination'); 297 298 // Try to load the data from internal storage. 299 if (isset($this->cache[$store])) { 300 return $this->cache[$store]; 301 } 302 303 $limit = (int) $this->getState('list.limit') - (int) $this->getState('list.links'); 304 305 // Create the pagination object and add the object to the internal cache. 306 $this->cache[$store] = new Pagination($this->getTotal(), $this->getStart(), $limit); 307 308 return $this->cache[$store]; 309 } 310 311 /** 312 * Method to get a store id based on the model configuration state. 313 * 314 * This is necessary because the model is used by the component and 315 * different modules that might need different sets of data or different 316 * ordering requirements. 317 * 318 * @param string $id An identifier string to generate the store id. 319 * 320 * @return string A store id. 321 * 322 * @since 1.6 323 */ 324 protected function getStoreId($id = '') 325 { 326 // Add the list state to the store id. 327 $id .= ':' . $this->getState('list.start'); 328 $id .= ':' . $this->getState('list.limit'); 329 $id .= ':' . $this->getState('list.ordering'); 330 $id .= ':' . $this->getState('list.direction'); 331 332 return md5($this->context . ':' . $id); 333 } 334 335 /** 336 * Method to get the total number of items for the data set. 337 * 338 * @return integer The total number of items available in the data set. 339 * 340 * @since 1.6 341 */ 342 public function getTotal() 343 { 344 // Get a storage key. 345 $store = $this->getStoreId('getTotal'); 346 347 // Try to load the data from internal storage. 348 if (isset($this->cache[$store])) { 349 return $this->cache[$store]; 350 } 351 352 try { 353 // Load the total and add the total to the internal cache. 354 $this->cache[$store] = (int) $this->_getListCount($this->_getListQuery()); 355 } catch (\RuntimeException $e) { 356 $this->setError($e->getMessage()); 357 358 return false; 359 } 360 361 return $this->cache[$store]; 362 } 363 364 /** 365 * Method to get the starting number of items for the data set. 366 * 367 * @return integer The starting number of items available in the data set. 368 * 369 * @since 1.6 370 */ 371 public function getStart() 372 { 373 $store = $this->getStoreId('getstart'); 374 375 // Try to load the data from internal storage. 376 if (isset($this->cache[$store])) { 377 return $this->cache[$store]; 378 } 379 380 $start = $this->getState('list.start'); 381 382 if ($start > 0) { 383 $limit = $this->getState('list.limit'); 384 $total = $this->getTotal(); 385 386 if ($start > $total - $limit) { 387 $start = max(0, (int) (ceil($total / $limit) - 1) * $limit); 388 } 389 } 390 391 // Add the total to the internal cache. 392 $this->cache[$store] = $start; 393 394 return $this->cache[$store]; 395 } 396 397 /** 398 * Get the filter form 399 * 400 * @param array $data data 401 * @param boolean $loadData load current data 402 * 403 * @return Form|null The \JForm object or null if the form can't be found 404 * 405 * @since 3.2 406 */ 407 public function getFilterForm($data = array(), $loadData = true) 408 { 409 // Try to locate the filter form automatically. Example: ContentModelArticles => "filter_articles" 410 if (empty($this->filterFormName)) { 411 $classNameParts = explode('Model', \get_called_class()); 412 413 if (\count($classNameParts) >= 2) { 414 $this->filterFormName = 'filter_' . str_replace('\\', '', strtolower($classNameParts[1])); 415 } 416 } 417 418 if (empty($this->filterFormName)) { 419 return null; 420 } 421 422 try { 423 // Get the form. 424 return $this->loadForm($this->context . '.filter', $this->filterFormName, array('control' => '', 'load_data' => $loadData)); 425 } catch (\RuntimeException $e) { 426 } 427 428 return null; 429 } 430 431 /** 432 * Method to get the data that should be injected in the form. 433 * 434 * @return mixed The data for the form. 435 * 436 * @since 3.2 437 */ 438 protected function loadFormData() 439 { 440 // Check the session for previously entered form data. 441 $data = Factory::getApplication()->getUserState($this->context, new \stdClass()); 442 443 // Pre-fill the list options 444 if (!property_exists($data, 'list')) { 445 $data->list = array( 446 'direction' => $this->getState('list.direction'), 447 'limit' => $this->getState('list.limit'), 448 'ordering' => $this->getState('list.ordering'), 449 'start' => $this->getState('list.start'), 450 ); 451 } 452 453 return $data; 454 } 455 456 /** 457 * Method to auto-populate the model state. 458 * 459 * This method should only be called once per instantiation and is designed 460 * to be called on the first call to the getState() method unless the model 461 * configuration flag to ignore the request is set. 462 * 463 * Note. Calling getState in this method will result in recursion. 464 * 465 * @param string $ordering An optional ordering field. 466 * @param string $direction An optional direction (asc|desc). 467 * 468 * @return void 469 * 470 * @since 1.6 471 */ 472 protected function populateState($ordering = null, $direction = null) 473 { 474 // If the context is set, assume that stateful lists are used. 475 if ($this->context) { 476 $app = Factory::getApplication(); 477 $inputFilter = InputFilter::getInstance(); 478 479 // Receive & set filters 480 if ($filters = $app->getUserStateFromRequest($this->context . '.filter', 'filter', array(), 'array')) { 481 foreach ($filters as $name => $value) { 482 // Exclude if forbidden 483 if (!\in_array($name, $this->filterForbiddenList)) { 484 $this->setState('filter.' . $name, $value); 485 } 486 } 487 } 488 489 $limit = 0; 490 491 // Receive & set list options 492 if ($list = $app->getUserStateFromRequest($this->context . '.list', 'list', array(), 'array')) { 493 foreach ($list as $name => $value) { 494 // Exclude if forbidden 495 if (!\in_array($name, $this->listForbiddenList)) { 496 // Extra validations 497 switch ($name) { 498 case 'fullordering': 499 $orderingParts = explode(' ', $value); 500 501 if (\count($orderingParts) >= 2) { 502 // Latest part will be considered the direction 503 $fullDirection = end($orderingParts); 504 505 if (\in_array(strtoupper($fullDirection), array('ASC', 'DESC', ''))) { 506 $this->setState('list.direction', $fullDirection); 507 } else { 508 $this->setState('list.direction', $direction); 509 510 // Fallback to the default value 511 $value = $ordering . ' ' . $direction; 512 } 513 514 unset($orderingParts[\count($orderingParts) - 1]); 515 516 // The rest will be the ordering 517 $fullOrdering = implode(' ', $orderingParts); 518 519 if (\in_array($fullOrdering, $this->filter_fields)) { 520 $this->setState('list.ordering', $fullOrdering); 521 } else { 522 $this->setState('list.ordering', $ordering); 523 524 // Fallback to the default value 525 $value = $ordering . ' ' . $direction; 526 } 527 } else { 528 $this->setState('list.ordering', $ordering); 529 $this->setState('list.direction', $direction); 530 531 // Fallback to the default value 532 $value = $ordering . ' ' . $direction; 533 } 534 break; 535 536 case 'ordering': 537 if (!\in_array($value, $this->filter_fields)) { 538 $value = $ordering; 539 } 540 break; 541 542 case 'direction': 543 if (!\in_array(strtoupper($value), array('ASC', 'DESC', ''))) { 544 $value = $direction; 545 } 546 break; 547 548 case 'limit': 549 $value = $inputFilter->clean($value, 'int'); 550 $limit = $value; 551 break; 552 553 case 'select': 554 $explodedValue = explode(',', $value); 555 556 foreach ($explodedValue as &$field) { 557 $field = $inputFilter->clean($field, 'cmd'); 558 } 559 560 $value = implode(',', $explodedValue); 561 break; 562 } 563 564 $this->setState('list.' . $name, $value); 565 } 566 } 567 } else // Keep B/C for components previous to jform forms for filters 568 { 569 // Pre-fill the limits 570 $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); 571 $this->setState('list.limit', $limit); 572 573 // Check if the ordering field is in the allowed list, otherwise use the incoming value. 574 $value = $app->getUserStateFromRequest($this->context . '.ordercol', 'filter_order', $ordering); 575 576 if (!\in_array($value, $this->filter_fields)) { 577 $value = $ordering; 578 $app->setUserState($this->context . '.ordercol', $value); 579 } 580 581 $this->setState('list.ordering', $value); 582 583 // Check if the ordering direction is valid, otherwise use the incoming value. 584 $value = $app->getUserStateFromRequest($this->context . '.orderdirn', 'filter_order_Dir', $direction); 585 586 if (!$value || !\in_array(strtoupper($value), array('ASC', 'DESC', ''))) { 587 $value = $direction; 588 $app->setUserState($this->context . '.orderdirn', $value); 589 } 590 591 $this->setState('list.direction', $value); 592 } 593 594 // Support old ordering field 595 $oldOrdering = $app->input->get('filter_order'); 596 597 if (!empty($oldOrdering) && \in_array($oldOrdering, $this->filter_fields)) { 598 $this->setState('list.ordering', $oldOrdering); 599 } 600 601 // Support old direction field 602 $oldDirection = $app->input->get('filter_order_Dir'); 603 604 if (!empty($oldDirection) && \in_array(strtoupper($oldDirection), array('ASC', 'DESC', ''))) { 605 $this->setState('list.direction', $oldDirection); 606 } 607 608 $value = $app->getUserStateFromRequest($this->context . '.limitstart', 'limitstart', 0, 'int'); 609 $limitstart = ($limit != 0 ? (floor($value / $limit) * $limit) : 0); 610 $this->setState('list.start', $limitstart); 611 } else { 612 $this->setState('list.start', 0); 613 $this->setState('list.limit', 0); 614 } 615 } 616 617 /** 618 * Gets the value of a user state variable and sets it in the session 619 * 620 * This is the same as the method in Application except that this also can optionally 621 * force you back to the first page when a filter has changed 622 * 623 * @param string $key The key of the user state variable. 624 * @param string $request The name of the variable passed in a request. 625 * @param string $default The default value for the variable if not found. Optional. 626 * @param string $type Filter for the variable, for valid values see {@link InputFilter::clean()}. Optional. 627 * @param boolean $resetPage If true, the limitstart in request is set to zero 628 * 629 * @return mixed The request user state. 630 * 631 * @since 1.6 632 */ 633 public function getUserStateFromRequest($key, $request, $default = null, $type = 'none', $resetPage = true) 634 { 635 $app = Factory::getApplication(); 636 $input = $app->input; 637 $old_state = $app->getUserState($key); 638 $cur_state = $old_state ?? $default; 639 $new_state = $input->get($request, null, $type); 640 641 // BC for Search Tools which uses different naming 642 if ($new_state === null && strpos($request, 'filter_') === 0) { 643 $name = substr($request, 7); 644 $filters = $app->input->get('filter', array(), 'array'); 645 646 if (isset($filters[$name])) { 647 $new_state = $filters[$name]; 648 } 649 } 650 651 if ($cur_state != $new_state && $new_state !== null && $resetPage) { 652 $input->set('limitstart', 0); 653 } 654 655 // Save the new value only if it is set in this request. 656 if ($new_state !== null) { 657 $app->setUserState($key, $new_state); 658 } else { 659 $new_state = $cur_state; 660 } 661 662 return $new_state; 663 } 664 665 /** 666 * Parse and transform the search string into a string fit for regex-ing arbitrary strings against 667 * 668 * @param string $search The search string 669 * @param string $regexDelimiter The regex delimiter to use for the quoting 670 * 671 * @return string Search string escaped for regex 672 * 673 * @since 3.4 674 */ 675 protected function refineSearchStringToRegex($search, $regexDelimiter = '/') 676 { 677 $searchArr = explode('|', trim($search, ' |')); 678 679 foreach ($searchArr as $key => $searchString) { 680 if (trim($searchString) === '') { 681 unset($searchArr[$key]); 682 continue; 683 } 684 685 $searchArr[$key] = str_replace(' ', '.*', preg_quote(trim($searchString), $regexDelimiter)); 686 } 687 688 return implode('|', $searchArr); 689 } 690 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |