PageRenderTime 49ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/search/querylib.php

https://github.com/cwaclawik/moodle
PHP | 495 lines | 252 code | 70 blank | 173 comment | 51 complexity | 4f2bfa11013460b5da41f9571a3dae3a MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause, LGPL-2.1
  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. * @version prepared for 2.0
  11. * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  12. */
  13. /**
  14. * includes and requires
  15. */
  16. require_once($CFG->dirroot.'/search/Zend/Search/Lucene.php');
  17. define('DEFAULT_POPUP_SETTINGS', "\"menubar=0,location=0,scrollbars,resizable,width=600,height=450\"");
  18. /**
  19. * a class that represents a single result record of the search engine
  20. */
  21. class SearchResult {
  22. public $url,
  23. $title,
  24. $doctype,
  25. $author,
  26. $score,
  27. $number,
  28. $courseid;
  29. }
  30. /**
  31. * split this into Cache class and extend to SearchCache?
  32. */
  33. class SearchCache {
  34. private $mode,
  35. $valid;
  36. // foresees other caching locations
  37. public function __construct($mode = 'session') {
  38. $accepted_modes = array('session');
  39. if (in_array($mode, $accepted_modes)) {
  40. $this->mode = $mode;
  41. } else {
  42. $this->mode = 'session';
  43. } //else
  44. $this->valid = true;
  45. }
  46. /**
  47. * returns the search cache status
  48. * @return boolean
  49. */
  50. public function can_cache() {
  51. return $this->valid;
  52. }
  53. /**
  54. *
  55. *
  56. */
  57. public function cache($id = false, $object = false) {
  58. //see if there was a previous query
  59. $last_term = $this->fetch('search_last_term');
  60. //if this query is different from the last, clear out the last one
  61. if ($id != false && $last_term != $id) {
  62. $this->clear($last_term);
  63. }
  64. //store the new query if id and object are passed in
  65. if ($object && $id) {
  66. $this->store('search_last_term', $id);
  67. $this->store($id, $object);
  68. return true;
  69. //otherwise return the stored results
  70. } else if ($id && $this->exists($id)) {
  71. return $this->fetch($id);
  72. }
  73. }
  74. /**
  75. * do key exist in cache ?
  76. * @param id the object key
  77. * @return boolean
  78. */
  79. private function exists($id) {
  80. switch ($this->mode) {
  81. case 'session' :
  82. return isset($_SESSION[$id]);
  83. }
  84. }
  85. /**
  86. * clears a cached object in cache
  87. * @param the object key to clear
  88. * @return void
  89. */
  90. private function clear($id) {
  91. switch ($this->mode) {
  92. case 'session' :
  93. unset($_SESSION[$id]);
  94. session_unregister($id);
  95. return;
  96. }
  97. }
  98. /**
  99. * fetches a cached object
  100. * @param id the object identifier
  101. * @return the object cached
  102. */
  103. private function fetch($id) {
  104. switch ($this->mode) {
  105. case 'session' :
  106. return ($this->exists($id)) ? unserialize($_SESSION[$id]) : false;
  107. }
  108. }
  109. /**
  110. * put an object in cache
  111. * @param id the key for that object
  112. * @param object the object to cache as a serialized value
  113. * @return void
  114. */
  115. private function store($id, $object) {
  116. switch ($this->mode) {
  117. case 'session' :
  118. $_SESSION[$id] = serialize($object);
  119. return;
  120. }
  121. }
  122. }
  123. /**
  124. * Represents a single query with results
  125. *
  126. */
  127. class SearchQuery {
  128. private $index,
  129. $term,
  130. $pagenumber,
  131. $cache,
  132. $validquery,
  133. $validindex,
  134. $results,
  135. $results_per_page,
  136. $total_results;
  137. /**
  138. * constructor records query parameters
  139. *
  140. */
  141. public function __construct($term = '', $page = 1, $results_per_page = 10, $cache = false) {
  142. global $CFG;
  143. $this->term = $term;
  144. $this->pagenumber = $page;
  145. $this->cache = $cache;
  146. $this->validquery = true;
  147. $this->validindex = true;
  148. $this->results_per_page = $results_per_page;
  149. $index_path = SEARCH_INDEX_PATH;
  150. try {
  151. $this->index = new Zend_Search_Lucene($index_path, false);
  152. } catch(Exception $e) {
  153. $this->validindex = false;
  154. return;
  155. }
  156. if (empty($this->term)) {
  157. $this->validquery = false;
  158. } else {
  159. $this->set_query($this->term);
  160. }
  161. }
  162. /**
  163. * determines state of query object depending on query entry and
  164. * tries to lauch search if all is OK
  165. * @return void (this is only a state changing trigger).
  166. */
  167. public function set_query($term = '') {
  168. if (!empty($term)) {
  169. $this->term = $term;
  170. }
  171. if (empty($this->term)) {
  172. $this->validquery = false;
  173. } else {
  174. $this->validquery = true;
  175. }
  176. if ($this->validquery and $this->validindex) {
  177. $this->results = $this->get_results();
  178. } else {
  179. $this->results = array();
  180. }
  181. }
  182. /**
  183. * accessor to the result table.
  184. * @return an array of result records
  185. */
  186. public function results() {
  187. return $this->results;
  188. }
  189. /**
  190. * do the effective collection of results
  191. * @param boolean $all
  192. * @uses USER
  193. */
  194. private function process_results($all=false) {
  195. global $USER;
  196. // unneeded since changing the default Zend Lexer
  197. // $term = mb_convert_case($this->term, MB_CASE_LOWER, 'UTF-8');
  198. $term = $this->term;
  199. $page = optional_param('page', 1, PARAM_INT);
  200. //experimental - return more results
  201. // $strip_arr = array('author:', 'title:', '+', '-', 'doctype:');
  202. // $stripped_term = str_replace($strip_arr, '', $term);
  203. // $search_string = $term." title:".$stripped_term." author:".$stripped_term;
  204. $search_string = $term;
  205. $hits = $this->index->find($search_string);
  206. //--
  207. $hitcount = count($hits);
  208. $this->total_results = $hitcount;
  209. if ($hitcount == 0) return array();
  210. $resultdoc = new SearchResult();
  211. $resultdocs = array();
  212. $searchables = search_collect_searchables(false, false);
  213. $realindex = 0;
  214. /**
  215. if (!$all) {
  216. if ($finalresults < $this->results_per_page) {
  217. $this->pagenumber = 1;
  218. } elseif ($this->pagenumber > $totalpages) {
  219. $this->pagenumber = $totalpages;
  220. }
  221. $start = ($this->pagenumber - 1) * $this->results_per_page;
  222. $end = $start + $this->results_per_page;
  223. if ($end > $finalresults) {
  224. $end = $finalresults;
  225. }
  226. } else {
  227. $start = 0;
  228. $end = $finalresults;
  229. } */
  230. for ($i = 0; $i < min($hitcount, ($page) * $this->results_per_page); $i++) {
  231. $hit = $hits[$i];
  232. //check permissions on each result
  233. if ($this->can_display($USER, $hit->docid, $hit->doctype, $hit->course_id, $hit->group_id, $hit->path, $hit->itemtype, $hit->context_id, $searchables )) {
  234. if ($i >= ($page - 1) * $this->results_per_page){
  235. $resultdoc->number = $realindex;
  236. $resultdoc->url = $hit->url;
  237. $resultdoc->title = $hit->title;
  238. $resultdoc->score = $hit->score;
  239. $resultdoc->doctype = $hit->doctype;
  240. $resultdoc->author = $hit->author;
  241. $resultdoc->courseid = $hit->course_id;
  242. //and store it
  243. $resultdocs[] = clone($resultdoc);
  244. }
  245. $realindex++;
  246. } else {
  247. // lowers total_results one unit
  248. $this->total_results--;
  249. }
  250. }
  251. $totalpages = ceil($this->total_results/$this->results_per_page);
  252. return $resultdocs;
  253. }
  254. /**
  255. * get results of a search query using a caching strategy if available
  256. * @return the result documents as an array of search objects
  257. */
  258. private function get_results() {
  259. $cache = new SearchCache();
  260. if ($this->cache && $cache->can_cache()) {
  261. if (!($resultdocs = $cache->cache($this->term))) {
  262. $resultdocs = $this->process_results();
  263. //cache the results so we don't have to compute this on every page-load
  264. $cache->cache($this->term, $resultdocs);
  265. //print "Using new results.";
  266. } else {
  267. //There was something in the cache, so we're using that to save time
  268. //print "Using cached results.";
  269. }
  270. } else {
  271. //no caching :(
  272. // print "Caching disabled!";
  273. $resultdocs = $this->process_results();
  274. }
  275. return $resultdocs;
  276. }
  277. /**
  278. * constructs the results paging links on results.
  279. * @return string the results paging links
  280. */
  281. public function page_numbers() {
  282. $pages = $this->total_pages();
  283. $query = htmlentities($this->term);
  284. $page = $this->pagenumber;
  285. $next = get_string('next', 'search');
  286. $back = get_string('back', 'search');
  287. $ret = "<div class='mdl-align' id='search_page_links'>";
  288. //Back is disabled if we're on page 1
  289. if ($page > 1) {
  290. $ret .= "<a href='query.php?query_string={$query}&page=".($page-1)."'>&lt; {$back}</a>&nbsp;";
  291. } else {
  292. $ret .= "&lt; {$back}&nbsp;";
  293. }
  294. //don't <a href> the current page
  295. for ($i = 1; $i <= $pages; $i++) {
  296. if ($page == $i) {
  297. $ret .= "($i)&nbsp;";
  298. } else {
  299. $ret .= "<a href='query.php?query_string={$query}&page={$i}'>{$i}</a>&nbsp;";
  300. }
  301. }
  302. //Next disabled if we're on the last page
  303. if ($page < $pages) {
  304. $ret .= "<a href='query.php?query_string={$query}&page=".($page+1)."'>{$next} &gt;</a>&nbsp;";
  305. } else {
  306. $ret .= "{$next} &gt;&nbsp;";
  307. }
  308. $ret .= "</div>";
  309. //shorten really long page lists, to stop table distorting width-ways
  310. if (strlen($ret) > 70) {
  311. $start = 4;
  312. $end = $page - 5;
  313. $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
  314. $start = $page + 5;
  315. $end = $pages - 3;
  316. $ret = preg_replace("/<a\D+\d+\D+>$start<\/a>.*?<a\D+\d+\D+>$end<\/a>/", '...', $ret);
  317. }
  318. return $ret;
  319. }
  320. /**
  321. * can the user see this result ?
  322. * @param user a reference upon the user to be checked for access
  323. * @param this_id the item identifier
  324. * @param doctype the search document type. MAtches the module or block or
  325. * extra search source definition
  326. * @param course_id the course reference of the searched result
  327. * @param group_id the group identity attached to the found resource
  328. * @param path the path that routes to the local lib.php of the searched
  329. * surrounding object fot that document
  330. * @param item_type a subclassing information for complex module data models
  331. * @uses CFG
  332. * // TODO reorder parameters more consistently
  333. */
  334. private function can_display(&$user, $this_id, $doctype, $course_id, $group_id, $path, $item_type, $context_id, &$searchables) {
  335. global $CFG, $DB;
  336. /**
  337. * course related checks
  338. */
  339. // admins can see everything, anyway.
  340. if (has_capability('moodle/site:doanything', get_context_instance(CONTEXT_SYSTEM))){
  341. return true;
  342. }
  343. // first check course compatibility against user : enrolled users to that course can see.
  344. $myCourses = get_my_courses($user->id);
  345. $unenroled = !in_array($course_id, array_keys($myCourses));
  346. // if guests are allowed, logged guest can see
  347. $isallowedguest = (isguest()) ? $DB->get_field('course', 'guest', array('id' => $course_id)) : false ;
  348. if ($unenroled && !$isallowedguest){
  349. return false;
  350. }
  351. // if user is enrolled or is allowed user and course is hidden, can he see it ?
  352. $visibility = $DB->get_field('course', 'visible', array('id' => $course_id));
  353. if ($visibility <= 0){
  354. if (!has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course_id))){
  355. return false;
  356. }
  357. }
  358. /**
  359. * prerecorded capabilities
  360. */
  361. // get context caching information and tries to discard unwanted records here
  362. /**
  363. * final checks
  364. */
  365. // then give back indexing data to the module for local check
  366. $searchable_instance = $searchables[$doctype];
  367. if ($searchable_instance->location == 'internal'){
  368. include_once "{$CFG->dirroot}/search/documents/{$doctype}_document.php";
  369. } else {
  370. include_once "{$CFG->dirroot}/{$searchable_instance->location}/{$doctype}/search_document.php";
  371. }
  372. $access_check_function = "{$doctype}_check_text_access";
  373. if (function_exists($access_check_function)){
  374. $modulecheck = $access_check_function($path, $item_type, $this_id, $user, $group_id, $context_id);
  375. // echo "module said $modulecheck for item $doctype/$item_type/$this_id";
  376. return($modulecheck);
  377. }
  378. return true;
  379. }
  380. /**
  381. *
  382. */
  383. public function count() {
  384. return $this->total_results;
  385. } //count
  386. /**
  387. *
  388. */
  389. public function is_valid() {
  390. return ($this->validquery and $this->validindex);
  391. }
  392. /**
  393. *
  394. */
  395. public function is_valid_query() {
  396. return $this->validquery;
  397. }
  398. /**
  399. *
  400. */
  401. public function is_valid_index() {
  402. return $this->validindex;
  403. }
  404. /**
  405. *
  406. */
  407. public function total_pages() {
  408. return ceil($this->count()/$this->results_per_page);
  409. }
  410. /**
  411. *
  412. */
  413. public function get_pagenumber() {
  414. return $this->pagenumber;
  415. }
  416. /**
  417. *
  418. */
  419. public function get_results_per_page() {
  420. return $this->results_per_page;
  421. }
  422. }
  423. ?>