[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/src/MVC/Model/ -> ListModel.php (source)

   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  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer