Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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  }