See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]
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 base class to be extended by search areas. 19 * 20 * @package core_search 21 * @copyright 2015 David Monllao {@link http://www.davidmonllao.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 /** 30 * Base search implementation. 31 * 32 * Components and plugins interested in filling the search engine with data should extend this class (or any extension of this 33 * class). 34 * 35 * @package core_search 36 * @copyright 2015 David Monllao {@link http://www.davidmonllao.com} 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 abstract class base { 40 41 /** 42 * The area name as defined in the class name. 43 * 44 * @var string 45 */ 46 protected $areaname = null; 47 48 /** 49 * The component frankenstyle name. 50 * 51 * @var string 52 */ 53 protected $componentname = null; 54 55 /** 56 * The component type (core or the plugin type). 57 * 58 * @var string 59 */ 60 protected $componenttype = null; 61 62 /** 63 * The context levels the search implementation is working on. 64 * 65 * @var array 66 */ 67 protected static $levels = [CONTEXT_SYSTEM]; 68 69 /** 70 * Constructor. 71 * 72 * @throws \coding_exception 73 * @return void 74 */ 75 public final function __construct() { 76 77 $classname = get_class($this); 78 79 // Detect possible issues when defining the class. 80 if (strpos($classname, '\search') === false) { 81 throw new \coding_exception('Search area classes should be located in \PLUGINTYPE_PLUGINNAME\search\AREANAME.'); 82 } else if (strpos($classname, '_') === false) { 83 throw new \coding_exception($classname . ' class namespace level 1 should be its component frankenstyle name'); 84 } 85 86 $this->areaname = substr(strrchr($classname, '\\'), 1); 87 $this->componentname = substr($classname, 0, strpos($classname, '\\')); 88 $this->areaid = \core_search\manager::generate_areaid($this->componentname, $this->areaname); 89 $this->componenttype = substr($this->componentname, 0, strpos($this->componentname, '_')); 90 } 91 92 /** 93 * Returns context levels property. 94 * 95 * @return int 96 */ 97 public static function get_levels() { 98 return static::$levels; 99 } 100 101 /** 102 * Returns the area id. 103 * 104 * @return string 105 */ 106 public function get_area_id() { 107 return $this->areaid; 108 } 109 110 /** 111 * Returns the moodle component name. 112 * 113 * It might be the plugin name (whole frankenstyle name) or the core subsystem name. 114 * 115 * @return string 116 */ 117 public function get_component_name() { 118 return $this->componentname; 119 } 120 121 /** 122 * Returns the component type. 123 * 124 * It might be a plugintype or 'core' for core subsystems. 125 * 126 * @return string 127 */ 128 public function get_component_type() { 129 return $this->componenttype; 130 } 131 132 /** 133 * Returns the area visible name. 134 * 135 * @param bool $lazyload Usually false, unless when in admin settings. 136 * @return string 137 */ 138 public function get_visible_name($lazyload = false) { 139 140 $component = $this->componentname; 141 142 // Core subsystem strings go to lang/XX/search.php. 143 if ($this->componenttype === 'core') { 144 $component = 'search'; 145 } 146 return get_string('search:' . $this->areaname, $component, null, $lazyload); 147 } 148 149 /** 150 * Returns the config var name. 151 * 152 * It depends on whether it is a moodle subsystem or a plugin as plugin-related config should remain in their own scope. 153 * 154 * @access private 155 * @return string Config var path including the plugin (or component) and the varname 156 */ 157 public function get_config_var_name() { 158 159 if ($this->componenttype === 'core') { 160 // Core subsystems config in core_search and setting name using only [a-zA-Z0-9_]+. 161 $parts = \core_search\manager::extract_areaid_parts($this->areaid); 162 return array('core_search', $parts[0] . '_' . $parts[1]); 163 } 164 165 // Plugins config in the plugin scope. 166 return array($this->componentname, 'search_' . $this->areaname); 167 } 168 169 /** 170 * Returns all the search area configuration. 171 * 172 * @return array 173 */ 174 public function get_config() { 175 list($componentname, $varname) = $this->get_config_var_name(); 176 177 $config = []; 178 $settingnames = self::get_settingnames(); 179 foreach ($settingnames as $name) { 180 $config[$varname . $name] = get_config($componentname, $varname . $name); 181 } 182 183 // Search areas are enabled by default. 184 if ($config[$varname . '_enabled'] === false) { 185 $config[$varname . '_enabled'] = 1; 186 } 187 return $config; 188 } 189 190 /** 191 * Return a list of all required setting names. 192 * 193 * @return array 194 */ 195 public static function get_settingnames() { 196 return array('_enabled', '_indexingstart', '_indexingend', '_lastindexrun', 197 '_docsignored', '_docsprocessed', '_recordsprocessed', '_partial'); 198 } 199 200 /** 201 * Is the search component enabled by the system administrator? 202 * 203 * @return bool 204 */ 205 public function is_enabled() { 206 list($componentname, $varname) = $this->get_config_var_name(); 207 208 $value = get_config($componentname, $varname . '_enabled'); 209 210 // Search areas are enabled by default. 211 if ($value === false) { 212 $value = 1; 213 } 214 return (bool)$value; 215 } 216 217 public function set_enabled($isenabled) { 218 list($componentname, $varname) = $this->get_config_var_name(); 219 return set_config($varname . '_enabled', $isenabled, $componentname); 220 } 221 222 /** 223 * Gets the length of time spent indexing this area (the last time it was indexed). 224 * 225 * @return int|bool Time in seconds spent indexing this area last time, false if never indexed 226 */ 227 public function get_last_indexing_duration() { 228 list($componentname, $varname) = $this->get_config_var_name(); 229 $start = get_config($componentname, $varname . '_indexingstart'); 230 $end = get_config($componentname, $varname . '_indexingend'); 231 if ($start && $end) { 232 return $end - $start; 233 } else { 234 return false; 235 } 236 } 237 238 /** 239 * Returns true if this area uses file indexing. 240 * 241 * @return bool 242 */ 243 public function uses_file_indexing() { 244 return false; 245 } 246 247 /** 248 * Returns a recordset ordered by modification date ASC. 249 * 250 * Each record can include any data self::get_document might need but it must: 251 * - Include an 'id' field: Unique identifier (in this area's scope) of a document to index in the search engine 252 * If the indexed content field can contain embedded files, the 'id' value should match the filearea itemid. 253 * - Only return data modified since $modifiedfrom, including $modifiedform to prevent 254 * some records from not being indexed (e.g. your-timemodified-fieldname >= $modifiedfrom) 255 * - Order the returned data by time modified in ascending order, as \core_search::manager will need to store the modified time 256 * of the last indexed document. 257 * 258 * Since Moodle 3.4, subclasses should instead implement get_document_recordset, which has 259 * an additional context parameter. This function continues to work for implementations which 260 * haven't been updated, or where the context parameter is not required. 261 * 262 * @param int $modifiedfrom 263 * @return \moodle_recordset 264 */ 265 public function get_recordset_by_timestamp($modifiedfrom = 0) { 266 $result = $this->get_document_recordset($modifiedfrom); 267 if ($result === false) { 268 throw new \coding_exception( 269 'Search area must implement get_document_recordset or get_recordset_by_timestamp'); 270 } 271 return $result; 272 } 273 274 /** 275 * Returns a recordset containing all items from this area, optionally within the given context, 276 * and including only items modifed from (>=) the specified time. The recordset must be ordered 277 * in ascending order of modified time. 278 * 279 * Each record can include any data self::get_document might need. It must include an 'id' 280 * field,a unique identifier (in this area's scope) of a document to index in the search engine. 281 * If the indexed content field can contain embedded files, the 'id' value should match the 282 * filearea itemid. 283 * 284 * The return value can be a recordset, null (if this area does not provide any results in the 285 * given context and there is no need to do a database query to find out), or false (if this 286 * facility is not currently supported by this search area). 287 * 288 * If this function returns false, then: 289 * - If indexing the entire system (no context restriction) the search indexer will try 290 * get_recordset_by_timestamp instead 291 * - If trying to index a context (e.g. when restoring a course), the search indexer will not 292 * index this area, so that restored content may not be indexed. 293 * 294 * The default implementation returns false, indicating that this facility is not supported and 295 * the older get_recordset_by_timestamp function should be used. 296 * 297 * This function must accept all possible values for the $context parameter. For example, if 298 * you are implementing this function for the forum module, it should still operate correctly 299 * if called with the context for a glossary module, or for the HTML block. (In these cases 300 * where it will not return any data, it may return null.) 301 * 302 * The $context parameter can also be null or the system context; both of these indicate that 303 * all data, without context restriction, should be returned. 304 * 305 * @param int $modifiedfrom Return only records modified after this date 306 * @param \context|null $context Context (null means no context restriction) 307 * @return \moodle_recordset|null|false Recordset / null if no results / false if not supported 308 * @since Moodle 3.4 309 */ 310 public function get_document_recordset($modifiedfrom = 0, \context $context = null) { 311 return false; 312 } 313 314 /** 315 * Checks if get_document_recordset is supported for this search area. 316 * 317 * For many uses you can simply call get_document_recordset and see if it returns false, but 318 * this function is useful when you don't want to actually call the function right away. 319 */ 320 public function supports_get_document_recordset() { 321 // Easiest way to check this is simply to see if the class has overridden the default 322 // function. 323 $method = new \ReflectionMethod($this, 'get_document_recordset'); 324 return $method->getDeclaringClass()->getName() !== self::class; 325 } 326 327 /** 328 * Returns the document related with the provided record. 329 * 330 * This method receives a record with the document id and other info returned by get_recordset_by_timestamp 331 * or get_recordset_by_contexts that might be useful here. The idea is to restrict database queries to 332 * minimum as this function will be called for each document to index. As an alternative, use cached data. 333 * 334 * Internally it should use \core_search\document to standarise the documents before sending them to the search engine. 335 * 336 * Search areas should send plain text to the search engine, use the following function to convert any user 337 * input data to plain text: {@link content_to_text} 338 * 339 * Valid keys for the options array are: 340 * indexfiles => File indexing is enabled if true. 341 * lastindexedtime => The last time this area was indexed. 0 if never indexed. 342 * 343 * The lastindexedtime value is not set if indexing a specific context rather than the whole 344 * system. 345 * 346 * @param \stdClass $record A record containing, at least, the indexed document id and a modified timestamp 347 * @param array $options Options for document creation 348 * @return \core_search\document 349 */ 350 abstract public function get_document($record, $options = array()); 351 352 /** 353 * Returns the document title to display. 354 * 355 * Allow to customize the document title string to display. 356 * 357 * @param \core_search\document $doc 358 * @return string Document title to display in the search results page 359 */ 360 public function get_document_display_title(\core_search\document $doc) { 361 362 return $doc->get('title'); 363 } 364 365 /** 366 * Return the context info required to index files for 367 * this search area. 368 * 369 * Should be onerridden by each search area. 370 * 371 * @return array 372 */ 373 public function get_search_fileareas() { 374 $fileareas = array(); 375 376 return $fileareas; 377 } 378 379 /** 380 * Files related to the current document are attached, 381 * to the document object ready for indexing by 382 * Global Search. 383 * 384 * The default implementation retrieves all files for 385 * the file areas returned by get_search_fileareas(). 386 * If you need to filter files to specific items per 387 * file area, you will need to override this method 388 * and explicitly provide the items. 389 * 390 * @param document $document The current document 391 * @return void 392 */ 393 public function attach_files($document) { 394 $fileareas = $this->get_search_fileareas(); 395 $contextid = $document->get('contextid'); 396 $component = $this->get_component_name(); 397 $itemid = $document->get('itemid'); 398 399 foreach ($fileareas as $filearea) { 400 $fs = get_file_storage(); 401 $files = $fs->get_area_files($contextid, $component, $filearea, $itemid, '', false); 402 403 foreach ($files as $file) { 404 $document->add_stored_file($file); 405 } 406 } 407 408 } 409 410 /** 411 * Can the current user see the document. 412 * 413 * @param int $id The internal search area entity id. 414 * @return int manager:ACCESS_xx constant 415 */ 416 abstract public function check_access($id); 417 418 /** 419 * Returns a url to the document, it might match self::get_context_url(). 420 * 421 * @param \core_search\document $doc 422 * @return \moodle_url 423 */ 424 abstract public function get_doc_url(\core_search\document $doc); 425 426 /** 427 * Returns a url to the document context. 428 * 429 * @param \core_search\document $doc 430 * @return \moodle_url 431 */ 432 abstract public function get_context_url(\core_search\document $doc); 433 434 /** 435 * Helper function that gets SQL useful for restricting a search query given a passed-in 436 * context, for data stored at course level. 437 * 438 * The SQL returned will be zero or more JOIN statements, surrounded by whitespace, which act 439 * as restrictions on the query based on the rows in a module table. 440 * 441 * You can pass in a null or system context, which will both return an empty string and no 442 * params. 443 * 444 * Returns an array with two nulls if there can be no results for a course within this context. 445 * 446 * If named parameters are used, these will be named gclcrs0, gclcrs1, etc. The table aliases 447 * used in SQL also all begin with gclcrs, to avoid conflicts. 448 * 449 * @param \context|null $context Context to restrict the query 450 * @param string $coursetable Name of alias for course table e.g. 'c' 451 * @param int $paramtype Type of SQL parameters to use (default question mark) 452 * @return array Array with SQL and parameters; both null if no need to query 453 * @throws \coding_exception If called with invalid params 454 */ 455 protected function get_course_level_context_restriction_sql(?\context $context, 456 $coursetable, $paramtype = SQL_PARAMS_QM) { 457 global $DB; 458 459 if (!$context) { 460 return ['', []]; 461 } 462 463 switch ($paramtype) { 464 case SQL_PARAMS_QM: 465 $param1 = '?'; 466 $param2 = '?'; 467 $key1 = 0; 468 $key2 = 1; 469 break; 470 case SQL_PARAMS_NAMED: 471 $param1 = ':gclcrs0'; 472 $param2 = ':gclcrs1'; 473 $key1 = 'gclcrs0'; 474 $key2 = 'gclcrs1'; 475 break; 476 default: 477 throw new \coding_exception('Unexpected $paramtype: ' . $paramtype); 478 } 479 480 $params = []; 481 switch ($context->contextlevel) { 482 case CONTEXT_SYSTEM: 483 $sql = ''; 484 break; 485 486 case CONTEXT_COURSECAT: 487 // Find all courses within the specified category or any sub-category. 488 $pathmatch = $DB->sql_like('gclcrscc2.path', 489 $DB->sql_concat('gclcrscc1.path', $param2)); 490 $sql = " JOIN {course_categories} gclcrscc1 ON gclcrscc1.id = $param1 491 JOIN {course_categories} gclcrscc2 ON gclcrscc2.id = $coursetable.category 492 AND (gclcrscc2.id = gclcrscc1.id OR $pathmatch) "; 493 $params[$key1] = $context->instanceid; 494 // Note: This param is a bit annoying as it obviously never changes, but sql_like 495 // throws a debug warning if you pass it anything with quotes in, so it has to be 496 // a bound parameter. 497 $params[$key2] = '/%'; 498 break; 499 500 case CONTEXT_COURSE: 501 // We just join again against the same course entry and confirm that it has the 502 // same id as the context. 503 $sql = " JOIN {course} gclcrsc ON gclcrsc.id = $coursetable.id 504 AND gclcrsc.id = $param1"; 505 $params[$key1] = $context->instanceid; 506 break; 507 508 case CONTEXT_BLOCK: 509 case CONTEXT_MODULE: 510 case CONTEXT_USER: 511 // Context cannot contain any courses. 512 return [null, null]; 513 514 default: 515 throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel); 516 } 517 518 return [$sql, $params]; 519 } 520 521 /** 522 * Gets a list of all contexts to reindex when reindexing this search area. The list should be 523 * returned in an order that is likely to be suitable when reindexing, for example with newer 524 * contexts first. 525 * 526 * The default implementation simply returns the system context, which will result in 527 * reindexing everything in normal date order (oldest first). 528 * 529 * @return \Iterator Iterator of contexts to reindex 530 */ 531 public function get_contexts_to_reindex() { 532 return new \ArrayIterator([\context_system::instance()]); 533 } 534 535 /** 536 * Returns an icon instance for the document. 537 * 538 * @param \core_search\document $doc 539 * @return \core_search\document_icon 540 */ 541 public function get_doc_icon(document $doc) : document_icon { 542 return new document_icon('i/empty'); 543 } 544 545 /** 546 * Returns a list of category names associated with the area. 547 * 548 * @return array 549 */ 550 public function get_category_names() { 551 return [manager::SEARCH_AREA_CATEGORY_OTHER]; 552 } 553 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body