[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_finder/src/Indexer/ -> Query.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Administrator
   5   * @subpackage  com_finder
   6   *
   7   * @copyright   (C) 2011 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9   */
  10  
  11  namespace Joomla\Component\Finder\Administrator\Indexer;
  12  
  13  use Exception;
  14  use Joomla\CMS\Component\ComponentHelper;
  15  use Joomla\CMS\Factory;
  16  use Joomla\CMS\Language\Text;
  17  use Joomla\CMS\Uri\Uri;
  18  use Joomla\Component\Finder\Administrator\Helper\LanguageHelper;
  19  use Joomla\Component\Finder\Site\Helper\RouteHelper;
  20  use Joomla\Database\DatabaseAwareTrait;
  21  use Joomla\Database\DatabaseInterface;
  22  use Joomla\Database\ParameterType;
  23  use Joomla\Registry\Registry;
  24  use Joomla\String\StringHelper;
  25  use Joomla\Utilities\ArrayHelper;
  26  
  27  // phpcs:disable PSR1.Files.SideEffects
  28  \defined('_JEXEC') or die;
  29  // phpcs:enable PSR1.Files.SideEffects
  30  
  31  /**
  32   * Query class for the Finder indexer package.
  33   *
  34   * @since  2.5
  35   */
  36  class Query
  37  {
  38      use DatabaseAwareTrait;
  39  
  40      /**
  41       * Flag to show whether the query can return results.
  42       *
  43       * @var    boolean
  44       * @since  2.5
  45       */
  46      public $search;
  47  
  48      /**
  49       * The query input string.
  50       *
  51       * @var    string
  52       * @since  2.5
  53       */
  54      public $input;
  55  
  56      /**
  57       * The language of the query.
  58       *
  59       * @var    string
  60       * @since  2.5
  61       */
  62      public $language;
  63  
  64      /**
  65       * The query string matching mode.
  66       *
  67       * @var    string
  68       * @since  2.5
  69       */
  70      public $mode;
  71  
  72      /**
  73       * The included tokens.
  74       *
  75       * @var    Token[]
  76       * @since  2.5
  77       */
  78      public $included = array();
  79  
  80      /**
  81       * The excluded tokens.
  82       *
  83       * @var    Token[]
  84       * @since  2.5
  85       */
  86      public $excluded = array();
  87  
  88      /**
  89       * The tokens to ignore because no matches exist.
  90       *
  91       * @var    Token[]
  92       * @since  2.5
  93       */
  94      public $ignored = array();
  95  
  96      /**
  97       * The operators used in the query input string.
  98       *
  99       * @var    array
 100       * @since  2.5
 101       */
 102      public $operators = array();
 103  
 104      /**
 105       * The terms to highlight as matches.
 106       *
 107       * @var    array
 108       * @since  2.5
 109       */
 110      public $highlight = array();
 111  
 112      /**
 113       * The number of matching terms for the query input.
 114       *
 115       * @var    integer
 116       * @since  2.5
 117       */
 118      public $terms;
 119  
 120      /**
 121       * Allow empty searches
 122       *
 123       * @var    boolean
 124       * @since  4.0.0
 125       */
 126      public $empty;
 127  
 128      /**
 129       * The static filter id.
 130       *
 131       * @var    string
 132       * @since  2.5
 133       */
 134      public $filter;
 135  
 136      /**
 137       * The taxonomy filters. This is a multi-dimensional array of taxonomy
 138       * branches as the first level and then the taxonomy nodes as the values.
 139       *
 140       * For example:
 141       * $filters = array(
 142       *     'Type' = array(10, 32, 29, 11, ...);
 143       *     'Label' = array(20, 314, 349, 91, 82, ...);
 144       *        ...
 145       * );
 146       *
 147       * @var    array
 148       * @since  2.5
 149       */
 150      public $filters = array();
 151  
 152      /**
 153       * The start date filter.
 154       *
 155       * @var    string
 156       * @since  2.5
 157       */
 158      public $date1;
 159  
 160      /**
 161       * The end date filter.
 162       *
 163       * @var    string
 164       * @since  2.5
 165       */
 166      public $date2;
 167  
 168      /**
 169       * The start date filter modifier.
 170       *
 171       * @var    string
 172       * @since  2.5
 173       */
 174      public $when1;
 175  
 176      /**
 177       * The end date filter modifier.
 178       *
 179       * @var    string
 180       * @since  2.5
 181       */
 182      public $when2;
 183  
 184      /**
 185       * Match search terms exactly or with a LIKE scheme
 186       *
 187       * @var    string
 188       * @since  4.2.0
 189       */
 190      public $wordmode;
 191  
 192      /**
 193       * Method to instantiate the query object.
 194       *
 195       * @param   array  $options  An array of query options.
 196       *
 197       * @since   2.5
 198       * @throws  Exception on database error.
 199       */
 200      public function __construct($options, DatabaseInterface $db = null)
 201      {
 202          if ($db === null) {
 203              @trigger_error(sprintf('Database will be mandatory in 5.0.'), E_USER_DEPRECATED);
 204              $db = Factory::getContainer()->get(DatabaseInterface::class);
 205          }
 206  
 207          $this->setDatabase($db);
 208  
 209          // Get the input string.
 210          $this->input = $options['input'] ?? '';
 211  
 212          // Get the empty query setting.
 213          $this->empty = isset($options['empty']) ? (bool) $options['empty'] : false;
 214  
 215          // Get the input language.
 216          $this->language = !empty($options['language']) ? $options['language'] : Helper::getDefaultLanguage();
 217  
 218          // Get the matching mode.
 219          $this->mode = 'AND';
 220  
 221          // Set the word matching mode
 222          $this->wordmode = !empty($options['word_match']) ? $options['word_match'] : 'exact';
 223  
 224          // Initialize the temporary date storage.
 225          $this->dates = new Registry();
 226  
 227          // Populate the temporary date storage.
 228          if (!empty($options['date1'])) {
 229              $this->dates->set('date1', $options['date1']);
 230          }
 231  
 232          if (!empty($options['date2'])) {
 233              $this->dates->set('date2', $options['date2']);
 234          }
 235  
 236          if (!empty($options['when1'])) {
 237              $this->dates->set('when1', $options['when1']);
 238          }
 239  
 240          if (!empty($options['when2'])) {
 241              $this->dates->set('when2', $options['when2']);
 242          }
 243  
 244          // Process the static taxonomy filters.
 245          if (!empty($options['filter'])) {
 246              $this->processStaticTaxonomy($options['filter']);
 247          }
 248  
 249          // Process the dynamic taxonomy filters.
 250          if (!empty($options['filters'])) {
 251              $this->processDynamicTaxonomy($options['filters']);
 252          }
 253  
 254          // Get the date filters.
 255          $d1 = $this->dates->get('date1');
 256          $d2 = $this->dates->get('date2');
 257          $w1 = $this->dates->get('when1');
 258          $w2 = $this->dates->get('when2');
 259  
 260          // Process the date filters.
 261          if (!empty($d1) || !empty($d2)) {
 262              $this->processDates($d1, $d2, $w1, $w2);
 263          }
 264  
 265          // Process the input string.
 266          $this->processString($this->input, $this->language, $this->mode);
 267  
 268          // Get the number of matching terms.
 269          foreach ($this->included as $token) {
 270              $this->terms += count($token->matches);
 271          }
 272  
 273          // Remove the temporary date storage.
 274          unset($this->dates);
 275  
 276          // Lastly, determine whether this query can return a result set.
 277  
 278          // Check if we have a query string.
 279          if (!empty($this->input)) {
 280              $this->search = true;
 281          } elseif ($this->empty && (!empty($this->filter) || !empty($this->filters) || !empty($this->date1) || !empty($this->date2))) {
 282              // Check if we can search without a query string.
 283              $this->search = true;
 284          } else {
 285              // We do not have a valid search query.
 286              $this->search = false;
 287          }
 288      }
 289  
 290      /**
 291       * Method to convert the query object into a URI string.
 292       *
 293       * @param   string  $base  The base URI. [optional]
 294       *
 295       * @return  string  The complete query URI.
 296       *
 297       * @since   2.5
 298       */
 299      public function toUri($base = '')
 300      {
 301          // Set the base if not specified.
 302          if ($base === '') {
 303              $base = 'index.php?option=com_finder&view=search';
 304          }
 305  
 306          // Get the base URI.
 307          $uri = Uri::getInstance($base);
 308  
 309          // Add the static taxonomy filter if present.
 310          if ((bool) $this->filter) {
 311              $uri->setVar('f', $this->filter);
 312          }
 313  
 314          // Get the filters in the request.
 315          $t = Factory::getApplication()->input->request->get('t', array(), 'array');
 316  
 317          // Add the dynamic taxonomy filters if present.
 318          if ((bool) $this->filters) {
 319              foreach ($this->filters as $nodes) {
 320                  foreach ($nodes as $node) {
 321                      if (!in_array($node, $t)) {
 322                          continue;
 323                      }
 324  
 325                      $uri->setVar('t[]', $node);
 326                  }
 327              }
 328          }
 329  
 330          // Add the input string if present.
 331          if (!empty($this->input)) {
 332              $uri->setVar('q', $this->input);
 333          }
 334  
 335          // Add the start date if present.
 336          if (!empty($this->date1)) {
 337              $uri->setVar('d1', $this->date1);
 338          }
 339  
 340          // Add the end date if present.
 341          if (!empty($this->date2)) {
 342              $uri->setVar('d2', $this->date2);
 343          }
 344  
 345          // Add the start date modifier if present.
 346          if (!empty($this->when1)) {
 347              $uri->setVar('w1', $this->when1);
 348          }
 349  
 350          // Add the end date modifier if present.
 351          if (!empty($this->when2)) {
 352              $uri->setVar('w2', $this->when2);
 353          }
 354  
 355          // Add a menu item id if one is not present.
 356          if (!$uri->getVar('Itemid')) {
 357              // Get the menu item id.
 358              $query = array(
 359                  'view' => $uri->getVar('view'),
 360                  'f'    => $uri->getVar('f'),
 361                  'q'    => $uri->getVar('q'),
 362              );
 363  
 364              $item = RouteHelper::getItemid($query);
 365  
 366              // Add the menu item id if present.
 367              if ($item !== null) {
 368                  $uri->setVar('Itemid', $item);
 369              }
 370          }
 371  
 372          return $uri->toString(array('path', 'query'));
 373      }
 374  
 375      /**
 376       * Method to get a list of excluded search term ids.
 377       *
 378       * @return  array  An array of excluded term ids.
 379       *
 380       * @since   2.5
 381       */
 382      public function getExcludedTermIds()
 383      {
 384          $results = array();
 385  
 386          // Iterate through the excluded tokens and compile the matching terms.
 387          for ($i = 0, $c = count($this->excluded); $i < $c; $i++) {
 388              foreach ($this->excluded[$i]->matches as $match) {
 389                  $results = array_merge($results, $match);
 390              }
 391          }
 392  
 393          // Sanitize the terms.
 394          $results = array_unique($results);
 395  
 396          return ArrayHelper::toInteger($results);
 397      }
 398  
 399      /**
 400       * Method to get a list of included search term ids.
 401       *
 402       * @return  array  An array of included term ids.
 403       *
 404       * @since   2.5
 405       */
 406      public function getIncludedTermIds()
 407      {
 408          $results = array();
 409  
 410          // Iterate through the included tokens and compile the matching terms.
 411          for ($i = 0, $c = count($this->included); $i < $c; $i++) {
 412              // Check if we have any terms.
 413              if (empty($this->included[$i]->matches)) {
 414                  continue;
 415              }
 416  
 417              // Get the term.
 418              $term = $this->included[$i]->term;
 419  
 420              // Prepare the container for the term if necessary.
 421              if (!array_key_exists($term, $results)) {
 422                  $results[$term] = array();
 423              }
 424  
 425              // Add the matches to the stack.
 426              foreach ($this->included[$i]->matches as $match) {
 427                  $results[$term] = array_merge($results[$term], $match);
 428              }
 429          }
 430  
 431          // Sanitize the terms.
 432          foreach ($results as $key => $value) {
 433              $results[$key] = array_unique($results[$key]);
 434              $results[$key] = ArrayHelper::toInteger($results[$key]);
 435          }
 436  
 437          return $results;
 438      }
 439  
 440      /**
 441       * Method to get a list of required search term ids.
 442       *
 443       * @return  array  An array of required term ids.
 444       *
 445       * @since   2.5
 446       */
 447      public function getRequiredTermIds()
 448      {
 449          $results = array();
 450  
 451          // Iterate through the included tokens and compile the matching terms.
 452          for ($i = 0, $c = count($this->included); $i < $c; $i++) {
 453              // Check if the token is required.
 454              if ($this->included[$i]->required) {
 455                  // Get the term.
 456                  $term = $this->included[$i]->term;
 457  
 458                  // Prepare the container for the term if necessary.
 459                  if (!array_key_exists($term, $results)) {
 460                      $results[$term] = array();
 461                  }
 462  
 463                  // Add the matches to the stack.
 464                  foreach ($this->included[$i]->matches as $match) {
 465                      $results[$term] = array_merge($results[$term], $match);
 466                  }
 467              }
 468          }
 469  
 470          // Sanitize the terms.
 471          foreach ($results as $key => $value) {
 472              $results[$key] = array_unique($results[$key]);
 473              $results[$key] = ArrayHelper::toInteger($results[$key]);
 474          }
 475  
 476          return $results;
 477      }
 478  
 479      /**
 480       * Method to process the static taxonomy input. The static taxonomy input
 481       * comes in the form of a pre-defined search filter that is assigned to the
 482       * search form.
 483       *
 484       * @param   integer  $filterId  The id of static filter.
 485       *
 486       * @return  boolean  True on success, false on failure.
 487       *
 488       * @since   2.5
 489       * @throws  Exception on database error.
 490       */
 491      protected function processStaticTaxonomy($filterId)
 492      {
 493          // Get the database object.
 494          $db = $this->getDatabase();
 495  
 496          // Initialize user variables
 497          $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels());
 498  
 499          // Load the predefined filter.
 500          $query = $db->getQuery(true)
 501              ->select('f.data, f.params')
 502              ->from($db->quoteName('#__finder_filters') . ' AS f')
 503              ->where('f.filter_id = ' . (int) $filterId);
 504  
 505          $db->setQuery($query);
 506          $return = $db->loadObject();
 507  
 508          // Check the returned filter.
 509          if (empty($return)) {
 510              return false;
 511          }
 512  
 513          // Set the filter.
 514          $this->filter = (int) $filterId;
 515  
 516          // Get a parameter object for the filter date options.
 517          $registry = new Registry($return->params);
 518          $params = $registry;
 519  
 520          // Set the dates if not already set.
 521          $this->dates->def('d1', $params->get('d1'));
 522          $this->dates->def('d2', $params->get('d2'));
 523          $this->dates->def('w1', $params->get('w1'));
 524          $this->dates->def('w2', $params->get('w2'));
 525  
 526          // Remove duplicates and sanitize.
 527          $filters = explode(',', $return->data);
 528          $filters = array_unique($filters);
 529          $filters = ArrayHelper::toInteger($filters);
 530  
 531          // Remove any values of zero.
 532          if (in_array(0, $filters, true) !== false) {
 533              unset($filters[array_search(0, $filters, true)]);
 534          }
 535  
 536          // Check if we have any real input.
 537          if (empty($filters)) {
 538              return true;
 539          }
 540  
 541          /*
 542           * Create the query to get filters from the database. We do this for
 543           * two reasons: one, it allows us to ensure that the filters being used
 544           * are real; two, we need to sort the filters by taxonomy branch.
 545           */
 546          $query->clear()
 547              ->select('t1.id, t1.title, t2.title AS branch')
 548              ->from($db->quoteName('#__finder_taxonomy') . ' AS t1')
 549              ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id')
 550              ->where('t1.state = 1')
 551              ->where('t1.access IN (' . $groups . ')')
 552              ->where('t1.id IN (' . implode(',', $filters) . ')')
 553              ->where('t2.state = 1')
 554              ->where('t2.access IN (' . $groups . ')');
 555  
 556          // Load the filters.
 557          $db->setQuery($query);
 558          $results = $db->loadObjectList();
 559  
 560          // Sort the filter ids by branch.
 561          foreach ($results as $result) {
 562              $this->filters[$result->branch][$result->title] = (int) $result->id;
 563          }
 564  
 565          return true;
 566      }
 567  
 568      /**
 569       * Method to process the dynamic taxonomy input. The dynamic taxonomy input
 570       * comes in the form of select fields that the user chooses from. The
 571       * dynamic taxonomy input is processed AFTER the static taxonomy input
 572       * because the dynamic options can be used to further narrow a static
 573       * taxonomy filter.
 574       *
 575       * @param   array  $filters  An array of taxonomy node ids.
 576       *
 577       * @return  boolean  True on success.
 578       *
 579       * @since   2.5
 580       * @throws  Exception on database error.
 581       */
 582      protected function processDynamicTaxonomy($filters)
 583      {
 584          // Initialize user variables
 585          $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels());
 586  
 587          // Remove duplicates and sanitize.
 588          $filters = array_unique($filters);
 589          $filters = ArrayHelper::toInteger($filters);
 590  
 591          // Remove any values of zero.
 592          if (in_array(0, $filters, true) !== false) {
 593              unset($filters[array_search(0, $filters, true)]);
 594          }
 595  
 596          // Check if we have any real input.
 597          if (empty($filters)) {
 598              return true;
 599          }
 600  
 601          // Get the database object.
 602          $db = $this->getDatabase();
 603  
 604          $query = $db->getQuery(true);
 605  
 606          /*
 607           * Create the query to get filters from the database. We do this for
 608           * two reasons: one, it allows us to ensure that the filters being used
 609           * are real; two, we need to sort the filters by taxonomy branch.
 610           */
 611          $query->select('t1.id, t1.title, t2.title AS branch')
 612              ->from($db->quoteName('#__finder_taxonomy') . ' AS t1')
 613              ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id')
 614              ->where('t1.state = 1')
 615              ->where('t1.access IN (' . $groups . ')')
 616              ->where('t1.id IN (' . implode(',', $filters) . ')')
 617              ->where('t2.state = 1')
 618              ->where('t2.access IN (' . $groups . ')');
 619  
 620          // Load the filters.
 621          $db->setQuery($query);
 622          $results = $db->loadObjectList();
 623  
 624          // Cleared filter branches.
 625          $cleared = array();
 626  
 627          /*
 628           * Sort the filter ids by branch. Because these filters are designed to
 629           * override and further narrow the items selected in the static filter,
 630           * we will clear the values from the static filter on a branch by
 631           * branch basis before adding the dynamic filters. So, if the static
 632           * filter defines a type filter of "articles" and three "category"
 633           * filters but the user only limits the category further, the category
 634           * filters will be flushed but the type filters will not.
 635           */
 636          foreach ($results as $result) {
 637              // Check if the branch has been cleared.
 638              if (!in_array($result->branch, $cleared, true)) {
 639                  // Clear the branch.
 640                  $this->filters[$result->branch] = array();
 641  
 642                  // Add the branch to the cleared list.
 643                  $cleared[] = $result->branch;
 644              }
 645  
 646              // Add the filter to the list.
 647              $this->filters[$result->branch][$result->title] = (int) $result->id;
 648          }
 649  
 650          return true;
 651      }
 652  
 653      /**
 654       * Method to process the query date filters to determine start and end
 655       * date limitations.
 656       *
 657       * @param   string  $date1  The first date filter.
 658       * @param   string  $date2  The second date filter.
 659       * @param   string  $when1  The first date modifier.
 660       * @param   string  $when2  The second date modifier.
 661       *
 662       * @return  boolean  True on success.
 663       *
 664       * @since   2.5
 665       */
 666      protected function processDates($date1, $date2, $when1, $when2)
 667      {
 668          // Clean up the inputs.
 669          $date1 = trim(StringHelper::strtolower($date1));
 670          $date2 = trim(StringHelper::strtolower($date2));
 671          $when1 = trim(StringHelper::strtolower($when1));
 672          $when2 = trim(StringHelper::strtolower($when2));
 673  
 674          // Get the time offset.
 675          $offset = Factory::getApplication()->get('offset');
 676  
 677          // Array of allowed when values.
 678          $whens = array('before', 'after', 'exact');
 679  
 680          // The value of 'today' is a special case that we need to handle.
 681          if ($date1 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) {
 682              $date1 = Factory::getDate('now', $offset)->format('%Y-%m-%d');
 683          }
 684  
 685          // Try to parse the date string.
 686          $date = Factory::getDate($date1, $offset);
 687  
 688          // Check if the date was parsed successfully.
 689          if ($date->toUnix() !== null) {
 690              // Set the date filter.
 691              $this->date1 = $date->toSql();
 692              $this->when1 = in_array($when1, $whens, true) ? $when1 : 'before';
 693          }
 694  
 695          // The value of 'today' is a special case that we need to handle.
 696          if ($date2 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) {
 697              $date2 = Factory::getDate('now', $offset)->format('%Y-%m-%d');
 698          }
 699  
 700          // Try to parse the date string.
 701          $date = Factory::getDate($date2, $offset);
 702  
 703          // Check if the date was parsed successfully.
 704          if ($date->toUnix() !== null) {
 705              // Set the date filter.
 706              $this->date2 = $date->toSql();
 707              $this->when2 = in_array($when2, $whens, true) ? $when2 : 'before';
 708          }
 709  
 710          return true;
 711      }
 712  
 713      /**
 714       * Method to process the query input string and extract required, optional,
 715       * and excluded tokens; taxonomy filters; and date filters.
 716       *
 717       * @param   string  $input  The query input string.
 718       * @param   string  $lang   The query input language.
 719       * @param   string  $mode   The query matching mode.
 720       *
 721       * @return  boolean  True on success.
 722       *
 723       * @since   2.5
 724       * @throws  Exception on database error.
 725       */
 726      protected function processString($input, $lang, $mode)
 727      {
 728          if ($input === null) {
 729              $input = '';
 730          }
 731  
 732          // Clean up the input string.
 733          $input  = html_entity_decode($input, ENT_QUOTES, 'UTF-8');
 734          $input  = StringHelper::strtolower($input);
 735          $input  = preg_replace('#\s+#mi', ' ', $input);
 736          $input  = trim($input);
 737          $debug  = Factory::getApplication()->get('debug_lang');
 738          $params = ComponentHelper::getParams('com_finder');
 739  
 740          /*
 741           * First, we need to handle string based modifiers. String based
 742           * modifiers could potentially include things like "category:blah" or
 743           * "before:2009-10-21" or "type:article", etc.
 744           */
 745          $patterns = array(
 746              'before' => Text::_('COM_FINDER_FILTER_WHEN_BEFORE'),
 747              'after'  => Text::_('COM_FINDER_FILTER_WHEN_AFTER'),
 748          );
 749  
 750          // Add the taxonomy branch titles to the possible patterns.
 751          foreach (Taxonomy::getBranchTitles() as $branch) {
 752              // Add the pattern.
 753              $patterns[$branch] = StringHelper::strtolower(Text::_(LanguageHelper::branchSingular($branch)));
 754          }
 755  
 756          // Container for search terms and phrases.
 757          $terms   = array();
 758          $phrases = array();
 759  
 760          // Cleared filter branches.
 761          $cleared = array();
 762  
 763          /*
 764           * Compile the suffix pattern. This is used to match the values of the
 765           * filter input string. Single words can be input directly, multi-word
 766           * values have to be wrapped in double quotes.
 767           */
 768          $quotes = html_entity_decode('&#8216;&#8217;&#39;', ENT_QUOTES, 'UTF-8');
 769          $suffix = '(([\w\d' . $quotes . '-]+)|\"([\w\d\s' . $quotes . '-]+)\")';
 770  
 771          /*
 772           * Iterate through the possible filter patterns and search for matches.
 773           * We need to match the key, colon, and a value pattern for the match
 774           * to be valid.
 775           */
 776          foreach ($patterns as $modifier => $pattern) {
 777              $matches = array();
 778  
 779              if ($debug) {
 780                  $pattern = substr($pattern, 2, -2);
 781              }
 782  
 783              // Check if the filter pattern is in the input string.
 784              if (preg_match('#' . $pattern . '\s*:\s*' . $suffix . '#mi', $input, $matches)) {
 785                  // Get the value given to the modifier.
 786                  $value = $matches[3] ?? $matches[1];
 787  
 788                  // Now we have to handle the filter string.
 789                  switch ($modifier) {
 790                      // Handle a before and after date filters.
 791                      case 'before':
 792                      case 'after':
 793                          // Get the time offset.
 794                          $offset = Factory::getApplication()->get('offset');
 795  
 796                          // Array of allowed when values.
 797                          $whens = array('before', 'after', 'exact');
 798  
 799                          // The value of 'today' is a special case that we need to handle.
 800                          if ($value === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) {
 801                              $value = Factory::getDate('now', $offset)->format('%Y-%m-%d');
 802                          }
 803  
 804                          // Try to parse the date string.
 805                          $date = Factory::getDate($value, $offset);
 806  
 807                          // Check if the date was parsed successfully.
 808                          if ($date->toUnix() !== null) {
 809                              // Set the date filter.
 810                              $this->date1 = $date->toSql();
 811                              $this->when1 = in_array($modifier, $whens, true) ? $modifier : 'before';
 812                          }
 813  
 814                          break;
 815  
 816                      // Handle a taxonomy branch filter.
 817                      default:
 818                          // Try to find the node id.
 819                          $return = Taxonomy::getNodeByTitle($modifier, $value);
 820  
 821                          // Check if the node id was found.
 822                          if ($return) {
 823                              // Check if the branch has been cleared.
 824                              if (!in_array($modifier, $cleared, true)) {
 825                                  // Clear the branch.
 826                                  $this->filters[$modifier] = array();
 827  
 828                                  // Add the branch to the cleared list.
 829                                  $cleared[] = $modifier;
 830                              }
 831  
 832                              // Add the filter to the list.
 833                              $this->filters[$modifier][$return->title] = (int) $return->id;
 834                          }
 835  
 836                          break;
 837                  }
 838  
 839                  // Clean up the input string again.
 840                  $input = str_replace($matches[0], '', $input);
 841                  $input = preg_replace('#\s+#mi', ' ', $input);
 842                  $input = trim($input);
 843              }
 844          }
 845  
 846          /*
 847           * Extract the tokens enclosed in double quotes so that we can handle
 848           * them as phrases.
 849           */
 850          if (StringHelper::strpos($input, '"') !== false) {
 851              $matches = array();
 852  
 853              // Extract the tokens enclosed in double quotes.
 854              if (preg_match_all('#\"([^"]+)\"#m', $input, $matches)) {
 855                  /*
 856                   * One or more phrases were found so we need to iterate through
 857                   * them, tokenize them as phrases, and remove them from the raw
 858                   * input string before we move on to the next processing step.
 859                   */
 860                  foreach ($matches[1] as $key => $match) {
 861                      // Find the complete phrase in the input string.
 862                      $pos = StringHelper::strpos($input, $matches[0][$key]);
 863                      $len = StringHelper::strlen($matches[0][$key]);
 864  
 865                      // Add any terms that are before this phrase to the stack.
 866                      if (trim(StringHelper::substr($input, 0, $pos))) {
 867                          $terms = array_merge($terms, explode(' ', trim(StringHelper::substr($input, 0, $pos))));
 868                      }
 869  
 870                      // Strip out everything up to and including the phrase.
 871                      $input = StringHelper::substr($input, $pos + $len);
 872  
 873                      // Clean up the input string again.
 874                      $input = preg_replace('#\s+#mi', ' ', $input);
 875                      $input = trim($input);
 876  
 877                      // Get the number of words in the phrase.
 878                      $parts = explode(' ', $match);
 879                      $tuplecount = $params->get('tuplecount', 1);
 880  
 881                      // Check if the phrase is longer than our $tuplecount.
 882                      if (count($parts) > $tuplecount && $tuplecount > 1) {
 883                          $chunk = array_slice($parts, 0, $tuplecount);
 884                          $parts = array_slice($parts, $tuplecount);
 885  
 886                          // If the chunk is not empty, add it as a phrase.
 887                          if (count($chunk)) {
 888                              $phrases[] = implode(' ', $chunk);
 889                              $terms[] = implode(' ', $chunk);
 890                          }
 891  
 892                          /*
 893                           * If the phrase is longer than $tuplecount words, we need to
 894                           * break it down into smaller chunks of phrases that
 895                           * are less than or equal to $tuplecount words. We overlap
 896                           * the chunks so that we can ensure that a match is
 897                           * found for the complete phrase and not just portions
 898                           * of it.
 899                           */
 900                          for ($i = 0, $c = count($parts); $i < $c; $i++) {
 901                              array_shift($chunk);
 902                              $chunk[] = array_shift($parts);
 903  
 904                              // If the chunk is not empty, add it as a phrase.
 905                              if (count($chunk)) {
 906                                  $phrases[] = implode(' ', $chunk);
 907                                  $terms[]   = implode(' ', $chunk);
 908                              }
 909                          }
 910                      } else {
 911                          // The phrase is <= $tuplecount words so we can use it as is.
 912                          $phrases[] = $match;
 913                          $terms[]   = $match;
 914                      }
 915                  }
 916              }
 917          }
 918  
 919          // Add the remaining terms if present.
 920          if ((bool) $input) {
 921              $terms = array_merge($terms, explode(' ', $input));
 922          }
 923  
 924          // An array of our boolean operators. $operator => $translation
 925          $operators = array(
 926              'AND' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_AND')),
 927              'OR'  => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_OR')),
 928              'NOT' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_NOT')),
 929          );
 930  
 931          // If language debugging is enabled you need to ignore the debug strings in matching.
 932          if (JDEBUG) {
 933              $debugStrings = array('**', '??');
 934              $operators    = str_replace($debugStrings, '', $operators);
 935          }
 936  
 937          /*
 938           * Iterate through the terms and perform any sorting that needs to be
 939           * done based on boolean search operators. Terms that are before an
 940           * and/or/not modifier have to be handled in relation to their operator.
 941           */
 942          for ($i = 0, $c = count($terms); $i < $c; $i++) {
 943              // Check if the term is followed by an operator that we understand.
 944              if (isset($terms[$i + 1]) && in_array($terms[$i + 1], $operators, true)) {
 945                  // Get the operator mode.
 946                  $op = array_search($terms[$i + 1], $operators, true);
 947  
 948                  // Handle the AND operator.
 949                  if ($op === 'AND' && isset($terms[$i + 2])) {
 950                      // Tokenize the current term.
 951                      $token = Helper::tokenize($terms[$i], $lang, true);
 952  
 953                      // @todo: The previous function call may return an array, which seems not to be handled by the next one, which expects an object
 954                      $token = $this->getTokenData(array_shift($token));
 955  
 956                      if ($params->get('filter_commonwords', 0) && $token->common) {
 957                          continue;
 958                      }
 959  
 960                      if ($params->get('filter_numeric', 0) && $token->numeric) {
 961                          continue;
 962                      }
 963  
 964                      // Set the required flag.
 965                      $token->required = true;
 966  
 967                      // Add the current token to the stack.
 968                      $this->included[] = $token;
 969                      $this->highlight  = array_merge($this->highlight, array_keys($token->matches));
 970  
 971                      // Skip the next token (the mode operator).
 972                      $this->operators[] = $terms[$i + 1];
 973  
 974                      // Tokenize the term after the next term (current plus two).
 975                      $other = Helper::tokenize($terms[$i + 2], $lang, true);
 976                      $other = $this->getTokenData(array_shift($other));
 977  
 978                      // Set the required flag.
 979                      $other->required = true;
 980  
 981                      // Add the token after the next token to the stack.
 982                      $this->included[] = $other;
 983                      $this->highlight  = array_merge($this->highlight, array_keys($other->matches));
 984  
 985                      // Remove the processed phrases if possible.
 986                      if (($pk = array_search($terms[$i], $phrases, true)) !== false) {
 987                          unset($phrases[$pk]);
 988                      }
 989  
 990                      if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) {
 991                          unset($phrases[$pk]);
 992                      }
 993  
 994                      // Remove the processed terms.
 995                      unset($terms[$i], $terms[$i + 1], $terms[$i + 2]);
 996  
 997                      // Adjust the loop.
 998                      $i += 2;
 999                  } elseif ($op === 'OR' && isset($terms[$i + 2])) {
1000                      // Handle the OR operator.
1001                      // Tokenize the current term.
1002                      $token = Helper::tokenize($terms[$i], $lang, true);
1003                      $token = $this->getTokenData(array_shift($token));
1004  
1005                      if ($params->get('filter_commonwords', 0) && $token->common) {
1006                          continue;
1007                      }
1008  
1009                      if ($params->get('filter_numeric', 0) && $token->numeric) {
1010                          continue;
1011                      }
1012  
1013                      // Set the required flag.
1014                      $token->required = false;
1015  
1016                      // Add the current token to the stack.
1017                      if ((bool) $token->matches) {
1018                          $this->included[] = $token;
1019                          $this->highlight  = array_merge($this->highlight, array_keys($token->matches));
1020                      } else {
1021                          $this->ignored[] = $token;
1022                      }
1023  
1024                      // Skip the next token (the mode operator).
1025                      $this->operators[] = $terms[$i + 1];
1026  
1027                      // Tokenize the term after the next term (current plus two).
1028                      $other = Helper::tokenize($terms[$i + 2], $lang, true);
1029                      $other = $this->getTokenData(array_shift($other));
1030  
1031                      // Set the required flag.
1032                      $other->required = false;
1033  
1034                      // Add the token after the next token to the stack.
1035                      if ((bool) $other->matches) {
1036                          $this->included[] = $other;
1037                          $this->highlight  = array_merge($this->highlight, array_keys($other->matches));
1038                      } else {
1039                          $this->ignored[] = $other;
1040                      }
1041  
1042                      // Remove the processed phrases if possible.
1043                      if (($pk = array_search($terms[$i], $phrases, true)) !== false) {
1044                          unset($phrases[$pk]);
1045                      }
1046  
1047                      if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) {
1048                          unset($phrases[$pk]);
1049                      }
1050  
1051                      // Remove the processed terms.
1052                      unset($terms[$i], $terms[$i + 1], $terms[$i + 2]);
1053  
1054                      // Adjust the loop.
1055                      $i += 2;
1056                  }
1057              } elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'OR') {
1058                  // Handle an orphaned OR operator.
1059                  // Skip the next token (the mode operator).
1060                  $this->operators[] = $terms[$i];
1061  
1062                  // Tokenize the next term (current plus one).
1063                  $other = Helper::tokenize($terms[$i + 1], $lang, true);
1064                  $other = $this->getTokenData(array_shift($other));
1065  
1066                  if ($params->get('filter_commonwords', 0) && $other->common) {
1067                      continue;
1068                  }
1069  
1070                  if ($params->get('filter_numeric', 0) && $other->numeric) {
1071                      continue;
1072                  }
1073  
1074                  // Set the required flag.
1075                  $other->required = false;
1076  
1077                  // Add the token after the next token to the stack.
1078                  if ((bool) $other->matches) {
1079                      $this->included[] = $other;
1080                      $this->highlight  = array_merge($this->highlight, array_keys($other->matches));
1081                  } else {
1082                      $this->ignored[] = $other;
1083                  }
1084  
1085                  // Remove the processed phrase if possible.
1086                  if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) {
1087                      unset($phrases[$pk]);
1088                  }
1089  
1090                  // Remove the processed terms.
1091                  unset($terms[$i], $terms[$i + 1]);
1092  
1093                  // Adjust the loop.
1094                  $i++;
1095              } elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'NOT') {
1096                  // Handle the NOT operator.
1097                  // Skip the next token (the mode operator).
1098                  $this->operators[] = $terms[$i];
1099  
1100                  // Tokenize the next term (current plus one).
1101                  $other = Helper::tokenize($terms[$i + 1], $lang, true);
1102                  $other = $this->getTokenData(array_shift($other));
1103  
1104                  if ($params->get('filter_commonwords', 0) && $other->common) {
1105                      continue;
1106                  }
1107  
1108                  if ($params->get('filter_numeric', 0) && $other->numeric) {
1109                      continue;
1110                  }
1111  
1112                  // Set the required flag.
1113                  $other->required = false;
1114  
1115                  // Add the next token to the stack.
1116                  if ((bool) $other->matches) {
1117                      $this->excluded[] = $other;
1118                  } else {
1119                      $this->ignored[] = $other;
1120                  }
1121  
1122                  // Remove the processed phrase if possible.
1123                  if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) {
1124                      unset($phrases[$pk]);
1125                  }
1126  
1127                  // Remove the processed terms.
1128                  unset($terms[$i], $terms[$i + 1]);
1129  
1130                  // Adjust the loop.
1131                  $i++;
1132              }
1133          }
1134  
1135          /*
1136           * Iterate through any search phrases and tokenize them. We handle
1137           * phrases as autonomous units and do not break them down into two and
1138           * three word combinations.
1139           */
1140          for ($i = 0, $c = count($phrases); $i < $c; $i++) {
1141              // Tokenize the phrase.
1142              $token = Helper::tokenize($phrases[$i], $lang, true);
1143  
1144              if (!count($token)) {
1145                  continue;
1146              }
1147  
1148              $token = $this->getTokenData(array_shift($token));
1149  
1150              if ($params->get('filter_commonwords', 0) && $token->common) {
1151                  continue;
1152              }
1153  
1154              if ($params->get('filter_numeric', 0) && $token->numeric) {
1155                  continue;
1156              }
1157  
1158              // Set the required flag.
1159              $token->required = true;
1160  
1161              // Add the current token to the stack.
1162              $this->included[] = $token;
1163              $this->highlight  = array_merge($this->highlight, array_keys($token->matches));
1164  
1165              // Remove the processed term if possible.
1166              if (($pk = array_search($phrases[$i], $terms, true)) !== false) {
1167                  unset($terms[$pk]);
1168              }
1169  
1170              // Remove the processed phrase.
1171              unset($phrases[$i]);
1172          }
1173  
1174          /*
1175           * Handle any remaining tokens using the standard processing mechanism.
1176           */
1177          if ((bool) $terms) {
1178              // Tokenize the terms.
1179              $terms  = implode(' ', $terms);
1180              $tokens = Helper::tokenize($terms, $lang, false);
1181  
1182              // Make sure we are working with an array.
1183              $tokens = is_array($tokens) ? $tokens : array($tokens);
1184  
1185              // Get the token data and required state for all the tokens.
1186              foreach ($tokens as $token) {
1187                  // Get the token data.
1188                  $token = $this->getTokenData($token);
1189  
1190                  if ($params->get('filter_commonwords', 0) && $token->common) {
1191                      continue;
1192                  }
1193  
1194                  if ($params->get('filter_numerics', 0) && $token->numeric) {
1195                      continue;
1196                  }
1197  
1198                  // Set the required flag for the token.
1199                  $token->required = $mode === 'AND' ? (!$token->phrase) : false;
1200  
1201                  // Add the token to the appropriate stack.
1202                  if ($token->required || (bool) $token->matches) {
1203                      $this->included[] = $token;
1204                      $this->highlight  = array_merge($this->highlight, array_keys($token->matches));
1205                  } else {
1206                      $this->ignored[] = $token;
1207                  }
1208              }
1209          }
1210  
1211          return true;
1212      }
1213  
1214      /**
1215       * Method to get the base and similar term ids and, if necessary, suggested
1216       * term data from the database. The terms ids are identified based on a
1217       * 'like' match in MySQL and/or a common stem. If no term ids could be
1218       * found, then we know that we will not be able to return any results for
1219       * that term and we should try to find a similar term to use that we can
1220       * match so that we can suggest the alternative search query to the user.
1221       *
1222       * @param   Token  $token  A Token object.
1223       *
1224       * @return  Token  A Token object.
1225       *
1226       * @since   2.5
1227       * @throws  Exception on database error.
1228       */
1229      protected function getTokenData($token)
1230      {
1231          // Get the database object.
1232          $db = $this->getDatabase();
1233  
1234          // Create a database query to build match the token.
1235          $query = $db->getQuery(true)
1236              ->select('t.term, t.term_id')
1237              ->from('#__finder_terms AS t');
1238  
1239          if ($token->phrase) {
1240              // Add the phrase to the query.
1241              $query->where('t.term = ' . $db->quote($token->term))
1242                  ->where('t.phrase = 1');
1243          } else {
1244              // Add the term to the query.
1245  
1246              $searchTerm = $token->term;
1247              $searchStem = $token->stem;
1248              $term = $query->quoteName('t.term');
1249              $stem = $query->quoteName('t.stem');
1250  
1251              if ($this->wordmode === 'begin') {
1252                  $searchTerm .= '%';
1253                  $searchStem .= '%';
1254                  $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)');
1255              } elseif ($this->wordmode === 'fuzzy') {
1256                  $searchTerm = '%' . $searchTerm . '%';
1257                  $searchStem = '%' . $searchStem . '%';
1258                  $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)');
1259              } else {
1260                  $query->where('(' . $term . ' = :searchTerm OR ' . $stem . ' = :searchStem)');
1261              }
1262  
1263              $query->bind(':searchTerm', $searchTerm, ParameterType::STRING)
1264                  ->bind(':searchStem', $searchStem, ParameterType::STRING);
1265  
1266              $query->where('t.phrase = 0')
1267                  ->where('t.language IN (\'*\',' . $db->quote($token->language) . ')');
1268          }
1269  
1270          // Get the terms.
1271          $db->setQuery($query);
1272          $matches = $db->loadObjectList();
1273  
1274          // Check the matching terms.
1275          if ((bool) $matches) {
1276              // Add the matches to the token.
1277              for ($i = 0, $c = count($matches); $i < $c; $i++) {
1278                  if (!isset($token->matches[$matches[$i]->term])) {
1279                      $token->matches[$matches[$i]->term] = array();
1280                  }
1281  
1282                  $token->matches[$matches[$i]->term][] = (int) $matches[$i]->term_id;
1283              }
1284          }
1285  
1286          // If no matches were found, try to find a similar but better token.
1287          if (empty($token->matches)) {
1288              // Create a database query to get the similar terms.
1289              $query->clear()
1290                  ->select('DISTINCT t.term_id AS id, t.term AS term')
1291                  ->from('#__finder_terms AS t')
1292                  // ->where('t.soundex = ' . soundex($db->quote($token->term)))
1293                  ->where('t.soundex = SOUNDEX(' . $db->quote($token->term) . ')')
1294                  ->where('t.phrase = ' . (int) $token->phrase);
1295  
1296              // Get the terms.
1297              $db->setQuery($query);
1298              $results = $db->loadObjectList();
1299  
1300              // Check if any similar terms were found.
1301              if (empty($results)) {
1302                  return $token;
1303              }
1304  
1305              // Stack for sorting the similar terms.
1306              $suggestions = array();
1307  
1308              // Get the levnshtein distance for all suggested terms.
1309              foreach ($results as $sk => $st) {
1310                  // Get the levenshtein distance between terms.
1311                  $distance = levenshtein($st->term, $token->term);
1312  
1313                  // Make sure the levenshtein distance isn't over 50.
1314                  if ($distance < 50) {
1315                      $suggestions[$sk] = $distance;
1316                  }
1317              }
1318  
1319              // Sort the suggestions.
1320              asort($suggestions, SORT_NUMERIC);
1321  
1322              // Get the closest match.
1323              $keys = array_keys($suggestions);
1324              $key  = $keys[0];
1325  
1326              // Add the suggested term.
1327              $token->suggestion = $results[$key]->term;
1328          }
1329  
1330          return $token;
1331      }
1332  }


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