Bumped the version up to 1.9.3
[moodle.git] / search / querylib.php
blobfdbbf4281e41435cda8fdd8b9370a3781373a9f7
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;
33 /**
34 * split this into Cache class and extend to SearchCache?
36 class SearchCache {
37 private $mode,
38 $valid;
40 // foresees other caching locations
41 public function __construct($mode = 'session') {
42 $accepted_modes = array('session');
44 if (in_array($mode, $accepted_modes)) {
45 $this->mode = $mode;
46 } else {
47 $this->mode = 'session';
48 } //else
50 $this->valid = true;
53 /**
54 * returns the search cache status
55 * @return boolean
57 public function can_cache() {
58 return $this->valid;
61 /**
65 public function cache($id = false, $object = false) {
66 //see if there was a previous query
67 $last_term = $this->fetch('search_last_term');
69 //if this query is different from the last, clear out the last one
70 if ($id != false and $last_term != $id) {
71 $this->clear($last_term);
74 //store the new query if id and object are passed in
75 if ($object and $id) {
76 $this->store('search_last_term', $id);
77 $this->store($id, $object);
78 return true;
79 //otherwise return the stored results
80 } else if ($id and $this->exists($id)) {
81 return $this->fetch($id);
85 /**
86 * do key exist in cache ?
87 * @param id the object key
88 * @return boolean
90 private function exists($id) {
91 switch ($this->mode) {
92 case 'session' :
93 return isset($_SESSION[$id]);
97 /**
98 * clears a cached object in cache
99 * @param the object key to clear
100 * @return void
102 private function clear($id) {
103 switch ($this->mode) {
104 case 'session' :
105 unset($_SESSION[$id]);
106 session_unregister($id);
107 return;
112 * fetches a cached object
113 * @param id the object identifier
114 * @return the object cached
116 private function fetch($id) {
117 switch ($this->mode) {
118 case 'session' :
119 return ($this->exists($id)) ? unserialize($_SESSION[$id]) : false;
124 * put an object in cache
125 * @param id the key for that object
126 * @param object the object to cache as a serialized value
127 * @return void
129 private function store($id, $object) {
130 switch ($this->mode) {
131 case 'session' :
132 $_SESSION[$id] = serialize($object);
133 return;
139 * Represents a single query with results
142 class SearchQuery {
143 private $index,
144 $term,
145 $pagenumber,
146 $cache,
147 $validquery,
148 $validindex,
149 $results,
150 $results_per_page,
151 $total_results;
154 * constructor records query parameters
157 public function __construct($term = '', $page = 1, $results_per_page = 10, $cache = false) {
158 global $CFG;
160 $this->term = $term;
161 $this->pagenumber = $page;
162 $this->cache = $cache;
163 $this->validquery = true;
164 $this->validindex = true;
165 $this->results_per_page = $results_per_page;
167 $index_path = SEARCH_INDEX_PATH;
169 try {
170 $this->index = new Zend_Search_Lucene($index_path, false);
171 } catch(Exception $e) {
172 $this->validindex = false;
173 return;
176 if (empty($this->term)) {
177 $this->validquery = false;
178 } else {
179 $this->set_query($this->term);
184 * determines state of query object depending on query entry and
185 * tries to lauch search if all is OK
186 * @return void (this is only a state changing trigger).
188 public function set_query($term = '') {
189 if (!empty($term)) {
190 $this->term = $term;
193 if (empty($this->term)) {
194 $this->validquery = false;
195 } else {
196 $this->validquery = true;
199 if ($this->validquery and $this->validindex) {
200 $this->results = $this->get_results();
201 } else {
202 $this->results = array();
207 * accessor to the result table.
208 * @return an array of result records
210 public function results() {
211 return $this->results;
215 * do the effective collection of results
216 * @param boolean $all
217 * @uses USER
219 private function process_results($all=false) {
220 global $USER;
222 $term = mb_convert_case($this->term, MB_CASE_LOWER, 'UTF-8');
224 //experimental - return more results
225 $strip_arr = array('author:', 'title:', '+', '-', 'doctype:');
226 $stripped_term = str_replace($strip_arr, '', $term);
228 $hits = $this->index->find($term." title:".$stripped_term." author:".$stripped_term);
229 //--
231 $hitcount = count($hits);
232 $this->total_results = $hitcount;
234 if ($hitcount == 0) return array();
236 $totalpages = ceil($hitcount/$this->results_per_page);
238 if (!$all) {
239 if ($hitcount < $this->results_per_page) {
240 $this->pagenumber = 1;
241 } else if ($this->pagenumber > $totalpages) {
242 $this->pagenumber = $totalpages;
245 $start = ($this->pagenumber - 1) * $this->results_per_page;
246 $end = $start + $this->results_per_page;
248 if ($end > $hitcount) {
249 $end = $hitcount;
251 } else {
252 $start = 0;
253 $end = $hitcount;
256 $resultdoc = new SearchResult();
257 $resultdocs = array();
259 for ($i = $start; $i < $end; $i++) {
260 $hit = $hits[$i];
262 //check permissions on each result
263 if ($this->can_display($USER, $hit->docid, $hit->doctype, $hit->course_id, $hit->group_id, $hit->path, $hit->itemtype, $hit->context_id )) {
264 $resultdoc->number = $i;
265 $resultdoc->url = $hit->url;
266 $resultdoc->title = $hit->title;
267 $resultdoc->score = $hit->score;
268 $resultdoc->doctype = $hit->doctype;
269 $resultdoc->author = $hit->author;
271 //and store it
272 $resultdocs[] = clone($resultdoc);
273 } else {
274 // lowers total_results one unit
275 $this->total_results--;
279 return $resultdocs;
283 * get results of a search query using a caching strategy if available
284 * @return the result documents as an array of search objects
286 private function get_results() {
287 $cache = new SearchCache();
289 if ($this->cache and $cache->can_cache()) {
290 if (!($resultdocs = $cache->cache($this->term))) {
291 $resultdocs = $this->process_results();
292 //cache the results so we don't have to compute this on every page-load
293 $cache->cache($this->term, $resultdocs);
294 //print "Using new results.";
295 } else {
296 //There was something in the cache, so we're using that to save time
297 //print "Using cached results.";
299 } else {
300 //no caching :(
301 //print "Caching disabled!";
302 $resultdocs = $this->process_results();
304 return $resultdocs;
308 * constructs the results paging links on results.
309 * @return string the results paging links
311 public function page_numbers() {
312 $pages = $this->total_pages();
313 $query = htmlentities($this->term);
314 $page = $this->pagenumber;
315 $next = get_string('next', 'search');
316 $back = get_string('back', 'search');
318 $ret = "<div align='center' id='search_page_links'>";
320 //Back is disabled if we're on page 1
321 if ($page > 1) {
322 $ret .= "<a href='query.php?query_string={$query}&page=".($page-1)."'>&lt; {$back}</a>&nbsp;";
323 } else {
324 $ret .= "&lt; {$back}&nbsp;";
327 //don't <a href> the current page
328 for ($i = 1; $i <= $pages; $i++) {
329 if ($page == $i) {
330 $ret .= "($i)&nbsp;";
331 } else {
332 $ret .= "<a href='query.php?query_string={$query}&page={$i}'>{$i}</a>&nbsp;";
336 //Next disabled if we're on the last page
337 if ($page < $pages) {
338 $ret .= "<a href='query.php?query_string={$query}&page=".($page+1)."'>{$next} &gt;</a>&nbsp;";
339 } else {
340 $ret .= "{$next} &gt;&nbsp;";
343 $ret .= "</div>";
345 //shorten really long page lists, to stop table distorting width-ways
346 if (strlen($ret) > 70) {
347 $start = 4;
348 $end = $page - 5;
349 $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
351 $start = $page + 5;
352 $end = $pages - 3;
353 $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
356 return $ret;
360 * can the user see this result ?
361 * @param user a reference upon the user to be checked for access
362 * @param this_id the item identifier
363 * @param doctype the search document type. MAtches the module or block or
364 * extra search source definition
365 * @param course_id the course reference of the searched result
366 * @param group_id the group identity attached to the found resource
367 * @param path the path that routes to the local lib.php of the searched
368 * surrounding object fot that document
369 * @param item_type a subclassing information for complex module data models
370 * @uses CFG
371 * // TODO reorder parameters more consistently
373 private function can_display(&$user, $this_id, $doctype, $course_id, $group_id, $path, $item_type, $context_id) {
374 global $CFG;
377 * course related checks
379 // admins can see everything, anyway.
380 if (isadmin()){
381 return true;
384 // first check course compatibility against user : enrolled users to that course can see.
385 $myCourses = get_my_courses($user->id);
386 $unenroled = !in_array($course_id, array_keys($myCourses));
388 // if guests are allowed, logged guest can see
389 $isallowedguest = (isguest()) ? get_field('course', 'guest', 'id', $course_id) : false ;
391 if ($unenroled && !$isallowedguest){
392 return false;
395 // if user is enrolled or is allowed user and course is hidden, can he see it ?
396 $visibility = get_field('course', 'visible', 'id', $course_id);
397 if ($visibility <= 0){
398 if (!has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course_id))){
399 return false;
404 * prerecorded capabilities
406 // get context caching information and tries to discard unwanted records here
410 * final checks
412 // then give back indexing data to the module for local check
413 include_once "{$CFG->dirroot}/search/documents/{$doctype}_document.php";
414 $access_check_function = "{$doctype}_check_text_access";
416 if (function_exists($access_check_function)){
417 $modulecheck = $access_check_function($path, $item_type, $this_id, $user, $group_id, $context_id);
418 // echo "module said $modulecheck for item $doctype/$item_type/$this_id";
419 return($modulecheck);
422 return true;
428 public function count() {
429 return $this->total_results;
430 } //count
435 public function is_valid() {
436 return ($this->validquery and $this->validindex);
442 public function is_valid_query() {
443 return $this->validquery;
449 public function is_valid_index() {
450 return $this->validindex;
456 public function total_pages() {
457 return ceil($this->count()/$this->results_per_page);
463 public function get_pagenumber() {
464 return $this->pagenumber;
470 public function get_results_per_page() {
471 return $this->results_per_page;