[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Search subsystem manager. 19 * 20 * @package core_search 21 * @copyright Prateek Sachan {@link http://prateeksachan.com} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core_search; 26 27 defined('MOODLE_INTERNAL') || die; 28 29 require_once($CFG->dirroot . '/lib/accesslib.php'); 30 31 /** 32 * Search subsystem manager. 33 * 34 * @package core_search 35 * @copyright Prateek Sachan {@link http://prateeksachan.com} 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class manager { 39 40 /** 41 * @var int Text contents. 42 */ 43 const TYPE_TEXT = 1; 44 45 /** 46 * @var int File contents. 47 */ 48 const TYPE_FILE = 2; 49 50 /** 51 * @var int User can not access the document. 52 */ 53 const ACCESS_DENIED = 0; 54 55 /** 56 * @var int User can access the document. 57 */ 58 const ACCESS_GRANTED = 1; 59 60 /** 61 * @var int The document was deleted. 62 */ 63 const ACCESS_DELETED = 2; 64 65 /** 66 * @var int Maximum number of results that will be retrieved from the search engine. 67 */ 68 const MAX_RESULTS = 100; 69 70 /** 71 * @var int Number of results per page. 72 */ 73 const DISPLAY_RESULTS_PER_PAGE = 10; 74 75 /** 76 * @var int The id to be placed in owneruserid when there is no owner. 77 */ 78 const NO_OWNER_ID = 0; 79 80 /** 81 * @var \core_search\base[] Enabled search areas. 82 */ 83 protected static $enabledsearchareas = null; 84 85 /** 86 * @var \core_search\base[] All system search areas. 87 */ 88 protected static $allsearchareas = null; 89 90 /** 91 * @var \core_search\manager 92 */ 93 protected static $instance = null; 94 95 /** 96 * @var \core_search\engine 97 */ 98 protected $engine = null; 99 100 /** 101 * Constructor, use \core_search\manager::instance instead to get a class instance. 102 * 103 * @param \core_search\base The search engine to use 104 */ 105 public function __construct($engine) { 106 $this->engine = $engine; 107 } 108 109 /** 110 * Returns an initialised \core_search instance. 111 * 112 * @see \core_search\engine::is_installed 113 * @see \core_search\engine::is_server_ready 114 * @throws \core_search\engine_exception 115 * @return \core_search\manager 116 */ 117 public static function instance() { 118 global $CFG; 119 120 // One per request, this should be purged during testing. 121 if (static::$instance !== null) { 122 return static::$instance; 123 } 124 125 if (empty($CFG->searchengine)) { 126 throw new \core_search\engine_exception('enginenotselected', 'search'); 127 } 128 129 if (!$engine = static::search_engine_instance()) { 130 throw new \core_search\engine_exception('enginenotfound', 'search', '', $CFG->searchengine); 131 } 132 133 if (!$engine->is_installed()) { 134 throw new \core_search\engine_exception('enginenotinstalled', 'search', '', $CFG->searchengine); 135 } 136 137 $serverstatus = $engine->is_server_ready(); 138 if ($serverstatus !== true) { 139 // Error message with no details as this is an exception that any user may find if the server crashes. 140 throw new \core_search\engine_exception('engineserverstatus', 'search'); 141 } 142 143 static::$instance = new \core_search\manager($engine); 144 return static::$instance; 145 } 146 147 /** 148 * Returns whether global search is enabled or not. 149 * 150 * @return bool 151 */ 152 public static function is_global_search_enabled() { 153 global $CFG; 154 return !empty($CFG->enableglobalsearch); 155 } 156 157 /** 158 * Returns an instance of the search engine. 159 * 160 * @return \core_search\engine 161 */ 162 public static function search_engine_instance() { 163 global $CFG; 164 165 $classname = '\\search_' . $CFG->searchengine . '\\engine'; 166 if (!class_exists($classname)) { 167 return false; 168 } 169 170 return new $classname(); 171 } 172 173 /** 174 * Returns the search engine. 175 * 176 * @return \core_search\engine 177 */ 178 public function get_engine() { 179 return $this->engine; 180 } 181 182 /** 183 * Returns a search area class name. 184 * 185 * @param string $areaid 186 * @return string 187 */ 188 protected static function get_area_classname($areaid) { 189 list($componentname, $areaname) = static::extract_areaid_parts($areaid); 190 return '\\' . $componentname . '\\search\\' . $areaname; 191 } 192 193 /** 194 * Returns a new area search indexer instance. 195 * 196 * @param string $areaid 197 * @return \core_search\base|bool False if the area is not available. 198 */ 199 public static function get_search_area($areaid) { 200 201 // Try both caches, it does not matter where it comes from. 202 if (!empty(static::$allsearchareas[$areaid])) { 203 return static::$allsearchareas[$areaid]; 204 } 205 if (!empty(static::$enabledsearchareas[$areaid])) { 206 return static::$enabledsearchareas[$areaid]; 207 } 208 209 $classname = static::get_area_classname($areaid); 210 if (class_exists($classname)) { 211 return new $classname(); 212 } 213 214 return false; 215 } 216 217 /** 218 * Return the list of available search areas. 219 * 220 * @param bool $enabled Return only the enabled ones. 221 * @return \core_search\base[] 222 */ 223 public static function get_search_areas_list($enabled = false) { 224 225 // Two different arrays, we don't expect these arrays to be big. 226 if (!$enabled && static::$allsearchareas !== null) { 227 return static::$allsearchareas; 228 } else if ($enabled && static::$enabledsearchareas !== null) { 229 return static::$enabledsearchareas; 230 } 231 232 $searchareas = array(); 233 234 $plugintypes = \core_component::get_plugin_types(); 235 foreach ($plugintypes as $plugintype => $unused) { 236 $plugins = \core_component::get_plugin_list($plugintype); 237 foreach ($plugins as $pluginname => $pluginfullpath) { 238 239 $componentname = $plugintype . '_' . $pluginname; 240 $searchclasses = \core_component::get_component_classes_in_namespace($componentname, 'search'); 241 foreach ($searchclasses as $classname => $classpath) { 242 $areaname = substr(strrchr($classname, '\\'), 1); 243 $areaid = static::generate_areaid($componentname, $areaname); 244 $searchclass = new $classname(); 245 if (!$enabled || ($enabled && $searchclass->is_enabled())) { 246 $searchareas[$areaid] = $searchclass; 247 } 248 } 249 } 250 } 251 252 $subsystems = \core_component::get_core_subsystems(); 253 foreach ($subsystems as $subsystemname => $subsystempath) { 254 $componentname = 'core_' . $subsystemname; 255 $searchclasses = \core_component::get_component_classes_in_namespace($componentname, 'search'); 256 257 foreach ($searchclasses as $classname => $classpath) { 258 $areaname = substr(strrchr($classname, '\\'), 1); 259 $areaid = static::generate_areaid($componentname, $areaname); 260 $searchclass = new $classname(); 261 if (!$enabled || ($enabled && $searchclass->is_enabled())) { 262 $searchareas[$areaid] = $searchclass; 263 } 264 } 265 } 266 267 // Cache results. 268 if ($enabled) { 269 static::$enabledsearchareas = $searchareas; 270 } else { 271 static::$allsearchareas = $searchareas; 272 } 273 274 return $searchareas; 275 } 276 277 /** 278 * Clears all static caches. 279 * 280 * @return void 281 */ 282 public static function clear_static() { 283 284 static::$enabledsearchareas = null; 285 static::$allsearchareas = null; 286 static::$instance = null; 287 } 288 289 /** 290 * Generates an area id from the componentname and the area name. 291 * 292 * There should not be any naming conflict as the area name is the 293 * class name in component/classes/search/. 294 * 295 * @param string $componentname 296 * @param string $areaname 297 * @return void 298 */ 299 public static function generate_areaid($componentname, $areaname) { 300 return $componentname . '-' . $areaname; 301 } 302 303 /** 304 * Returns all areaid string components (component name and area name). 305 * 306 * @param string $areaid 307 * @return array Component name (Frankenstyle) and area name (search area class name) 308 */ 309 public static function extract_areaid_parts($areaid) { 310 return explode('-', $areaid); 311 } 312 313 /** 314 * Returns the contexts the user can access. 315 * 316 * The returned value is a multidimensional array because some search engines can group 317 * information and there will be a performance benefit on passing only some contexts 318 * instead of the whole context array set. 319 * 320 * @param array|false $limitcourseids An array of course ids to limit the search to. False for no limiting. 321 * @return bool|array Indexed by area identifier (component + area name). Returns true if the user can see everything. 322 */ 323 protected function get_areas_user_accesses($limitcourseids = false) { 324 global $CFG, $USER; 325 326 // All results for admins. Eventually we could add a new capability for managers. 327 if (is_siteadmin()) { 328 return true; 329 } 330 331 $areasbylevel = array(); 332 333 // Split areas by context level so we only iterate only once through courses and cms. 334 $searchareas = static::get_search_areas_list(true); 335 foreach ($searchareas as $areaid => $unused) { 336 $classname = static::get_area_classname($areaid); 337 $searcharea = new $classname(); 338 foreach ($classname::get_levels() as $level) { 339 $areasbylevel[$level][$areaid] = $searcharea; 340 } 341 } 342 343 // This will store area - allowed contexts relations. 344 $areascontexts = array(); 345 346 if (empty($limitcourseids) && !empty($areasbylevel[CONTEXT_SYSTEM])) { 347 // We add system context to all search areas working at this level. Here each area is fully responsible of 348 // the access control as we can not automate much, we can not even check guest access as some areas might 349 // want to allow guests to retrieve data from them. 350 351 $systemcontextid = \context_system::instance()->id; 352 foreach ($areasbylevel[CONTEXT_SYSTEM] as $areaid => $searchclass) { 353 $areascontexts[$areaid][$systemcontextid] = $systemcontextid; 354 } 355 } 356 357 if (!empty($areasbylevel[CONTEXT_USER])) { 358 if ($usercontext = \context_user::instance($USER->id, IGNORE_MISSING)) { 359 // Extra checking although only logged users should reach this point, guest users have a valid context id. 360 foreach ($areasbylevel[CONTEXT_USER] as $areaid => $searchclass) { 361 $areascontexts[$areaid][$usercontext->id] = $usercontext->id; 362 } 363 } 364 } 365 366 // Get the courses where the current user has access. 367 $courses = enrol_get_my_courses(array('id', 'cacherev')); 368 369 if (empty($limitcourseids) || in_array(SITEID, $limitcourseids)) { 370 $courses[SITEID] = get_course(SITEID); 371 } 372 373 foreach ($courses as $course) { 374 if (!empty($limitcourseids) && !in_array($course->id, $limitcourseids)) { 375 // Skip non-included courses. 376 continue; 377 } 378 379 // Info about the course modules. 380 $modinfo = get_fast_modinfo($course); 381 382 if (!empty($areasbylevel[CONTEXT_COURSE])) { 383 // Add the course contexts the user can view. 384 385 $coursecontext = \context_course::instance($course->id); 386 foreach ($areasbylevel[CONTEXT_COURSE] as $areaid => $searchclass) { 387 if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext)) { 388 $areascontexts[$areaid][$coursecontext->id] = $coursecontext->id; 389 } 390 } 391 } 392 393 if (!empty($areasbylevel[CONTEXT_MODULE])) { 394 // Add the module contexts the user can view (cm_info->uservisible). 395 396 foreach ($areasbylevel[CONTEXT_MODULE] as $areaid => $searchclass) { 397 398 // Removing the plugintype 'mod_' prefix. 399 $modulename = substr($searchclass->get_component_name(), 4); 400 401 $modinstances = $modinfo->get_instances_of($modulename); 402 foreach ($modinstances as $modinstance) { 403 if ($modinstance->uservisible) { 404 $areascontexts[$areaid][$modinstance->context->id] = $modinstance->context->id; 405 } 406 } 407 } 408 } 409 } 410 411 return $areascontexts; 412 } 413 414 /** 415 * Returns requested page of documents plus additional information for paging. 416 * 417 * This function does not perform any kind of security checking for access, the caller code 418 * should check that the current user have moodle/search:query capability. 419 * 420 * If a page is requested that is beyond the last result, the last valid page is returned in 421 * results, and actualpage indicates which page was returned. 422 * 423 * @param stdClass $formdata 424 * @param int $pagenum The 0 based page number. 425 * @return object An object with 3 properties: 426 * results => An array of \core_search\documents for the actual page. 427 * totalcount => Number of records that are possibly available, to base paging on. 428 * actualpage => The actual page returned. 429 */ 430 public function paged_search(\stdClass $formdata, $pagenum) { 431 $out = new \stdClass(); 432 433 $perpage = static::DISPLAY_RESULTS_PER_PAGE; 434 435 // Make sure we only allow request up to max page. 436 $pagenum = min($pagenum, (static::MAX_RESULTS / $perpage) - 1); 437 438 // Calculate the first and last document number for the current page, 1 based. 439 $mindoc = ($pagenum * $perpage) + 1; 440 $maxdoc = ($pagenum + 1) * $perpage; 441 442 // Get engine documents, up to max. 443 $docs = $this->search($formdata, $maxdoc); 444 445 $resultcount = count($docs); 446 if ($resultcount < $maxdoc) { 447 // This means it couldn't give us results to max, so the count must be the max. 448 $out->totalcount = $resultcount; 449 } else { 450 // Get the possible count reported by engine, and limit to our max. 451 $out->totalcount = $this->engine->get_query_total_count(); 452 $out->totalcount = min($out->totalcount, static::MAX_RESULTS); 453 } 454 455 // Determine the actual page. 456 if ($resultcount < $mindoc) { 457 // We couldn't get the min docs for this page, so determine what page we can get. 458 $out->actualpage = floor(($resultcount - 1) / $perpage); 459 } else { 460 $out->actualpage = $pagenum; 461 } 462 463 // Split the results to only return the page. 464 $out->results = array_slice($docs, $out->actualpage * $perpage, $perpage, true); 465 466 return $out; 467 } 468 469 /** 470 * Returns documents from the engine based on the data provided. 471 * 472 * This function does not perform any kind of security checking, the caller code 473 * should check that the current user have moodle/search:query capability. 474 * 475 * It might return the results from the cache instead. 476 * 477 * @param stdClass $formdata 478 * @param int $limit The maximum number of documents to return 479 * @return \core_search\document[] 480 */ 481 public function search(\stdClass $formdata, $limit = 0) { 482 global $USER; 483 484 $limitcourseids = false; 485 if (!empty($formdata->courseids)) { 486 $limitcourseids = $formdata->courseids; 487 } 488 489 // Clears previous query errors. 490 $this->engine->clear_query_error(); 491 492 $areascontexts = $this->get_areas_user_accesses($limitcourseids); 493 if (!$areascontexts) { 494 // User can not access any context. 495 $docs = array(); 496 } else { 497 $docs = $this->engine->execute_query($formdata, $areascontexts, $limit); 498 } 499 500 return $docs; 501 } 502 503 /** 504 * Merge separate index segments into one. 505 */ 506 public function optimize_index() { 507 $this->engine->optimize(); 508 } 509 510 /** 511 * Index all documents. 512 * 513 * @param bool $fullindex Whether we should reindex everything or not. 514 * @throws \moodle_exception 515 * @return bool Whether there was any updated document or not. 516 */ 517 public function index($fullindex = false) { 518 global $CFG; 519 520 // Unlimited time. 521 \core_php_time_limit::raise(); 522 523 // Notify the engine that an index starting. 524 $this->engine->index_starting($fullindex); 525 526 $sumdocs = 0; 527 528 $searchareas = $this->get_search_areas_list(true); 529 foreach ($searchareas as $areaid => $searcharea) { 530 531 if (CLI_SCRIPT && !PHPUNIT_TEST) { 532 mtrace('Processing ' . $searcharea->get_visible_name() . ' area'); 533 } 534 535 // Notify the engine that an area is starting. 536 $this->engine->area_index_starting($searcharea, $fullindex); 537 538 $indexingstart = time(); 539 540 // This is used to store this component config. 541 list($componentconfigname, $varname) = $searcharea->get_config_var_name(); 542 543 $numrecords = 0; 544 $numdocs = 0; 545 $numdocsignored = 0; 546 $lastindexeddoc = 0; 547 548 $prevtimestart = intval(get_config($componentconfigname, $varname . '_indexingstart')); 549 550 if ($fullindex === true) { 551 $referencestarttime = 0; 552 } else { 553 $referencestarttime = $prevtimestart; 554 } 555 556 // Getting the recordset from the area. 557 $recordset = $searcharea->get_recordset_by_timestamp($referencestarttime); 558 559 // Pass get_document as callback. 560 $fileindexing = $this->engine->file_indexing_enabled() && $searcharea->uses_file_indexing(); 561 $options = array('indexfiles' => $fileindexing, 'lastindexedtime' => $prevtimestart); 562 $iterator = new \core\dml\recordset_walk($recordset, array($searcharea, 'get_document'), $options); 563 foreach ($iterator as $document) { 564 if (!$document instanceof \core_search\document) { 565 continue; 566 } 567 568 if ($prevtimestart == 0) { 569 // If we have never indexed this area before, it must be new. 570 $document->set_is_new(true); 571 } 572 573 if ($fileindexing) { 574 // Attach files if we are indexing. 575 $searcharea->attach_files($document); 576 } 577 578 if ($this->engine->add_document($document, $fileindexing)) { 579 $numdocs++; 580 } else { 581 $numdocsignored++; 582 } 583 584 $lastindexeddoc = $document->get('modified'); 585 $numrecords++; 586 } 587 588 if (CLI_SCRIPT && !PHPUNIT_TEST) { 589 if ($numdocs > 0) { 590 mtrace('Processed ' . $numrecords . ' records containing ' . $numdocs . ' documents for ' . 591 $searcharea->get_visible_name() . ' area.'); 592 } else { 593 mtrace('No new documents to index for ' . $searcharea->get_visible_name() . ' area.'); 594 } 595 } 596 597 // Notify the engine this area is complete, and only mark times if true. 598 if ($this->engine->area_index_complete($searcharea, $numdocs, $fullindex)) { 599 $sumdocs += $numdocs; 600 601 // Store last index run once documents have been commited to the search engine. 602 set_config($varname . '_indexingstart', $indexingstart, $componentconfigname); 603 set_config($varname . '_indexingend', time(), $componentconfigname); 604 set_config($varname . '_docsignored', $numdocsignored, $componentconfigname); 605 set_config($varname . '_docsprocessed', $numdocs, $componentconfigname); 606 set_config($varname . '_recordsprocessed', $numrecords, $componentconfigname); 607 if ($lastindexeddoc > 0) { 608 set_config($varname . '_lastindexrun', $lastindexeddoc, $componentconfigname); 609 } 610 } 611 } 612 613 if ($sumdocs > 0) { 614 $event = \core\event\search_indexed::create( 615 array('context' => \context_system::instance())); 616 $event->trigger(); 617 } 618 619 $this->engine->index_complete($sumdocs, $fullindex); 620 621 return (bool)$sumdocs; 622 } 623 624 /** 625 * Resets areas config. 626 * 627 * @throws \moodle_exception 628 * @param string $areaid 629 * @return void 630 */ 631 public function reset_config($areaid = false) { 632 633 if (!empty($areaid)) { 634 $searchareas = array(); 635 if (!$searchareas[$areaid] = static::get_search_area($areaid)) { 636 throw new \moodle_exception('errorareanotavailable', 'search', '', $areaid); 637 } 638 } else { 639 // Only the enabled ones. 640 $searchareas = static::get_search_areas_list(true); 641 } 642 643 foreach ($searchareas as $searcharea) { 644 list($componentname, $varname) = $searcharea->get_config_var_name(); 645 $config = $searcharea->get_config(); 646 647 foreach ($config as $key => $value) { 648 // We reset them all but the enable/disabled one. 649 if ($key !== $varname . '_enabled') { 650 set_config($key, 0, $componentname); 651 } 652 } 653 } 654 } 655 656 /** 657 * Deletes an area's documents or all areas documents. 658 * 659 * @param string $areaid The area id or false for all 660 * @return void 661 */ 662 public function delete_index($areaid = false) { 663 if (!empty($areaid)) { 664 $this->engine->delete($areaid); 665 $this->reset_config($areaid); 666 } else { 667 $this->engine->delete(); 668 $this->reset_config(); 669 } 670 } 671 672 /** 673 * Deletes index by id. 674 * 675 * @param int Solr Document string $id 676 */ 677 public function delete_index_by_id($id) { 678 $this->engine->delete_by_id($id); 679 } 680 681 /** 682 * Returns search areas configuration. 683 * 684 * @param \core_search\base[] $searchareas 685 * @return \stdClass[] $configsettings 686 */ 687 public function get_areas_config($searchareas) { 688 689 $vars = array('indexingstart', 'indexingend', 'lastindexrun', 'docsignored', 'docsprocessed', 'recordsprocessed'); 690 691 $configsettings = array(); 692 foreach ($searchareas as $searcharea) { 693 694 $areaid = $searcharea->get_area_id(); 695 696 $configsettings[$areaid] = new \stdClass(); 697 list($componentname, $varname) = $searcharea->get_config_var_name(); 698 699 if (!$searcharea->is_enabled()) { 700 // We delete all indexed data on disable so no info. 701 foreach ($vars as $var) { 702 $configsettings[$areaid]->{$var} = 0; 703 } 704 } else { 705 foreach ($vars as $var) { 706 $configsettings[$areaid]->{$var} = get_config($componentname, $varname .'_' . $var); 707 } 708 } 709 710 // Formatting the time. 711 if (!empty($configsettings[$areaid]->lastindexrun)) { 712 $configsettings[$areaid]->lastindexrun = userdate($configsettings[$areaid]->lastindexrun); 713 } else { 714 $configsettings[$areaid]->lastindexrun = get_string('never'); 715 } 716 } 717 return $configsettings; 718 } 719 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |