source: trunk/include/functions_search.inc.php @ 30353

Last change on this file since 30353 was 29804, checked in by rvelices, 10 years ago
  • Property svn:eol-style set to LF
File size: 37.3 KB
Line 
1<?php
2// +-----------------------------------------------------------------------+
3// | Piwigo - a PHP based photo gallery                                    |
4// +-----------------------------------------------------------------------+
5// | Copyright(C) 2008-2014 Piwigo Team                  http://piwigo.org |
6// | Copyright(C) 2003-2008 PhpWebGallery Team    http://phpwebgallery.net |
7// | Copyright(C) 2002-2003 Pierrick LE GALL   http://le-gall.net/pierrick |
8// +-----------------------------------------------------------------------+
9// | This program is free software; you can redistribute it and/or modify  |
10// | it under the terms of the GNU General Public License as published by  |
11// | the Free Software Foundation                                          |
12// |                                                                       |
13// | This program is distributed in the hope that it will be useful, but   |
14// | WITHOUT ANY WARRANTY; without even the implied warranty of            |
15// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      |
16// | General Public License for more details.                              |
17// |                                                                       |
18// | You should have received a copy of the GNU General Public License     |
19// | along with this program; if not, write to the Free Software           |
20// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
21// | USA.                                                                  |
22// +-----------------------------------------------------------------------+
23
24/**
25 * @package functions\search
26 */
27
28
29/**
30 * Returns search rules stored into a serialized array in "search"
31 * table. Each search rules set is numericaly identified.
32 *
33 * @param int $search_id
34 * @return array
35 */
36function get_search_array($search_id)
37{
38  if (!is_numeric($search_id))
39  {
40    die('Search id must be an integer');
41  }
42
43  $query = '
44SELECT rules
45  FROM '.SEARCH_TABLE.'
46  WHERE id = '.$search_id.'
47;';
48  list($serialized_rules) = pwg_db_fetch_row(pwg_query($query));
49
50  return unserialize($serialized_rules);
51}
52
53/**
54 * Returns the SQL clause for a search.
55 * Transforms the array returned by get_search_array() into SQL sub-query.
56 *
57 * @param array $search
58 * @return string
59 */
60function get_sql_search_clause($search)
61{
62  // SQL where clauses are stored in $clauses array during query
63  // construction
64  $clauses = array();
65
66  foreach (array('file','name','comment','author') as $textfield)
67  {
68    if (isset($search['fields'][$textfield]))
69    {
70      $local_clauses = array();
71      foreach ($search['fields'][$textfield]['words'] as $word)
72      {
73        if ('author' == $textfield)
74        {
75          $local_clauses[] = $textfield."='".$word."'";
76        }
77        else
78        {
79          $local_clauses[] = $textfield." LIKE '%".$word."%'";
80        }
81      }
82
83      // adds brackets around where clauses
84      $local_clauses = prepend_append_array_items($local_clauses, '(', ')');
85
86      $clauses[] = implode(
87        ' '.$search['fields'][$textfield]['mode'].' ',
88        $local_clauses
89        );
90    }
91  }
92
93  if (isset($search['fields']['allwords']))
94  {
95    $fields = array('file', 'name', 'comment');
96
97    if (isset($search['fields']['allwords']['fields']) and count($search['fields']['allwords']['fields']) > 0)
98    {
99      $fields = array_intersect($fields, $search['fields']['allwords']['fields']);
100    }
101   
102    // in the OR mode, request bust be :
103    // ((field1 LIKE '%word1%' OR field2 LIKE '%word1%')
104    // OR (field1 LIKE '%word2%' OR field2 LIKE '%word2%'))
105    //
106    // in the AND mode :
107    // ((field1 LIKE '%word1%' OR field2 LIKE '%word1%')
108    // AND (field1 LIKE '%word2%' OR field2 LIKE '%word2%'))
109    $word_clauses = array();
110    foreach ($search['fields']['allwords']['words'] as $word)
111    {
112      $field_clauses = array();
113      foreach ($fields as $field)
114      {
115        $field_clauses[] = $field." LIKE '%".$word."%'";
116      }
117      // adds brackets around where clauses
118      $word_clauses[] = implode(
119        "\n          OR ",
120        $field_clauses
121        );
122    }
123
124    array_walk(
125      $word_clauses,
126      create_function('&$s','$s="(".$s.")";')
127      );
128
129    // make sure the "mode" is either OR or AND
130    if ($search['fields']['allwords']['mode'] != 'AND' and $search['fields']['allwords']['mode'] != 'OR')
131    {
132      $search['fields']['allwords']['mode'] = 'AND';
133    }
134
135    $clauses[] = "\n         ".
136      implode(
137        "\n         ". $search['fields']['allwords']['mode']. "\n         ",
138        $word_clauses
139        );
140  }
141
142  foreach (array('date_available', 'date_creation') as $datefield)
143  {
144    if (isset($search['fields'][$datefield]))
145    {
146      $clauses[] = $datefield." = '".$search['fields'][$datefield]['date']."'";
147    }
148
149    foreach (array('after','before') as $suffix)
150    {
151      $key = $datefield.'-'.$suffix;
152
153      if (isset($search['fields'][$key]))
154      {
155        $clauses[] = $datefield.
156          ($suffix == 'after'             ? ' >' : ' <').
157          ($search['fields'][$key]['inc'] ? '='  : '').
158          " '".$search['fields'][$key]['date']."'";
159      }
160    }
161  }
162
163  if (isset($search['fields']['cat']))
164  {
165    if ($search['fields']['cat']['sub_inc'])
166    {
167      // searching all the categories id of sub-categories
168      $cat_ids = get_subcat_ids($search['fields']['cat']['words']);
169    }
170    else
171    {
172      $cat_ids = $search['fields']['cat']['words'];
173    }
174
175    $local_clause = 'category_id IN ('.implode(',', $cat_ids).')';
176    $clauses[] = $local_clause;
177  }
178
179  // adds brackets around where clauses
180  $clauses = prepend_append_array_items($clauses, '(', ')');
181
182  $where_separator =
183    implode(
184      "\n    ".$search['mode'].' ',
185      $clauses
186      );
187
188  $search_clause = $where_separator;
189
190  return $search_clause;
191}
192
193/**
194 * Returns the list of items corresponding to the advanced search array.
195 *
196 * @param array $search
197 * @param string $images_where optional additional restriction on images table
198 * @return array
199 */
200function get_regular_search_results($search, $images_where='')
201{
202  global $conf;
203  $forbidden = get_sql_condition_FandF(
204        array
205          (
206            'forbidden_categories' => 'category_id',
207            'visible_categories' => 'category_id',
208            'visible_images' => 'id'
209          ),
210        "\n  AND"
211    );
212
213  $items = array();
214  $tag_items = array();
215
216  if (isset($search['fields']['tags']))
217  {
218    $tag_items = get_image_ids_for_tags(
219      $search['fields']['tags']['words'],
220      $search['fields']['tags']['mode']
221      );
222  }
223
224  $search_clause = get_sql_search_clause($search);
225
226  if (!empty($search_clause))
227  {
228    $query = '
229SELECT DISTINCT(id)
230  FROM '.IMAGES_TABLE.' i
231    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id
232  WHERE '.$search_clause;
233    if (!empty($images_where))
234    {
235      $query .= "\n  AND ".$images_where;
236    }
237    $query .= $forbidden.'
238  '.$conf['order_by'];
239    $items = array_from_query($query, 'id');
240  }
241
242  if ( !empty($tag_items) )
243  {
244    switch ($search['mode'])
245    {
246      case 'AND':
247        if (empty($search_clause))
248        {
249          $items = $tag_items;
250        }
251        else
252        {
253          $items = array_values( array_intersect($items, $tag_items) );
254        }
255        break;
256      case 'OR':
257        $before_count = count($items);
258        $items = array_unique(
259          array_merge(
260            $items,
261            $tag_items
262            )
263          );
264        break;
265    }
266  }
267
268  return $items;
269}
270
271
272
273define('QST_QUOTED',         0x01);
274define('QST_NOT',            0x02);
275define('QST_OR',             0x04);
276define('QST_WILDCARD_BEGIN', 0x08);
277define('QST_WILDCARD_END',   0x10);
278define('QST_WILDCARD', QST_WILDCARD_BEGIN|QST_WILDCARD_END);
279define('QST_BREAK',          0x20);
280
281/**
282 * A search scope applies to a single token and restricts the search to a subset of searchable fields.
283 */
284class QSearchScope
285{
286  var $id;
287  var $aliases;
288  var $is_text;
289  var $nullable;
290
291  function __construct($id, $aliases, $nullable=false, $is_text=true)
292  {
293    $this->id = $id;
294    $this->aliases = $aliases;
295    $this->is_text = $is_text;
296    $this->nullable =$nullable;
297  }
298
299  function parse($token)
300  {
301    if (!$this->nullable && 0==strlen($token->term))
302      return false;
303    return true;
304  }
305 
306  function process_char(&$ch, &$crt_token)
307  {
308    return false;
309  }
310}
311
312class QNumericRangeScope extends QSearchScope
313{
314  private $epsilon;
315  function __construct($id, $aliases, $nullable=false, $epsilon=0)
316  {
317    parent::__construct($id, $aliases, $nullable, false);
318    $this->epsilon = $epsilon;
319  }
320
321  function parse($token)
322  {
323    $str = $token->term;
324    $strict = array(0,0);
325    if ( ($pos = strpos($str, '..')) !== false)
326      $range = array( substr($str,0,$pos), substr($str, $pos+2));
327    elseif ('>' == @$str[0])// ratio:>1
328    {
329      $range = array( substr($str,1), '');
330      $strict[0] = 1;
331    }
332    elseif ('<' == @$str[0]) // size:<5mp
333    {
334      $range = array('', substr($str,1));
335      $strict[1] = 1;
336    }
337    elseif( ($token->modifier & QST_WILDCARD_BEGIN) )
338      $range = array('', $str);
339    elseif( ($token->modifier & QST_WILDCARD_END) )
340      $range = array($str, '');
341    else
342      $range = array($str, $str);
343
344    foreach ($range as $i =>&$val)
345    {
346      if (preg_match('#^(-?[0-9.]+)/([0-9.]+)$#i', $val, $matches))
347      {
348        $val = floatval($matches[1]/$matches[2]);
349      }
350      elseif (preg_match('/^(-?[0-9.]+)([km])?/i', $val, $matches))
351      {
352        $val = floatval($matches[1]);
353        if (isset($matches[2]))
354        {
355          if ($matches[2]=='k' || $matches[2]=='K')
356          {
357            $val *= 1000;
358            if ($i) $val += 999;
359          }
360          if ($matches[2]=='m' || $matches[2]=='M')
361          {
362            $val *= 1000000;
363            if ($i) $val += 999999;
364          }
365        }
366      }
367      else
368        $val = '';
369      if (is_numeric($val))
370      {
371        if ($i ^ $strict[$i])
372          $val += $this->epsilon;
373        else
374          $val -= $this->epsilon;
375      }
376    }
377
378    if (!$this->nullable && $range[0]=='' && $range[1] == '')
379      return false;
380    $token->scope_data = array( 'range'=>$range, 'strict'=>$strict );
381    return true;
382  }
383
384  function get_sql($field, $token)
385  {
386    $clauses = array();
387    if ($token->scope_data['range'][0]!='')
388      $clauses[] = $field.' >'.($token->scope_data['strict'][0]?'':'=').$token->scope_data['range'][0].' ';
389    if ($token->scope_data['range'][1]!='')
390      $clauses[] = $field.' <'.($token->scope_data['strict'][1]?'':'=').$token->scope_data['range'][1].' ';
391
392    if (empty($clauses))
393    {
394      if ($token->modifier & QST_WILDCARD)
395        return $field.' IS NOT NULL';
396      else
397        return $field.' IS NULL';
398    }
399    return '('.implode(' AND ', $clauses).')';
400  }
401}
402
403
404class QDateRangeScope extends QSearchScope
405{
406  function __construct($id, $aliases, $nullable=false)
407  {
408    parent::__construct($id, $aliases, $nullable, false);
409  }
410
411  function parse($token)
412  {
413    $str = $token->term;
414    $strict = array(0,0);
415    if ( ($pos = strpos($str, '..')) !== false)
416      $range = array( substr($str,0,$pos), substr($str, $pos+2));
417    elseif ('>' == @$str[0])
418    {
419      $range = array( substr($str,1), '');
420      $strict[0] = 1;
421    }
422    elseif ('<' == @$str[0])
423    {
424      $range = array('', substr($str,1));
425      $strict[1] = 1;
426    }
427    elseif( ($token->modifier & QST_WILDCARD_BEGIN) )
428      $range = array('', $str);
429    elseif( ($token->modifier & QST_WILDCARD_END) )
430      $range = array($str, '');
431    else
432      $range = array($str, $str);
433
434    foreach ($range as $i =>&$val)
435    {
436      if (preg_match('/([0-9]{4})-?((?:1[0-2])|(?:0?[1-9]))?-?((?:(?:[1-3][0-9])|(?:0?[1-9])))?/', $val, $matches))
437      {
438        array_shift($matches);
439        if (!isset($matches[1]))
440          $matches[1] = ($i ^ $strict[$i]) ? 12 : 1;
441        if (!isset($matches[2]))
442          $matches[2] = ($i ^ $strict[$i]) ? 31 : 1;
443        $val = implode('-', $matches);
444        if ($i ^ $strict[$i])
445          $val .= ' 23:59:59';
446      }
447      elseif (strlen($val))
448        return false;
449    }
450
451    if (!$this->nullable && $range[0]=='' && $range[1] == '')
452      return false;
453
454    $token->scope_data = $range;
455    return true;
456  }
457
458  function get_sql($field, $token)
459  {
460    $clauses = array();
461    if ($token->scope_data[0]!='')
462      $clauses[] = $field.' >= \'' . $token->scope_data[0].'\'';
463    if ($token->scope_data[1]!='')
464      $clauses[] = $field.' <= \'' . $token->scope_data[1].'\'';
465
466    if (empty($clauses))
467    {
468      if ($token->modifier & QST_WILDCARD)
469        return $field.' IS NOT NULL';
470      else
471        return $field.' IS NULL';
472    }
473    return '('.implode(' AND ', $clauses).')';
474  }
475}
476
477/**
478 * Analyzes and splits the quick/query search query $q into tokens.
479 * q='john bill' => 2 tokens 'john' 'bill'
480 * Special characters for MySql full text search (+,<,>,~) appear in the token modifiers.
481 * The query can contain a phrase: 'Pierre "New York"' will return 'pierre' qnd 'new york'.
482 *
483 * @param string $q
484 */
485
486/** Represents a single word or quoted phrase to be searched.*/
487class QSingleToken
488{
489  var $is_single = true;
490  var $modifier;
491  var $term; /* the actual word/phrase string*/
492  var $variants = array();
493  var $scope;
494
495  var $scope_data;
496  var $idx;
497
498  function __construct($term, $modifier, $scope)
499  {
500    $this->term = $term;
501    $this->modifier = $modifier;
502    $this->scope = $scope;
503  }
504
505  function __toString()
506  {
507    $s = '';
508    if (isset($this->scope))
509      $s .= $this->scope->id .':';
510    if ($this->modifier & QST_WILDCARD_BEGIN)
511      $s .= '*';
512    if ($this->modifier & QST_QUOTED)
513      $s .= '"';
514    $s .= $this->term;
515    if ($this->modifier & QST_QUOTED)
516      $s .= '"';
517    if ($this->modifier & QST_WILDCARD_END)
518      $s .= '*';
519    return $s;
520  }
521}
522
523/** Represents an expression of several words or sub expressions to be searched.*/
524class QMultiToken
525{
526  var $is_single = false;
527  var $modifier;
528  var $tokens = array(); // the actual array of QSingleToken or QMultiToken
529
530  function __toString()
531  {
532    $s = '';
533    for ($i=0; $i<count($this->tokens); $i++)
534    {
535      $modifier = $this->tokens[$i]->modifier;
536      if ($i)
537        $s .= ' ';
538      if ($modifier & QST_OR)
539        $s .= 'OR ';
540      if ($modifier & QST_NOT)
541        $s .= 'NOT ';
542      if (! ($this->tokens[$i]->is_single) )
543      {
544        $s .= '(';
545        $s .= $this->tokens[$i];
546        $s .= ')';
547      }
548      else
549      {
550        $s .= $this->tokens[$i];
551      }
552    }
553    return $s;
554  }
555
556  private function push(&$token, &$modifier, &$scope)
557  {
558    if (strlen($token) || (isset($scope) && $scope->nullable))
559    {
560      if (isset($scope))
561        $modifier |= QST_BREAK;
562      $this->tokens[] = new QSingleToken($token, $modifier, $scope);
563    }
564    $token = "";
565    $modifier = 0;
566    $scope = null;
567  }
568
569  /**
570  * Parses the input query string by tokenizing the input, generating the modifiers (and/or/not/quotation/wildcards...).
571  * Recursivity occurs when parsing ()
572  * @param string $q the actual query to be parsed
573  * @param int $qi the character index in $q where to start parsing
574  * @param int $level the depth from root in the tree (number of opened and unclosed opening brackets)
575  */
576  protected function parse_expression($q, &$qi, $level, $root)
577  {
578    $crt_token = "";
579    $crt_modifier = 0;
580    $crt_scope = null;
581
582    for ($stop=false; !$stop && $qi<strlen($q); $qi++)
583    {
584      $ch = $q[$qi];
585      if ( ($crt_modifier&QST_QUOTED)==0)
586      {
587        switch ($ch)
588        {
589          case '(':
590            if (strlen($crt_token))
591              $this->push($crt_token, $crt_modifier, $crt_scope);
592            $sub = new QMultiToken;
593            $qi++;
594            $sub->parse_expression($q, $qi, $level+1, $root);
595            $sub->modifier = $crt_modifier;
596            if (isset($crt_scope) && $crt_scope->is_text)
597            {
598              $sub->apply_scope($crt_scope); // eg. 'tag:(John OR Bill)'
599            }
600            $this->tokens[] = $sub;
601            $crt_modifier = 0;
602            $crt_scope = null;
603            break;
604          case ')':
605            if ($level>0)
606              $stop = true;
607            break;
608          case ':':
609            $scope = @$root->scopes[strtolower($crt_token)];
610            if (!isset($scope) || isset($crt_scope))
611            { // white space
612              $this->push($crt_token, $crt_modifier, $crt_scope);
613            }
614            else
615            {
616              $crt_token = "";
617              $crt_scope = $scope;
618            }
619            break;
620          case '"':
621            if (strlen($crt_token))
622              $this->push($crt_token, $crt_modifier, $crt_scope);
623            $crt_modifier |= QST_QUOTED;
624            break;
625          case '-':
626            if (strlen($crt_token) || isset($crt_scope))
627              $crt_token .= $ch;
628            else
629              $crt_modifier |= QST_NOT;
630            break;
631          case '*':
632            if (strlen($crt_token))
633              $crt_token .= $ch; // wildcard end later
634            else
635              $crt_modifier |= QST_WILDCARD_BEGIN;
636            break;
637          case '.':
638            if (isset($crt_scope) && !$crt_scope->is_text)
639            {
640              $crt_token .= $ch;
641              break;
642            }
643            if (strlen($crt_token) && preg_match('/[0-9]/', substr($crt_token,-1))
644              && $qi+1<strlen($q) && preg_match('/[0-9]/', $q[$qi+1]))
645            {// dot between digits is not a separator e.g. F2.8
646              $crt_token .= $ch;
647              break;
648            }
649            // else white space go on..
650          default:
651            if (!$crt_scope || !$crt_scope->process_char($ch, $crt_token))
652            {
653              if (strpos(' ,.;!?', $ch)!==false)
654              { // white space
655                $this->push($crt_token, $crt_modifier, $crt_scope);
656              }
657              else
658                $crt_token .= $ch;
659            }
660            break;
661        }
662      }
663      else
664      {// quoted
665        if ($ch=='"')
666        {
667          if ($qi+1 < strlen($q) && $q[$qi+1]=='*')
668          {
669            $crt_modifier |= QST_WILDCARD_END;
670            $qi++;
671          }
672          $this->push($crt_token, $crt_modifier, $crt_scope);
673        }
674        else
675          $crt_token .= $ch;
676      }
677    }
678
679    $this->push($crt_token, $crt_modifier, $crt_scope);
680
681    for ($i=0; $i<count($this->tokens); $i++)
682    {
683      $token = $this->tokens[$i];
684      $remove = false;
685      if ($token->is_single)
686      {
687        if ( ($token->modifier & QST_QUOTED)==0
688          && substr($token->term, -1)=='*' )
689        {
690          $token->term = rtrim($token->term, '*');
691          $token->modifier |= QST_WILDCARD_END;
692        }
693
694        if ( !isset($token->scope)
695          && ($token->modifier & (QST_QUOTED|QST_WILDCARD))==0 )
696        {
697          if ('not' == strtolower($token->term))
698          {
699            if ($i+1 < count($this->tokens))
700              $this->tokens[$i+1]->modifier |= QST_NOT;
701            $token->term = "";
702          }
703          if ('or' == strtolower($token->term))
704          {
705            if ($i+1 < count($this->tokens))
706              $this->tokens[$i+1]->modifier |= QST_OR;
707            $token->term = "";
708          }
709          if ('and' == strtolower($token->term))
710          {
711            $token->term = "";
712          }
713        }
714
715        if (!strlen($token->term)
716          && (!isset($token->scope) || !$token->scope->nullable) )
717        {
718          $remove = true;
719        }
720
721        if ( isset($token->scope)
722          && !$token->scope->parse($token))
723          $remove = true;
724      }
725      elseif (!count($token->tokens))
726      {
727          $remove = true;
728      }
729      if ($remove)
730      {
731        array_splice($this->tokens, $i, 1);
732        if ($i<count($this->tokens) && $this->tokens[$i]->is_single)
733        {
734          $this->tokens[$i]->modifier |= QST_BREAK;
735        }
736        $i--;
737      }
738    }
739
740    if ($level>0 && count($this->tokens) && $this->tokens[0]->is_single)
741    {
742      $this->tokens[0]->modifier |= QST_BREAK;
743    }
744  }
745
746  /**
747  * Applies recursively a search scope to all sub single tokens. We allow 'tag:(John Bill)' but we cannot evaluate
748  * scopes on expressions so we rewrite as '(tag:John tag:Bill)'
749  */
750  private function apply_scope(QSearchScope $scope)
751  {
752    for ($i=0; $i<count($this->tokens); $i++)
753    {
754      if ($this->tokens[$i]->is_single)
755      {
756        if (!isset($this->tokens[$i]->scope))
757          $this->tokens[$i]->scope = $scope;
758      }
759      else
760        $this->tokens[$i]->apply_scope($scope);
761    }
762  }
763
764  private static function priority($modifier)
765  {
766    return $modifier & QST_OR ? 0 :1;
767  }
768
769  /* because evaluations occur left to right, we ensure that 'a OR b c d' is interpreted as 'a OR (b c d)'*/
770  protected function check_operator_priority()
771  {
772    for ($i=0; $i<count($this->tokens); $i++)
773    {
774      if (!$this->tokens[$i]->is_single)
775        $this->tokens[$i]->check_operator_priority();
776      if ($i==1)
777        $crt_prio = self::priority($this->tokens[$i]->modifier);
778      if ($i<=1)
779        continue;
780      $prio = self::priority($this->tokens[$i]->modifier);
781      if ($prio > $crt_prio)
782      {// e.g. 'a OR b c d' i=2, operator(c)=AND -> prio(AND) > prio(OR) = operator(b)
783        $term_count = 2; // at least b and c to be regrouped
784        for ($j=$i+1; $j<count($this->tokens); $j++)
785        {
786          if (self::priority($this->tokens[$j]->modifier) >= $prio)
787            $term_count++; // also take d
788          else
789            break;
790        }
791
792        $i--; // move pointer to b
793        // crate sub expression (b c d)
794        $sub = new QMultiToken;
795        $sub->tokens = array_splice($this->tokens, $i, $term_count);
796
797        // rewrite ourseleves as a (b c d)
798        array_splice($this->tokens, $i, 0, array($sub));
799        $sub->modifier = $sub->tokens[0]->modifier & QST_OR;
800        $sub->tokens[0]->modifier &= ~QST_OR;
801
802        $sub->check_operator_priority();
803      }
804      else
805        $crt_prio = $prio;
806    }
807  }
808}
809
810class QExpression extends QMultiToken
811{
812  var $scopes = array();
813  var $stokens = array();
814  var $stoken_modifiers = array();
815
816  function __construct($q, $scopes)
817  {
818    foreach ($scopes as $scope)
819    {
820      $this->scopes[$scope->id] = $scope;
821      foreach ($scope->aliases as $alias)
822        $this->scopes[strtolower($alias)] = $scope;
823    }
824    $i = 0;
825    $this->parse_expression($q, $i, 0, $this);
826    //manipulate the tree so that 'a OR b c' is the same as 'b c OR a'
827    $this->check_operator_priority();
828    $this->build_single_tokens($this, 0);
829  }
830
831  private function build_single_tokens(QMultiToken $expr, $this_is_not)
832  {
833    for ($i=0; $i<count($expr->tokens); $i++)
834    {
835      $token = $expr->tokens[$i];
836      $crt_is_not = ($token->modifier ^ $this_is_not) & QST_NOT; // no negation OR double negation -> no negation;
837
838      if ($token->is_single)
839      {
840        $token->idx = count($this->stokens);
841        $this->stokens[] = $token;
842
843        $modifier = $token->modifier;
844        if ($crt_is_not)
845          $modifier |= QST_NOT;
846        else
847          $modifier &= ~QST_NOT;
848        $this->stoken_modifiers[] = $modifier;
849      }
850      else
851        $this->build_single_tokens($token, $crt_is_not);
852    }
853  }
854}
855
856/**
857  Structure of results being filled from different tables
858*/
859class QResults
860{
861  var $all_tags;
862  var $tag_ids;
863  var $tag_iids;
864  var $images_iids;
865  var $iids;
866}
867
868function qsearch_get_text_token_search_sql($token, $fields)
869{
870  $clauses = array();
871  $variants = array_merge(array($token->term), $token->variants);
872  $fts = array();
873  foreach ($variants as $variant)
874  {
875    $use_ft = mb_strlen($variant)>3;
876    if ($token->modifier & QST_WILDCARD_BEGIN)
877      $use_ft = false;
878    if ($token->modifier & (QST_QUOTED|QST_WILDCARD_END) == (QST_QUOTED|QST_WILDCARD_END))
879      $use_ft = false;
880
881    if ($use_ft)
882    {
883      $max = max( array_map( 'mb_strlen',
884        preg_split('/['.preg_quote('-\'!"#$%&()*+,./:;<=>?@[\]^`{|}~','/').']+/', $variant)
885        ) );
886      if ($max<4)
887        $use_ft = false;
888    }
889
890    if (!$use_ft)
891    {// odd term or too short for full text search; fallback to regex but unfortunately this is diacritic/accent sensitive
892      $pre = ($token->modifier & QST_WILDCARD_BEGIN) ? '' : '[[:<:]]';
893      $post = ($token->modifier & QST_WILDCARD_END) ? '' : '[[:>:]]';
894      foreach( $fields as $field)
895        $clauses[] = $field.' REGEXP \''.$pre.addslashes(preg_quote($variant)).$post.'\'';
896    }
897    else
898    {
899      $ft = $variant;
900      if ($token->modifier & QST_QUOTED)
901        $ft = '"'.$ft.'"';
902      if ($token->modifier & QST_WILDCARD_END)
903        $ft .= '*';
904      $fts[] = $ft;
905    }
906  }
907
908  if (count($fts))
909  {
910    $clauses[] = 'MATCH('.implode(', ',$fields).') AGAINST( \''.addslashes(implode(' ',$fts)).'\' IN BOOLEAN MODE)';
911  }
912  return $clauses;
913}
914
915function qsearch_get_images(QExpression $expr, QResults $qsr)
916{
917  $qsr->images_iids = array_fill(0, count($expr->stokens), array());
918
919  $query_base = 'SELECT id from '.IMAGES_TABLE.' i WHERE
920';
921  for ($i=0; $i<count($expr->stokens); $i++)
922  {
923    $token = $expr->stokens[$i];
924    $scope_id = isset($token->scope) ? $token->scope->id : 'photo';
925    $clauses = array();
926
927    $like = addslashes($token->term);
928    $like = str_replace( array('%','_'), array('\\%','\\_'), $like); // escape LIKE specials %_
929    $file_like = 'CONVERT(file, CHAR) LIKE \'%'.$like.'%\'';
930
931    switch ($scope_id)
932    {
933      case 'photo':
934        $clauses[] = $file_like;
935        $clauses = array_merge($clauses, qsearch_get_text_token_search_sql($token, array('name','comment')));
936        break;
937
938      case 'file':
939        $clauses[] = $file_like;
940        break;
941      case 'width':
942      case 'height':
943        $clauses[] = $token->scope->get_sql($scope_id, $token);
944        break;
945      case 'ratio':
946        $clauses[] = $token->scope->get_sql('width/height', $token);
947        break;
948      case 'size':
949        $clauses[] = $token->scope->get_sql('width*height', $token);
950        break;
951      case 'hits':
952        $clauses[] = $token->scope->get_sql('hit', $token);
953        break;
954      case 'score':
955        $clauses[] = $token->scope->get_sql('rating_score', $token);
956        break;
957      case 'filesize':
958        $clauses[] = $token->scope->get_sql('1024*filesize', $token);
959        break;
960      case 'created':
961        $clauses[] = $token->scope->get_sql('date_creation', $token);
962        break;
963      case 'posted':
964        $clauses[] = $token->scope->get_sql('date_available', $token);
965        break;
966      case 'id':
967        $clauses[] = $token->scope->get_sql($scope_id, $token);
968        break;
969      default:
970        // allow plugins to have their own scope with columns added in db by themselves
971        $clauses = trigger_change('qsearch_get_images_sql_scopes', $clauses, $token, $expr);
972        break;
973    }
974    if (!empty($clauses))
975    {
976      $query = $query_base.'('.implode("\n OR ", $clauses).')';
977      $qsr->images_iids[$i] = query2array($query,null,'id');
978    }
979  }
980}
981
982function qsearch_get_tags(QExpression $expr, QResults $qsr)
983{
984  $token_tag_ids = $qsr->tag_iids = array_fill(0, count($expr->stokens), array() );
985  $all_tags = array();
986
987  for ($i=0; $i<count($expr->stokens); $i++)
988  {
989    $token = $expr->stokens[$i];
990    if (isset($token->scope) && 'tag' != $token->scope->id)
991      continue;
992    if (empty($token->term))
993      continue;
994
995    $clauses = qsearch_get_text_token_search_sql( $token, array('name'));
996    $query = 'SELECT * FROM '.TAGS_TABLE.'
997WHERE ('. implode("\n OR ",$clauses) .')';
998    $result = pwg_query($query);
999    while ($tag = pwg_db_fetch_assoc($result))
1000    {
1001      $token_tag_ids[$i][] = $tag['id'];
1002      $all_tags[$tag['id']] = $tag;
1003    }
1004  }
1005
1006  // check adjacent short words
1007  for ($i=0; $i<count($expr->stokens)-1; $i++)
1008  {
1009    if ( (strlen($expr->stokens[$i]->term)<=3 || strlen($expr->stokens[$i+1]->term)<=3)
1010      && (($expr->stoken_modifiers[$i] & (QST_QUOTED|QST_WILDCARD)) == 0)
1011      && (($expr->stoken_modifiers[$i+1] & (QST_BREAK|QST_QUOTED|QST_WILDCARD)) == 0) )
1012    {
1013      $common = array_intersect( $token_tag_ids[$i], $token_tag_ids[$i+1] );
1014      if (count($common))
1015      {
1016        $token_tag_ids[$i] = $token_tag_ids[$i+1] = $common;
1017      }
1018    }
1019  }
1020
1021  // get images
1022  $positive_ids = $not_ids = array();
1023  for ($i=0; $i<count($expr->stokens); $i++)
1024  {
1025    $tag_ids = $token_tag_ids[$i];
1026    $token = $expr->stokens[$i];
1027
1028    if (!empty($tag_ids))
1029    {
1030      $query = '
1031SELECT image_id FROM '.IMAGE_TAG_TABLE.'
1032  WHERE tag_id IN ('.implode(',',$tag_ids).')
1033  GROUP BY image_id';
1034      $qsr->tag_iids[$i] = query2array($query, null, 'image_id');
1035      if ($expr->stoken_modifiers[$i]&QST_NOT)
1036        $not_ids = array_merge($not_ids, $tag_ids);
1037      else
1038      {
1039        if (strlen($token->term)>2 || count($expr->stokens)==1 || isset($token->scope) || ($token->modifier&(QST_WILDCARD|QST_QUOTED)) )
1040        {// add tag ids to list only if the word is not too short (such as de / la /les ...)
1041          $positive_ids = array_merge($positive_ids, $tag_ids);
1042        }
1043      }
1044    }
1045    elseif (isset($token->scope) && 'tag' == $token->scope->id && strlen($token->term)==0)
1046    {
1047      if ($token->modifier & QST_WILDCARD)
1048      {// eg. 'tag:*' returns all tagged images
1049        $qsr->tag_iids[$i] = query2array('SELECT DISTINCT image_id FROM '.IMAGE_TAG_TABLE, null, 'image_id');
1050      }
1051      else
1052      {// eg. 'tag:' returns all untagged images
1053        $qsr->tag_iids[$i] = query2array('SELECT id FROM '.IMAGES_TABLE.' LEFT JOIN '.IMAGE_TAG_TABLE.' ON id=image_id WHERE image_id IS NULL', null, 'id');
1054      }
1055    }
1056  }
1057
1058  $all_tags = array_intersect_key($all_tags, array_flip( array_diff($positive_ids, $not_ids) ) );
1059  usort($all_tags, 'tag_alpha_compare');
1060  foreach ( $all_tags as &$tag )
1061  {
1062    $tag['name'] = trigger_change('render_tag_name', $tag['name'], $tag);
1063  }
1064  $qsr->all_tags = $all_tags;
1065  $qsr->tag_ids = $token_tag_ids;
1066}
1067
1068
1069
1070function qsearch_eval(QMultiToken $expr, QResults $qsr, &$qualifies, &$ignored_terms)
1071{
1072  $qualifies = false; // until we find at least one positive term
1073  $ignored_terms = array();
1074
1075  $ids = $not_ids = array();
1076
1077  for ($i=0; $i<count($expr->tokens); $i++)
1078  {
1079    $crt = $expr->tokens[$i];
1080    if ($crt->is_single)
1081    {
1082      $crt_ids = $qsr->iids[$crt->idx] = array_unique( array_merge($qsr->images_iids[$crt->idx], $qsr->tag_iids[$crt->idx]) );
1083      $crt_qualifies = count($crt_ids)>0 || count($qsr->tag_ids[$crt->idx])>0;
1084      $crt_ignored_terms = $crt_qualifies ? array() : array((string)$crt);
1085    }
1086    else
1087      $crt_ids = qsearch_eval($crt, $qsr, $crt_qualifies, $crt_ignored_terms);
1088
1089    $modifier = $crt->modifier;
1090    if ($modifier & QST_NOT)
1091      $not_ids = array_unique( array_merge($not_ids, $crt_ids));
1092    else
1093    {
1094      $ignored_terms = array_merge($ignored_terms, $crt_ignored_terms);
1095      if ($modifier & QST_OR)
1096      {
1097        $ids = array_unique( array_merge($ids, $crt_ids) );
1098        $qualifies |= $crt_qualifies;
1099      }
1100      elseif ($crt_qualifies)
1101      {
1102        if ($qualifies)
1103          $ids = array_intersect($ids, $crt_ids);
1104        else
1105          $ids = $crt_ids;
1106        $qualifies = true;
1107      }
1108    }
1109  }
1110
1111  if (count($not_ids))
1112    $ids = array_diff($ids, $not_ids);
1113  return $ids;
1114}
1115
1116
1117/**
1118 * Returns the search results corresponding to a quick/query search.
1119 * A quick/query search returns many items (search is not strict), but results
1120 * are sorted by relevance unless $super_order_by is true. Returns:
1121 *  array (
1122 *    'items' => array of matching images
1123 *    'qs'    => array(
1124 *      'unmatched_terms' => array of terms from the input string that were not matched
1125 *      'matching_tags' => array of matching tags
1126 *      'matching_cats' => array of matching categories
1127 *      'matching_cats_no_images' =>array(99) - matching categories without images
1128 *      )
1129 *    )
1130 *
1131 * @param string $q
1132 * @param bool $super_order_by
1133 * @param string $images_where optional additional restriction on images table
1134 * @return array
1135 */
1136function get_quick_search_results($q, $options)
1137{
1138  global $persistent_cache, $conf, $user;
1139
1140  $cache_key = $persistent_cache->make_key( array(
1141    strtolower($q),
1142    $conf['order_by'],
1143    $user['id'],$user['cache_update_time'],
1144    isset($options['permissions']) ? (boolean)$options['permissions'] : true,
1145    isset($options['images_where']) ? $options['images_where'] : '',
1146    ) );
1147  if ($persistent_cache->get($cache_key, $res))
1148  {
1149    return $res;
1150  }
1151
1152  $res = get_quick_search_results_no_cache($q, $options);
1153
1154  if ( count($res['items']) )
1155  {// cache the results only if not empty - otherwise it is useless
1156    $persistent_cache->set($cache_key, $res, 300);
1157  }
1158  return $res;
1159}
1160
1161/**
1162 * @see get_quick_search_results but without result caching
1163 */
1164function get_quick_search_results_no_cache($q, $options)
1165{
1166  global $conf;
1167
1168  $q = trim(stripslashes($q));
1169  $search_results =
1170    array(
1171      'items' => array(),
1172      'qs' => array('q'=>$q),
1173    );
1174
1175  $q = trigger_change('qsearch_pre', $q);
1176
1177  $scopes = array();
1178  $scopes[] = new QSearchScope('tag', array('tags'));
1179  $scopes[] = new QSearchScope('photo', array('photos'));
1180  $scopes[] = new QSearchScope('file', array('filename'));
1181  $scopes[] = new QNumericRangeScope('width', array());
1182  $scopes[] = new QNumericRangeScope('height', array());
1183  $scopes[] = new QNumericRangeScope('ratio', array(), false, 0.001);
1184  $scopes[] = new QNumericRangeScope('size', array());
1185  $scopes[] = new QNumericRangeScope('filesize', array());
1186  $scopes[] = new QNumericRangeScope('hits', array('hit', 'visit', 'visits'));
1187  $scopes[] = new QNumericRangeScope('score', array('rating'), true);
1188  $scopes[] = new QNumericRangeScope('id', array());
1189
1190  $createdDateAliases = array('taken', 'shot');
1191  $postedDateAliases = array('added');
1192  if ($conf['calendar_datefield'] == 'date_creation')
1193    $createdDateAliases[] = 'date';
1194  else
1195    $postedDateAliases[] = 'date';
1196  $scopes[] = new QDateRangeScope('created', $createdDateAliases, true);
1197  $scopes[] = new QDateRangeScope('posted', $postedDateAliases);
1198
1199  // allow plugins to add their own scopes
1200  $scopes = trigger_change('qsearch_get_scopes', $scopes);
1201  $expression = new QExpression($q, $scopes);
1202
1203  // get inflections for terms
1204  $inflector = null;
1205  $lang_code = substr(get_default_language(),0,2);
1206  @include_once(PHPWG_ROOT_PATH.'include/inflectors/'.$lang_code.'.php');
1207  $class_name = 'Inflector_'.$lang_code;
1208  if (class_exists($class_name))
1209  {
1210    $inflector = new $class_name;
1211    foreach( $expression->stokens as $token)
1212    {
1213      if (isset($token->scope) && !$token->scope->is_text)
1214        continue;
1215      if (strlen($token->term)>2
1216        && ($token->modifier & (QST_QUOTED|QST_WILDCARD))==0
1217        && strcspn($token->term, '\'0123456789') == strlen($token->term) )
1218      {
1219        $token->variants = array_unique( array_diff( $inflector->get_variants($token->term), array($token->term) ) );
1220      }
1221    }
1222  }
1223
1224
1225  trigger_notify('qsearch_expression_parsed', $expression);
1226//var_export($expression);
1227
1228  if (count($expression->stokens)==0)
1229  {
1230    return $search_results;
1231  }
1232  $qsr = new QResults;
1233  qsearch_get_tags($expression, $qsr);
1234  qsearch_get_images($expression, $qsr);
1235
1236  // allow plugins to evaluate their own scopes
1237  trigger_notify('qsearch_before_eval', $expression, $qsr);
1238
1239  $ids = qsearch_eval($expression, $qsr, $tmp, $search_results['qs']['unmatched_terms']);
1240
1241  $debug[] = "<!--\nparsed: ".$expression;
1242  $debug[] = count($expression->stokens).' tokens';
1243  for ($i=0; $i<count($expression->stokens); $i++)
1244  {
1245    $debug[] = $expression->stokens[$i].': '.count($qsr->tag_ids[$i]).' tags, '.count($qsr->tag_iids[$i]).' tiids, '.count($qsr->images_iids[$i]).' iiids, '.count($qsr->iids[$i]).' iids'
1246      .' modifier:'.dechex($expression->stoken_modifiers[$i])
1247      .( !empty($expression->stokens[$i]->variants) ? ' variants: '.implode(', ',$expression->stokens[$i]->variants): '');
1248  }
1249  $debug[] = 'before perms '.count($ids);
1250
1251  $search_results['qs']['matching_tags'] = $qsr->all_tags;
1252  $search_results = trigger_change('qsearch_results', $search_results, $expression, $qsr);
1253
1254  global $template;
1255
1256  if (empty($ids))
1257  {
1258    $debug[] = '-->';
1259    $template->append('footer_elements', implode("\n", $debug) );
1260    return $search_results;
1261  }
1262
1263  $permissions = !isset($options['permissions']) ? true : $options['permissions'];
1264
1265  $where_clauses = array();
1266  $where_clauses[]='i.id IN ('. implode(',', $ids) . ')';
1267  if (!empty($options['images_where']))
1268  {
1269    $where_clauses[]='('.$options['images_where'].')';
1270  }
1271  if ($permissions)
1272  {
1273    $where_clauses[] = get_sql_condition_FandF(
1274        array
1275          (
1276            'forbidden_categories' => 'category_id',
1277            'forbidden_images' => 'i.id'
1278          ),
1279        null,true
1280      );
1281  }
1282
1283  $query = '
1284SELECT DISTINCT(id) FROM '.IMAGES_TABLE.' i';
1285  if ($permissions)
1286  {
1287    $query .= '
1288    INNER JOIN '.IMAGE_CATEGORY_TABLE.' AS ic ON id = ic.image_id';
1289  }
1290  $query .= '
1291  WHERE '.implode("\n AND ", $where_clauses)."\n".
1292  $conf['order_by'];
1293
1294  $ids = query2array($query, null, 'id');
1295
1296  $debug[] = count($ids).' final photo count -->';
1297  $template->append('footer_elements', implode("\n", $debug) );
1298
1299  $search_results['items'] = $ids;
1300  return $search_results;
1301}
1302
1303/**
1304 * Returns an array of 'items' corresponding to the search id.
1305 * It can be either a quick search or a regular search.
1306 *
1307 * @param int $search_id
1308 * @param bool $super_order_by
1309 * @param string $images_where optional aditional restriction on images table
1310 * @return array
1311 */
1312function get_search_results($search_id, $super_order_by, $images_where='')
1313{
1314  $search = get_search_array($search_id);
1315  if ( !isset($search['q']) )
1316  {
1317    $result['items'] = get_regular_search_results($search, $images_where);
1318    return $result;
1319  }
1320  else
1321  {
1322    return get_quick_search_results($search['q'], array('super_order_by'=>$super_order_by, 'images_where'=>$images_where) );
1323  }
1324}
1325
1326?>
Note: See TracBrowser for help on using the repository browser.