MDL-21782 guest filed is not in course table any more
[moodle.git] / search / querylib.php
1 <?php
2 /**
3 * Global Search Engine for Moodle
4 *
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
11 */
13 /**
14 * includes and requires
15 */
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;
31 }
34 /**
35 * split this into Cache class and extend to SearchCache?
36 */
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;
52     }
54     /**
55     * returns the search cache status
56     * @return boolean
57     */
58     public function can_cache() {
59         return $this->valid;
60     }
62     /**
63     *
64     *
65     */
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);
73         }
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);
83         }
84     }
86     /**
87     * do key exist in cache ?
88     * @param id the object key
89     * @return boolean
90     */
91     private function exists($id) {
92         switch ($this->mode) {
93             case 'session' :
94             return isset($_SESSION[$id]);
95         }
96     }
98     /**
99     * clears a cached object in cache
100     * @param the object key to clear
101     * @return void
102     */
103     private function clear($id) {
104         switch ($this->mode) {
105             case 'session' :
106                 unset($_SESSION[$id]);
107                 session_unregister($id);
108             return;
109         }
110     }
112     /**
113     * fetches a cached object
114     * @param id the object identifier
115     * @return the object cached
116     */
117     private function fetch($id) {
118         switch ($this->mode) {
119             case 'session' :
120                 return ($this->exists($id)) ? unserialize($_SESSION[$id]) : false;
121         }
122     }
124     /**
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
129     */
130     private function store($id, $object) {
131         switch ($this->mode) {
132             case 'session' :
133                 $_SESSION[$id] = serialize($object);
134             return;
135         }
136     }
139 /**
140 * Represents a single query with results
142 */
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;
154     /**
155     * constructor records query parameters
156     *
157     */
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;
175         }
177         if (empty($this->term)) {
178             $this->validquery = false;
179         } else {
180             $this->set_query($this->term);
181         }
182     }
184     /**
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).
188     */
189     public function set_query($term = '') {
190         if (!empty($term)) {
191             $this->term = $term;
192         }
194         if (empty($this->term)) {
195             $this->validquery = false;
196         } else {
197             $this->validquery = true;
198         }
200         if ($this->validquery and $this->validindex) {
201             $this->results = $this->get_results();
202         } else {
203             $this->results = array();
204         }
205     }
207     /**
208     * accessor to the result table.
209     * @return an array of result records
210     */
211     public function results() {
212         return $this->results;
213     }
215     /**
216     * do the effective collection of results
217     * @param boolean $all
218     * @uses USER
219     */
220     private function process_results($all=false) {
221         global $USER;
223         // unneeded since changing the default Zend Lexer
224         // $term = mb_convert_case($this->term, MB_CASE_LOWER, 'UTF-8');
225         $term = $this->term;
226         $page = optional_param('page', 1, PARAM_INT);
228         //experimental - return more results
229         // $strip_arr = array('author:', 'title:', '+', '-', 'doctype:');
230         // $stripped_term = str_replace($strip_arr, '', $term);
232         // $search_string = $term." title:".$stripped_term." author:".$stripped_term;
233         $search_string = $term;
234         $hits = $this->index->find($search_string);
235         //--
237         $hitcount = count($hits);
238         $this->total_results = $hitcount;
240         if ($hitcount == 0) return array();
242         $resultdoc  = new SearchResult();
243         $resultdocs = array();
244         $searchables = search_collect_searchables(false, false);
246         $realindex = 0;
248         /**
249         if (!$all) {
250             if ($finalresults < $this->results_per_page) {
251                 $this->pagenumber = 1;
252             } elseif ($this->pagenumber > $totalpages) {
253                 $this->pagenumber = $totalpages;
254             }
256             $start = ($this->pagenumber - 1) * $this->results_per_page;
257             $end = $start + $this->results_per_page;
259             if ($end > $finalresults) {
260                 $end = $finalresults;
261             }
262         } else {
263             $start = 0;
264             $end = $finalresults;
265         } */
267         for ($i = 0; $i < min($hitcount, ($page) * $this->results_per_page); $i++) {
268             $hit = $hits[$i];
270             //check permissions on each result
271             if ($this->can_display($USER, $hit->docid, $hit->doctype, $hit->course_id, $hit->group_id, $hit->path, $hit->itemtype, $hit->context_id, $searchables )) {
272                 if ($i >= ($page - 1) * $this->results_per_page){
273                     $resultdoc->number  = $realindex;
274                     $resultdoc->url     = $hit->url;
275                     $resultdoc->title   = $hit->title;
276                     $resultdoc->score   = $hit->score;
277                     $resultdoc->doctype = $hit->doctype;
278                     $resultdoc->author  = $hit->author;
279                     $resultdoc->courseid = $hit->course_id;
280                     $resultdoc->userid = $hit->user_id;
282                     //and store it
283                     $resultdocs[] = clone($resultdoc);
284                 }
285                 $realindex++;
286             } else {
287                // lowers total_results one unit
288                $this->total_results--;
289             }
290         }
292         $totalpages = ceil($this->total_results/$this->results_per_page);
295         return $resultdocs;
296     }
298     /**
299     * get results of a search query using a caching strategy if available
300     * @return the result documents as an array of search objects
301     */
302     private function get_results() {
303         $cache = new SearchCache();
305         if ($this->cache && $cache->can_cache()) {
306             if (!($resultdocs = $cache->cache($this->term))) {
307                 $resultdocs = $this->process_results();
308                 //cache the results so we don't have to compute this on every page-load
309                 $cache->cache($this->term, $resultdocs);
310                 //print "Using new results.";
311             } else {
312             //There was something in the cache, so we're using that to save time
313             //print "Using cached results.";
314             }
315         } else {
316             //no caching :(
317             // print "Caching disabled!";
318             $resultdocs = $this->process_results();
319         }
320         return $resultdocs;
321     }
323     /**
324     * constructs the results paging links on results.
325     * @return string the results paging links
326     */
327     public function page_numbers() {
328       $pages  = $this->total_pages();
329       $query = htmlentities($this->term,ENT_NOQUOTES,'utf-8');
330       $page   = $this->pagenumber;
331       $next   = get_string('next', 'search');
332       $back   = get_string('back', 'search');
334       $ret = "<div align='center' id='search_page_links'>";
336       //Back is disabled if we're on page 1
337       if ($page > 1) {
338         $ret .= "<a href='query.php?query_string={$query}&page=".($page-1)."'>&lt; {$back}</a>&nbsp;";
339       } else {
340         $ret .= "&lt; {$back}&nbsp;";
341       }
343       //don't <a href> the current page
344       for ($i = 1; $i <= $pages; $i++) {
345         if ($page == $i) {
346           $ret .= "($i)&nbsp;";
347         } else {
348           $ret .= "<a href='query.php?query_string={$query}&page={$i}'>{$i}</a>&nbsp;";
349         }
350       }
352       //Next disabled if we're on the last page
353       if ($page < $pages) {
354         $ret .= "<a href='query.php?query_string={$query}&page=".($page+1)."'>{$next} &gt;</a>&nbsp;";
355       } else {
356         $ret .= "{$next} &gt;&nbsp;";
357       }
359       $ret .= "</div>";
361       //shorten really long page lists, to stop table distorting width-ways
362       if (strlen($ret) > 70) {
363         $start = 4;
364         $end = $page - 5;
365         $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
367         $start = $page + 5;
368         $end = $pages - 3;
369         $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
370       }
372       return $ret;
373     }
375     /**
376     * can the user see this result ?
377     * @param user a reference upon the user to be checked for access
378     * @param this_id the item identifier
379     * @param doctype the search document type. MAtches the module or block or
380     * extra search source definition
381     * @param course_id the course reference of the searched result
382     * @param group_id the group identity attached to the found resource
383     * @param path the path that routes to the local lib.php of the searched
384     * surrounding object fot that document
385     * @param item_type a subclassing information for complex module data models
386     * @uses CFG
387     * // TODO reorder parameters more consistently
388     */
389     private function can_display(&$user, $this_id, $doctype, $course_id, $group_id, $path, $item_type, $context_id, &$searchables) {
390         global $CFG, $DB;
392       /**
393       * course related checks
394       */
395       // admins can see everything, anyway.
396       if (has_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM))){
397         return true;
398       }
400         // first check course compatibility against user : enrolled users to that course can see.
401         $myCourses = enrol_get_users_courses($user->id, true);
402         $unenroled = !in_array($course_id, array_keys($myCourses));
404         // if guests are allowed, logged guest can see
405         $isallowedguest = false; //TODO: this will be harder to do now because we do not have guest field in course table any more
407         if ($unenroled && !$isallowedguest){
408             return false;
409         }
411         // if user is enrolled or is allowed user and course is hidden, can he see it ?
412         $visibility = $DB->get_field('course', 'visible', array('id' => $course_id));
413         if ($visibility <= 0){
414             if (!has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course_id))){
415                 return false;
416             }
417         }
419         /**
420         * prerecorded capabilities
421         */
422         // get context caching information and tries to discard unwanted records here
425         /**
426         * final checks
427         */
428         // then give back indexing data to the module for local check
429         $searchable_instance = $searchables[$doctype];
430         if ($searchable_instance->location == 'internal'){
431             include_once "{$CFG->dirroot}/search/documents/{$doctype}_document.php";
432         } else {
433             include_once "{$CFG->dirroot}/{$searchable_instance->location}/{$doctype}/search_document.php";
434         }
435         $access_check_function = "{$doctype}_check_text_access";
437         if (function_exists($access_check_function)){
438             $modulecheck = $access_check_function($path, $item_type, $this_id, $user, $group_id, $context_id);
439             // echo "module said $modulecheck for item $doctype/$item_type/$this_id";
440             return($modulecheck);
441         }
443         return true;
444     }
446     /**
447     *
448     */
449     public function count() {
450       return $this->total_results;
451     } //count
453     /**
454     *
455     */
456     public function is_valid() {
457       return ($this->validquery and $this->validindex);
458     }
460     /**
461     *
462     */
463     public function is_valid_query() {
464       return $this->validquery;
465     }
467     /**
468     *
469     */
470     public function is_valid_index() {
471       return $this->validindex;
472     }
474     /**
475     *
476     */
477     public function total_pages() {
478       return ceil($this->count()/$this->results_per_page);
479     }
481     /**
482     *
483     */
484     public function get_pagenumber() {
485       return $this->pagenumber;
486     }
488     /**
489     *
490     */
491     public function get_results_per_page() {
492       return $this->results_per_page;
493     }
495 ?>