Updated the 19 build version to 20100224
[moodle.git] / search / querylib.php
blobb7b9714216730a34f2b621750b5498924f9cb822
1 <?php
2 /**
3 * Global Search Engine for Moodle
5 * @package search
6 * @category core
7 * @subpackage search_engine
8 * @author Michael Champanis (mchampan) [cynnical@gmail.com], Valery Fremaux [valery.fremaux@club-internet.fr] > 1.8
9 * @date 2008/03/31
10 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
13 /**
14 * includes and requires
16 require_once("{$CFG->dirroot}/search/Zend/Search/Lucene.php");
18 define('DEFAULT_POPUP_SETTINGS', "\"menubar=0,location=0,scrollbars,resizable,width=600,height=450\"");
20 /**
21 * a class that represents a single result record of the search engine
22 */
23 class SearchResult {
24 public $url,
25 $title,
26 $doctype,
27 $author,
28 $score,
29 $number,
30 $courseid;
34 /**
35 * split this into Cache class and extend to SearchCache?
37 class SearchCache {
38 private $mode,
39 $valid;
41 // foresees other caching locations
42 public function __construct($mode = 'session') {
43 $accepted_modes = array('session');
45 if (in_array($mode, $accepted_modes)) {
46 $this->mode = $mode;
47 } else {
48 $this->mode = 'session';
49 } //else
51 $this->valid = true;
54 /**
55 * returns the search cache status
56 * @return boolean
58 public function can_cache() {
59 return $this->valid;
62 /**
66 public function cache($id = false, $object = false) {
67 //see if there was a previous query
68 $last_term = $this->fetch('search_last_term');
70 //if this query is different from the last, clear out the last one
71 if ($id != false && $last_term != $id) {
72 $this->clear($last_term);
75 //store the new query if id and object are passed in
76 if ($object && $id) {
77 $this->store('search_last_term', $id);
78 $this->store($id, $object);
79 return true;
80 //otherwise return the stored results
81 } else if ($id && $this->exists($id)) {
82 return $this->fetch($id);
86 /**
87 * do key exist in cache ?
88 * @param id the object key
89 * @return boolean
91 private function exists($id) {
92 switch ($this->mode) {
93 case 'session' :
94 return isset($_SESSION[$id]);
98 /**
99 * clears a cached object in cache
100 * @param the object key to clear
101 * @return void
103 private function clear($id) {
104 switch ($this->mode) {
105 case 'session' :
106 unset($_SESSION[$id]);
107 session_unregister($id);
108 return;
113 * fetches a cached object
114 * @param id the object identifier
115 * @return the object cached
117 private function fetch($id) {
118 switch ($this->mode) {
119 case 'session' :
120 return ($this->exists($id)) ? unserialize($_SESSION[$id]) : false;
125 * put an object in cache
126 * @param id the key for that object
127 * @param object the object to cache as a serialized value
128 * @return void
130 private function store($id, $object) {
131 switch ($this->mode) {
132 case 'session' :
133 $_SESSION[$id] = serialize($object);
134 return;
140 * Represents a single query with results
143 class SearchQuery {
144 private $index,
145 $term,
146 $pagenumber,
147 $cache,
148 $validquery,
149 $validindex,
150 $results,
151 $results_per_page,
152 $total_results;
155 * constructor records query parameters
158 public function __construct($term = '', $page = 1, $results_per_page = 10, $cache = false) {
159 global $CFG;
161 $this->term = $term;
162 $this->pagenumber = $page;
163 $this->cache = $cache;
164 $this->validquery = true;
165 $this->validindex = true;
166 $this->results_per_page = $results_per_page;
168 $index_path = SEARCH_INDEX_PATH;
170 try {
171 $this->index = new Zend_Search_Lucene($index_path, false);
172 } catch(Exception $e) {
173 $this->validindex = false;
174 return;
177 if (empty($this->term)) {
178 $this->validquery = false;
179 } else {
180 $this->set_query($this->term);
185 * determines state of query object depending on query entry and
186 * tries to lauch search if all is OK
187 * @return void (this is only a state changing trigger).
189 public function set_query($term = '') {
190 if (!empty($term)) {
191 $this->term = $term;
194 if (empty($this->term)) {
195 $this->validquery = false;
196 } else {
197 $this->validquery = true;
200 if ($this->validquery and $this->validindex) {
201 $this->results = $this->get_results();
202 } else {
203 $this->results = array();
208 * accessor to the result table.
209 * @return an array of result records
211 public function results() {
212 return $this->results;
216 * do the effective collection of results
217 * @param boolean $all
218 * @uses USER
220 private function process_results($all=false) {
221 global $USER;
223 // $term = mb_convert_case($this->term, MB_CASE_LOWER, 'UTF-8');
224 $term = $this->term;
225 $page = optional_param('page', 1, PARAM_INT);
227 //experimental - return more results
228 // $strip_arr = array('author:', 'title:', '+', '-', 'doctype:');
229 // $stripped_term = str_replace($strip_arr, '', $term);
231 // $search_string = $term." title:".$stripped_term." author:".$stripped_term;
232 $search_string = $term;
233 $hits = $this->index->find($search_string);
234 //--
236 $hitcount = count($hits);
237 $this->total_results = $hitcount;
239 if ($hitcount == 0) return array();
241 $resultdoc = new SearchResult();
242 $resultdocs = array();
243 $searchables = search_collect_searchables(false, false);
245 $realindex = 0;
248 if (!$all) {
249 if ($finalresults < $this->results_per_page) {
250 $this->pagenumber = 1;
251 } elseif ($this->pagenumber > $totalpages) {
252 $this->pagenumber = $totalpages;
255 $start = ($this->pagenumber - 1) * $this->results_per_page;
256 $end = $start + $this->results_per_page;
258 if ($end > $finalresults) {
259 $end = $finalresults;
261 } else {
262 $start = 0;
263 $end = $finalresults;
264 } */
266 for ($i = 0; $i < min($hitcount, ($page) * $this->results_per_page); $i++) {
267 $hit = $hits[$i];
269 //check permissions on each result
270 if ($this->can_display($USER, $hit->docid, $hit->doctype, $hit->course_id, $hit->group_id, $hit->path, $hit->itemtype, $hit->context_id, $searchables )) {
271 if ($i >= ($page - 1) * $this->results_per_page){
272 $resultdoc->number = $realindex;
273 $resultdoc->url = $hit->url;
274 $resultdoc->title = $hit->title;
275 $resultdoc->score = $hit->score;
276 $resultdoc->doctype = $hit->doctype;
277 $resultdoc->author = $hit->author;
278 $resultdoc->courseid = $hit->course_id;
279 $resultdoc->userid = $hit->user_id;
281 //and store it
282 $resultdocs[] = clone($resultdoc);
284 $realindex++;
285 } else {
286 // lowers total_results one unit
287 $this->total_results--;
291 $totalpages = ceil($this->total_results/$this->results_per_page);
294 return $resultdocs;
298 * get results of a search query using a caching strategy if available
299 * @return the result documents as an array of search objects
301 private function get_results() {
302 $cache = new SearchCache();
304 if ($this->cache && $cache->can_cache()) {
305 if (!($resultdocs = $cache->cache($this->term))) {
306 $resultdocs = $this->process_results();
307 //cache the results so we don't have to compute this on every page-load
308 $cache->cache($this->term, $resultdocs);
309 //print "Using new results.";
310 } else {
311 //There was something in the cache, so we're using that to save time
312 //print "Using cached results.";
314 } else {
315 //no caching :(
316 // print "Caching disabled!";
317 $resultdocs = $this->process_results();
319 return $resultdocs;
323 * constructs the results paging links on results.
324 * @return string the results paging links
326 public function page_numbers() {
327 $pages = $this->total_pages();
328 // $query = htmlentities($this->term);
329 // http://moodle.org/mod/forum/discuss.php?d=115788
330 $query = htmlentities($this->term,ENT_NOQUOTES,'utf-8');
331 $page = $this->pagenumber;
332 $next = get_string('next', 'search');
333 $back = get_string('back', 'search');
335 $ret = "<div align='center' id='search_page_links'>";
337 //Back is disabled if we're on page 1
338 if ($page > 1) {
339 $ret .= "<a href='query.php?query_string={$query}&page=".($page-1)."'>&lt; {$back}</a>&nbsp;";
340 } else {
341 $ret .= "&lt; {$back}&nbsp;";
344 //don't <a href> the current page
345 for ($i = 1; $i <= $pages; $i++) {
346 if ($page == $i) {
347 $ret .= "($i)&nbsp;";
348 } else {
349 $ret .= "<a href='query.php?query_string={$query}&page={$i}'>{$i}</a>&nbsp;";
353 //Next disabled if we're on the last page
354 if ($page < $pages) {
355 $ret .= "<a href='query.php?query_string={$query}&page=".($page+1)."'>{$next} &gt;</a>&nbsp;";
356 } else {
357 $ret .= "{$next} &gt;&nbsp;";
360 $ret .= "</div>";
362 //shorten really long page lists, to stop table distorting width-ways
363 if (strlen($ret) > 70) {
364 $start = 4;
365 $end = $page - 5;
366 $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
368 $start = $page + 5;
369 $end = $pages - 3;
370 $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
373 return $ret;
377 * can the user see this result ?
378 * @param user a reference upon the user to be checked for access
379 * @param this_id the item identifier
380 * @param doctype the search document type. MAtches the module or block or
381 * extra search source definition
382 * @param course_id the course reference of the searched result
383 * @param group_id the group identity attached to the found resource
384 * @param path the path that routes to the local lib.php of the searched
385 * surrounding object fot that document
386 * @param item_type a subclassing information for complex module data models
387 * @uses CFG
388 * // TODO reorder parameters more consistently
390 private function can_display(&$user, $this_id, $doctype, $course_id, $group_id, $path, $item_type, $context_id, &$searchables) {
391 global $CFG;
394 * course related checks
396 // admins can see everything, anyway.
397 if (has_capability('moodle/site:doanything', get_context_instance(CONTEXT_SYSTEM))){
398 return true;
401 // first check course compatibility against user : enrolled users to that course can see.
402 $myCourses = get_my_courses($user->id);
403 $unenroled = !in_array($course_id, array_keys($myCourses));
405 // if guests are allowed, logged guest can see
406 $isallowedguest = (isguest()) ? get_field('course', 'guest', 'id', $course_id) : false ;
408 if ($unenroled && !$isallowedguest){
409 return false;
412 // if user is enrolled or is allowed user and course is hidden, can he see it ?
413 $visibility = get_field('course', 'visible', 'id', $course_id);
414 if ($visibility <= 0){
415 if (!has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course_id))){
416 return false;
421 * prerecorded capabilities
423 // get context caching information and tries to discard unwanted records here
427 * final checks
429 // then give back indexing data to the module for local check
430 $searchable_instance = $searchables[$doctype];
431 if ($searchable_instance->location == 'internal'){
432 include_once "{$CFG->dirroot}/search/documents/{$doctype}_document.php";
433 } else {
434 include_once "{$CFG->dirroot}/{$searchable_instance->location}/$doctype/search_document.php";
436 $access_check_function = "{$doctype}_check_text_access";
438 if (function_exists($access_check_function)){
439 $modulecheck = $access_check_function($path, $item_type, $this_id, $user, $group_id, $context_id);
440 // echo "module said $modulecheck for item $doctype/$item_type/$this_id";
441 return($modulecheck);
444 return true;
450 public function count() {
451 return $this->total_results;
452 } //count
457 public function is_valid() {
458 return ($this->validquery and $this->validindex);
464 public function is_valid_query() {
465 return $this->validquery;
471 public function is_valid_index() {
472 return $this->validindex;
478 public function total_pages() {
479 return ceil($this->count()/$this->results_per_page);
485 public function get_pagenumber() {
486 return $this->pagenumber;
492 public function get_results_per_page() {
493 return $this->results_per_page;
499 public function __destruct(){
500 unset($this->index);