1<?php
2
3define('EXACT_TAG_MATCH', getOption('exact_tag_match'));
4define('SEARCH_DURATION', 3000);
5define('SEARCH_CACHE_DURATION', getOption('search_cache_duration'));
6
7/**
8 * search class
9 * @package core
10 * @subpackage classes\objects
11 */
12class SearchEngine {
13
14	public $fieldList = NULL;
15	public $page = 1;
16	public $images = NULL;
17	public $albums = NULL;
18	public $articles = NULL;
19	public $pages = NULL;
20	public $pattern;
21	public $tagPattern;
22	private $exact = false;
23	protected $dynalbumname = NULL;
24	protected $album = NULL;
25	protected $words;
26	protected $dates;
27	protected $whichdates = 'date'; // for zenpage date searches, which date field to search
28	protected $search_no_albums = false; // omit albums
29	protected $search_no_images = false; // omit images
30	protected $search_no_pages = false; // omit pages
31	protected $search_no_news = false; // omit news
32	protected $search_unpublished = false; // will override the loggedin checks with respect to unpublished items
33	protected $search_structure; // relates translatable names to search fields
34	protected $iteration = 0; // used by apply_filter('search_statistics') to indicate sequential searches of different objects
35	protected $processed_search = NULL;
36	protected $album_list = NULL; // list of albums to search
37	protected $album_list_exclude = null; // list of albums to exclude from search
38	protected $category_list = NULL; // list of categories for a news search
39	protected $searches = NULL; // remember the criteria for past searches
40	protected $extraparams = array(); // allow plugins to add to search parameters
41//	mimic album object
42	public $loaded = false;
43	public $table = 'albums';
44	public $transient = true;
45
46	/**
47	 * Constuctor
48	 *
49	 * @param bool $dynamic_album set true for dynamic albums (limits the search fields)
50	 * @return SearchEngine
51	 */
52	function __construct($dynamic_album = false) {
53		global $_zp_exifvars, $_zp_gallery;
54		switch ((int) getOption('exact_tag_match')) {
55			case 0:
56				// partial
57				$this->tagPattern = array('type' => 'like', 'open' => '%', 'close' => '%');
58				break;
59			case 1:
60				// exact
61				$this->tagPattern = array('type' => '=', 'open' => '', 'close' => '');
62				break;
63			case 2:
64				//word
65				$this->tagPattern = array('type' => 'regexp', 'open' => '[[:<:]]', 'close' => '[[:>:]]');
66				break;
67		}
68
69		switch ((int) getOption('exact_string_match')) {
70			case 0:
71				// pattern
72				$this->pattern = array('type' => 'like', 'open' => '%', 'close' => '%');
73				break;
74			case 1:
75				// partial start
76				$this->pattern = array('type' => 'regexp', 'open' => '[[:<:]]', 'close' => '');
77				break;
78			case 2:
79				//word
80				$this->pattern = array('type' => 'regexp', 'open' => '[[:<:]]', 'close' => '[[:>:]]');
81				break;
82		}
83
84		$this->extraparams['albumssorttype'] = getOption('search_album_sort_type');
85		$this->extraparams['albumssortdirection'] = getOption('search_album_sort_direction') ? 'DESC' : '';
86		$this->extraparams['imagessorttype'] = getOption('search_image_sort_type');
87		$this->extraparams['imagessortdirection'] = getOption('search_image_sort_direction') ? 'DESC' : '';
88		$this->extraparams['newssorttype'] = getOption('search_newsarticle_sort_type');
89		$this->extraparams['newssortdirection'] = getOption('search_newsarticle_sort_direction') ? 'DESC' : '';
90		$this->extraparams['pagessorttype'] = getOption('search_page_sort_type');
91		$this->extraparams['pagessortdirection'] = getOption('search_page_sort_direction') ? 'DESC' : '';
92
93//image/album fields
94		$this->search_structure['title'] = gettext('Title');
95		$this->search_structure['desc'] = gettext('Description');
96		$this->search_structure['tags'] = gettext('Tags');
97		$this->search_structure['tags_exact'] = ''; //	internal use only field
98		$this->search_structure['filename'] = gettext('File/Folder name');
99		$this->search_structure['date'] = gettext('Date');
100		$this->search_structure['custom_data'] = gettext('Custom data');
101		$this->search_structure['location'] = gettext('Location/Place');
102		$this->search_structure['city'] = gettext('City');
103		$this->search_structure['state'] = gettext('State');
104		$this->search_structure['country'] = gettext('Country');
105		$this->search_structure['copyright'] = gettext('Copyright');
106		$this->search_structure['owner'] = gettext('Owner');
107		$this->search_structure['credit'] = gettext('Credit');
108		$this->search_structure['lastchangeuser'] = gettext('Last change user');
109		if (extensionEnabled('zenpage') && !$dynamic_album) {
110//zenpage fields
111			$this->search_structure['content'] = gettext('Content');
112			$this->search_structure['extracontent'] = gettext('ExtraContent');
113			$this->search_structure['author'] = gettext('Author');
114			$this->search_structure['titlelink'] = gettext('TitleLink');
115			$this->search_structure['news_categories'] = gettext('Categories');
116		}
117//metadata fields
118		foreach ($_zp_exifvars as $field => $row) {
119			if ($row[4] && $row[5]) { //	only those that are "real" and "processed"
120				$this->search_structure[strtolower($field)] = $row[2];
121			}
122		}
123		$this->search_structure = zp_apply_filter('searchable_fields', $this->search_structure);
124		if (isset($_REQUEST['words'])) {
125			$this->words = removeTrailingSlash(strtr(sanitize($_REQUEST['words'], 4), array('__23__' => '#', '__25__' => '%', '__26__' => '&', '__2F__' => '/')));
126		} else {
127			$this->words = NULL;
128			if (isset($_REQUEST['date'])) { // words & dates are mutually exclusive
129				$this->dates = removeTrailingSlash(sanitize($_REQUEST['date'], 3));
130				if (isset($_REQUEST['whichdate'])) {
131					$this->whichdates = sanitize($_REQUEST['whichdate']);
132				}
133			} else {
134				$this->dates = NULL;
135			}
136		}
137		$this->fieldList = $this->parseQueryFields();
138		if (isset($_REQUEST['inalbums'])) {
139			$list = trim(sanitize($_REQUEST['inalbums'], 3));
140			if (strlen($list) > 0) {
141				switch ($list) {
142					case "0":
143						$this->search_no_albums = true;
144						setOption('search_no_albums', 1, false);
145						break;
146					case "1":
147						$this->search_no_albums = false;
148						setOption('search_no_albums', 0, false);
149						break;
150					default:
151						$this->album_list = explode(',', $list);
152						break;
153				}
154			}
155		}
156
157		if (isset($_REQUEST['excludealbums'])) {
158			$list = trim(sanitize($_REQUEST['excludealbums'], 3));
159			if (!empty($list)) {
160				$this->album_list_exclude = explode(',', $list);
161			}
162		}
163
164		if (isset($_REQUEST['inimages'])) {
165			$list = trim(sanitize($_REQUEST['inimages'], 3));
166			if (strlen($list) > 0) {
167				switch ($list) {
168					case "0":
169						$this->search_no_images = true;
170						setOption('search_no_images', 1, false);
171						break;
172					case "1":
173						$this->search_no_images = false;
174						setOption('search_no_images', 0, false);
175						break;
176				}
177			}
178		}
179		if (isset($_REQUEST['inpages'])) {
180			$list = trim(sanitize($_REQUEST['inpages'], 3));
181			if (strlen($list) > 0) {
182				switch ($list) {
183					case "0":
184						$this->search_no_pages = true;
185						setOption('search_no_pages', 1, false);
186						break;
187				}
188			}
189		}
190		if (isset($_REQUEST['innews'])) {
191			$list = trim(sanitize($_REQUEST['innews'], 3));
192			if (strlen($list) > 0) {
193				switch ($list) {
194					case "0":
195						$this->search_no_news = true;
196						setOption('search_no_news', 1, false);
197						break;
198					case "1":
199						break;
200					default:
201						$this->category_list = explode(',', $list);
202						break;
203				}
204			}
205		}
206		$this->images = NULL;
207		$this->albums = NULL;
208		$this->searches = array('images' => NULL, 'albums' => NULL, 'pages' => NULL, 'news' => NULL);
209		zp_apply_filter('search_instantiate', $this);
210	}
211
212	/**
213	 * mimic an album object
214	 * @return number
215	 */
216	function getID() {
217		return 0;
218	}
219
220	/**
221	 * Returns a list of search fields display names indexed by the search mask
222	 *
223	 * @return array
224	 */
225	function getSearchFieldList() {
226		$list = array();
227		foreach ($this->search_structure as $key => $display) {
228			if ($display) {
229				$list[$display] = $key;
230			}
231		}
232		return $list;
233	}
234
235	/**
236	 * Returns an array of the enabled search fields
237	 *
238	 * @return array
239	 */
240	function allowedSearchFields() {
241		$setlist = array();
242		$fields = strtolower(getOption('search_fields'));
243		if (is_numeric($fields)) {
244			$setlist = $this->numericFields($fields);
245		} else {
246			$list = explode(',', $fields);
247			foreach ($this->search_structure as $key => $display) {
248				if (in_array($key, $list)) {
249					$setlist[$display] = $key;
250				}
251			}
252		}
253		return $setlist;
254	}
255
256	/**
257	 * converts old style bitmask field spec into field list array
258	 *
259	 * @param bit $fields
260	 * @return array
261	 */
262	protected function numericFields($fields) {
263		if ($fields == 0)
264			$fields = 0x0fff;
265		if ($fields & 0x01)
266			$list[$this->search_structure['title']] = 'title';
267		if ($fields & 0x02)
268			$list[$this->search_structure['desc']] = 'desc';
269		if ($fields & 0x04)
270			$list[$this->search_structure['tags']] = 'tags';
271		if ($fields & 0x08)
272			$list[$this->search_structure['filename']] = 'filename';
273		return $list;
274	}
275
276	/**
277	 * creates a search query from the search words
278	 *
279	 * @param bool $long set to false to omit albumname and page parts
280	 *
281	 * @return string
282	 */
283	function getSearchParams($long = true) {
284		global $_zp_page;
285		$r = '';
286		$w = urlencode(trim($this->codifySearchString()));
287		if (!empty($w)) {
288			$r .= '&words=' . $w;
289		}
290		$d = trim($this->dates);
291		if (!empty($d)) {
292			$r .= '&date=' . $d;
293			$d = trim($this->whichdates);
294			if ($d != 'date') {
295				$r.= '&whichdates=' . $d;
296			}
297		}
298		$r .= $this->getSearchFieldsText($this->fieldList);
299		if ($long) {
300			$a = $this->dynalbumname;
301			if ($a) {
302				$r .= '&albumname=' . $a;
303			}
304			if (empty($this->album_list)) {
305				if ($this->search_no_albums) {
306					$r .= '&inalbums=0';
307				}
308			} else {
309				$r .= '&inalbums=' . implode(',', $this->album_list);
310			}
311
312			if (empty($this->album_list_exclude)) {
313				if ($this->search_no_albums) {
314					$r .= '&inalbums=0';
315				}
316			} else {
317				$r .= '&excludealbums=' . implode(',', $this->album_list_exclude);
318			}
319
320			if ($this->search_no_images) {
321				$r .= '&inimages=0';
322			}
323			if ($this->search_no_pages) {
324				$r .= '&inpages=0';
325			}
326			if (empty($this->categories)) {
327				if ($this->search_no_news) {
328					$r .= '&innews=0';
329				}
330			} else {
331				$r .= '&innews=' . implode(',', $this->categories);
332			}
333			if ($_zp_page > 1) {
334				$this->page = $_zp_page;
335				$r .= '&page=' . $_zp_page;
336			}
337		}
338		foreach ($this->extraparams as $p => $v) {
339			$r .= '&' . $p . '=' . $v;
340		}
341		return $r;
342	}
343
344	/**
345	 *
346	 * Retrieves search extra parameters
347	 * @return array
348	 */
349	function getSearchExtra() {
350		return $this->extraparams;
351	}
352
353	/**
354	 *
355	 * Stores extra search params for plugin use
356	 * @param array $extra
357	 */
358	function setSearchExtra($extra) {
359		$this->extraparams = $extra;
360	}
361
362	/**
363	 * sets sort directions
364	 *
365	 * @param bool $val the direction
366	 * @param string $what 'images' if you want the image direction,
367	 *        'albums' if you want it for the album
368	 */
369	function setSortDirection($val, $what = 'images') {
370		if ($val) {
371			$this->extraparams[$what . 'sortdirection'] = 'DESC';
372		} else {
373			$this->extraparams[$what . 'sortdirection'] = 'ASC';
374		}
375	}
376
377	/**
378	 * Stores the sort type
379	 *
380	 * @param string $sorttype the sort type
381	 * @param string $what 'images' or 'albums'
382	 */
383	function setSortType($sorttype, $what = 'images') {
384		$this->extraparams[$what . 'sorttype'] = $sorttype;
385	}
386
387	/**
388	 * Returns the "searchstring" element of a query parameter set
389	 *
390	 * @param array $fields the fields required
391	 * @param string $param the query parameter (possibly with the intro character
392	 * @return string
393	 */
394	function getSearchFieldsText($fields, $param = '&searchfields=') {
395		$default = $this->allowedSearchFields();
396		$diff = array_diff($default, $fields);
397		if (count($diff) > 0) {
398			foreach ($fields as $field) {
399				$param .= $field . ',';
400			}
401			return substr($param, 0, -1);
402		}
403		return '';
404	}
405
406	/**
407	 * Parses and stores a search string
408	 * NOTE!! this function assumes that the 'words' part of the list has been urlencoded!!!
409	 *
410	 * @param string $paramstr the string containing the search words
411	 */
412	function setSearchParams($paramstr) {
413		$params = explode('&', $paramstr);
414		foreach ($params as $param) {
415			$e = strpos($param, '=');
416			$p = substr($param, 0, $e);
417			$v = substr($param, $e + 1);
418			switch ($p) {
419				case 'words':
420					$this->words = urldecode($v);
421					break;
422				case 'date':
423					$this->dates = $v;
424					break;
425				case 'whichdates':
426					$this->whichdates = $v;
427					break;
428				case 'searchfields':
429					if (is_numeric($v)) {
430						$this->fieldList = $this->numericFields($v);
431					} else {
432						$this->fieldList = array();
433						$list = explode(',', strtolower($v));
434						foreach ($this->search_structure as $key => $row) {
435							if (in_array(strtolower($key), $list)) {
436								$this->fieldList[] = $key;
437							}
438						}
439					}
440					break;
441				case 'page':
442					$this->page = $v;
443					break;
444				case 'albumname':
445					$alb = newAlbum($v, true, true);
446					if ($alb->loaded) {
447						$this->album = $alb;
448						$this->dynalbumname = $v;
449						$this->setSortType($this->album->getSortType('album'), 'albums');
450						$this->setSortDirection($this->album->getSortDirection('album'), 'albums');
451						$this->setSortType($this->album->getSortType(), 'images');
452						$this->setSortDirection($this->album->getSortDirection('image'), 'images');
453					}
454					break;
455				case 'inimages':
456					if (strlen($v) > 0) {
457						switch ($v) {
458							case "0":
459								$this->search_no_images = true;
460								setOption('search_no_images', 1, false);
461								break;
462							case "1":
463								$this->search_no_images = false;
464								setOption('search_no_images', 0, false);
465								break;
466						}
467					}
468					break;
469				case 'inalbums':
470					if (strlen($v) > 0) {
471						switch ($v) {
472							case "0":
473								$this->search_no_albums = true;
474								setOption('search_no_albums', 1, false);
475								break;
476							case "1":
477								$this->search_no_albums = false;
478								setOption('search_no_albums', 0, false);
479								break;
480							default:
481								$this->album_list = explode(',', $v);
482								break;
483						}
484					}
485					break;
486				case 'excludealbums':
487					if (strlen($v) > 0) {
488						$this->album_list_exclude = explode(',', $v);
489					}
490					break;
491				case 'unpublished':
492					$this->search_unpublished = (bool) $v;
493					break;
494				default:
495					$this->extraparams[$p] = $v;
496					break;
497			}
498		}
499		if (!empty($this->words)) {
500			$this->dates = ''; // words and dates are mutually exclusive
501		}
502	}
503
504// call to always return unpublished items
505	function setSearchUnpublished() {
506		$this->search_unpublished = true;
507	}
508
509	/**
510	 * Returns the search words variable
511	 *
512	 * @return string
513	 */
514	function getSearchWords() {
515		return $this->words;
516	}
517
518	/**
519	 * Returns the search dates variable
520	 *
521	 * @return string
522	 */
523	function getSearchDate() {
524		return $this->dates;
525	}
526
527	/**
528	 * Returns the search fields variable
529	 *
530	 * @param bool $array set to true to return the fields as array elements. Otherwise
531	 * a comma delimited string is returned
532	 *
533	 * @return mixed
534	 */
535	function getSearchFields($array = false) {
536		if ($array)
537			return $this->fieldList;
538		return implode(',', $this->fieldList);
539	}
540
541	/**
542	 * Parses a search string
543	 * Items within quotations are treated as atomic
544	 * AND, OR and NOT are converted to &, |, and !
545	 *
546	 * Returns an array of search elements
547	 *
548	 * @return array
549	 */
550	function getSearchString() {
551		if ($this->processed_search) {
552			return $this->processed_search;
553		}
554		$searchstring = trim($this->words);
555		$space_is = getOption('search_space_is');
556		$opChars = array('&' => 1, '|' => 1, '!' => 1, ',' => 1, '(' => 2);
557		if ($space_is) {
558			$opChars[' '] = 1;
559		}
560		$c1 = ' ';
561		$result = array();
562		$target = "";
563		$i = 0;
564		do {
565			$c = substr($searchstring, $i, 1);
566			$op = '';
567			switch ($c) {
568				case "'":
569				case '"':
570				case '`':
571					$j = strpos(str_replace('\\' . $c, '__', $searchstring), $c, $i + 1);
572					if ($j !== false) {
573						$target .= stripcslashes(substr($searchstring, $i + 1, $j - $i - 1));
574						$i = $j;
575					} else {
576						$target .= $c;
577					}
578					$c1 = $c;
579					break;
580				case ' ':
581					$j = $i + 1;
582					while ($j < strlen($searchstring) && $searchstring[$j] == ' ') {
583						$j++;
584					}
585					switch ($space_is) {
586						case 'OR':
587						case 'AND':
588							if ($j < strlen($searchstring)) {
589								$c3 = $searchstring[$j];
590								if (array_key_exists($c3, $opChars) && $opChars[$c3] == 1) {
591									$nextop = $c3 != '!';
592								} else if (substr($searchstring . ' ', $j, 4) == 'AND ') {
593									$nextop = true;
594								} else if (substr($searchstring . ' ', $j, 3) == 'OR ') {
595									$nextop = true;
596								} else {
597									$nextop = false;
598								}
599							}
600							if (!$nextop) {
601								if (!empty($target)) {
602									$r = trim($target);
603									if (!empty($r)) {
604										$last = $result[] = $r;
605										$target = '';
606									}
607								}
608								if ($space_is == 'AND') {
609									$c1 = '&';
610								} else {
611									$c1 = '|';
612								}
613								$target = '';
614								$last = $result[] = $c1;
615							}
616							break;
617						default:
618							$c1 = $c;
619							$target .= str_pad('', $j - $i);
620							break;
621					}
622					$i = $j - 1;
623					break;
624				case ',':
625					if (!empty($target)) {
626						$r = trim($target);
627						if (!empty($r)) {
628							switch ($r) {
629								case 'AND':
630									$r = '&';
631									break;
632								case 'OR':
633									$r = '|';
634									break;
635								case 'NOT':
636									$r = '!';
637									break;
638							}
639							$last = $result[] = $r;
640							$target = '';
641						}
642					}
643					$c2 = substr($searchstring, $i + 1, 1);
644					switch ($c2) {
645						case 'A':
646							if (substr($searchstring . ' ', $i + 1, 4) == 'AND ')
647								$c2 = '&';
648							break;
649						case 'O':
650							if (substr($searchstring . ' ', $i + 1, 3) == 'OR ')
651								$c2 = '|';
652							break;
653						case 'N':
654							if (substr($searchstring . ' ', $i + 1, 4) == 'NOT ')
655								$c2 = '!';
656							break;
657					}
658					if (!((isset($opChars[$c2]) && $opChars[$c2] == 1) || (isset($opChars[$last]) && $opChars[$last] == 1))) {
659						$last = $result[] = '|';
660						$c1 = $c;
661					}
662					break;
663				case '!':
664				case '&':
665				case '|':
666				case '(':
667				case ')':
668					if (!empty($target)) {
669						$r = trim($target);
670						if (!empty($r)) {
671							$last = $result[] = $r;
672							$target = '';
673						}
674					}
675					$c1 = $c;
676					$target = '';
677					$last = $result[] = $c;
678					$j = $i + 1;
679					break;
680				case 'A':
681					if (substr($searchstring . ' ', $i, 4) == 'AND ') {
682						$op = '&';
683						$skip = 3;
684					}
685				case 'O':
686					if (substr($searchstring . ' ', $i, 3) == 'OR ') {
687						$op = '|';
688						$skip = 2;
689					}
690				case 'N':
691					if (substr($searchstring . ' ', $i, 4) == 'NOT ') {
692						$op = '!';
693						$skip = 3;
694					}
695					if ($op) {
696						if (!empty($target)) {
697							$r = trim($target);
698							if (!empty($r)) {
699								$last = $result[] = $r;
700								$target = '';
701							}
702						}
703						$c1 = $op;
704						$target = '';
705						$last = $result[] = $op;
706						$j = $i + $skip;
707						while ($j < strlen($searchstring) && substr($searchstring, $j, 1) == ' ') {
708							$j++;
709						}
710						$i = $j - 1;
711					} else {
712						$c1 = $c;
713						$target .= $c;
714					}
715					break;
716
717				default:
718					$c1 = $c;
719					$target .= $c;
720					break;
721			}
722		} while ($i++ < strlen($searchstring));
723		if (!empty($target)) {
724			$last = $result[] = trim($target);
725		}
726		$lasttoken = '';
727		foreach ($result as $key => $token) {
728			if ($token == '|' && $lasttoken == '|') { // remove redundant OR ops
729				unset($result[$key]);
730			}
731			$lasttoken = $token;
732		}
733		if (array_key_exists($lasttoken, $opChars) && $opChars[$lasttoken] == 1) {
734			array_pop($result);
735		}
736
737		$this->processed_search = zp_apply_filter('search_criteria', $result);
738		return $this->processed_search;
739	}
740
741	/**
742	 * recodes the search words replacing the boolean operators with text versions
743	 *
744	 * @param string $quote how to represent quoted strings
745	 *
746	 * @return string
747	 *
748	 */
749	function codifySearchString() {
750		$searchstring = $this->getSearchString();
751		$sanitizedwords = '';
752		if (is_array($searchstring)) {
753			foreach ($searchstring as $singlesearchstring) {
754				switch ($singlesearchstring) {
755					case '&':
756						$sanitizedwords .= " AND ";
757						break;
758					case '|':
759						$sanitizedwords .= " OR ";
760						break;
761					case '!':
762						$sanitizedwords .= " NOT ";
763						break;
764					case '(':
765					case ')':
766						$sanitizedwords .= $singlesearchstring;
767						break;
768					default:
769						$sanitizedwords .= search_quote(sanitize($singlesearchstring, 3));
770						break;
771				}
772			}
773		}
774
775		$sanitizedwords = trim(str_replace(array('   ', '  ',), ' ', $sanitizedwords));
776		$sanitizedwords = trim(str_replace('( ', '(', $sanitizedwords));
777		$sanitizedwords = trim(str_replace(' )', ')', $sanitizedwords));
778		return $sanitizedwords;
779	}
780
781	/**
782	 * Returns the number of albums found in a search
783	 *
784	 * @return int
785	 */
786	function getNumAlbums() {
787		if (is_null($this->albums)) {
788			$this->getAlbums(0, NULL, NULL, false);
789		}
790		return count($this->albums);
791	}
792
793	/**
794	 * Returns the set of fields from the url query/post
795	 * @return int
796	 * @since 1.1.3
797	 */
798	function parseQueryFields() {
799		$fields = array();
800		if (isset($_REQUEST['searchfields'])) {
801			$fs = sanitize($_REQUEST['searchfields']);
802			if (is_numeric($fs)) {
803				$fields = array_flip($this->numericFields($fs));
804			} else {
805				$fields = explode(',', $fs);
806			}
807		} else {
808			foreach ($_REQUEST as $key => $value) {
809				if (strpos($key, 'SEARCH_') !== false) {
810					$fields[substr($key, 7)] = $value;
811				}
812			}
813		}
814		return $fields;
815	}
816
817	/**
818	 *
819	 * Returns an array of News article IDs belonging to the search categories
820	 */
821	protected function subsetNewsCategories() {
822		global $_zp_zenpage;
823		if (!is_array($this->category_list))
824			return false;
825		$cat = '';
826		$list = $_zp_zenpage->getAllCategories();
827		if (!empty($list)) {
828			foreach ($list as $category) {
829				if (in_array($category['title'], $this->category_list)) {
830					$catobj = new ZenpageCategory($category['titlelink']);
831					$cat .= ' `cat_id`=' . $catobj->getID() . ' OR';
832					$subcats = $catobj->getCategories();
833					if ($subcats) {
834						foreach ($subcats as $subcat) {
835							$catobj = new ZenpageCategory($subcat);
836							$cat .= ' `cat_id`=' . $catobj->getID() . ' OR';
837						}
838					}
839				}
840			}
841			if ($cat) {
842				$cat = ' WHERE ' . substr($cat, 0, -3);
843			}
844		}
845		$sql = 'SELECT DISTINCT `news_id` FROM ' . prefix('news2cat') . $cat;
846		$result = query($sql);
847		$list = array();
848		if ($result) {
849			while ($row = db_fetch_assoc($result)) {
850				$list[] = $row['news_id'];
851			}
852			db_free_result($result);
853		}
854		return $list;
855	}
856
857	/**
858	 * Takes a list of IDs and makes a where clause
859	 *
860	 * @param array $idlist list of IDs for a where clause
861	 */
862	protected static function compressedIDList($idlist) {
863		$idlist = array_unique($idlist);
864		asort($idlist);
865		return '`id` IN (' . implode(',', $idlist) . ')';
866	}
867
868	/**
869	 * get connical sort key and direction parameters.
870	 * @param type $sorttype sort field desired
871	 * @param type $sortdirection DESC or ASC
872	 * @param type $defaulttype if no sort type otherwise selected use this one
873	 * @param type $table the database table being searched
874	 * @return array
875	 */
876	protected function sortKey($sorttype, $sortdirection, $defaulttype, $table) {
877		if (is_null($sorttype)) {
878			if (array_key_exists($table . 'sorttype', $this->extraparams)) {
879				$sorttype = $this->extraparams[$table . 'sorttype'];
880			} else if (array_key_exists('sorttype', $this->extraparams)) {
881				$sorttype = $this->extraparams['sorttype'];
882			}
883		}
884		$sorttype = lookupSortKey($sorttype, $defaulttype, $table);
885		if (is_null($sortdirection)) {
886			if (array_key_exists($table . 'sortdirection', $this->extraparams)) {
887				$sortdirection = $this->extraparams[$table . 'sortdirection'];
888			} else if (array_key_exists('sortdirection', $this->extraparams)) {
889				$sortdirection = $this->extraparams['sortdirection'];
890			}
891		}
892		return array($sorttype, $sortdirection);
893	}
894
895	/**
896	 * returns the results of a date search
897	 * @param string $searchstring the search target
898	 * @param string $searchdate the date target
899	 * @param string $tbl the database table to search
900	 * @param string $sorttype what to sort on
901	 * @param string $sortdirection what direction
902	 * @return string
903	 * @since 1.1.3
904	 */
905	function searchDate($searchstring, $searchdate, $tbl, $sorttype, $sortdirection, $whichdate = 'date') {
906		global $_zp_current_album, $_zp_gallery;
907		$sql = 'SELECT DISTINCT `id`, `show`,`title`';
908		switch ($tbl) {
909			case 'pages':
910			case 'news':
911				$sql .= ',`titlelink` ';
912				break;
913			case 'albums':
914				$sql .= ",`desc`,`folder` ";
915				break;
916			default:
917				$sql .= ",`desc`,`albumid`,`filename`,`location`,`city`,`state`,`country` ";
918				break;
919		}
920		$sql .= "FROM " . prefix($tbl) . " WHERE ";
921		if (!zp_loggedin()) {
922			$sql .= "`show` = 1 AND (";
923		}
924
925		if (!empty($searchdate)) {
926			if ($searchdate == "0000-00") {
927				$sql .= "`$whichdate`=\"0000-00-00 00:00:00\"";
928			} else {
929				$datesize = sizeof(explode('-', $searchdate));
930// search by day
931				if ($datesize == 3) {
932					$d1 = $searchdate . " 00:00:00";
933					$d2 = $searchdate . " 23:59:59";
934					$sql .= "`$whichdate` >= \"$d1\" AND `$whichdate` < \"$d2\"";
935				}
936// search by month
937				else if ($datesize == 2) {
938					$d1 = $searchdate . "-01 00:00:00";
939					$d = strtotime($d1);
940					$d = strtotime('+ 1 month', $d);
941					$d2 = substr(date('Y-m-d H:m:s', $d), 0, 7) . "-01 00:00:00";
942					$sql .= "`$whichdate` >= \"$d1\" AND `$whichdate` < \"$d2\"";
943				} else {
944					$sql .= "`$whichdate`<\"0000-00-00 00:00:00\"";
945				}
946			}
947		}
948		if (!zp_loggedin()) {
949			$sql .= ")";
950		}
951
952		switch ($tbl) {
953			case 'news':
954			case 'pages':
955				if (empty($sorttype)) {
956					$key = '`date` DESC';
957				} else {
958					$key = trim($sorttype . ' ' . $sortdirection);
959				}
960				break;
961			case 'albums':
962				if (is_null($sorttype)) {
963					if (empty($this->album)) {
964						list($key, $sortdirection) = $this->sortKey($_zp_gallery->getSortType(), $sortdirection, 'title', 'albums');
965						if ($key != '`sort_order`') {
966							if ($_zp_gallery->getSortDirection()) {
967								$key .= " DESC";
968							}
969						}
970					} else {
971						$key = $this->album->getAlbumSortKey();
972						if ($key != '`sort_order`' && $key != 'RAND()') {
973							if ($this->album->getSortDirection('album')) {
974								$key .= " DESC";
975							}
976						}
977					}
978				} else {
979					list($key, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'albums');
980					$key = trim($key . ' ' . $sortdirection);
981				}
982				break;
983			default:
984				$hidealbums = getNotViewableAlbums();
985				if (!is_null($hidealbums)) {
986					foreach ($hidealbums as $id) {
987						$sql .= ' AND `albumid`!=' . $id;
988					}
989				}
990				if (is_null($sorttype)) {
991					if (empty($this->album)) {
992						list($key, $sortdirection) = $this->sortKey(IMAGE_SORT_TYPE, $sortdirection, 'title', 'images');
993						if ($key != '`sort_order`') {
994							if (IMAGE_SORT_DIRECTION) {
995								$key .= " DESC";
996							}
997						}
998					} else {
999						$key = $thie->album->getImageSortKey();
1000						if ($key != '`sort_order`' && $key != 'RAND()') {
1001							if ($this->album->getSortDirection('image')) {
1002								$key .= " DESC";
1003							}
1004						}
1005					}
1006				} else {
1007					list($key, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'images');
1008					$key = trim($key . ' ' . $sortdirection);
1009				}
1010				break;
1011		}
1012		$sql .= " ORDER BY " . $key;
1013		return $sql;
1014	}
1015
1016	/**
1017	 * Searches the table for tags
1018	 * Returns an array of database records.
1019	 *
1020	 * @param array $searchstring
1021	 * @param string $tbl set DB table name to be searched
1022	 * @param string $sorttype what to sort on
1023	 * @param string $sortdirection what direction
1024	 * @return array
1025	 */
1026	protected function searchFieldsAndTags($searchstring, $tbl, $sorttype, $sortdirection) {
1027		global $_zp_gallery;
1028		$weights = $idlist = array();
1029		$sql = $allIDs = NULL;
1030		$tagPattern = $this->tagPattern;
1031// create an array of [tag, objectid] pairs for tags
1032		$tag_objects = array();
1033		$fields = $this->fieldList;
1034		if (count($fields) == 0) { // then use the default ones
1035			$fields = $this->allowedSearchFields();
1036		}
1037		foreach ($fields as $key => $field) {
1038			switch ($field) {
1039				case 'news_categories':
1040					if ($tbl != 'news') {
1041						break;
1042					}
1043					unset($fields[$key]);
1044					query('SET @serachfield="news_categories"');
1045					$tagsql = 'SELECT @serachfield AS field, t.`title` AS name, o.`news_id` AS `objectid` FROM ' . prefix('news_categories') . ' AS t, ' . prefix('news2cat') . ' AS o WHERE t.`id`=o.`cat_id` AND (';
1046					foreach ($searchstring as $singlesearchstring) {
1047						switch ($singlesearchstring) {
1048							case '&':
1049							case '!':
1050							case '|':
1051							case '(':
1052							case ')':
1053								break;
1054							case '*':
1055								$targetfound = true;
1056								$tagsql .= "COALESCE(title, '') != '' OR ";
1057								break;
1058							default:
1059								$targetfound = true;
1060								$tagsql .= '`title` = ' . db_quote($singlesearchstring) . ' OR ';
1061						}
1062					}
1063					$tagsql = substr($tagsql, 0, strlen($tagsql) - 4) . ') ORDER BY t.`id`';
1064					$objects = query_full_array($tagsql, false);
1065					if (is_array($objects)) {
1066						$tag_objects = $objects;
1067					}
1068					break;
1069				case 'tags_exact':
1070					$tagPattern = array('type' => '=', 'open' => '', 'close' => '');
1071				case 'tags':
1072					unset($fields[$key]);
1073					query('SET @serachfield="tags"');
1074					$tagsql = 'SELECT @serachfield AS field, t.`name`, o.`objectid` FROM ' . prefix('tags') . ' AS t, ' . prefix('obj_to_tag') . ' AS o WHERE t.`id`=o.`tagid` AND o.`type`="' . $tbl . '" AND (';
1075					foreach ($searchstring as $singlesearchstring) {
1076						switch ($singlesearchstring) {
1077							case '&':
1078							case '!':
1079							case '|':
1080							case '(':
1081							case ')':
1082								break;
1083							case '*':
1084								query('SET @emptyfield="*"');
1085								$tagsql = str_replace('t.`name`', '@emptyfield as name', $tagsql);
1086								$tagsql .= "t.`name` IS NOT NULL OR ";
1087								break;
1088							default:
1089								$targetfound = true;
1090								if ($tagPattern['type'] == 'like') {
1091									$target = db_LIKE_escape($singlesearchstring);
1092								} else {
1093									$target = $singlesearchstring;
1094								}
1095								$tagsql .= 't.`name` ' . strtoupper($tagPattern['type']) . ' ' . db_quote($tagPattern['open'] . $target . $tagPattern['close']) . ' OR ';
1096						}
1097					}
1098					$tagsql = substr($tagsql, 0, strlen($tagsql) - 4) . ') ORDER BY t.`id`';
1099					$objects = query_full_array($tagsql, false);
1100					if (is_array($objects)) {
1101						$tag_objects = array_merge($tag_objects, $objects);
1102					}
1103					break;
1104				default:
1105					break;
1106			}
1107		}
1108
1109
1110// create an array of [name, objectid] pairs for the search fields.
1111		$field_objects = array();
1112		if (count($fields) > 0) {
1113			$columns = array();
1114			$dbfields = db_list_fields($tbl);
1115			if (is_array($dbfields)) {
1116				foreach ($dbfields as $row) {
1117					$columns[] = strtolower($row['Field']);
1118				}
1119			}
1120			foreach ($searchstring as $singlesearchstring) {
1121				switch ($singlesearchstring) {
1122					case '!':
1123					case '&':
1124					case '|':
1125					case '(':
1126					case ')':
1127						break;
1128					default:
1129						$targetfound = true;
1130						query('SET @serachtarget=' . db_quote($singlesearchstring));
1131						foreach ($fields as $fieldname) {
1132							if ($tbl == 'albums' && strtolower($fieldname) == 'filename') {
1133								$fieldname = 'folder';
1134							} else {
1135								$fieldname = strtolower($fieldname);
1136							}
1137							if ($fieldname && in_array($fieldname, $columns)) {
1138								query('SET @serachfield=' . db_quote($fieldname));
1139								switch ($singlesearchstring) {
1140									case '*':
1141										$sql = 'SELECT @serachtarget AS name, @serachfield AS field, `id` AS `objectid` FROM ' . prefix($tbl) . ' WHERE (' . "COALESCE(`$fieldname`, '') != ''" . ') ORDER BY `id`';
1142										break;
1143									default:
1144										if ($this->pattern['type'] == 'like') {
1145											$target = db_LIKE_escape($singlesearchstring);
1146										} else {
1147											$target = $singlesearchstring;
1148										}
1149										$fieldsql = ' `' . $fieldname . '` ' . strtoupper($this->pattern['type']) . ' ' . db_quote($this->pattern['open'] . $target . $this->pattern['close']);
1150										$sql = 'SELECT @serachtarget AS name, @serachfield AS field, `id` AS `objectid` FROM ' . prefix($tbl) . ' WHERE (' . $fieldsql . ') ORDER BY `id`';
1151								}
1152								$objects = query_full_array($sql, false);
1153								if (is_array($objects)) {
1154									$field_objects = array_merge($field_objects, $objects);
1155								}
1156							}
1157						}
1158				}
1159			}
1160		}
1161
1162// now do the boolean logic of the search string
1163		$exact = $tagPattern['type'] == '=';
1164		$objects = array_merge($tag_objects, $field_objects);
1165		if (count($objects) != 0) {
1166			$tagid = '';
1167			$taglist = array();
1168
1169			foreach ($objects as $object) {
1170				$tagid = strtolower($object['name']);
1171				if (!isset($taglist[$tagid]) || !is_array($taglist[$tagid])) {
1172					$taglist[$tagid] = array();
1173				}
1174				$taglist[$tagid][] = $object['objectid'];
1175			}
1176			$op = '';
1177			$idstack = array();
1178			$opstack = array();
1179			while (count($searchstring) > 0) {
1180				$singlesearchstring = array_shift($searchstring);
1181				switch ($singlesearchstring) {
1182					case '&':
1183					case '!':
1184					case '|':
1185						$op = $op . $singlesearchstring;
1186						break;
1187					case '(':
1188						array_push($idstack, $idlist);
1189						array_push($opstack, $op);
1190						$idlist = array();
1191						$op = '';
1192						break;
1193					case ')':
1194						$objectid = $idlist;
1195						$idlist = array_pop($idstack);
1196						$op = array_pop($opstack);
1197						switch ($op) {
1198							case '&':
1199								if (is_array($objectid)) {
1200									$idlist = array_intersect($idlist, $objectid);
1201								} else {
1202									$idlist = array();
1203								}
1204								break;
1205							case '!':
1206								break; // Paren followed by NOT is nonsensical?
1207							case '&!':
1208								if (is_array($objectid)) {
1209									$idlist = array_diff($idlist, $objectid);
1210								}
1211								break;
1212							case '';
1213							case '|':
1214								if (is_array($objectid)) {
1215									$idlist = array_merge($idlist, $objectid);
1216								}
1217								break;
1218						}
1219						$op = '';
1220						break;
1221					default:
1222						$lookfor = strtolower($singlesearchstring);
1223						$objectid = NULL;
1224						foreach ($taglist as $key => $objlist) {
1225							if (($exact && $lookfor == $key) || (!$exact && preg_match('|' . preg_quote($lookfor) . '|', $key))) {
1226								if (is_array($objectid)) {
1227									$objectid = array_merge($objectid, $objlist);
1228								} else {
1229									$objectid = $objlist;
1230								}
1231							}
1232						}
1233						switch ($op) {
1234							case '&':
1235								if (is_array($objectid)) {
1236									$idlist = array_intersect($idlist, $objectid);
1237								} else {
1238									$idlist = array();
1239								}
1240								break;
1241							case '!':
1242								if (is_null($allIDs)) {
1243									$allIDs = array();
1244									$result = query("SELECT `id` FROM " . prefix($tbl));
1245									if ($result) {
1246										while ($row = db_fetch_assoc($result)) {
1247											$allIDs[] = $row['id'];
1248										}
1249										db_free_result($result);
1250									}
1251								}
1252								if (is_array($objectid)) {
1253									$idlist = array_merge($idlist, array_diff($allIDs, $objectid));
1254								}
1255								break;
1256							case '&!':
1257								if (is_array($objectid)) {
1258									$idlist = array_diff($idlist, $objectid);
1259								}
1260								break;
1261							case '';
1262							case '|':
1263								if (is_array($objectid)) {
1264									$idlist = array_merge($idlist, $objectid);
1265								}
1266								break;
1267						}
1268						$op = '';
1269						break;
1270				}
1271			}
1272		}
1273
1274// we now have an id list of the items that were found and will create the SQL Search to retrieve their records
1275		if (count($idlist) > 0) {
1276			$weights = array_count_values($idlist);
1277			arsort($weights, SORT_NUMERIC);
1278			$sql = 'SELECT DISTINCT `id`,`show`,';
1279
1280			switch ($tbl) {
1281				case 'news':
1282					if ($this->search_unpublished || zp_loggedin(MANAGE_ALL_NEWS_RIGHTS)) {
1283						$show = '';
1284					} else {
1285						$show = "`show` = 1 AND ";
1286					}
1287					$sql .= '`title`, `titlelink` ';
1288					if (is_array($this->category_list)) {
1289						$news_list = $this->subsetNewsCategories();
1290						$idlist = array_intersect($news_list, $idlist);
1291						if (count($idlist) == 0) {
1292							return array(false, array());
1293						}
1294					}
1295					if (empty($sorttype)) {
1296						$key = '`date` DESC';
1297					} else {
1298						$key = trim($sorttype . $sortdirection);
1299					}
1300					if ($show) {
1301						$show .= '`date`<=' . db_quote(date('Y-m-d H:i:s')) . ' AND ';
1302					}
1303					break;
1304				case 'pages':
1305					if (zp_loggedin(MANAGE_ALL_PAGES_RIGHTS)) {
1306						$show = '';
1307					} else {
1308						$show = "`show` = 1 AND ";
1309					}
1310					$sql .= '`title`, `titlelink` ';
1311					if (empty($sorttype)) {
1312						$key = '`date` DESC';
1313					} else {
1314						$key = trim($sorttype . $sortdirection);
1315					}
1316					if ($show) {
1317						$show .= '`date`<=' . db_quote(date('Y-m-d H:i:s')) . ' AND ';
1318					}
1319					break;
1320				case 'albums':
1321					if ($this->search_unpublished || zp_loggedin()) {
1322						$show = '';
1323					} else {
1324						$show = "`show` = 1 AND ";
1325					}
1326					$sql .= "`folder`, `title` ";
1327					if (is_null($sorttype)) {
1328						if (empty($this->album)) {
1329							list($key, $sortdirection) = $this->sortKey($_zp_gallery->getSortType(), $sortdirection, 'title', 'albums');
1330							if ($_zp_gallery->getSortDirection()) {
1331								$key .= " DESC";
1332							}
1333						} else {
1334							$key = $this->album->getAlbumSortKey();
1335							if ($key != '`sort_order`' && $key != 'RAND()') {
1336								if ($this->album->getSortDirection('album')) {
1337									$key .= " DESC";
1338								}
1339							}
1340						}
1341					} else {
1342						list($key, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'albums');
1343						$key = trim($key . ' ' . $sortdirection);
1344					}
1345					break;
1346				default: // images
1347					if ($this->search_unpublished || zp_loggedin()) {
1348						$show = '';
1349					} else {
1350						$show = "`show` = 1 AND ";
1351					}
1352					$sql .= "`albumid`, `filename`, `title` ";
1353					if (is_null($sorttype)) {
1354						if (empty($this->album)) {
1355							list($key, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'images');
1356							if ($sortdirection) {
1357								$key .= " DESC";
1358							}
1359						} else {
1360							$key = $this->album->getImageSortKey();
1361							if ($key != '`sort_order`') {
1362								if ($this->album->getSortDirection('image')) {
1363									$key .= " DESC";
1364								}
1365							}
1366						}
1367					} else {
1368						list($key, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'images');
1369						$key = trim($key . ' ' . $sortdirection);
1370					}
1371					break;
1372			}
1373			$sql .= "FROM " . prefix($tbl) . " WHERE " . $show;
1374			$sql .= '(' . self::compressedIDList($idlist) . ')';
1375			$sql .= " ORDER BY " . $key;
1376			return array($sql, $weights);
1377		}
1378		return array(false, array());
1379	}
1380
1381	/**
1382	 * Returns an array of albums found in the search
1383	 * @param string $sorttype what to sort on
1384	 * @param string $sortdirection what direction
1385	 * @param bool $mine set true/false to override ownership
1386	 *
1387	 * @return array
1388	 */
1389	private function getSearchAlbums($sorttype, $sortdirection, $mine = NULL) {
1390		if (getOption('search_no_albums') || $this->search_no_albums) {
1391			return array();
1392		}
1393		list($sorttype, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'albums');
1394		$albums = array();
1395		$searchstring = $this->getSearchString();
1396		if (empty($searchstring)) {
1397			return array();
1398		} // nothing to find
1399		$criteria = $this->getCacheTag('albums', serialize($searchstring), $sorttype . ' ' . $sortdirection . ' '. $mine);
1400		if ($this->albums && $criteria == $this->searches['albums']) {
1401			return $this->albums;
1402		}
1403		$albums = $this->getCachedSearch($criteria);
1404		if (is_null($albums)) {
1405			if (is_null($mine) && zp_loggedin(MANAGE_ALL_ALBUM_RIGHTS)) {
1406				$mine = true;
1407			}
1408			$result = $albums = array();
1409			list ($search_query, $weights) = $this->searchFieldsAndTags($searchstring, 'albums', $sorttype, $sortdirection);
1410			if (!empty($search_query)) {
1411				$search_result = query($search_query);
1412				if ($search_result) {
1413					while ($row = db_fetch_assoc($search_result)) {
1414						$albumname = $row['folder'];
1415						if ($albumname != $this->dynalbumname) {
1416							if (file_exists(ALBUM_FOLDER_SERVERPATH . internalToFilesystem($albumname))) {
1417								$album = newAlbum($albumname);
1418								$uralbum = getUrAlbum($album);
1419								$viewUnpublished = ($this->search_unpublished || zp_loggedin() && $uralbum->albumSubRights() & (MANAGED_OBJECT_RIGHTS_EDIT | MANAGED_OBJECT_RIGHTS_VIEW));
1420								switch (themeObject::checkScheduledPublishing($row)) {
1421									case 1:
1422										$album->setShow(0);
1423										$album->save();
1424									case 2:
1425										$row['show'] = 0;
1426								}
1427								if ($mine || (is_null($mine) && $album->isMyItem(LIST_RIGHTS)) || (checkAlbumPassword($albumname) && (($album->checkAccess() && $album->isPublic()) || $viewUnpublished))) {
1428									if ((empty($this->album_list) || in_array($albumname, $this->album_list)) && !$this->excludeAlbum($albumname)) {
1429										$result[] = array('title' => $row['title'], 'name' => $albumname, 'weight' => $weights[$row['id']]);
1430									}
1431								}
1432							}
1433						}
1434					}
1435					db_free_result($search_result);
1436					$sortdir = self::getSortdirBool($sortdirection);
1437					if (is_null($sorttype)) {
1438						$result = sortMultiArray($result, 'weight', $sortdir, true, false, false, array('weight'));
1439					}
1440					if ($sorttype == '`title`') {
1441						$result = sortByMultilingual($result, 'title', $sortdir);
1442					}
1443					foreach ($result as $album) {
1444						$albums[] = $album['name'];
1445					}
1446				}
1447			}
1448			zp_apply_filter('search_statistics', $searchstring, 'albums', !empty($albums), $this->dynalbumname, $this->iteration++);
1449			$this->cacheSearch($criteria, $albums);
1450		}
1451		$this->albums = $albums;
1452		$this->searches['albums'] = $criteria;
1453		return $albums;
1454	}
1455
1456	/**
1457	 * Returns an array of album names found in the search.
1458	 * If $page is not zero, it returns the current page's albums
1459	 *
1460	 * @param int $page the page number we are on
1461	 * @param string $sorttype what to sort on
1462	 * @param string $sortdirection what direction
1463	 * @param bool $care set to false if the order of the albums does not matter
1464	 * @param bool $mine set true/false to override ownership
1465	 *
1466	 * @return array
1467	 */
1468	function getAlbums($page = 0, $sorttype = NULL, $sortdirection = NULL, $care = true, $mine = NULL) {
1469		$this->albums = $this->getSearchAlbums($sorttype, $sortdirection, $mine);
1470		if ($page == 0) {
1471			return $this->albums;
1472		} else {
1473			$albums_per_page = max(1, getOption('albums_per_page'));
1474			return array_slice($this->albums, $albums_per_page * ($page - 1), $albums_per_page);
1475		}
1476	}
1477
1478	/**
1479	 * Checks if the album should be excluded from results
1480	 * Subalbums and their contents inherit the exclusion.
1481	 *
1482	 * @since ZenphotoCMS 1.5.8
1483	 *
1484	 * @param string $albumname
1485	 * @return boolean
1486	 */
1487	function excludeAlbum($albumname) {
1488		$exclude = false;
1489		if (!is_null($this->album_list_exclude)) {
1490			if (in_array($albumname, $this->album_list_exclude)) {
1491				return true;
1492			} else {
1493				foreach ($this->album_list_exclude as $excludealbum) {
1494					if (strpos($albumname, $excludealbum) === 0) {
1495						return true;
1496					}
1497				}
1498			}
1499		}
1500		return $exclude;
1501	}
1502
1503	/**
1504	 * Returns the index of the album within the search albums
1505	 *
1506	 * @param string $curalbum The album sought
1507	 * @return int
1508	 */
1509	function getAlbumIndex($curalbum) {
1510		$albums = $this->getAlbums(0);
1511		return array_search($curalbum, $albums);
1512	}
1513
1514	/**
1515	 * Returns the album following the current one
1516	 *
1517	 * @param string $curalbum the name of the current album
1518	 * @return object
1519	 */
1520	function getNextAlbum($curalbum) {
1521		global $_zp_gallery;
1522		$albums = $this->getAlbums(0);
1523		$inx = array_search($curalbum, $albums) + 1;
1524		if ($inx >= 0 && $inx < count($albums)) {
1525			return newAlbum($albums[$inx]);
1526		}
1527		return null;
1528	}
1529
1530	/**
1531	 * Returns the album preceding the current one
1532	 *
1533	 * @param string $curalbum the name of the current album
1534	 * @return object
1535	 */
1536	function getPrevAlbum($curalbum) {
1537		global $_zp_gallery;
1538		$albums = $this->getAlbums(0);
1539		$inx = array_search($curalbum, $albums) - 1;
1540		if ($inx >= 0 && $inx < count($albums)) {
1541			return newAlbum($albums[$inx]);
1542		}
1543		return null;
1544	}
1545
1546	/**
1547	 * Returns the number of images found in the search
1548	 *
1549	 * @return int
1550	 */
1551	function getNumImages() {
1552		if (is_null($this->images)) {
1553			$this->getImages(0);
1554		}
1555		return count($this->images);
1556	}
1557
1558	/**
1559	 * Returns an array of image names found in the search
1560	 *
1561	 * @param string $sorttype what to sort on
1562	 * @param string $sortdirection what direction
1563	 * @param bool $mine set true/false to overried ownership
1564	 * @return array
1565	 */
1566	private function getSearchImages($sorttype, $sortdirection, $mine = NULL) {
1567		if (getOption('search_no_images') || $this->search_no_images) {
1568			return array();
1569		}
1570		list($sorttype, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'images');
1571		if (is_null($mine) && zp_loggedin(MANAGE_ALL_ALBUM_RIGHTS)) {
1572			$mine = true;
1573		}
1574		$searchstring = $this->getSearchString();
1575		$searchdate = $this->dates;
1576		if (empty($searchstring) && empty($searchdate)) {
1577			return array();
1578		} // nothing to find
1579		$criteria = $this->getCacheTag('images', serialize($searchstring) . ' ' . $searchdate, $sorttype . ' ' . $sortdirection . ' '.$mine);
1580		if ($criteria == $this->searches['images']) {
1581			return $this->images;
1582		}
1583		$images = $this->getCachedSearch($criteria);
1584		if (is_null($images)) {
1585			if (empty($searchdate)) {
1586				list ($search_query, $weights) = $this->searchFieldsAndTags($searchstring, 'images', $sorttype, $sortdirection);
1587			} else {
1588				$search_query = $this->searchDate($searchstring, $searchdate, 'images', $sorttype, $sortdirection);
1589			}
1590			if (empty($search_query)) {
1591				$search_result = false;
1592			} else {
1593				$search_result = query($search_query);
1594			}
1595			$albums_seen = $images = array();
1596			if ($search_result) {
1597				while ($row = db_fetch_assoc($search_result)) {
1598					$albumid = $row['albumid'];
1599					if (array_key_exists($albumid, $albums_seen)) {
1600						$albumrow = $albums_seen[$albumid];
1601					} else {
1602						$query = "SELECT folder, `show` FROM " . prefix('albums') . " WHERE id = $albumid";
1603						$row2 = query_single_row($query); // id is unique
1604						if ($row2) {
1605							$albumname = $row2['folder'];
1606							$allow = false;
1607							$album = newAlbum($albumname);
1608							$uralbum = getUrAlbum($album);
1609							$viewUnpublished = ($this->search_unpublished || zp_loggedin() && $uralbum->albumSubRights() & (MANAGED_OBJECT_RIGHTS_EDIT | MANAGED_OBJECT_RIGHTS_VIEW));
1610							switch (themeObject::checkScheduledPublishing($row)) {
1611								case 1:
1612									$imageobj = newImage($album, $row['filename']);
1613									$imageobj->setShow(0);
1614									$imageobj->save();
1615								case 2:
1616									$row['show'] = 0;
1617									break;
1618							}
1619							$viewUnpublished = ($mine || is_null($mine)) && ($album->isMyItem(LIST_RIGHTS) || checkAlbumPassword($albumname) && ($album->isPublic() || $viewUnpublished));
1620							if ($viewUnpublished) {
1621								$allow = (empty($this->album_list) || in_array($albumname, $this->album_list)) && !$this->excludeAlbum($albumname);
1622							}
1623							$albums_seen[$albumid] = $albumrow = array('allow' => $allow, 'viewUnpublished' => $viewUnpublished, 'folder' => $albumname, 'localpath' => ALBUM_FOLDER_SERVERPATH . internalToFilesystem($albumname) . '/');
1624						} else {
1625							$albums_seen[$albumid] = $albumrow = array('allow' => false, 'viewUnpublished' => false, 'folder' => '', 'localpath' => '');
1626						}
1627					}
1628					if ($albumrow['allow'] && ($row['show'] || $albumrow['viewUnpublished'])) {
1629						if (file_exists($albumrow['localpath'] . internalToFilesystem($row['filename']))) { //	still exists
1630							$data = array('title' => $row['title'], 'filename' => $row['filename'], 'folder' => $albumrow['folder']);
1631							if (isset($weights)) {
1632								$data['weight'] = $weights[$row['id']];
1633							}
1634							$images[] = $data;
1635						}
1636					}
1637				}
1638				db_free_result($search_result);
1639				$sortdir = self::getSortdirBool($sortdirection);
1640				if (is_null($sorttype) && isset($weights)) {
1641					$images = sortMultiArray($images, 'weight', $sortdir, true, false, false, array('weight'));
1642				}
1643				if ($sorttype == '`title`') {
1644					$images = sortByMultilingual($images, 'title', $sortdir);
1645				}
1646			}
1647			if (empty($searchdate)) {
1648				zp_apply_filter('search_statistics', $searchstring, 'images', !empty($images), $this->dynalbumname, $this->iteration++);
1649			}
1650			$this->cacheSearch($criteria, $images);
1651		}
1652		$this->searches['images'] = $criteria;
1653		return $images;
1654	}
1655
1656	/**
1657	 * Returns an array of images found in the search
1658	 * It will return a "page's worth" if $page is non zero
1659	 *
1660	 * @param int $page the page number desired
1661	 * @param int $firstPageCount count of images that go on the album/image transition page
1662	 * @param string $sorttype what to sort on
1663	 * @param string $sortdirection what direction
1664	 * @param bool $care placeholder to make the getImages methods all the same.
1665	 * @param bool $mine set true/false to overried ownership
1666	 * @return array
1667	 */
1668	function getImages($page = 0, $firstPageCount = 0, $sorttype = NULL, $sortdirection = NULL, $care = true, $mine = NULL) {
1669		$this->images = $this->getSearchImages($sorttype, $sortdirection, $mine);
1670		if ($page == 0) {
1671			return $this->images;
1672		} else {
1673			if (empty($this->images)) {
1674				return array();
1675			}
1676			// Only return $firstPageCount images if we are on the first page and $firstPageCount > 0
1677			if (($page == 1) && ($firstPageCount > 0)) {
1678				$pageStart = 0;
1679				$images_per_page = $firstPageCount;
1680			} else {
1681				if ($firstPageCount > 0) {
1682					$fetchPage = $page - 2;
1683				} else {
1684					$fetchPage = $page - 1;
1685				}
1686				$images_per_page = max(1, getOption('images_per_page'));
1687				$pageStart = $firstPageCount + $images_per_page * $fetchPage;
1688			}
1689			$slice = array_slice($this->images, $pageStart, $images_per_page);
1690			return $slice;
1691		}
1692	}
1693
1694	/**
1695	 * Returns the index of this image in the search images
1696	 *
1697	 * @param string $album The folder name of the image
1698	 * @param string $filename the filename of the image
1699	 * @return int
1700	 */
1701	function getImageIndex($album, $filename) {
1702		$images = $this->getImages();
1703		$c = 0;
1704		foreach ($images as $image) {
1705			if (($album == $image['folder']) && ($filename == $image['filename'])) {
1706				return $c;
1707			}
1708			$c++;
1709		}
1710		return false;
1711	}
1712
1713	/**
1714	 * Returns a specific image
1715	 *
1716	 * @param int $index the index of the image desired
1717	 * @return object
1718	 */
1719	function getImage($index) {
1720		global $_zp_gallery;
1721		if (!is_null($this->images)) {
1722			$this->getImages();
1723		}
1724		if ($index >= 0 && $index < $this->getNumImages()) {
1725			$img = $this->images[$index];
1726			return newImage(newAlbum($img['folder']), $img['filename']);
1727		}
1728		return false;
1729	}
1730
1731	function getDynamicAlbum() {
1732		return $this->album;
1733	}
1734
1735	/**
1736	 *
1737	 * return the list of albums found
1738	 */
1739	function getAlbumList() {
1740		return $this->album_list;
1741	}
1742
1743	/**
1744	 *
1745	 * return the list of categories found
1746	 */
1747	function getCategoryList() {
1748		return $this->category_list;
1749	}
1750
1751	/**
1752	 *
1753	 * Returns pages from a search
1754	 * @param bool $published ignored, left for parameter compatibility
1755	 * @param bool $toplevel ignored, left for parameter compatibility
1756	 * @param int $number ignored, left for parameter compatibility
1757	 * @param string $sorttype the sort key
1758	 * @param strng $sortdirection the sort order
1759	 *
1760	 * @return array
1761	 */
1762	function getPages($published = NULL, $toplevel = false, $number = NULL, $sorttype = NULL, $sortdirection = NULL) {
1763		return $this->getSearchPages($sorttype, $sortdirection);
1764	}
1765
1766	/**
1767	 * Returns a list of Pages Titlelinks found in the search
1768	 *
1769	 * @parm string $sorttype optional sort field
1770	 * @param string $sortdirection optional ordering
1771	 *
1772	 * @return array
1773	 */
1774	private function getSearchPages($sorttype, $sortdirection) {
1775		if (!extensionEnabled('zenpage') || getOption('search_no_pages') || $this->search_no_pages)
1776			return array();
1777		list($sorttype, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'pages');
1778		$searchstring = $this->getSearchString();
1779		$searchdate = $this->dates;
1780		if (empty($searchstring) && empty($searchdate)) {
1781			return array();
1782		} // nothing to find
1783		$criteria = $this->getCacheTag('pages', serialize($searchstring) . ' ' . $searchdate, $sorttype . ' ' . $sortdirection);
1784		if ($this->pages && $criteria == $this->searches['pages']) {
1785			return $this->pages;
1786		}
1787		$pages = $this->getCachedSearch($criteria);
1788		if (is_null($pages)) {
1789			$pages = $result = array();
1790			if (empty($searchdate)) {
1791				list ($search_query, $weights) = $this->searchFieldsAndTags($searchstring, 'pages', $sorttype, $sortdirection);
1792				if (empty($search_query)) {
1793					$search_result = false;
1794				} else {
1795					$search_result = query($search_query);
1796				}
1797				zp_apply_filter('search_statistics', $searchstring, 'pages', !$search_result, false, $this->iteration++);
1798			} else {
1799				$search_query = $this->searchDate($searchstring, $searchdate, 'pages', NULL, NULL);
1800				$search_result = query($search_query);
1801			}
1802			if ($search_result) {
1803				while ($row = db_fetch_assoc($search_result)) {
1804					$pageobj = new ZenpagePage($row['titlelink']);
1805					if((zp_loggedin() && $pageobj->isMyItem(LIST_RIGHTS)) || ($pageobj->isPublic() || $this->search_unpublished)) {
1806						$data = array('title' => $row['title'], 'titlelink' => $row['titlelink']);
1807						if (isset($weights)) {
1808							$data['weight'] = $weights[$row['id']];
1809						}
1810						$result[] = $data;
1811					}
1812				}
1813				db_free_result($search_result);
1814			}
1815			$sortdir = self::getSortdirBool($sortdirection);
1816			if (is_null($sorttype) && isset($weights)) {
1817				$result = sortMultiArray($result, 'weight', $sortdir, true, false, false, array('weight'));
1818			}
1819			if ($sorttype == '`title`') {
1820				$result = sortByMultilingual($result, 'title', $sortdir);
1821			}
1822			foreach ($result as $page) {
1823				$pages[] = $page['titlelink'];
1824			}
1825			$this->cacheSearch($criteria, $pages);
1826		}
1827		$this->pages = $pages;
1828		$this->searches['pages'] = $criteria;
1829		return $this->pages;
1830	}
1831
1832	/**
1833	 * Returns a list of News Titlelinks found in the search
1834	 *
1835	 * @param int $articles_per_page The number of articles to get
1836	 * @param bool $published placeholder for consistent parameter list
1837	 * @param bool $ignorepagination ignore pagination
1838	 * @param string $sorttype field to sort on
1839	 * @param string $sortdirection sort order
1840	 *
1841	 * @return array
1842	 */
1843	function getArticles($articles_per_page = 0, $published = NULL, $ignorepagination = false, $sorttype = NULL, $sortdirection = NULL) {
1844		$articles = $this->getSearchArticles($sorttype, $sortdirection);
1845		if (empty($articles)) {
1846			return array();
1847		} else {
1848			if ($ignorepagination || !$articles_per_page) {
1849				return $articles;
1850			}
1851			return array_slice($articles, Zenpage::getOffset($articles_per_page), $articles_per_page);
1852		}
1853	}
1854
1855	/**
1856	 * Returns a list of News Titlelinks found in the search
1857	 *
1858	 * @param string $sorttype field to sort on
1859	 * @param string $sortdirection sort order
1860	 *
1861	 * @return array
1862	 */
1863	private function getSearchArticles($sorttype, $sortdirection) {
1864		if (!extensionEnabled('zenpage') || getOption('search_no_news') || $this->search_no_news) {
1865			return array();
1866		}
1867		list($sorttype, $sortdirection) = $this->sortKey($sorttype, $sortdirection, 'title', 'news');
1868		$searchstring = $this->getSearchString();
1869		$searchdate = $this->dates;
1870		if (empty($searchstring) && empty($searchdate)) {
1871			return array();
1872		} // nothing to find
1873		$criteria = $this->getCacheTag('news', serialize($searchstring) . ' ' . $searchdate, $sorttype . ' ' . $sortdirection);
1874		if ($this->articles && $criteria == $this->searches['news']) {
1875			return $this->articles;
1876		}
1877		$result = $this->getCachedSearch($criteria);
1878		if (is_null($result)) {
1879			$result = array();
1880			if (empty($searchdate)) {
1881				list ($search_query, $weights) = $this->searchFieldsAndTags($searchstring, 'news', $sorttype, $sortdirection);
1882			} else {
1883				$search_query = $this->searchDate($searchstring, $searchdate, 'news', $sorttype, $sortdirection, $this->whichdates);
1884			}
1885			if (empty($search_query)) {
1886				$search_result = false;
1887			} else {
1888				$search_result = query($search_query);
1889			}
1890			zp_apply_filter('search_statistics', $searchstring, 'news', !empty($search_result), false, $this->iteration++);
1891			if ($search_result) {
1892				while ($row = db_fetch_assoc($search_result)) {
1893					$articleobj = new ZenpageNews($row['titlelink']);
1894					if((zp_loggedin() && $articleobj->isMyItem(LIST_RIGHTS)) || ($articleobj->isPublic() || $this->search_unpublished)) {
1895						$data = array('title' => $row['title'], 'titlelink' => $row['titlelink']);
1896						if (isset($weights)) {
1897							$data['weight'] = $weights[$row['id']];
1898						}
1899						$result[] = $data;
1900					}
1901				}
1902				db_free_result($search_result);
1903			}
1904			$sortdir = self::getSortdirBool($sortdirection);
1905			if (is_null($sorttype) && isset($weights)) {
1906				$result = sortMultiArray($result, 'weight', $sortdir, true, false, false, array('weight'));
1907			}
1908			if ($sorttype == '`title`') {
1909				$result = sortByMultilingual($result, 'title', $sortdir);
1910			}
1911			$this->cacheSearch($criteria, $result);
1912		}
1913		$this->articles = $result;
1914		$this->searches['news'] = $criteria;
1915		return $this->articles;
1916	}
1917
1918	function clearSearchWords() {
1919		$this->processed_search = '';
1920		$this->words = '';
1921	}
1922
1923	/**
1924	 *
1925	 * creates a unique id for a search
1926	 * @param string $table	Database table
1927	 * @param string $search	Search string
1928	 * @param string $sort	Sort criteria
1929	 */
1930	protected function getCacheTag($table, $search, $sort) {
1931		$user = 'guest';
1932		$authCookies = Zenphoto_Authority::getAuthCookies();
1933		if (!empty($authCookies)) { // some sort of password exists, play it safe and make the tag unique
1934			$user = getUserIP();
1935		}
1936		$array = array('item' => $table, 'fields' => implode(', ', $this->fieldList), 'search' => $search, 'sort' => $sort, 'user' => $user);
1937		$dynalbum = $this->getDynamicAlbum();
1938		if($dynalbum) {
1939			$array['dynalbum'] = $dynalbum->name;
1940		}
1941		return $array;
1942	}
1943
1944	/**
1945	 *
1946	 * Caches a search
1947	 * @param string $criteria
1948	 * @param string $found reslts of the search
1949	 */
1950	private function cacheSearch($criteria, $found) {
1951		if (SEARCH_CACHE_DURATION) {
1952			$criteria = serialize($criteria);
1953			$sql = 'SELECT `id`, `data`, `date` FROM ' . prefix('search_cache') . ' WHERE `criteria` = ' . db_quote($criteria);
1954			$result = query_single_row($sql);
1955			if ($result) {
1956				$sql = 'UPDATE ' . prefix('search_cache') . ' SET `data` = ' . db_quote(serialize($found)) . ', `date` = ' . db_quote(date('Y-m-d H:m:s')) . ' WHERE `id` = ' . $result['id'];
1957				query($sql);
1958			} else {
1959				$sql = 'INSERT INTO ' . prefix('search_cache') . ' (criteria, data, date) VALUES (' . db_quote($criteria) . ', ' . db_quote(serialize($found)) . ', ' . db_quote(date('Y-m-d H:m:s')) . ')';
1960				query($sql);
1961			}
1962		}
1963	}
1964
1965	/**
1966	 *
1967	 * Fetches a search from the cache if it exists and has not expired
1968	 * @param string $criteria
1969	 */
1970	private function getCachedSearch($criteria) {
1971		if (SEARCH_CACHE_DURATION) {
1972			$sql = 'SELECT `id`, `date`, `data` FROM ' . prefix('search_cache') . ' WHERE `criteria` = ' . db_quote(serialize($criteria));
1973			$result = query_single_row($sql);
1974			if ($result) {
1975				if ((time() - strtotime($result['date'])) > SEARCH_CACHE_DURATION * 60) {
1976					query('DELETE FROM ' . prefix('search_cache') . ' WHERE `id` = ' . $result['id']);
1977				} else {
1978					if ($result = getSerializedArray($result['data'])) {
1979						return $result;
1980					}
1981				}
1982			}
1983		}
1984		return NULL;
1985	}
1986
1987	/**
1988	 * Clears the entire search cache table
1989	 */
1990	static function clearSearchCache() {
1991		$check = query_single_row('SELECT id FROM ' . prefix('search_cache'). ' LIMIT 1');
1992		if($check) {
1993			query('TRUNCATE TABLE ' . prefix('search_cache'));
1994		}
1995	}
1996
1997	/**
1998	 * Returns true if $sortdir is set to "DESC", otherwise false, for use with sorting functions
1999	 * @param string $sortdirection Traditional speaking values "ASC" or "DESC"
2000	 *
2001	 * @since ZenphotoCMS 1.5.8
2002	 *
2003	 * @return boolean
2004	 */
2005	static function getSortdirBool($sortdirection = 'asc') {
2006		$dir = false; // ascending default
2007		if (strtolower($sortdirection) == 'desc') {
2008			$dir = true;
2009		}
2010		return $dir;
2011	}
2012
2013}
2014
2015// search class end
2016
2017/**
2018 *
2019 * encloses search word in quotes if needed
2020 * @param string $word
2021 * @return string
2022 */
2023function search_quote($word) {
2024	if (is_numeric($word) || preg_match("/[ &|!'\"`,()]/", $word)) {
2025		$word = '"' . str_replace("\\'", "'", addslashes($word)) . '"';
2026	}
2027	return $word;
2028}
2029
2030?>