[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/search/classes/ -> manager.php (source)

   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  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1