$ADMIN->add('reportplugins', $page);
}
+// Global Search engine plugins.
+$ADMIN->add('modules', new admin_category('searchplugins', new lang_string('search', 'admin')));
+$temp = new admin_settingpage('manageglobalsearch', new lang_string('globalsearchmanage', 'admin'));
+
+$pages = array();
+$engines = array();
+foreach (core_component::get_plugin_list('search') as $engine => $plugindir) {
+ $engines[$engine] = new lang_string('pluginname', 'search_' . $engine);
+ $settingspath = "$plugindir/settings.php";
+ if (file_exists($settingspath)) {
+ $settings = new admin_settingpage('search' . $engine,
+ new lang_string('pluginname', 'search_' . $engine), 'moodle/site:config');
+ include($settingspath);
+ if ($settings) {
+ $pages[] = $settings;
+ }
+ }
+}
+
+// Setup status.
+$temp->add(new admin_setting_searchsetupinfo());
+
+// Search engine selection.
+$temp->add(new admin_setting_heading('searchengineheading', new lang_string('searchengine', 'admin'), ''));
+$temp->add(new admin_setting_configselect('searchengine',
+ new lang_string('selectsearchengine', 'admin'), '', 'solr', $engines));
+
+// Enable search areas.
+$temp->add(new admin_setting_heading('searchareasheading', new lang_string('availablesearchareas', 'admin'), ''));
+$searchareas = \core_search\manager::get_search_areas_list();
+foreach ($searchareas as $areaid => $searcharea) {
+ list($componentname, $varname) = $searcharea->get_config_var_name();
+ $temp->add(new admin_setting_configcheckbox($componentname . '/enable' . $varname, $searcharea->get_visible_name(true),
+ '', 1, 1, 0));
+}
+$ADMIN->add('searchplugins', $temp);
+
+foreach ($pages as $page) {
+ $ADMIN->add('searchplugins', $page);
+}
+
/// Add all admin tools
if ($hassiteconfig) {
$ADMIN->add('modules', new admin_category('tools', new lang_string('tools', 'admin')));
$optionalsubsystems->add(new admin_setting_configcheckbox('enableplagiarism', new lang_string('enableplagiarism','plagiarism'), new lang_string('configenableplagiarism','plagiarism'), 0));
$optionalsubsystems->add(new admin_setting_configcheckbox('enablebadges', new lang_string('enablebadges', 'badges'), new lang_string('configenablebadges', 'badges'), 1));
+
+ $optionalsubsystems->add(new admin_setting_configcheckbox('enableglobalsearch', new lang_string('enableglobalsearch', 'admin'),
+ new lang_string('enableglobalsearch_desc', 'admin'), 0, 1, 0));
}
$string['authsettings'] = 'Manage authentication';
$string['autolang'] = 'Language autodetect';
$string['autologinguests'] = 'Auto-login guests';
+$string['availablesearchareas'] = 'Available areas for search';
$string['availableto'] = 'Available to';
$string['availablelicenses'] = 'Available licences';
$string['backgroundcolour'] = 'Transparent colour';
$string['enablecssoptimiser_desc'] = 'When enabled CSS will be run through an optimisation process before being cached. The optimiser processes the CSS removing duplicate rules and styles, as well as white space removable and reformatting. Please note turning this on at the same time as theme designer mode is awful for performance but will help theme designers create optimised CSS.';
$string['enabled'] = 'Enabled';
$string['enabledevicedetection'] = 'Enable device detection';
+$string['enableglobalsearch'] = 'Enable global search';
+$string['enableglobalsearch_desc'] = 'If enabled, data will be indexed and syncronised by a scheduled task.';
$string['enablegravatar'] = 'Enable Gravatar';
$string['enablegravatar_help'] = 'When enabled Moodle will attempt to fetch a user profile picture from Gravatar if the user has not uploaded an image.';
$string['enablemobilewebservice'] = 'Enable web services for mobile devices';
$string['enablerecordcache'] = 'Enable record cache';
$string['enablerssfeeds'] = 'Enable RSS feeds';
$string['enablesafebrowserintegration'] = 'Enable Safe Exam Browser integration';
+$string['enablesearchareas'] = 'Enable search areas';
$string['enablestats'] = 'Enable statistics';
$string['enabletrusttext'] = 'Enable trusted content';
$string['enablewebservices'] = 'Enable web services';
$string['generalsettings'] = 'General settings';
$string['geoipfile'] = 'GeoIP city data file';
$string['getremoteaddrconf'] = 'Logged IP address source';
+$string['globalsearch'] = 'Global search';
+$string['globalsearchmanage'] = 'Manage global search';
$string['groupenrolmentkeypolicy'] = 'Group enrolment key policy';
$string['groupenrolmentkeypolicy_desc'] = 'Turning this on will make Moodle check group enrolment keys against a valid password policy.';
$string['googlemapkey3'] = 'Google Maps API V3 key';
$string['ignore'] = 'Ignore';
$string['includemoduleuserdata'] = 'Include module user data';
$string['incompatibleblocks'] = 'Incompatible blocks';
+$string['indexdata'] = 'Index data';
$string['installhijacked'] = 'Installation must be finished from the original IP address, sorry.';
$string['installsessionerror'] = 'Can not initialise PHP session, please verify that your browser accepts cookies.';
$string['intlrecommended'] = 'Intl extension is used to improve internationalization support, such as locale aware sorting.';
$string['save'] = 'Save';
$string['savechanges'] = 'Save changes';
$string['search'] = 'Search';
+$string['searchengine'] = 'Search engine';
$string['searchinsettings'] = 'Search in settings';
$string['searchresults'] = 'Search results';
+$string['searchsetupinfo'] = 'Search setup';
$string['sectionerror'] = 'Section error!';
$string['secureforms'] = 'Use additional form security';
$string['security'] = 'Security';
$string['selectdevice'] = 'Select device';
+$string['selectsearchengine'] = 'Select search engine';
$string['selecttheme'] = 'Select theme for {$a} device';
$string['server'] = 'Server';
$string['serverchecks'] = 'Server checks';
$string['settingfileuploads'] = 'File uploading is required for normal operation, please enable it in PHP configuration.';
$string['settingmemorylimit'] = 'Insufficient memory detected, please set higher memory limit in PHP settings.';
$string['settingsafemode'] = 'Moodle is not fully compatible with safe mode, please ask server administrator to turn it off. Running Moodle under safe mode is not supported, please expect various problems if you do so.';
+$string['setupsearchengine'] = 'Setup search engine';
$string['showcommentscount'] = 'Show comments count';
$string['showdetails'] = 'Show details';
$string['showuseridentity'] = 'Show user identity';
$string['taskdeleteunconfirmedusers'] = 'Delete unconfirmed users';
$string['taskeventscron'] = 'Background processing for events';
$string['taskfiletrashcleanup'] = 'Cleanup files in trash';
+$string['taskglobalsearch'] = 'Global search indexing';
$string['taskgradecron'] = 'Background processing for gradebook';
$string['tasklegacycron'] = 'Legacy cron processing for plugins';
$string['taskmessagingcleanup'] = 'Background processing for messaging';
$string['cachedef_plugin_manager'] = 'Plugin info manager';
$string['cachedef_questiondata'] = 'Question definitions';
$string['cachedef_repositories'] = 'Repositories instances data';
+$string['cachedef_search_results'] = 'Search results user data';
$string['cachedef_string'] = 'Language string cache';
$string['cachedef_tags'] = 'Tags collections and areas';
$string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
$string['eventemailfailed'] = 'Email failed to send';
$string['eventname'] = 'Event name';
$string['eventrecentactivityviewed'] = 'Recent activity viewed';
+$string['eventsearchindexed'] = 'Search data indexed';
$string['eventunknownlogged'] = 'Unknown event';
$string['eventusercreated'] = 'User created';
$string['eventuserdeleted'] = 'User deleted';
$string['advancedsearch'] = 'Advanced search';
$string['all'] = 'All';
+$string['allareas'] = 'All areas';
$string['author'] = 'Author';
$string['authorname'] = 'Author name';
$string['back'] = 'Back';
$string['databasestate'] = 'Indexing database state';
$string['datadirectory'] = 'Data directory';
$string['deletionsinindex'] = 'Deletions in index';
+$string['docmodifiedon'] = 'Last modified on {$a}';
$string['doctype'] = 'Doctype';
+$string['doctypenotsupported'] = 'The specified doc type is not yet supported';
$string['documents'] = 'documents';
$string['documentsfor'] = 'Documents for';
$string['documentsindatabase'] = 'Documents in database';
$string['documentsinindex'] = 'Documents in index';
$string['duration'] = 'Duration';
$string['emptydatabaseerror'] = 'Database table is not present, or contains no index records.';
+$string['enginenotfound'] = 'Engine {$a} not found.';
+$string['enginenotinstalled'] = '{$a} not installed.';
+$string['engineserverstatus'] = 'The search engine is not available. Please contact your administrator.';
$string['enteryoursearchquery'] = 'Enter your search query';
$string['errors'] = 'Errors';
+$string['errorareanotavailable'] = '{$a} search area is not available.';
$string['filesinindexdirectory'] = 'Files in index directory';
+$string['filterheader'] = 'Filter';
+$string['fromtime'] = 'Modified after';
+$string['globalsearch'] = 'Global search';
$string['globalsearchdisabled'] = 'Global searching is not enabled.';
$string['checkdb'] = 'Check database';
$string['checkdbadvice'] = 'Check your database for any problems.';
$string['checkdir'] = 'Check dir';
$string['checkdiradvice'] = 'Ensure the data directory exists and is writable.';
+$string['incourse'] = 'in course {$a}';
+$string['index'] = 'Index';
$string['invalidindexerror'] = 'Index directory either contains an invalid index, or nothing at all.';
$string['ittook'] = 'It took';
$string['next'] = 'Next';
$string['noindexmessage'] = 'Admin: There appears to be no search index. Please';
+$string['noresults'] = 'No results';
$string['normalsearch'] = 'Normal search';
$string['openedon'] = 'opened on';
+$string['optimize'] = 'Optimize';
+$string['queryerror'] = 'The query you provided could not be parsed by the search engine: {$a}';
$string['resultsreturnedfor'] = 'results returned for';
$string['runindexer'] = 'Run indexer (real)';
$string['runindexertest'] = 'Run indexer test';
$string['score'] = 'Score';
$string['search'] = 'Search';
+$string['searcharea'] = 'Search area';
$string['searching'] = 'Searching in ...';
$string['searchnotpermitted'] = 'You are not allowed to do a search';
+$string['searchsetupdescription'] = 'The following steps help you to set up Moodle global search.';
$string['seconds'] = 'seconds';
$string['solutions'] = 'Solutions';
$string['statistics'] = 'Statistics';
+$string['step'] = 'Step';
$string['thesewordshelpimproverank'] = 'These words help improve rank';
$string['thesewordsmustappear'] = 'These words must appear';
$string['thesewordsmustnotappear'] = 'These words must not appear';
$string['title'] = 'Title';
$string['tofetchtheseresults'] = 'to fetch these results';
$string['totalsize'] = 'Total size';
+$string['totime'] = 'Modified before';
$string['type'] = 'Type';
$string['uncompleteindexingerror'] = 'Indexing was not successfully completed, please restart it.';
$string['versiontoolow'] = 'Sorry, global search requires PHP 5.0.0 or later';
+$string['viewresultincontext'] = 'View this result in context';
$string['whichmodulestosearch?'] = 'Which modules to search?';
$string['wordsintitle'] = 'Words in title';
return true;
}
}
+
+
+/**
+ * Search setup steps info.
+ *
+ * @package core
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_searchsetupinfo extends admin_setting {
+
+ /**
+ * Calls parent::__construct with specific arguments
+ */
+ public function __construct() {
+ $this->nosave = true;
+ parent::__construct('searchsetupinfo', '', '', '');
+ }
+
+ /**
+ * Always returns true, does nothing
+ *
+ * @return true
+ */
+ public function get_setting() {
+ return true;
+ }
+
+ /**
+ * Always returns true, does nothing
+ *
+ * @return true
+ */
+ public function get_defaultsetting() {
+ return true;
+ }
+
+ /**
+ * Always returns '', does not write anything
+ *
+ * @param array $data
+ * @return string Always returns ''
+ */
+ public function write_setting($data) {
+ // Do not write any setting.
+ return '';
+ }
+
+ /**
+ * Builds the HTML to display the control
+ *
+ * @param string $data Unused
+ * @param string $query
+ * @return string
+ */
+ public function output_html($data, $query='') {
+ global $CFG, $OUTPUT;
+
+ $return = '';
+ $brtag = html_writer::empty_tag('br');
+
+ // Available search areas.
+ $searchareas = \core_search\manager::get_search_areas_list();
+ $anyenabled = false;
+ $anyindexed = false;
+ foreach ($searchareas as $areaid => $searcharea) {
+ list($componentname, $varname) = $searcharea->get_config_var_name();
+ if (!$anyenabled) {
+ $anyenabled = get_config($componentname, 'enable' . $varname);
+ }
+ if (!$anyindexed) {
+ $anyindexed = get_config($componentname, $varname . '_indexingstart');
+ }
+ if ($anyenabled && $anyindexed) {
+ break;
+ }
+ }
+
+ $return .= $OUTPUT->heading(get_string('searchsetupinfo', 'admin'), 3, 'main');
+
+ $table = new html_table();
+ $table->head = array(get_string('step', 'search'), get_string('status'));
+ $table->colclasses = array('leftalign step', 'leftalign status');
+ $table->id = 'searchsetup';
+ $table->attributes['class'] = 'admintable generaltable';
+ $table->data = array();
+
+ $return .= $brtag . get_string('searchsetupdescription', 'search') . $brtag . $brtag;
+
+ // Enable global search.
+ $row = array();
+ $url = new moodle_url("/admin/search.php?query=enableglobalsearch");
+ $row[0] = '1. ' . html_writer::tag('a', get_string('enableglobalsearch', 'admin'),
+ array('href' => $url));
+ $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
+ if (\core_search\manager::is_global_search_enabled()) {
+ $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
+ }
+ $row[1] = $status;
+ $table->data[] = $row;
+
+ // Select a search engine.
+ $row = array();
+ $url = new moodle_url('/admin/settings.php?section=manageglobalsearch#admin-searchengine');
+ $row[0] = '2. ' . html_writer::tag('a', get_string('selectsearchengine', 'admin'),
+ array('href' => $url));
+
+ $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
+ if (!empty($CFG->searchengine)) {
+ $status = html_writer::tag('span', get_string('pluginname', 'search_' . $CFG->searchengine),
+ array('class' => 'statusok'));
+
+ }
+ $row[1] = $status;
+ $table->data[] = $row;
+
+ // Available areas.
+ $row = array();
+ $url = new moodle_url('/admin/settings.php?section=manageglobalsearch#id_s_mod_assign_enablesearch_activity');
+ $row[0] = '3. ' . html_writer::tag('a', get_string('enablesearchareas', 'admin'),
+ array('href' => $url));
+
+ $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
+ if ($anyenabled) {
+ $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
+
+ }
+ $row[1] = $status;
+ $table->data[] = $row;
+
+ // Setup search engine.
+ $row = array();
+ if (empty($CFG->searchengine)) {
+ $row[0] = '4. ' . get_string('setupsearchengine', 'admin');
+ $row[1] = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
+ } else {
+ $url = new moodle_url('/admin/settings.php?section=search' . $CFG->searchengine);
+ $row[0] = '4. ' . html_writer::tag('a', get_string('setupsearchengine', 'admin'),
+ array('href' => $url));
+ // Check the engine status.
+ $searchengine = \core_search\manager::search_engine_instance();
+ $serverstatus = $searchengine->is_server_ready();
+ if ($serverstatus === true) {
+ $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
+ } else {
+ $status = html_writer::tag('span', $serverstatus, array('class' => 'statuscritical'));
+ }
+ $row[1] = $status;
+ }
+ $table->data[] = $row;
+
+ // Indexed data.
+ $row = array();
+ $url = new moodle_url('/report/search/index.php#searchindexform');
+ $row[0] = '5. ' . html_writer::tag('a', get_string('indexdata', 'admin'), array('href' => $url));
+ if ($anyindexed) {
+ $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
+ } else {
+ $status = html_writer::tag('span', get_string('no'), array('class' => 'statuscritical'));
+ }
+ $row[1] = $status;
+ $table->data[] = $row;
+
+ $return .= html_writer::table($table);
+
+ return highlight($query, $return);
+ }
+
+}
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search box.
+ *
+ * @module core/search-input
+ * @class search-input
+ * @package core
+ * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 3.1
+ */
+define(['jquery'], function($) {
+
+ /**
+ * This search box div node.
+ *
+ * @private
+ */
+ var wrapper = null;
+
+ /**
+ * Toggles the form visibility.
+ *
+ * @param {Event} ev
+ * @method toggleForm
+ * @private
+ */
+ var toggleForm = function(ev) {
+
+ if (wrapper.hasClass('expanded')) {
+ hideForm();
+ } else {
+ showForm(ev);
+ }
+ };
+
+ /**
+ * Shows the form or submits it depending on the window size.
+ *
+ * @param {Event} ev
+ * @method showForm
+ * @private
+ */
+ var showForm = function(ev) {
+
+ var windowWidth = $(document).width();
+
+ if (windowWidth <= 767 && (ev.type === 'click' || ev.type === 'keydown')) {
+ // Move to the search page when using small window sizes as the input requires too much space.
+ submitForm();
+ return;
+ } else if (windowWidth <= 767) {
+ // Ignore mousedown events in while using small window sizes.
+ return;
+ }
+
+ if (ev.type === 'keydown') {
+ // We don't want to submit the form unless the user hits enter.
+ ev.preventDefault();
+ }
+
+ wrapper.addClass('expanded');
+ wrapper.find('form').addClass('expanded');
+ wrapper.find('input').focus();
+ };
+
+ /**
+ * Hides the form.
+ *
+ * @method hideForm
+ * @private
+ */
+ var hideForm = function() {
+ wrapper.removeClass('expanded');
+ wrapper.find('form').removeClass('expanded');
+ };
+
+ /**
+ * Submits the form.
+ *
+ * @param {Event} ev
+ * @method submitForm
+ * @private
+ */
+ var submitForm = function() {
+ wrapper.find('form').submit();
+ };
+
+ return /** @alias module:core/search-input */ {
+ // Public variables and functions.
+
+ /**
+ * Assigns listeners to the requested select box.
+ *
+ * @method init
+ * @param {Number} id The search wrapper div id
+ */
+ init: function(id) {
+ wrapper = $('#' + id);
+ wrapper.on('click mouseover keydown', 'div', toggleForm);
+ }
+ };
+});
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data indexed event.
+ *
+ * @package core
+ * @copyright 2015 David Monllao - http://www.davidmonllao.com
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when new data has been indexed.
+ *
+ * @package core
+ * @since Moodle 3.1
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_indexed extends base {
+
+ /**
+ * Initialise required event data properties.
+ */
+ protected function init() {
+ $this->data['crud'] = 'c';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Returns localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventsearchindexed');
+ }
+
+ /**
+ * Returns non-localised event description.
+ *
+ * @return string
+ */
+ public function get_description() {
+ if (!empty($this->userid)) {
+ return "The user with id '{$this->userid}' updated the search engine data";
+ } else {
+ return 'The search engine data has been updated';
+ }
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/search/admin.php');
+ }
+}
'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
'calendar_upcoming', 'comments', 'community',
'completionstatus', 'course_list', 'course_overview',
- 'course_summary', 'feedback', 'glossary_random', 'html',
+ 'course_summary', 'feedback', 'globalsearch', 'glossary_random', 'html',
'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
'navigation', 'news_items', 'online_users', 'participants',
'private_files', 'quiz_results', 'recent_activity',
'report' => array(
'backups', 'completion', 'configlog', 'courseoverview', 'eventlist',
- 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance',
- 'usersessions',
+ 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'search',
+ 'security', 'stats', 'performance', 'usersessions'
),
'repository' => array(
'wikimedia', 'youtube'
),
+ 'search' => array(
+ 'solr'
+ ),
+
'scormreport' => array(
'basic',
'interactions',
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Defines classes used for plugin info.
+ *
+ * @package core
+ * @copyright 2015 Daniel Neis
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\plugininfo;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class for search plugins
+ *
+ * @package core
+ * @copyright 2015 Daniel Neis
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search extends base {
+
+ /**
+ * Is uninstall allowed or not.
+ *
+ * @return bool
+ */
+ public function is_uninstall_allowed() {
+ return true;
+ }
+
+ /**
+ * Returns the node name used in admin settings menu for this plugin settings (if applicable).
+ *
+ * @return null|string node name or null if plugin does not create settings node (default)
+ */
+ public function get_settings_section_name() {
+ return 'searchsetting' . $this->name;
+ }
+}
'tags' => array(
'mode' => cache_store::MODE_REQUEST,
'simplekeys' => true,
- )
+ ),
+
+ // Caches search results.
+ 'search_results' => array(
+ 'mode' => cache_store::MODE_SESSION,
+ 'simplekeys' => true,
+ 'staticacceleration' => true,
+ 'staticaccelerationsize' => 3
+ ),
);
array(
'eventname' => '\core\event\user_updated',
'callback' => 'core_badges_observer::profile_criteria_review',
- )
-
+ ),
+ array(
+ 'eventname' => '\core\event\search_indexed',
+ 'callback' => 'core_search_observer::invalidate_caches',
+ ),
);
// List of all events triggered by Moodle can be found using Events list report.
}
}
+/**
+ * Converts editor text input in different formats to plain text.
+ *
+ * @param string $content The text as entered by the user
+ * @param int $contentformat FORMAT_MOODLE, FORMAT_HTML, FORMAT_PLAIN or FORMAT_MARKDOWN
+ * @return string Plain text.
+ */
+function editor_input_to_text($content, $contentformat) {
+
+ switch ($contentformat) {
+ case FORMAT_PLAIN:
+ return $content;
+ case FORMAT_MARKDOWN:
+ $html = markdown_to_html($content);
+ return html_to_text($html, 75, false);
+ default:
+ // FORMAT_HTML and FORMAT_MOODLE.
+ return html_to_text($content, 75, false);
+ }
+}
+
/**
* Base abstract text editor class.
*
self::TYPE_SETTING, null, 'tags');
}
+ // Search.
+ if (!empty($CFG->enableglobalsearch)) {
+ $node = $coursenode->add(get_string('search', 'search'), new moodle_url('/search/index.php'),
+ self::TYPE_SETTING, null, 'search');
+ }
+
if (isloggedin()) {
// Calendar
$calendarurl = new moodle_url('/calendar/view.php', array('view' => 'month'));
return html_writer::tag('ul', implode("\n", $lis), $attrs);
}
+ /**
+ * Returns a search box.
+ *
+ * @param string $id The search box wrapper div id, defaults to an autogenerated one.
+ * @return string HTML with the search form hidden by default.
+ */
+ public function search_box($id = false) {
+ global $CFG;
+
+ // Accessing $CFG directly as using \core_search::is_global_search_enabled would
+ // result in an extra included file for each site, even the ones where global search
+ // is disabled.
+ if (empty($CFG->enableglobalsearch)) {
+ return '';
+ }
+
+ if ($id == false) {
+ $id = uniqid();
+ } else {
+ // Needs to be cleaned, we use it for the input id.
+ $id = clean_param($id, PARAM_ALPHANUMEXT);
+ }
+
+ // JS to animate the form.
+ $this->page->requires->js_call_amd('core/search-input', 'init', array($id));
+
+ $searchicon = html_writer::tag('div', $this->pix_icon('a/search', get_string('search', 'search'), 'moodle'),
+ array('role' => 'button', 'tabindex' => 0));
+ $formattrs = array('class' => 'search-input-form', 'action' => $CFG->wwwroot . '/search/index.php');
+ $inputattrs = array('type' => 'text', 'name' => 'q', 'placeholder' => get_string('search', 'search'),
+ 'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id);
+
+ $contents = html_writer::tag('label', get_string('search', 'search'),
+ array('for' => 'id_q_' . $id, 'class' => 'accesshide')) . html_writer::tag('input', '', $inputattrs);
+ $searchinput = html_writer::tag('form', $contents, $formattrs);
+
+ return html_writer::tag('div', $searchicon . $searchinput, array('class' => 'search-input-wrapper', 'id' => $id));
+ }
+
/**
* Construct a user menu, returning HTML that can be echoed out by a
* layout file.
get_message_processors(false, true, true);
filter_manager::reset_caches();
core_filetypes::reset_caches();
+ \core_search\manager::clear_static();
// Reset static unit test options.
if (class_exists('\availability_date\condition', false)) {
<testsuite name="core_comment_testsuite">
<directory suffix="_test.php">comment/tests</directory>
</testsuite>
+ <testsuite name="core_search_testsuite">
+ <directory suffix="_test.php">search/tests</directory>
+ </testsuite>
<!--Plugin suites: use admin/tool/phpunit/cli/util.php to build phpunit.xml from phpunit.xml.dist with up-to-date list of plugins in current install-->
<!--@plugin_suites_start@-->
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search base class to be extended by search areas.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search\area;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base search implementation.
+ *
+ * Components and plugins interested in filling the search engine
+ * with data should extend this class (or any extension of this class)
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class base {
+
+ /**
+ * The area name as defined in the class name.
+ *
+ * @var string
+ */
+ protected $areaname = null;
+
+ /**
+ * The component frankenstyle name.
+ *
+ * @var string
+ */
+ protected $componentname = null;
+
+ /**
+ * The component type (core or the plugin type).
+ *
+ * @var string
+ */
+ protected $componenttype = null;
+
+ /**
+ * The context levels the search implementation is working on.
+ *
+ * @var array
+ */
+ protected static $levels = [CONTEXT_SYSTEM];
+
+ /**
+ * Constructor.
+ *
+ * @throws \coding_exception
+ * @return void
+ */
+ public final function __construct() {
+
+ $classname = get_class($this);
+
+ // Detect possible issues when defining the class.
+ if (strpos($classname, '\search') === false) {
+ throw new \coding_exception('Search area classes should be located in \PLUGINTYPE_PLUGINNAME\search\AREANAME.');
+ } else if (strpos($classname, '_') === false) {
+ throw new \coding_exception($classname . ' class namespace level 1 should be its component frankenstyle name');
+ }
+
+ $this->areaname = substr(strrchr($classname, '\\'), 1);
+ $this->componentname = substr($classname, 0, strpos($classname, '\\'));
+ $this->areaid = \core_search\manager::generate_areaid($this->componentname, $this->areaname);
+ $this->componenttype = substr($this->componentname, 0, strpos($this->componentname, '_'));
+ }
+
+ /**
+ * Returns context levels property.
+ *
+ * @return int
+ */
+ public static function get_levels() {
+ return static::$levels;
+ }
+
+ /**
+ * Returns the area id.
+ *
+ * @return string
+ */
+ public function get_area_id() {
+ return $this->areaid;
+ }
+
+ /**
+ * Returns the moodle component name.
+ *
+ * It might be the plugin name (whole frankenstyle name) or the core subsystem name.
+ *
+ * @return string
+ */
+ public function get_component_name() {
+ return $this->componentname;
+ }
+
+ /**
+ * Returns the component type.
+ *
+ * It might be a plugintype or 'core' for core subsystems.
+ *
+ * @return string
+ */
+ public function get_component_type() {
+ return $this->componenttype;
+ }
+
+ /**
+ * Returns the area visible name.
+ *
+ * @param bool $lazyload Usually false, unless when in admin settings.
+ * @return string
+ */
+ public function get_visible_name($lazyload = false) {
+ return get_string('search:' . $this->areaname, $this->componentname, null, $lazyload);
+ }
+
+ /**
+ * Returns the config var name.
+ *
+ * It depends on whether it is a moodle subsystem or a plugin as plugin-related config should remain in their own scope.
+ *
+ * @return string Config var path including the plugin (or component) and the varname
+ */
+ public function get_config_var_name() {
+
+ if ($this->componenttype === 'core') {
+ // Core subsystems config in search.
+ return array('search', $this->areaid);
+ }
+
+ // Plugins config in the plugin scope.
+ return array($this->componentname, 'search_' . $this->areaname);
+ }
+
+ /**
+ * Is the search component enabled by the system administrator?
+ *
+ * @return bool
+ */
+ public function is_enabled() {
+ list($componentname, $varname) = $this->get_config_var_name();
+ return (bool)get_config($componentname, 'enable' . $varname);
+ }
+
+ /**
+ * Returns a recordset ordered by modification date ASC.
+ *
+ * Each record can include any data self::get_document might need but it must:
+ * - Include an 'id' field: Unique identifier (in this area's scope) of a document to index in the search engine
+ * If the indexed content field can contain embedded files, the 'id' value should match the filearea itemid.
+ * - Only return data modified since $modifiedfrom, including $modifiedform to prevent
+ * some records from not being indexed (e.g. your-timemodified-fieldname >= $modifiedfrom)
+ * - Order the returned data by time modified in ascending order, as \core_search::manager will need to store the modified time
+ * of the last indexed document.
+ *
+ * @param int $modifiedfrom
+ * @return moodle_recordset
+ */
+ abstract public function get_recordset_by_timestamp($modifiedfrom = 0);
+
+ /**
+ * Returns the document related with the provided record.
+ *
+ * This method receives a record with the document id and other info returned by get_recordset_by_timestamp
+ * or get_recordset_by_contexts that might be useful here. The idea is to restrict database queries to
+ * minimum as this function will be called for each document to index. As an alternative, use cached data.
+ *
+ * Internally it should use \core_search\document to standarise the documents before sending them to the search engine.
+ *
+ * Search areas should send plain text to the search engine, use the following function to convert any user
+ * input data to plain text: {@link editor_input_to_text}
+ *
+ * @param \stdClass $record A record containing, at least, the indexed document id and a modified timestamp
+ * @return \core_search\document
+ */
+ abstract public function get_document($record);
+
+ /**
+ * Can the current user see the document.
+ *
+ * @param int $id The internal search area entity id.
+ * @return bool True if the user can see it, false otherwise
+ */
+ abstract public function check_access($id);
+
+ /**
+ * Returns a url to the document, it might match self::get_context_url().
+ *
+ * @param \core_search\document $doc
+ * @return \moodle_url
+ */
+ abstract public function get_doc_url(\core_search\document $doc);
+
+ /**
+ * Returns a url to the document context.
+ *
+ * @param \core_search\document $doc
+ * @return \moodle_url
+ */
+ abstract public function get_context_url(\core_search\document $doc);
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search area base class for activities.
+ *
+ * @package core_search
+ * @copyright 2016 Dan Poltawski
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search\area;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base implementation for activity modules.
+ *
+ * @package core_search
+ * @copyright 2016 Dan Poltawski
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class base_activity extends base_mod {
+
+ /**
+ * @var string The time modified field name.
+ *
+ * Activities not using timemodified as field name
+ * can overwrite this constant.
+ */
+ const MODIFIED_FIELD_NAME = 'timemodified';
+
+ /**
+ * The context levels the search area is working on.
+ * @var array
+ */
+ protected static $levels = [CONTEXT_MODULE];
+
+ /**
+ * Returns recordset containing required data for indexing activities.
+ *
+ * @param int $modifiedfrom timestamp
+ * @return \moodle_recordset
+ */
+ public function get_recordset_by_timestamp($modifiedfrom = 0) {
+ global $DB;
+ return $DB->get_recordset_select($this->get_module_name(), static::MODIFIED_FIELD_NAME . ' >= ?', array($modifiedfrom));
+ }
+
+ /**
+ * Returns the document associated with this activity.
+ *
+ * This default implementation for activities sets the activity name to title and the activity intro to
+ * content. Any activity can overwrite this function if it is interested in setting other fields than the
+ * default ones, or to fill description optional fields with extra stuff.
+ *
+ * @param stdClass $record
+ * @return \core_search\document
+ */
+ public function get_document($record) {
+
+ try {
+ $cm = $this->get_cm($this->get_module_name(), $record->id, $record->course);
+ $context = \context_module::instance($cm->id);
+ } catch (\dml_missing_record_exception $ex) {
+ // Notify it as we run here as admin, we should see everything.
+ debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' .
+ $ex->getMessage(), DEBUG_DEVELOPER);
+ return false;
+ } catch (\dml_exception $ex) {
+ // Notify it as we run here as admin, we should see everything.
+ debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document: ' . $ex->getMessage(), DEBUG_DEVELOPER);
+ return false;
+ }
+
+ // Prepare associative array with data from DB.
+ $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+ $doc->set('title', $record->name);
+ $doc->set('content', editor_input_to_text($record->intro, $record->introformat));
+ $doc->set('contextid', $context->id);
+ $doc->set('type', \core_search\manager::TYPE_TEXT);
+ $doc->set('courseid', $record->course);
+ $doc->set('modified', $record->{static::MODIFIED_FIELD_NAME});
+
+ return $doc;
+ }
+
+ /**
+ * Whether the user can access the document or not.
+ *
+ * @throws \dml_missing_record_exception
+ * @throws \dml_exception
+ * @param int $id The activity instance id.
+ * @return bool
+ */
+ public function check_access($id) {
+ global $DB;
+
+ $activity = $this->get_activity($id);
+ try {
+ $cminfo = $this->get_cm($this->get_module_name(), $activity->id, $activity->course);
+ $cminfo->get_course_module_record();
+ } catch (\dml_missing_record_exception $ex) {
+ return \core_search\manager::ACCESS_DELETED;
+ } catch (\dml_exception $ex) {
+ return \core_search\manager::ACCESS_DENIED;
+ }
+
+ // Recheck uservisible although it should have already been checked in core_search.
+ if ($cminfo->uservisible === false) {
+ return \core_search\manager::ACCESS_DENIED;
+ }
+
+ return \core_search\manager::ACCESS_GRANTED;
+ }
+
+ /**
+ * Link to the module instance.
+ *
+ * @param \core_search\document $doc
+ * @return \moodle_url
+ */
+ public function get_doc_url(\core_search\document $doc) {;
+ return $this->get_context_url($doc);
+ }
+
+ /**
+ * Link to the module instance.
+ *
+ * @param \core_search\document $doc
+ * @return \moodle_url
+ */
+ public function get_context_url(\core_search\document $doc) {
+ $cminfo = $this->get_cm($this->get_module_name(), strval($doc->get('itemid')), $doc->get('courseid'));
+ return new \moodle_url('/mod/' . $this->get_module_name() . '/view.php', array('id' => $cminfo->id));
+ }
+
+ /**
+ * Returns the module name.
+ *
+ * @return string
+ */
+ protected function get_module_name() {
+ return substr($this->componentname, 4);
+ }
+
+ /**
+ * Returns an activity instance. Internally uses the class component to know which activity module should be retrieved.
+ *
+ * @param int $instanceid
+ * @return stdClass
+ */
+ protected function get_activity($instanceid) {
+ global $DB;
+
+ if (empty($this->activitiesdata[$this->get_module_name()][$instanceid])) {
+ $this->activitiesdata[$this->get_module_name()][$instanceid] = $DB->get_record($this->get_module_name(),
+ array('id' => $instanceid), '*', MUST_EXIST);
+ }
+ return $this->activitiesdata[$this->get_module_name()][$instanceid];
+
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search area base class for areas working at module level.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search\area;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base implementation for search areas working at module level.
+ *
+ * Even if the search area works at multiple levels, if module is one of these levels
+ * it should extend this class, as this class provides helper methods for module level search management.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class base_mod extends base {
+
+ /**
+ * The context levels the search area is working on.
+ *
+ * This can be overwriten by the search area if it works at multiple
+ * levels.
+ *
+ * @var array
+ */
+ protected static $levels = [CONTEXT_MODULE];
+
+ /**
+ * Gets the course module for the required instanceid + modulename.
+ *
+ * The returned data depends on the logged user, when calling this through
+ * self::get_document the admin user is used so everything would be returned.
+ *
+ * No need more internal caching here, modinfo is already cached.
+ *
+ * @throws \dml_missing_record_exception
+ * @param string $modulename The module name
+ * @param int $instanceid Module instance id (depends on the module)
+ * @param int $courseid Helps speeding up things
+ * @return \cm_info
+ */
+ protected function get_cm($modulename, $instanceid, $courseid) {
+
+ $modinfo = get_fast_modinfo($courseid);
+
+ // Hopefully not many, they are indexed by cmid.
+ $instances = $modinfo->get_instances_of($modulename);
+ foreach ($instances as $cminfo) {
+ if ($cminfo->instance === $instanceid) {
+ return $cminfo;
+ }
+ }
+
+ // Nothing found.
+ throw new \dml_missing_record_exception($modulename);
+
+ return $cm;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Document representation.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Represents a document to index.
+ *
+ * Note that, if you are writting a search engine and you want to change \core_search\document
+ * behaviour, you can overwrite this class, will be automatically loaded from \search_YOURENGINE\document.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class document implements \renderable, \templatable {
+
+ /**
+ * @var array $data The document data.
+ */
+ protected $data = array();
+
+ /**
+ * @var array Extra data needed to render the document.
+ */
+ protected $extradata = array();
+
+ /**
+ * @var \moodle_url Link to the document.
+ */
+ protected $docurl = null;
+
+ /**
+ * @var \moodle_url Link to the document context.
+ */
+ protected $contexturl = null;
+
+ /**
+ * @var int|null The content field filearea.
+ */
+ protected $contentfilearea = null;
+
+ /**
+ * @var int|null The content field itemid.
+ */
+ protected $contentitemid = null;
+
+ /**
+ * All required fields any doc should contain.
+ *
+ * We have to choose a format to specify field types, using solr format as we have to choose one and solr is the
+ * default search engine.
+ *
+ * Search engine plugins are responsible of setting their appropriate field types and map these naming to whatever format
+ * they need.
+ *
+ * @var array
+ */
+ protected static $requiredfields = array(
+ 'id' => array(
+ 'type' => 'string',
+ 'stored' => true,
+ 'indexed' => false
+ ),
+ 'itemid' => array(
+ 'type' => 'int',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ 'title' => array(
+ 'type' => 'string',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ 'content' => array(
+ 'type' => 'string',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ 'contextid' => array(
+ 'type' => 'int',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ 'areaid' => array(
+ 'type' => 'string',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ 'type' => array(
+ 'type' => 'int',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ 'courseid' => array(
+ 'type' => 'int',
+ 'stored' => true,
+ 'indexed' => false
+ ),
+ 'modified' => array(
+ 'type' => 'tdate',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ );
+
+ /**
+ * All optional fields docs can contain.
+ *
+ * Although it matches solr fields format, this is just to define the field types. Search
+ * engine plugins are responsible of setting their appropriate field types and map these
+ * naming to whatever format they need.
+ *
+ * @var array
+ */
+ protected static $optionalfields = array(
+ 'userid' => array(
+ 'type' => 'int',
+ 'stored' => true,
+ 'indexed' => false
+ ),
+ 'description1' => array(
+ 'type' => 'string',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ 'description2' => array(
+ 'type' => 'string',
+ 'stored' => true,
+ 'indexed' => true
+ ),
+ );
+
+ /**
+ * We ensure that the document has a unique id across search areas.
+ *
+ * @param int $itemid An id unique to the search area
+ * @param string $componentname The search area component Frankenstyle name
+ * @param string $areaname The area name (the search area class name)
+ * @return void
+ */
+ public function __construct($itemid, $componentname, $areaname) {
+
+ if (!is_numeric($itemid)) {
+ throw new \coding_exception('The itemid should be an integer');
+ }
+
+ $this->data['areaid'] = \core_search\manager::generate_areaid($componentname, $areaname);
+ $this->data['id'] = $this->data['areaid'] . '-' . $itemid;
+ $this->data['itemid'] = intval($itemid);
+ }
+
+ /**
+ * Setter.
+ *
+ * Basic checkings to prevent common issues.
+ *
+ * If the field is a string tags will be stripped, if it is an integer or a date it
+ * will be casted to a PHP integer. tdate fields values are expected to be timestamps.
+ *
+ * @throws \coding_exception
+ * @param string $fieldname The field name
+ * @param string|int $value The value to store
+ * @return string|int The stored value
+ */
+ public function set($fieldname, $value) {
+
+ if (!empty(static::$requiredfields[$fieldname])) {
+ $fielddata = static::$requiredfields[$fieldname];
+ } else if (!empty(static::$optionalfields[$fieldname])) {
+ $fielddata = static::$optionalfields[$fieldname];
+ }
+
+ if (empty($fielddata)) {
+ throw new \coding_exception('"' . $fieldname . '" field does not exist.');
+ }
+
+ // tdate fields should be set as timestamps, later they might be converted to
+ // a date format, it depends on the search engine.
+ if (($fielddata['type'] === 'int' || $fielddata['type'] === 'tdate') && !is_numeric($value)) {
+ throw new \coding_exception('"' . $fieldname . '" value should be an integer and its value is "' . $value . '"');
+ }
+
+ // We want to be strict here, there might be engines that expect us to
+ // provide them data with the proper type already set.
+ if ($fielddata['type'] === 'int' || $fielddata['type'] === 'tdate') {
+ $this->data[$fieldname] = intval($value);
+ } else {
+ $this->data[$fieldname] = trim($value, "\r\n");
+ }
+
+ return $this->data[$fieldname];
+ }
+
+ /**
+ * Sets data to this->extradata
+ *
+ * This data can be retrieved using \core_search\document->get($fieldname).
+ *
+ * @param string $fieldname
+ * @param string $value
+ * @return void
+ */
+ public function set_extra($fieldname, $value) {
+ $this->extradata[$fieldname] = $value;
+ }
+
+ /**
+ * Getter.
+ *
+ * Use self::is_set if you are not sure if this field is set or not
+ * as otherwise it will trigger a \coding_exception
+ *
+ * @throws \coding_exception
+ * @param string $field
+ * @return string|int
+ */
+ public function get($field) {
+
+ if (isset($this->data[$field])) {
+ return $this->data[$field];
+ }
+
+ // Fallback to extra data.
+ if (isset($this->extradata[$field])) {
+ return $this->extradata[$field];
+ }
+
+ throw new \coding_exception('Field "' . $field . '" is not set in the document');
+ }
+
+ /**
+ * Checks if a field is set.
+ *
+ * @param string $field
+ * @return bool
+ */
+ public function is_set($field) {
+ return (isset($this->data[$field]) || isset($this->extradata[$field]));
+ }
+
+ /**
+ * Returns all default fields definitions.
+ *
+ * @return array
+ */
+ public static function get_default_fields_definition() {
+ return static::$requiredfields + static::$optionalfields;
+ }
+
+ /**
+ * Formats the timestamp preparing the time fields to be inserted into the search engine.
+ *
+ * By default it just returns a timestamp so any search engine could just store integers
+ * and use integers comparison to get documents between x and y timestamps, but search
+ * engines might be interested in using their own field formats. They can do it extending
+ * this class in \search_xxx\document.
+ *
+ * @param int $timestamp
+ * @return string
+ */
+ public static function format_time_for_engine($timestamp) {
+ return $timestamp;
+ }
+
+ /**
+ * Formats a string value for the search engine.
+ *
+ * Search engines may overwrite this method to apply restrictions, like limiting the size.
+ * The default behaviour is just returning the string.
+ *
+ * @param string $string
+ * @return string
+ */
+ public static function format_string_for_engine($string) {
+ return $string;
+ }
+
+ /**
+ * Returns a timestamp from the value stored in the search engine.
+ *
+ * By default it just returns a timestamp so any search engine could just store integers
+ * and use integers comparison to get documents between x and y timestamps, but search
+ * engines might be interested in using their own field formats. They should do it extending
+ * this class in \search_xxx\document.
+ *
+ * @param string $time
+ * @return int
+ */
+ public static function import_time_from_engine($time) {
+ return $time;
+ }
+
+ /**
+ * Returns how text is returned from the search engine.
+ *
+ * @return int
+ */
+ protected function get_text_format() {
+ return FORMAT_PLAIN;
+ }
+
+ /**
+ * Fills the document with data coming from the search engine.
+ *
+ * @throws \core_search\engine_exception
+ * @param array $docdata
+ * @return void
+ */
+ public function set_data_from_engine($docdata) {
+ $fields = static::$requiredfields + static::$optionalfields;
+ foreach ($fields as $fieldname => $field) {
+
+ // Optional params might not be there.
+ if (isset($docdata[$fieldname])) {
+ if ($field['type'] === 'tdate') {
+ // Time fields may need a preprocessing.
+ $this->set($fieldname, static::import_time_from_engine($docdata[$fieldname]));
+ } else {
+ // No way we can make this work if there is any multivalue field.
+ if (is_array($docdata[$fieldname])) {
+ throw new \core_search\engine_exception('multivaluedfield', 'search_solr', '', $fieldname);
+ }
+ $this->set($fieldname, $docdata[$fieldname]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the document url.
+ *
+ * @param \moodle_url $url
+ * @return void
+ */
+ public function set_doc_url(\moodle_url $url) {
+ $this->docurl = $url;
+ }
+
+ /**
+ * Gets the url to the doc.
+ *
+ * @return \moodle_url
+ */
+ public function get_doc_url() {
+ return $this->docurl;
+ }
+
+ public function set_context_url(\moodle_url $url) {
+ $this->contexturl = $url;
+ }
+
+ /**
+ * Gets the url to the context.
+ *
+ * @return \moodle_url
+ */
+ public function get_context_url() {
+ return $this->contexturl;
+ }
+
+ /**
+ * Returns the document ready to submit to the search engine.
+ *
+ * @throws \coding_exception
+ * @return array
+ */
+ public function export_for_engine() {
+
+ // We don't want to affect the document instance.
+ $data = $this->data;
+
+ // Apply specific engine-dependant formats and restrictions.
+ foreach (static::$requiredfields as $fieldname => $field) {
+
+ // We also check that we have everything we need.
+ if (!isset($data[$fieldname])) {
+ throw new \coding_exception('Missing "' . $fieldname . '" field in document with id "' . $this->data['id'] . '"');
+ }
+
+ if ($field['type'] === 'tdate') {
+ // Overwrite the timestamp with the engine dependant format.
+ $data[$fieldname] = static::format_time_for_engine($data[$fieldname]);
+ } else if ($field['type'] === 'string') {
+ // Overwrite the timestamp with the engine dependant format.
+ $data[$fieldname] = static::format_string_for_engine($data[$fieldname]);
+ }
+ }
+
+ foreach (static::$optionalfields as $fieldname => $field) {
+ if (!isset($data[$fieldname])) {
+ continue;
+ }
+ if ($field['type'] === 'tdate') {
+ // Overwrite the timestamp with the engine dependant format.
+ $data[$fieldname] = static::format_time_for_engine($data[$fieldname]);
+ } else if ($field['type'] === 'string') {
+ // Overwrite the timestamp with the engine dependant format.
+ $data[$fieldname] = static::format_string_for_engine($data[$fieldname]);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Export the document data to be used as a template context.
+ *
+ * Adding more info than the required one as people might be interested in extending the template.
+ *
+ * Although content is a required field when setting up the document, it accepts '' (empty) values
+ * as they may be the result of striping out HTML.
+ *
+ * @param renderer_base $output The renderer.
+ * @return array
+ */
+ public function export_for_template(\renderer_base $output) {
+
+ list($componentname, $areaname) = \core_search\manager::extract_areaid_parts($this->get('areaid'));
+
+ $data = [
+ 'courseurl' => new \moodle_url('/course/view.php?id=' . $this->get('courseid')),
+ 'coursefullname' => format_string($this->get('coursefullname'), true, array('context' => $this->get('contextid'))),
+ 'modified' => userdate($this->get('modified')),
+ 'title' => format_string($this->get('title'), true, array('context' => $this->get('contextid'))),
+ 'docurl' => $this->get_doc_url(),
+ 'content' => $this->is_set('content') ? $this->format_text($this->get('content')) : null,
+ 'contexturl' => $this->get_context_url(),
+ 'description1' => $this->is_set('description1') ? $this->format_text($this->get('description1')) : null,
+ 'description2' => $this->is_set('description2') ? $this->format_text($this->get('description2')) : null,
+ ];
+
+ if ($this->is_set('userid')) {
+ $data['userurl'] = new \moodle_url('/user/view.php', array('id' => $this->get('userid'), 'course' => $this->get('courseid')));
+ $data['userfullname'] = format_string($this->get('userfullname'), true, array('context' => $this->get('contextid')));
+ }
+
+ return $data;
+ }
+
+ /**
+ * Formats a text string coming from the search engine.
+ *
+ * By default just return the text as it is:
+ * - Search areas are responsible of sending just plain data, the search engine may
+ * append HTML or markdown to it (highlighing for example).
+ * - The view is responsible of shortening the text if it is too big
+ *
+ * @param string $text Text to format
+ * @return string HTML text to be renderer
+ */
+ protected function format_text($text) {
+ return format_text($text, $this->get_text_format(), array('context' => $this->get('contextid')));
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search documents factory.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search document factory.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class document_factory {
+
+ /**
+ * The document class used by search engines.
+ *
+ * Defined as an array to prevent unexpected caching issues, it should only contain one search
+ * engine as only one search engine will be used during a request. This might change during
+ * testing, remember to use document_factory::clean_statics in that case.
+ *
+ * @var array
+ */
+ protected static $docclassnames = array();
+
+ /**
+ * Returns the appropiate document object as it depends on the engine.
+ *
+ * @param int $itemid Document itemid
+ * @param string $componentname Document component name
+ * @param string $areaname Document area name
+ * @param \core_search\engine $engine Falls back to the search engine in use.
+ * @return \core_search\document Base document or the engine implementation.
+ */
+ public static function instance($itemid, $componentname, $areaname, $engine = false) {
+
+ if ($engine === false) {
+ $search = \core_search\manager::instance();
+ $engine = $search->get_engine();
+ }
+
+ $pluginname = $engine->get_plugin_name();
+
+ if (!empty(self::$docclassnames[$pluginname])) {
+ return new self::$docclassnames[$pluginname]($itemid, $componentname, $areaname);
+ }
+
+ self::$docclassnames[$pluginname] = $engine->get_document_classname();
+
+ return new self::$docclassnames[$pluginname]($itemid, $componentname, $areaname);
+ }
+
+ /**
+ * Clears static vars.
+ *
+ * @return void
+ */
+ public static function clean_static() {
+ self::$docclassnames = array();
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Base class for search engines.
+ *
+ * All search engines must extend this class.
+ *
+ * @package core_search
+ * @copyright 2015 Daniel Neis
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class for search engines.
+ *
+ * All search engines must extend this class.
+ *
+ * @package core_search
+ * @copyright 2015 Daniel Neis
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class engine {
+
+ /**
+ * The search engine configuration.
+ *
+ * @var stdClass
+ */
+ protected $config = null;
+
+ /**
+ * Last executed query error, if there was any.
+ * @var string
+ */
+ protected $queryerror = null;
+
+ /**
+ * @var array Internal cache.
+ */
+ protected $cachedareas = array();
+
+ /**
+ * @var array Internal cache.
+ */
+ protected $cachedcourses = array();
+
+ /**
+ * User data required to show their fullnames. Indexed by userid.
+ *
+ * @var stdClass[]
+ */
+ protected static $cachedusers = array();
+
+ /**
+ * @var string Frankenstyle plugin name.
+ */
+ protected $pluginname = null;
+
+ /**
+ * Initialises the search engine configuration.
+ *
+ * Search engine availability should be checked separately.
+ *
+ * @see self::is_installed
+ * @see self::is_server_ready
+ * @return void
+ */
+ public function __construct() {
+
+ $classname = get_class($this);
+ if (strpos($classname, '\\') === false) {
+ throw new \coding_exception('"' . $classname . '" class should specify its component namespace and it should be named engine.');
+ } else if (strpos($classname, '_') === false) {
+ throw new \coding_exception('"' . $classname . '" class namespace should be its frankenstyle name');
+ }
+
+ // This is search_xxxx config.
+ $this->pluginname = substr($classname, 0, strpos($classname, '\\'));
+ if ($config = get_config($this->pluginname)) {
+ $this->config = $config;
+ } else {
+ $this->config = new stdClass();
+ }
+ }
+
+ /**
+ * Returns a course instance checking internal caching.
+ *
+ * @param int $courseid
+ * @return stdClass
+ */
+ protected function get_course($courseid) {
+ if (!empty($this->cachedcourses[$courseid])) {
+ return $this->cachedcourses[$courseid];
+ }
+
+ // No need to clone, only read.
+ $this->cachedcourses[$courseid] = get_course($courseid, false);
+
+ return $this->cachedcourses[$courseid];
+ }
+
+ /**
+ * Returns user data checking the internal static cache.
+ *
+ * Including here the minimum required user information as this may grow big.
+ *
+ * @param int $userid
+ * @return stdClass
+ */
+ public function get_user($userid) {
+ global $DB;
+
+ if (empty(self::$cachedusers[$userid])) {
+ $fields = get_all_user_name_fields(true);
+ self::$cachedusers[$userid] = $DB->get_record('user', array('id' => $userid), 'id, ' . $fields);
+ }
+ return self::$cachedusers[$userid];
+ }
+
+ /**
+ * Returns a search instance of the specified area checking internal caching.
+ *
+ * @param string $areaid Area id
+ * @return \core_search\area\base
+ */
+ protected function get_search_area($areaid) {
+
+ if (isset($this->cachedareas[$areaid]) && $this->cachedareas[$areaid] === false) {
+ // We already checked that area and it is not available.
+ return false;
+ }
+
+ if (!isset($this->cachedareas[$areaid])) {
+ // First result that matches this area.
+
+ $this->cachedareas[$areaid] = \core_search\manager::get_search_area($areaid);
+ if ($this->cachedareas[$areaid] === false) {
+ // The area does not exist or it is not available any more.
+
+ $this->cachedareas[$areaid] = false;
+ return false;
+ }
+
+ if (!$this->cachedareas[$areaid]->is_enabled()) {
+ // We skip the area if it is not enabled.
+
+ // Marking it as false so next time we don' need to check it again.
+ $this->cachedareas[$areaid] = false;
+
+ return false;
+ }
+ }
+
+ return $this->cachedareas[$areaid];
+ }
+
+ /**
+ * Returns a document instance prepared to be rendered.
+ *
+ * @param \core_search\area\base $searcharea
+ * @param array $docdata
+ * @return \core_search\document
+ */
+ protected function to_document(\core_search\area\base $searcharea, $docdata) {
+
+ list($componentname, $areaname) = \core_search\manager::extract_areaid_parts($docdata['areaid']);
+ $doc = \core_search\document_factory::instance($docdata['itemid'], $componentname, $areaname, $this);
+ $doc->set_data_from_engine($docdata);
+ $doc->set_doc_url($searcharea->get_doc_url($doc));
+ $doc->set_context_url($searcharea->get_context_url($doc));
+
+ // Uses the internal caches to get required data needed to render the document later.
+ $course = $this->get_course($doc->get('courseid'));
+ $doc->set_extra('coursefullname', $course->fullname);
+
+ if ($doc->is_set('userid')) {
+ $user = $this->get_user($doc->get('userid'));
+ $doc->set_extra('userfullname', fullname($user));
+ }
+
+ return $doc;
+ }
+
+ /**
+ * Returns the plugin name.
+ *
+ * @return string Frankenstyle plugin name.
+ */
+ public function get_plugin_name() {
+ return $this->pluginname;
+ }
+
+ /**
+ * Gets the document class used by this search engine.
+ *
+ * Search engines can overwrite \core_search\document with \search_ENGINENAME\document class.
+ *
+ * Looks for a document class in the current search engine namespace, falling back to \core_search\document.
+
+ * Publicly available because search areas do not have access to the engine details,
+ * \core_search\document_factory accesses this function.
+ *
+ * @return string
+ */
+ public function get_document_classname() {
+ $classname = $this->pluginname . '\\document';
+ if (!class_exists($classname)) {
+ $classname = '\\core_search\\document';
+ }
+ return $classname;
+ }
+
+ /**
+ * Optimizes the search engine.
+ *
+ * Should be overwritten if the search engine can optimize its contents.
+ *
+ * @return void
+ */
+ public function optimize() {
+ // Nothing by default.
+ }
+
+ /**
+ * Does the system satisfy all the requirements.
+ *
+ * Should be overwritten if the search engine has any system dependencies
+ * that needs to be checked.
+ *
+ * @return bool
+ */
+ public function is_installed() {
+ return true;
+ }
+
+ /**
+ * Returns any error reported by the search engine when executing the provided query.
+ *
+ * It should be called from static::execute_query when an exception is triggered.
+ *
+ * @return string
+ */
+ public function get_query_error() {
+ return $this->queryerror;
+ }
+
+ /**
+ * Clears the current query error value.
+ *
+ * @return void
+ */
+ public function clear_query_error() {
+ $this->queryerror = null;
+ }
+
+ /**
+ * Is the server ready to use?
+ *
+ * This should also check that the search engine configuration is ok.
+ *
+ * @return true|string Returns true if all good or an error string.
+ */
+ abstract function is_server_ready();
+
+ /**
+ * Adds a document to the search engine.
+ *
+ * @param array $doc
+ * @return void
+ */
+ abstract function add_document($doc);
+
+ /**
+ * Commits changes to the server.
+ *
+ * @return void
+ */
+ abstract function commit();
+
+ /**
+ * Executes the query on the engine.
+ *
+ * @param stdClass $filters Query and filters to apply.
+ * @param array $usercontexts Contexts where the user has access. True if the user can access all contexts.
+ * @return \core_search\document[] Results or false if no results
+ */
+ abstract function execute_query($filters, $usercontexts);
+
+ /**
+ * Delete all documents.
+ *
+ * @param string $areaid To filter by area
+ * @return void
+ */
+ abstract function delete($areaid = null);
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search engine exceptions.
+ *
+ * @package core_search
+ * @copyright David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Dummy class to identify search engine exceptions.
+ *
+ * @package core_search
+ * @copyright David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class engine_exception extends \moodle_exception {
+}
+
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search subsystem manager.
+ *
+ * @package core_search
+ * @copyright Prateek Sachan {@link http://prateeksachan.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->dirroot . '/lib/accesslib.php');
+
+/**
+ * Search subsystem manager.
+ *
+ * @package core_search
+ * @copyright Prateek Sachan {@link http://prateeksachan.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+ /**
+ * @var int Text contents.
+ */
+ const TYPE_TEXT = 1;
+
+ /**
+ * @var int User can not access the document.
+ */
+ const ACCESS_DENIED = 0;
+
+ /**
+ * @var int User can access the document.
+ */
+ const ACCESS_GRANTED = 1;
+
+ /**
+ * @var int The document was deleted.
+ */
+ const ACCESS_DELETED = 2;
+
+ /**
+ * @var int Maximum number of results that will be retrieved from the search engine.
+ */
+ const MAX_RESULTS = 100;
+
+ /**
+ * @var int Number of results per page.
+ */
+ const DISPLAY_RESULTS_PER_PAGE = 10;
+
+ /**
+ * @var \core_search\area\base[] Enabled search areas.
+ */
+ protected static $enabledsearchareas = null;
+
+ /**
+ * @var \core_search\area\base[] All system search areas.
+ */
+ protected static $allsearchareas = null;
+
+ /**
+ * @var \core_search\manager
+ */
+ protected static $instance = null;
+
+ /**
+ * @var \core_search\engine
+ */
+ protected $engine = null;
+
+ /**
+ * Constructor, use \core_search\manager::instance instead to get a class instance.
+ *
+ * @param \core_search\area\base The search engine to use
+ */
+ public function __construct($engine) {
+ $this->engine = $engine;
+ }
+
+ /**
+ * Returns an initialised \core_search instance.
+ *
+ * It requires global search to be enabled. Use \core_search\manager::is_global_search_enabled
+ * to verify it is enabled.
+ *
+ * @throws \moodle_exception
+ * @throws \core_search\engine_exception
+ * @return \core_search\manager
+ */
+ public static function instance() {
+ global $CFG;
+
+ // One per request, this should be purged during testing.
+ if (static::$instance !== null) {
+ return static::$instance;
+ }
+
+ if (!static::is_global_search_enabled()) {
+ throw new \moodle_exception('globalsearchdisabled', 'search');
+ }
+
+ if (!$engine = static::search_engine_instance()) {
+ throw new \core_search\engine_exception('enginenotfound', 'search', '', $CFG->searchengine);
+ }
+
+ if (!$engine->is_installed()) {
+ throw new \core_search\engine_exception('enginenotinstalled', 'search', '', $CFG->searchengine);
+ }
+
+ $serverstatus = $engine->is_server_ready();
+ if ($serverstatus !== true) {
+ // Error message with no details as this is an exception that any user may find if the server crashes.
+ throw new \core_search\engine_exception('engineserverstatus', 'search');
+ }
+
+ static::$instance = new \core_search\manager($engine);
+ return static::$instance;
+ }
+
+ /**
+ * Returns whether global search is enabled or not.
+ *
+ * @return bool
+ */
+ public static function is_global_search_enabled() {
+ global $CFG;
+ return !empty($CFG->enableglobalsearch);
+ }
+
+ /**
+ * Returns an instance of the search engine.
+ *
+ * @return \core_search\engine
+ */
+ public static function search_engine_instance() {
+ global $CFG;
+
+ $classname = '\\search_' . $CFG->searchengine . '\\engine';
+ if (!class_exists($classname)) {
+ return false;
+ }
+
+ return new $classname();
+ }
+
+ /**
+ * Returns the search engine.
+ *
+ * @return \core_search\engine
+ */
+ public function get_engine() {
+ return $this->engine;
+ }
+
+ /**
+ * Returns a search area class name.
+ *
+ * @param string $areaid
+ * @return string
+ */
+ protected static function get_area_classname($areaid) {
+ list($componentname, $areaname) = static::extract_areaid_parts($areaid);
+ return '\\' . $componentname . '\\search\\' . $areaname;
+ }
+
+ /**
+ * Returns a new area search indexer instance.
+ *
+ * @param string $areaid
+ * @return \core_search\area\base|bool False if the area is not available.
+ */
+ public static function get_search_area($areaid) {
+
+ // Try both caches, it does not matter where it comes from.
+ if (!empty(static::$allsearchareas[$areaid])) {
+ return static::$allsearchareas[$areaid];
+ }
+ if (!empty(static::$enabledsearchareas[$areaid])) {
+ return static::$enabledsearchareas[$areaid];
+ }
+
+ $classname = static::get_area_classname($areaid);
+ if (class_exists($classname)) {
+ return new $classname();
+ }
+
+ return false;
+ }
+
+ /**
+ * Return the list of available search areas.
+ *
+ * @param bool $enabled Return only the enabled ones.
+ * @return \core_search\area\base[]
+ */
+ public static function get_search_areas_list($enabled = false) {
+
+ // Two different arrays, we don't expect these arrays to be big.
+ if (!$enabled && static::$allsearchareas !== null) {
+ return static::$allsearchareas;
+ } else if ($enabled && static::$enabledsearchareas !== null) {
+ return static::$enabledsearchareas;
+ }
+
+ $searchareas = array();
+
+ $plugintypes = \core_component::get_plugin_types();
+ foreach ($plugintypes as $plugintype => $unused) {
+ $plugins = \core_component::get_plugin_list($plugintype);
+ foreach ($plugins as $pluginname => $pluginfullpath) {
+
+ $componentname = $plugintype . '_' . $pluginname;
+ $searchclasses = \core_component::get_component_classes_in_namespace($componentname, 'search');
+ foreach ($searchclasses as $classname => $classpath) {
+ $areaname = substr(strrchr($classname, '\\'), 1);
+ $areaid = static::generate_areaid($componentname, $areaname);
+ $searchclass = new $classname();
+ if (!$enabled || ($enabled && $searchclass->is_enabled())) {
+ $searchareas[$areaid] = $searchclass;
+ }
+ }
+ }
+ }
+
+ $subsystems = \core_component::get_core_subsystems();
+ foreach ($subsystems as $subsystemname => $subsystempath) {
+ $componentname = 'core_' . $subsystemname;
+ $searchclasses = \core_component::get_component_classes_in_namespace($componentname, 'search');
+
+ foreach ($searchclasses as $classname => $classpath) {
+ $areaname = substr(strrchr($classname, '\\'), 1);
+ $areaid = static::generate_areaid($componentname, $areaname);
+ $searchclass = new $classname();
+ if (!$enabled || ($enabled && $searchclass->is_enabled())) {
+ $searchareas[$areaid] = $searchclass;
+ }
+ }
+ }
+
+ // Cache results.
+ if ($enabled) {
+ static::$enabledsearchareas = $searchareas;
+ } else {
+ static::$allsearchareas = $searchareas;
+ }
+
+ return $searchareas;
+ }
+
+ /**
+ * Clears all static caches.
+ *
+ * @return void
+ */
+ public static function clear_static() {
+
+ static::$enabledsearchareas = null;
+ static::$allsearchareas = null;
+ static::$instance = null;
+ }
+
+ /**
+ * Generates an area id from the componentname and the area name.
+ *
+ * There should not be any naming conflict as the area name is the
+ * class name in component/classes/search/.
+ *
+ * @param string $componentname
+ * @param string $areaname
+ * @return void
+ */
+ public static function generate_areaid($componentname, $areaname) {
+ return $componentname . '-' . $areaname;
+ }
+
+ /**
+ * Returns all areaid string components (component name and area name).
+ *
+ * @param string $areaid
+ * @return array Component name (Frankenstyle) and area name (search area class name)
+ */
+ public static function extract_areaid_parts($areaid) {
+ return explode('-', $areaid);
+ }
+
+ /**
+ * Returns the contexts the user can access.
+ *
+ * The returned value is a multidimensional array because some search engines can group
+ * information and there will be a performance benefit on passing only some contexts
+ * instead of the whole context array set.
+ *
+ * @return bool|array Indexed by area identifier (component + area name). Returns true if the user can see everything.
+ */
+ protected function get_areas_user_accesses() {
+ global $CFG, $USER;
+
+ // All results for admins. Eventually we could add a new capability for managers.
+ if (is_siteadmin()) {
+ return true;
+ }
+
+ $areasbylevel = array();
+
+ // Split areas by context level so we only iterate only once through courses and cms.
+ $searchareas = static::get_search_areas_list(true);
+ foreach ($searchareas as $areaid => $unused) {
+ $classname = static::get_area_classname($areaid);
+ $searcharea = new $classname();
+ foreach ($classname::get_levels() as $level) {
+ $areasbylevel[$level][$areaid] = $searcharea;
+ }
+ }
+
+ // This will store area - allowed contexts relations.
+ $areascontexts = array();
+
+ if (!empty($areasbylevel[CONTEXT_SYSTEM])) {
+ // We add system context to all search areas working at this level. Here each area is fully responsible of
+ // the access control as we can not automate much, we can not even check guest access as some areas might
+ // want to allow guests to retrieve data from them.
+
+ $systemcontextid = \context_system::instance()->id;
+ foreach ($areasbylevel[CONTEXT_SYSTEM] as $areaid => $searchclass) {
+ $areascontexts[$areaid][] = $systemcontextid;
+ }
+ }
+
+ // Get the courses where the current user has access.
+ $courses = enrol_get_my_courses(array('id', 'cacherev'));
+ if (isloggedin() || (isguestuser() && !$CFG->forcelogin)) {
+ $courses[SITEID] = get_course(SITEID);
+ }
+ $site = \course_modinfo::instance(SITEID);
+ foreach ($courses as $course) {
+
+ // Info about the course modules.
+ $modinfo = get_fast_modinfo($course);
+
+ if (!empty($areasbylevel[CONTEXT_COURSE])) {
+ // Add the course contexts the user can view.
+
+ $coursecontext = \context_course::instance($course->id);
+ foreach ($areasbylevel[CONTEXT_COURSE] as $areaid => $searchclass) {
+ if ($course->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+ $areascontexts[$areaid][$coursecontext->id] = $coursecontext->id;
+ }
+ }
+ }
+
+ if (!empty($areasbylevel[CONTEXT_MODULE])) {
+ // Add the module contexts the user can view (cm_info->uservisible).
+
+ foreach ($areasbylevel[CONTEXT_MODULE] as $areaid => $searchclass) {
+
+ // Removing the plugintype 'mod_' prefix.
+ $modulename = substr($searchclass->get_component_name(), 4);
+
+ $modinstances = $modinfo->get_instances_of($modulename);
+ foreach ($modinstances as $modinstance) {
+ if ($modinstance->uservisible) {
+ $areascontexts[$areaid][$modinstance->context->id] = $modinstance->context->id;
+ }
+ }
+ }
+ }
+ }
+
+ return $areascontexts;
+ }
+
+ /**
+ * Returns documents from the engine based on the data provided.
+ *
+ * It might return the results from the cache instead.
+ *
+ * @param stdClass $formdata
+ * @return \core_search\document[]
+ */
+ public function search(\stdClass $formdata) {
+
+ $cache = \cache::make('core', 'search_results');
+
+ // Generate a string from all query filters
+ // Not including $areascontext here, being a user cache it is not needed.
+ $querykey = $this->generate_query_key($formdata);
+
+ // Look for cached results before executing it.
+ if ($results = $cache->get($querykey)) {
+ return $results;
+ }
+
+ // Clears previous query errors.
+ $this->engine->clear_query_error();
+
+ $areascontexts = $this->get_areas_user_accesses();
+ if (!$areascontexts) {
+ // User can not access any context.
+ $docs = array();
+ } else {
+ $docs = $this->engine->execute_query($formdata, $areascontexts);
+ }
+
+ // Cache results.
+ $cache->set($querykey, $docs);
+
+ return $docs;
+ }
+
+ /**
+ * We generate the key ourselves so MUC knows that it contains simplekeys.
+ *
+ * @param stdClass $formdata
+ * @return string
+ */
+ protected function generate_query_key($formdata) {
+
+ // Empty values by default (although q should always have a value).
+ $fields = array('q', 'title', 'areaid', 'timestart', 'timeend', 'page');
+
+ // Just in this function scope.
+ $params = clone $formdata;
+ foreach ($fields as $field) {
+ if (empty($params->{$field})) {
+ $params->{$field} = '';
+ }
+ }
+
+ // Although it is not likely, we prevent cache hits if available search areas change during the session.
+ $enabledareas = implode('-', array_keys(static::get_search_areas_list(true)));
+
+ return md5($params->q . 'title=' . $params->title . 'areaid=' . $params->areaid .
+ 'timestart=' . $params->timestart . 'timeend=' . $params->timeend . 'page=' . $params->page .
+ $enabledareas);
+ }
+
+ /**
+ * Merge separate index segments into one.
+ */
+ public function optimize_index() {
+ $this->engine->optimize();
+ }
+
+ /**
+ * Index all documents.
+ *
+ * @param bool $fullindex Whether we should reindex everything or not.
+ * @throws \moodle_exception
+ * @return bool Whether there was any updated document or not.
+ */
+ public function index($fullindex = false) {
+ global $CFG;
+
+ // Unlimited time.
+ \core_php_time_limit::raise();
+
+ $anyupdate = false;
+
+ $searchareas = $this->get_search_areas_list(true);
+ foreach ($searchareas as $areaid => $searcharea) {
+
+ if (CLI_SCRIPT && !PHPUNIT_TEST) {
+ mtrace('Processing ' . $searcharea->get_visible_name() . ' area');
+ }
+
+ $indexingstart = time();
+
+ // This is used to store this component config.
+ list($componentconfigname, $varname) = $searcharea->get_config_var_name();
+
+ $numrecords = 0;
+ $numdocs = 0;
+ $numdocsignored = 0;
+ $lastindexeddoc = 0;
+
+ if ($fullindex === true) {
+ $prevtimestart = 0;
+ } else {
+ $prevtimestart = intval(get_config($componentconfigname, $varname . '_indexingstart'));
+ }
+
+ // Getting the recordset from the area.
+ $recordset = $searcharea->get_recordset_by_timestamp($prevtimestart);
+
+ // Pass get_document as callback.
+ $iterator = new \core\dml\recordset_walk($recordset, array($searcharea, 'get_document'));
+ foreach ($iterator as $document) {
+
+ if (!$document instanceof \core_search\document) {
+ continue;
+ }
+
+ $docdata = $document->export_for_engine();
+ switch ($docdata['type']) {
+ case static::TYPE_TEXT:
+ $this->engine->add_document($docdata);
+ $numdocs++;
+ break;
+ default:
+ $numdocsignored++;
+ $iterator->close();
+ throw new \moodle_exception('doctypenotsupported', 'search');
+ }
+
+ $lastindexeddoc = $document->get('modified');
+ $numrecords++;
+ }
+
+ if ($numdocs > 0) {
+ $anyupdate = true;
+
+ // Commit all remaining documents.
+ $this->engine->commit();
+
+ if (CLI_SCRIPT && !PHPUNIT_TEST) {
+ mtrace('Processed ' . $numrecords . ' records containing ' . $numdocs . ' documents for ' .
+ $searcharea->get_visible_name() . ' area. Commits completed.');
+ }
+ } else if (CLI_SCRIPT && !PHPUNIT_TEST) {
+ mtrace('No new documents to index for ' . $searcharea->get_visible_name() . ' area.');
+ }
+
+ // Store last index run once documents have been commited to the search engine.
+ set_config($varname . '_indexingstart', $indexingstart, $componentconfigname);
+ set_config($varname . '_indexingend', time(), $componentconfigname);
+ set_config($varname . '_docsignored', $numdocsignored, $componentconfigname);
+ set_config($varname . '_docsprocessed', $numdocs, $componentconfigname);
+ set_config($varname . '_recordsprocessed', $numrecords, $componentconfigname);
+ if ($lastindexeddoc > 0) {
+ set_config($varname . '_lastindexrun', $lastindexeddoc, $componentconfigname);
+ }
+ }
+
+ if ($anyupdate) {
+ $event = \core\event\search_indexed::create(
+ array('context' => \context_system::instance()));
+ $event->trigger();
+ }
+
+ return $anyupdate;
+ }
+
+ /**
+ * Resets areas config.
+ *
+ * @throws \moodle_exception
+ * @param string $areaid
+ * @return void
+ */
+ public function reset_config($areaid = false) {
+
+ if (!empty($areaid)) {
+ $searchareas = array();
+ if (!$searchareas[$areaid] = static::get_search_area($areaid)) {
+ throw new \moodle_exception('errorareanotavailable', 'search', '', $areaid);
+ }
+ } else {
+ // Only the enabled ones.
+ $searchareas = static::get_search_areas_list(true);
+ }
+
+ foreach ($searchareas as $searcharea) {
+
+ list($areaid, $varname) = $searcharea->get_config_var_name();
+
+ set_config($varname . '_indexingstart', 0, $areaid);
+ set_config($varname . '_indexingend', 0, $areaid);
+ set_config($varname . '_lastindexrun', 0, $areaid);
+ set_config($varname . '_docsignored', 0, $areaid);
+ set_config($varname . '_docsprocessed', 0, $areaid);
+ set_config($varname . '_recordsprocessed', 0, $areaid);
+ }
+ }
+
+ /**
+ * Deletes an area's documents or all areas documents.
+ *
+ * @param string $areaid The area id or false for all
+ * @return void
+ */
+ public function delete_index($areaid = false) {
+ if (!empty($areaid)) {
+ $this->engine->delete($areaid);
+ $this->reset_config($areaid);
+ } else {
+ $this->engine->delete();
+ $this->reset_config();
+ }
+ $this->engine->commit();
+ }
+
+ /**
+ * Deletes index by id.
+ *
+ * @param int Solr Document string $id
+ */
+ public function delete_index_by_id($id) {
+ $this->engine->delete_by_id($id);
+ $this->engine->commit();
+ }
+
+ /**
+ * Returns search areas configuration.
+ *
+ * @param \core_search\area\base[] $searchareas
+ * @return \stdClass[] $configsettings
+ */
+ public function get_areas_config($searchareas) {
+
+ $allconfigs = get_config('search');
+ $vars = array('indexingstart', 'indexingend', 'lastindexrun', 'docsignored', 'docsprocessed', 'recordsprocessed');
+
+ $configsettings = array();
+ foreach ($searchareas as $searcharea) {
+
+ $areaid = $searcharea->get_area_id();
+
+ $configsettings[$areaid] = new \stdClass();
+ list($componentname, $varname) = $searcharea->get_config_var_name();
+
+ if (!$searcharea->is_enabled()) {
+ // We delete all indexed data on disable so no info.
+ foreach ($vars as $var) {
+ $configsettings[$areaid]->{$var} = 0;
+ }
+ } else {
+ foreach ($vars as $var) {
+ $configsettings[$areaid]->{$var} = get_config($componentname, $varname .'_' . $var);
+ }
+ }
+
+ // Formatting the time.
+ if (!empty($configsettings[$areaid]->lastindexrun)) {
+ $configsettings[$areaid]->lastindexrun = userdate($configsettings[$areaid]->lastindexrun);
+ } else {
+ $configsettings[$areaid]->lastindexrun = get_string('never');
+ }
+ }
+ return $configsettings;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Events observer for core_search purposes.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event observer for search.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_search_observer {
+
+ /**
+ * Invalidates search results cache.
+ *
+ * Quite aggressive as search_results cache is per session.
+ *
+ * @param \core\event\search_indexed $event
+ * @return void
+ */
+ public static function invalidate_caches(\core\event\search_indexed $event) {
+ $cache = \cache::make('core', 'search_results');
+ $cache->purge();
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Global search search form definition
+ *
+ * @package core_search
+ * @copyright Prateek Sachan {@link http://prateeksachan.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search\output\form;
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/formslib.php');
+
+class search extends \moodleform {
+
+ /**
+ * Form definition.
+ *
+ * @return void
+ */
+ function definition() {
+ global $CFG;
+
+ $mform =& $this->_form;
+ $mform->addElement('header', 'search', get_string('search', 'search'));
+
+ // Help info depends on the selected search engine.
+ $mform->addElement('text', 'q', get_string('enteryoursearchquery', 'search'));
+ $mform->addHelpButton('q', 'searchinfo', $this->_customdata['searchengine']);
+ $mform->setType('q', PARAM_TEXT);
+ $mform->addRule('q', get_string('required'), 'required', null, 'client');
+
+ $mform->addElement('header', 'filtersection', get_string('filterheader', 'search'));
+ $mform->setExpanded('filtersection', false);
+
+ $mform->addElement('text', 'title', get_string('title', 'search'));
+ $mform->setType('title', PARAM_TEXT);
+
+ $search = \core_search\manager::instance();
+
+ $searchareas = \core_search\manager::get_search_areas_list(true);
+ $areanames = array('' => get_string('allareas', 'search'));
+ foreach ($searchareas as $areaid => $searcharea) {
+ $areanames[$areaid] = $searcharea->get_visible_name();
+ }
+ $mform->addElement('select', 'areaid', get_string('searcharea', 'search'), $areanames);
+
+ $mform->addElement('date_time_selector', 'timestart', get_string('fromtime', 'search'), array('optional' => true));
+ $mform->setDefault('timestart', 0);
+
+ $mform->addElement('date_time_selector', 'timeend', get_string('totime', 'search'), array('optional' => true));
+ $mform->setDefault('timeend', 0);
+
+ $this->add_action_buttons(false, get_string('search', 'search'));
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search renderer.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_search\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search renderer.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends \plugin_renderer_base {
+
+ /**
+ * @var int Max number chars to display of a string value
+ */
+ const SEARCH_RESULT_STRING_SIZE = 100;
+
+ /**
+ * @var int Max number chars to display of a text value
+ */
+
+ const SEARCH_RESULT_TEXT_SIZE = 500;
+
+ /**
+ * Renders search results.
+ *
+ * @param \core_search\document[] $results
+ * @param int $page
+ * @param \moodle_url $url
+ * @return string HTML
+ */
+ public function render_results($results, $page = 0, $url) {
+
+ // Paging bar.
+ $perpage = \core_search\manager::DISPLAY_RESULTS_PER_PAGE;
+ $content = $this->output->paging_bar(count($results), $page, $perpage, $url);
+
+ // Results.
+ $resultshtml = array();
+ $hits = array_slice($results, $page * $perpage, $perpage, true);
+ foreach ($hits as $hit) {
+ $resultshtml[] = $this->render_result($hit);
+ }
+ $content .= \html_writer::tag('div', implode('<hr/>', $resultshtml), array('class' => 'search-results'));
+
+ // Paging bar.
+ $content .= $this->output->paging_bar(count($results), $page, $perpage, $url);
+
+ return $content;
+ }
+
+ /**
+ * Displaying search results.
+ *
+ * @param \core_search\document Containing a single search response to be displayed.a
+ * @return string HTML
+ */
+ public function render_result(\core_search\document $doc) {
+ $docdata = $doc->export_for_template($this);
+
+ // Limit text fields size.
+ $docdata['title'] = shorten_text($docdata['title'], static::SEARCH_RESULT_STRING_SIZE, true);
+ $docdata['content'] = $docdata['content'] ? shorten_text($docdata['content'], static::SEARCH_RESULT_TEXT_SIZE, true) : '';
+ $docdata['description1'] = $docdata['description1'] ? shorten_text($docdata['description1'], static::SEARCH_RESULT_TEXT_SIZE, true) : '';
+ $docdata['description2'] = $docdata['description2'] ? shorten_text($docdata['description2'], static::SEARCH_RESULT_TEXT_SIZE, true) : '';
+
+ return $this->output->render_from_template('core_search/result', $docdata);
+ }
+
+ /**
+ * Returns a box with a search disabled lang string.
+ *
+ * @return string HTML
+ */
+ public function render_search_disabled() {
+ $content = $this->output->box_start();
+ $content .= $this->output->notification(get_string('globalsearchdisabled', 'search'), 'notifymessage');
+ $content .= $this->output->box_end();
+ return $content;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Global Search index page for entering queries and display of results
+ *
+ * @package core_search
+ * @copyright Prateek Sachan {@link http://prateeksachan.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+
+$page = optional_param('page', 0, PARAM_INT);
+$q = optional_param('q', '', PARAM_NOTAGS);
+$title = optional_param('title', '', PARAM_NOTAGS);
+$areaid = optional_param('areaid', false, PARAM_ALPHANUMEXT);
+// Moving timestart and timeend further down as they might come as an array if they come from the form.
+
+$pagetitle = get_string('globalsearch', 'search');
+$PAGE->set_context(context_system::instance());
+$PAGE->set_pagelayout('standard');
+$PAGE->set_title($pagetitle);
+$PAGE->set_heading($pagetitle);
+
+if (!empty($CFG->forcelogin)) {
+ require_login();
+}
+
+$searchrenderer = $PAGE->get_renderer('core_search');
+
+if (\core_search\manager::is_global_search_enabled() === false) {
+ $PAGE->set_url(new moodle_url('/search/index.php'));
+ echo $OUTPUT->header();
+ echo $OUTPUT->heading($pagetitle);
+ echo $searchrenderer->render_search_disabled();
+ echo $OUTPUT->footer();
+ exit;
+}
+
+$search = \core_search\manager::instance();
+
+// We first get the submitted data as we want to set it all in the page URL.
+$mform = new \core_search\output\form\search(null, array('searchengine' => $search->get_engine()->get_plugin_name()));
+
+$data = $mform->get_data();
+if (!$data && $q) {
+ // Data can also come from the URL.
+
+ $data = new stdClass();
+ $data->q = $q;
+ $data->title = $title;
+ $data->areaid = $areaid;
+ $data->timestart = optional_param('timestart', 0, PARAM_INT);
+ $data->timeend = optional_param('timeend', 0, PARAM_INT);
+ $mform->set_data($data);
+}
+
+// Set the page URL.
+$urlparams = array('page' => $page);
+if ($data) {
+ $urlparams['q'] = $data->q;
+ $urlparams['title'] = $data->title;
+ $urlparams['areaid'] = $data->areaid;
+ $urlparams['timestart'] = $data->timestart;
+ $urlparams['timeend'] = $data->timeend;
+}
+$url = new moodle_url('/search/index.php', $urlparams);
+$PAGE->set_url($url);
+
+// We are ready to render.
+echo $OUTPUT->header();
+echo $OUTPUT->heading($pagetitle);
+
+// Get the results.
+if ($data) {
+ $data->page = $page;
+ $results = $search->search($data);
+}
+
+if ($errorstr = $search->get_engine()->get_query_error()) {
+ echo $OUTPUT->notification(get_string('queryerror', 'search', $errorstr), 'notifyproblem');
+} else if (empty($results) && !empty($data)) {
+ echo $OUTPUT->notification(get_string('noresults', 'search'), 'notifymessage');
+}
+
+$mform->display();
+
+if (!empty($results)) {
+ echo $searchrenderer->render_results($results, $page, $url);
+}
+
+echo $OUTPUT->footer();
--- /dev/null
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core_search/result
+
+ Template which shows a search result.
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * courseurl
+ * coursefullname
+ * title
+ * docurl
+ * contexturl
+
+ Optional context variables for this template:
+ * content
+ * userurl
+ * userfullname
+ * description1
+ * description2
+
+ Example context (json):
+ {
+ "courseurl": "https://example.com/course/view.php?id=2",
+ "coursefullname": "Example course",
+ "title": "Example title",
+ "docurl": "https://example.com/mod/example/view.php?id=4",
+ "content": "I am content",
+ "contexturl": "https://example.com/mod/example/view.php?id=2"
+ "userurl": "https://example.com/user/profile.php?id=3",
+ "userfullname": "Example User Full Name",
+ }
+}}
+<div class="result">
+ <h4 class="result-title">
+ <a href="{{{docurl}}}">{{title}}</a>
+ </h4>
+ {{#content}}
+ <div class="result-content">{{{content}}}</div>
+ {{/content}}
+ {{#description1}}
+ <div class="result-content">{{{description1}}}</div>
+ {{/description1}}
+ {{#description2}}
+ <div class="result-content">{{{description2}}}</div>
+ {{/description2}}
+ <div class="result-context-info">
+ <a href="{{{contexturl}}}">{{#str}}viewresultincontext, search{{/str}}</a> -
+ <a href="{{{courseurl}}}">{{#str}}incourse, search, {{coursefullname}}{{/str}}</a>
+ {{#userfullname}}
+ - <a href="{{{userurl}}}">{{#str}}byname, moodle, {{userfullname}}{{/str}}</a>
+ {{/userfullname}}
+ </div>
+</div>
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search engine base unit tests.
+ *
+ * @package core_search
+ * @category phpunit
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/testable_core_search.php');
+
+/**
+ * Search engine base unit tests.
+ *
+ * @package core_search
+ * @category phpunit
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_engine_testcase extends advanced_testcase {
+
+ public function setUp() {
+ $this->resetAfterTest();
+ set_config('enableglobalsearch', true);
+
+ // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
+ $search = testable_core_search::instance();
+ }
+
+ /**
+ * Engine basic info.
+ *
+ * @return void
+ */
+ public function test_engine_info() {
+ $engine = new \mock_search\engine();
+
+ $this->assertEquals('mock_search', $engine->get_plugin_name());
+
+ // Resolves to the default one.
+ $this->assertEquals('\\core_search\\document', $engine->get_document_classname());
+ }
+
+ /**
+ * Test engine caches.
+ *
+ * @return void
+ */
+ public function test_engine_caches() {
+ global $DB;
+
+ $engine = new \mock_search\engine();
+
+ $course1 = self::getDataGenerator()->create_course();
+
+ $this->assertEquals($course1->id, $engine->get_course($course1->id)->id);
+ $dbreads = $DB->perf_get_reads();
+ $engine->get_course($course1->id);
+ $this->assertEquals($dbreads, $DB->perf_get_reads());
+ $fakearea1 = \core_search\manager::generate_areaid('plugintype_unexisting', 'fakearea');
+ $fakearea2 = \core_search\manager::generate_areaid('mod_unexisting', 'morefake');
+ $this->assertFalse($engine->get_search_area($fakearea1));
+ $this->assertFalse($engine->get_search_area($fakearea2));
+ $this->assertFalse($engine->get_search_area($fakearea2));
+
+ $areaid = \core_search\manager::generate_areaid('mod_forum', 'post');
+ $this->assertInstanceOf('\\mod_forum\\search\\post', $engine->get_search_area($areaid));
+ $dbreads = $DB->perf_get_reads();
+ $this->assertInstanceOf('\\mod_forum\\search\\post', $engine->get_search_area($areaid));
+ $this->assertEquals($dbreads, $DB->perf_get_reads());
+
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+namespace core_mocksearch\search;
+
+/**
+ * Component implementing search for testing purposes.
+ *
+ * @package core_search
+ * @category phpunit
+ * @copyright David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+class role_capabilities extends \core_search\area\base {
+
+ /**
+ * To make things easier, base class required config stuff.
+ *
+ * @return bool
+ */
+ public function is_enabled() {
+ return true;
+ }
+
+ public function get_recordset_by_timestamp($modifiedfrom = 0) {
+ global $DB;
+ // Filter by capability as we want this quick.
+ return $DB->get_recordset_sql("SELECT id, contextid, roleid, capability FROM {role_capabilities} where timemodified >= ? and capability = ?", array($modifiedfrom, 'moodle/course:renameroles'));
+ }
+
+ public function get_document($record) {
+ global $USER;
+
+ // Prepare associative array with data from DB.
+ $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+ $doc->set('title', $record->capability . ' roleid ' . $record->roleid);
+ $doc->set('content', $record->capability . ' roleid ' . $record->roleid . ' message');
+ $doc->set('contextid', $record->contextid);
+ $doc->set('type', \core_search\manager::TYPE_TEXT);
+ $doc->set('courseid', SITEID);
+ $doc->set('userid', $USER->id);
+ $doc->set('modified', time());
+
+ return $doc;
+ }
+
+ public function check_access($id) {
+ return \core_search\manager::ACCESS_GRANTED;
+ }
+
+ public function get_doc_url(\core_search\document $doc) {
+ return new \moodle_url('/index.php');
+ }
+
+ public function get_context_url(\core_search\document $doc) {
+ return new \moodle_url('/index.php');
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+namespace mock_search;
+
+/**
+ * Search engine for testing purposes.
+ *
+ * @package core_search
+ * @category phpunit
+ * @copyright David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+class engine extends \core_search\engine {
+
+ public function is_installed() {
+ return true;
+ }
+
+ public function is_server_ready() {
+ return true;
+ }
+
+ public function add_document($doc) {
+ // No need to implement.
+ }
+
+ public function commit() {
+ // No need to implement.
+ }
+
+ public function execute_query($data, $usercontexts) {
+ // No need to implement.
+ }
+
+ public function delete($areaid = null) {
+ return null;
+ }
+
+ public function get_course($courseid) {
+ return parent::get_course($courseid);
+ }
+
+ public function get_search_area($areaid) {
+ return parent::get_search_area($areaid);
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Core search class adapted to unit test.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/mock_search_engine.php');
+
+/**
+ * Core search class adapted to unit test.
+ *
+ * @package core_search
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_core_search extends \core_search\manager {
+
+ /**
+ * Attaches the mock engine to search.
+ *
+ * Auto enables global search.
+ *
+ * @param \core_search\engine|bool $searchengine
+ * @return testable_core_search
+ */
+ public static function instance($searchengine = false) {
+
+ // One per request, this should be purged during testing.
+ if (self::$instance !== null) {
+ return self::$instance;
+ }
+
+ set_config('enableglobalsearch', true);
+
+ // Default to the mock one.
+ if ($searchengine === false) {
+ $searchengine = new \mock_search\engine();
+ }
+
+ self::$instance = new testable_core_search($searchengine);
+
+ return self::$instance;
+ }
+
+ /**
+ * Changes visibility.
+ *
+ * @return array
+ */
+ public function get_areas_user_accesses() {
+ return parent::get_areas_user_accesses();
+ }
+
+ /**
+ * Adds an enabled search component to the search areas list.
+ *
+ * @param string $areaid
+ * @param \core_search\area\base $searcharea
+ * @return void
+ */
+ public function add_search_area($areaid, \core_search\area\base $searcharea) {
+ self::$enabledsearchareas[$areaid] = $searcharea;
+ self::$allsearchareas[$areaid] = $searcharea;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search manager unit tests.
+ *
+ * @package core_search
+ * @category phpunit
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(__DIR__ . '/fixtures/testable_core_search.php');
+
+/**
+ * Unit tests for search manager.
+ *
+ * @package core_search
+ * @category phpunit
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_manager_testcase extends advanced_testcase {
+
+ protected $forumpostareaid = null;
+
+ public function setUp() {
+ $this->forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post');
+ }
+
+ public function test_search_enabled() {
+
+ $this->resetAfterTest();
+
+ // Disabled by default.
+ $this->assertFalse(\core_search\manager::is_global_search_enabled());
+
+ set_config('enableglobalsearch', true);
+ $this->assertTrue(\core_search\manager::is_global_search_enabled());
+
+ set_config('enableglobalsearch', false);
+ $this->assertFalse(\core_search\manager::is_global_search_enabled());
+ }
+
+ public function test_search_areas() {
+ global $CFG;
+
+ $this->resetAfterTest();
+
+ set_config('enableglobalsearch', true);
+
+ $fakeareaid = \core_search\manager::generate_areaid('mod_unexisting', 'chihuaquita');
+
+ $searcharea = \core_search\manager::get_search_area($this->forumpostareaid);
+ $this->assertInstanceOf('\core_search\area\base', $searcharea);
+
+ $this->assertFalse(\core_search\manager::get_search_area($fakeareaid));
+
+ $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list());
+ $this->assertArrayNotHasKey($fakeareaid, \core_search\manager::get_search_areas_list());
+
+ // Enabled by default once global search is enabled.
+ $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
+
+ list($componentname, $varname) = $searcharea->get_config_var_name();
+ set_config('enable' . $varname, false, $componentname);
+ \core_search\manager::clear_static();
+
+ $this->assertArrayNotHasKey('mod_forum', \core_search\manager::get_search_areas_list(true));
+
+ set_config('enable' . $varname, true, $componentname);
+
+ // Although the result is wrong, we want to check that \core_search\manager::get_search_areas_list returns cached results.
+ $this->assertArrayNotHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
+
+ // Now we check the real result.
+ \core_search\manager::clear_static();
+ $this->assertArrayHasKey($this->forumpostareaid, \core_search\manager::get_search_areas_list(true));
+ }
+
+ public function test_search_config() {
+
+ $this->resetAfterTest();
+
+ $search = testable_core_search::instance();
+
+ // We should test both plugin types and core subsystems. No core subsystems available yet.
+ $searcharea = $search->get_search_area($this->forumpostareaid);
+
+ list($componentname, $varname) = $searcharea->get_config_var_name();
+
+ // Just with a couple of vars should be enough.
+ $start = time() - 100;
+ $end = time();
+ set_config($varname . '_indexingstart', $start, $componentname);
+ set_config($varname . '_indexingend', $end, $componentname);
+
+ $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
+ $this->assertEquals($start, $configs[$this->forumpostareaid]->indexingstart);
+ $this->assertEquals($end, $configs[$this->forumpostareaid]->indexingend);
+
+ try {
+ $fakeareaid = \core_search\manager::generate_areaid('mod_unexisting', 'chihuaquita');
+ $search->reset_config($fakeareaid);
+ $this->fail('An exception should be triggered if the provided search area does not exist.');
+ } catch (moodle_exception $ex) {
+ $this->assertContains($fakeareaid . ' search area is not available.', $ex->getMessage());
+ }
+
+ $search->reset_config($this->forumpostareaid);
+ $this->assertEquals(0, get_config($componentname, $varname . '_indexingstart'));
+ $this->assertEquals(0, get_config($componentname, $varname . '_indexingend'));
+ $this->assertEquals(0, get_config($componentname, $varname . '_lastindexrun'));
+
+ // No caching.
+ $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
+ $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingstart);
+ $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingend);
+
+ set_config($varname . '_indexingstart', $start, $componentname);
+ set_config($varname . '_indexingend', $end, $componentname);
+
+ // All components config should be reset.
+ $search->reset_config();
+ $this->assertEquals(0, get_config($componentname, $varname . '_indexingstart'));
+ $this->assertEquals(0, get_config($componentname, $varname . '_indexingend'));
+ $this->assertEquals(0, get_config($componentname, $varname . '_lastindexrun'));
+
+ // No caching.
+ $configs = $search->get_areas_config(array($this->forumpostareaid => $searcharea));
+ $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingstart);
+ $this->assertEquals(0, $configs[$this->forumpostareaid]->indexingend);
+ }
+
+ /**
+ * Adding this test here as get_areas_user_accesses process is the same, results just depend on the context level.
+ *
+ * @return void
+ */
+ public function test_search_user_accesses() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $frontpage = $DB->get_record('course', array('id' => SITEID));
+ $course1 = $this->getDataGenerator()->create_course();
+ $course2 = $this->getDataGenerator()->create_course();
+ $teacher = $this->getDataGenerator()->create_user();
+ $student = $this->getDataGenerator()->create_user();
+ $noaccess = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($teacher->id, $course1->id, 'teacher');
+ $this->getDataGenerator()->enrol_user($student->id, $course1->id, 'student');
+
+ $frontpageforum = $this->getDataGenerator()->create_module('forum', array('course' => $frontpage->id));
+ $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
+ $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
+ $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
+ $frontpageforumcontext = context_module::instance($frontpageforum->cmid);
+ $context1 = context_module::instance($forum1->cmid);
+ $context2 = context_module::instance($forum2->cmid);
+ $context3 = context_module::instance($forum3->cmid);
+
+ $search = testable_core_search::instance();
+
+ $this->setAdminUser();
+ $this->assertTrue($search->get_areas_user_accesses());
+
+ // Can access the frontpage ones.
+ $this->setUser($noaccess);
+ $contexts = $search->get_areas_user_accesses();
+ $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id), $contexts[$this->forumpostareaid]);
+
+ $this->setUser($teacher);
+ $contexts = $search->get_areas_user_accesses();
+ $frontpageandcourse1 = array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id,
+ $context2->id => $context2->id);
+ $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
+
+ $this->setUser($student);
+ $contexts = $search->get_areas_user_accesses();
+ $this->assertEquals($frontpageandcourse1, $contexts[$this->forumpostareaid]);
+
+ // Hide the activity.
+ set_coursemodule_visible($forum2->cmid, 0);
+ $contexts = $search->get_areas_user_accesses();
+ $this->assertEquals(array($frontpageforumcontext->id => $frontpageforumcontext->id, $context1->id => $context1->id),
+ $contexts[$this->forumpostareaid]);
+ }
+}
defined('MOODLE_INTERNAL') || die();
-$version = 2016021800.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2016021800.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.