mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 14:27:22 +01:00
495 lines
14 KiB
PHP
495 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Global Search Engine for Moodle
|
|
*
|
|
* @package search
|
|
* @category core
|
|
* @subpackage search_engine
|
|
* @author Michael Champanis (mchampan) [cynnical@gmail.com], Valery Fremaux [valery.fremaux@club-internet.fr] > 1.8
|
|
* @date 2008/03/31
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
|
*/
|
|
|
|
/**
|
|
* includes and requires
|
|
*/
|
|
require_once($CFG->dirroot.'/search/Zend/Search/Lucene.php');
|
|
|
|
define('DEFAULT_POPUP_SETTINGS', "\"menubar=0,location=0,scrollbars,resizable,width=600,height=450\"");
|
|
|
|
/**
|
|
* a class that represents a single result record of the search engine
|
|
*/
|
|
class SearchResult {
|
|
public $url,
|
|
$title,
|
|
$doctype,
|
|
$author,
|
|
$score,
|
|
$number,
|
|
$courseid;
|
|
}
|
|
|
|
|
|
/**
|
|
* split this into Cache class and extend to SearchCache?
|
|
*/
|
|
class SearchCache {
|
|
private $mode,
|
|
$valid;
|
|
|
|
// foresees other caching locations
|
|
public function __construct($mode = 'session') {
|
|
$accepted_modes = array('session');
|
|
|
|
if (in_array($mode, $accepted_modes)) {
|
|
$this->mode = $mode;
|
|
} else {
|
|
$this->mode = 'session';
|
|
} //else
|
|
|
|
$this->valid = true;
|
|
}
|
|
|
|
/**
|
|
* returns the search cache status
|
|
* @return boolean
|
|
*/
|
|
public function can_cache() {
|
|
return $this->valid;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*
|
|
*/
|
|
public function cache($id = false, $object = false) {
|
|
//see if there was a previous query
|
|
$last_term = $this->fetch('search_last_term');
|
|
|
|
//if this query is different from the last, clear out the last one
|
|
if ($id != false && $last_term != $id) {
|
|
$this->clear($last_term);
|
|
}
|
|
|
|
//store the new query if id and object are passed in
|
|
if ($object && $id) {
|
|
$this->store('search_last_term', $id);
|
|
$this->store($id, $object);
|
|
return true;
|
|
//otherwise return the stored results
|
|
} else if ($id && $this->exists($id)) {
|
|
return $this->fetch($id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* do key exist in cache ?
|
|
* @param id the object key
|
|
* @return boolean
|
|
*/
|
|
private function exists($id) {
|
|
switch ($this->mode) {
|
|
case 'session' :
|
|
return isset($_SESSION[$id]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* clears a cached object in cache
|
|
* @param the object key to clear
|
|
* @return void
|
|
*/
|
|
private function clear($id) {
|
|
switch ($this->mode) {
|
|
case 'session' :
|
|
unset($_SESSION[$id]);
|
|
session_unregister($id);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* fetches a cached object
|
|
* @param id the object identifier
|
|
* @return the object cached
|
|
*/
|
|
private function fetch($id) {
|
|
switch ($this->mode) {
|
|
case 'session' :
|
|
return ($this->exists($id)) ? unserialize($_SESSION[$id]) : false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* put an object in cache
|
|
* @param id the key for that object
|
|
* @param object the object to cache as a serialized value
|
|
* @return void
|
|
*/
|
|
private function store($id, $object) {
|
|
switch ($this->mode) {
|
|
case 'session' :
|
|
$_SESSION[$id] = serialize($object);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a single query with results
|
|
*
|
|
*/
|
|
class SearchQuery {
|
|
private $index,
|
|
$term,
|
|
$pagenumber,
|
|
$cache,
|
|
$validquery,
|
|
$validindex,
|
|
$results,
|
|
$results_per_page,
|
|
$total_results;
|
|
|
|
/**
|
|
* constructor records query parameters
|
|
*
|
|
*/
|
|
public function __construct($term = '', $page = 1, $results_per_page = 10, $cache = false) {
|
|
global $CFG;
|
|
|
|
$this->term = $term;
|
|
$this->pagenumber = $page;
|
|
$this->cache = $cache;
|
|
$this->validquery = true;
|
|
$this->validindex = true;
|
|
$this->results_per_page = $results_per_page;
|
|
|
|
$index_path = SEARCH_INDEX_PATH;
|
|
|
|
try {
|
|
$this->index = new Zend_Search_Lucene($index_path, false);
|
|
} catch(Exception $e) {
|
|
$this->validindex = false;
|
|
return;
|
|
}
|
|
|
|
if (empty($this->term)) {
|
|
$this->validquery = false;
|
|
} else {
|
|
$this->set_query($this->term);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* determines state of query object depending on query entry and
|
|
* tries to lauch search if all is OK
|
|
* @return void (this is only a state changing trigger).
|
|
*/
|
|
public function set_query($term = '') {
|
|
if (!empty($term)) {
|
|
$this->term = $term;
|
|
}
|
|
|
|
if (empty($this->term)) {
|
|
$this->validquery = false;
|
|
} else {
|
|
$this->validquery = true;
|
|
}
|
|
|
|
if ($this->validquery and $this->validindex) {
|
|
$this->results = $this->get_results();
|
|
} else {
|
|
$this->results = array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* accessor to the result table.
|
|
* @return an array of result records
|
|
*/
|
|
public function results() {
|
|
return $this->results;
|
|
}
|
|
|
|
/**
|
|
* do the effective collection of results
|
|
* @param boolean $all
|
|
* @uses USER
|
|
*/
|
|
private function process_results($all=false) {
|
|
global $USER;
|
|
|
|
// unneeded since changing the default Zend Lexer
|
|
// $term = mb_convert_case($this->term, MB_CASE_LOWER, 'UTF-8');
|
|
$term = $this->term;
|
|
$page = optional_param('page', 1, PARAM_INT);
|
|
|
|
//experimental - return more results
|
|
// $strip_arr = array('author:', 'title:', '+', '-', 'doctype:');
|
|
// $stripped_term = str_replace($strip_arr, '', $term);
|
|
|
|
// $search_string = $term." title:".$stripped_term." author:".$stripped_term;
|
|
$search_string = $term;
|
|
$hits = $this->index->find($search_string);
|
|
//--
|
|
|
|
$hitcount = count($hits);
|
|
$this->total_results = $hitcount;
|
|
|
|
if ($hitcount == 0) return array();
|
|
|
|
$resultdoc = new SearchResult();
|
|
$resultdocs = array();
|
|
$searchables = search_collect_searchables(false, false);
|
|
|
|
$realindex = 0;
|
|
|
|
/**
|
|
if (!$all) {
|
|
if ($finalresults < $this->results_per_page) {
|
|
$this->pagenumber = 1;
|
|
} elseif ($this->pagenumber > $totalpages) {
|
|
$this->pagenumber = $totalpages;
|
|
}
|
|
|
|
$start = ($this->pagenumber - 1) * $this->results_per_page;
|
|
$end = $start + $this->results_per_page;
|
|
|
|
if ($end > $finalresults) {
|
|
$end = $finalresults;
|
|
}
|
|
} else {
|
|
$start = 0;
|
|
$end = $finalresults;
|
|
} */
|
|
|
|
for ($i = 0; $i < min($hitcount, ($page) * $this->results_per_page); $i++) {
|
|
$hit = $hits[$i];
|
|
|
|
//check permissions on each result
|
|
if ($this->can_display($USER, $hit->docid, $hit->doctype, $hit->course_id, $hit->group_id, $hit->path, $hit->itemtype, $hit->context_id, $searchables )) {
|
|
if ($i >= ($page - 1) * $this->results_per_page){
|
|
$resultdoc->number = $realindex;
|
|
$resultdoc->url = $hit->url;
|
|
$resultdoc->title = $hit->title;
|
|
$resultdoc->score = $hit->score;
|
|
$resultdoc->doctype = $hit->doctype;
|
|
$resultdoc->author = $hit->author;
|
|
$resultdoc->courseid = $hit->course_id;
|
|
$resultdoc->userid = $hit->user_id;
|
|
|
|
//and store it
|
|
$resultdocs[] = clone($resultdoc);
|
|
}
|
|
$realindex++;
|
|
} else {
|
|
// lowers total_results one unit
|
|
$this->total_results--;
|
|
}
|
|
}
|
|
|
|
$totalpages = ceil($this->total_results/$this->results_per_page);
|
|
|
|
|
|
return $resultdocs;
|
|
}
|
|
|
|
/**
|
|
* get results of a search query using a caching strategy if available
|
|
* @return the result documents as an array of search objects
|
|
*/
|
|
private function get_results() {
|
|
$cache = new SearchCache();
|
|
|
|
if ($this->cache && $cache->can_cache()) {
|
|
if (!($resultdocs = $cache->cache($this->term))) {
|
|
$resultdocs = $this->process_results();
|
|
//cache the results so we don't have to compute this on every page-load
|
|
$cache->cache($this->term, $resultdocs);
|
|
//print "Using new results.";
|
|
} else {
|
|
//There was something in the cache, so we're using that to save time
|
|
//print "Using cached results.";
|
|
}
|
|
} else {
|
|
//no caching :(
|
|
// print "Caching disabled!";
|
|
$resultdocs = $this->process_results();
|
|
}
|
|
return $resultdocs;
|
|
}
|
|
|
|
/**
|
|
* constructs the results paging links on results.
|
|
* @return string the results paging links
|
|
*/
|
|
public function page_numbers() {
|
|
$pages = $this->total_pages();
|
|
$query = htmlentities($this->term,ENT_NOQUOTES,'utf-8');
|
|
$page = $this->pagenumber;
|
|
$next = get_string('next', 'search');
|
|
$back = get_string('back', 'search');
|
|
|
|
$ret = "<div align='center' id='search_page_links'>";
|
|
|
|
//Back is disabled if we're on page 1
|
|
if ($page > 1) {
|
|
$ret .= "<a href='query.php?query_string={$query}&page=".($page-1)."'>< {$back}</a> ";
|
|
} else {
|
|
$ret .= "< {$back} ";
|
|
}
|
|
|
|
//don't <a href> the current page
|
|
for ($i = 1; $i <= $pages; $i++) {
|
|
if ($page == $i) {
|
|
$ret .= "($i) ";
|
|
} else {
|
|
$ret .= "<a href='query.php?query_string={$query}&page={$i}'>{$i}</a> ";
|
|
}
|
|
}
|
|
|
|
//Next disabled if we're on the last page
|
|
if ($page < $pages) {
|
|
$ret .= "<a href='query.php?query_string={$query}&page=".($page+1)."'>{$next} ></a> ";
|
|
} else {
|
|
$ret .= "{$next} > ";
|
|
}
|
|
|
|
$ret .= "</div>";
|
|
|
|
//shorten really long page lists, to stop table distorting width-ways
|
|
if (strlen($ret) > 70) {
|
|
$start = 4;
|
|
$end = $page - 5;
|
|
$ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
|
|
|
|
$start = $page + 5;
|
|
$end = $pages - 3;
|
|
$ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* can the user see this result ?
|
|
* @param user a reference upon the user to be checked for access
|
|
* @param this_id the item identifier
|
|
* @param doctype the search document type. MAtches the module or block or
|
|
* extra search source definition
|
|
* @param course_id the course reference of the searched result
|
|
* @param group_id the group identity attached to the found resource
|
|
* @param path the path that routes to the local lib.php of the searched
|
|
* surrounding object fot that document
|
|
* @param item_type a subclassing information for complex module data models
|
|
* @uses CFG
|
|
* // TODO reorder parameters more consistently
|
|
*/
|
|
private function can_display(&$user, $this_id, $doctype, $course_id, $group_id, $path, $item_type, $context_id, &$searchables) {
|
|
global $CFG, $DB;
|
|
|
|
/**
|
|
* course related checks
|
|
*/
|
|
// admins can see everything, anyway.
|
|
if (has_capability('moodle/site:doanything', get_context_instance(CONTEXT_SYSTEM))){
|
|
return true;
|
|
}
|
|
|
|
// first check course compatibility against user : enrolled users to that course can see.
|
|
$myCourses = get_my_courses($user->id);
|
|
$unenroled = !in_array($course_id, array_keys($myCourses));
|
|
|
|
// if guests are allowed, logged guest can see
|
|
$isallowedguest = (isguestuser()) ? $DB->get_field('course', 'guest', array('id' => $course_id)) : false ;
|
|
|
|
if ($unenroled && !$isallowedguest){
|
|
return false;
|
|
}
|
|
|
|
// if user is enrolled or is allowed user and course is hidden, can he see it ?
|
|
$visibility = $DB->get_field('course', 'visible', array('id' => $course_id));
|
|
if ($visibility <= 0){
|
|
if (!has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course_id))){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* prerecorded capabilities
|
|
*/
|
|
// get context caching information and tries to discard unwanted records here
|
|
|
|
|
|
/**
|
|
* final checks
|
|
*/
|
|
// then give back indexing data to the module for local check
|
|
$searchable_instance = $searchables[$doctype];
|
|
if ($searchable_instance->location == 'internal'){
|
|
include_once "{$CFG->dirroot}/search/documents/{$doctype}_document.php";
|
|
} else {
|
|
include_once "{$CFG->dirroot}/{$searchable_instance->location}/{$doctype}/search_document.php";
|
|
}
|
|
$access_check_function = "{$doctype}_check_text_access";
|
|
|
|
if (function_exists($access_check_function)){
|
|
$modulecheck = $access_check_function($path, $item_type, $this_id, $user, $group_id, $context_id);
|
|
// echo "module said $modulecheck for item $doctype/$item_type/$this_id";
|
|
return($modulecheck);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function count() {
|
|
return $this->total_results;
|
|
} //count
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function is_valid() {
|
|
return ($this->validquery and $this->validindex);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function is_valid_query() {
|
|
return $this->validquery;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function is_valid_index() {
|
|
return $this->validindex;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function total_pages() {
|
|
return ceil($this->count()/$this->results_per_page);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function get_pagenumber() {
|
|
return $this->pagenumber;
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
public function get_results_per_page() {
|
|
return $this->results_per_page;
|
|
}
|
|
}
|
|
?>
|