[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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('‘’'', 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 }
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 |