Source for file query.php

Documentation is available at query.php

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

Documentation generated on Tue, 19 Nov 2013 15:11:28 +0100 by phpDocumentor 1.4.3