first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
<?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 category.
*
* @package core_search
* @copyright Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search;
defined('MOODLE_INTERNAL') || die();
/**
* Search area category.
*
* @package core_search
* @copyright Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class area_category {
/**
* Category name.
* @var string
*/
protected $name;
/**
* Category visible name.
* @var string
*/
protected $visiblename;
/**
* Category order.
* @var int
*/
protected $order = 0;
/**
* Category areas.
* @var \core_search\base[]
*/
protected $areas = [];
/**
* Constructor.
*
* @param string $name Unique name of the category.
* @param string $visiblename Visible name of the category.
* @param int $order Category position in the list (smaller numbers will be displayed first).
* @param \core_search\base[] $areas A list of search areas associated with this category.
*/
public function __construct(string $name, string $visiblename, int $order = 0, array $areas = []) {
$this->name = $name;
$this->visiblename = $visiblename;
$this->order = $order;
$this->set_areas($areas);
}
/**
* Get name.
*
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* Get visible name.
*
* @return string
*/
public function get_visiblename() {
return $this->visiblename;
}
/**
* Get order to display.
*
* @return int
*/
public function get_order() {
return $this->order;
}
/**
* Return a keyed by area id list of areas for this category.
*
* @return \core_search\base[]
*/
public function get_areas() {
return $this->areas;
}
/**
* Set list of search areas for this category,
*
* @param \core_search\base[] $areas
*/
public function set_areas(array $areas) {
foreach ($areas as $area) {
if ($area instanceof base && !key_exists($area->get_area_id(), $this->areas)) {
$this->areas[$area->get_area_id()] = $area;
}
}
}
}
+560
View File
@@ -0,0 +1,560 @@
<?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;
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];
/**
* An area id from the componentname and the area name.
*
* @var string
*/
public $areaid;
/**
* Constructor.
*
* @throws \coding_exception
* @return void
*/
final public 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) {
$component = $this->componentname;
// Core subsystem strings go to lang/XX/search.php.
if ($this->componenttype === 'core') {
$component = 'search';
}
return get_string('search:' . $this->areaname, $component, 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.
*
* @access private
* @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 core_search and setting name using only [a-zA-Z0-9_]+.
$parts = \core_search\manager::extract_areaid_parts($this->areaid);
return array('core_search', $parts[0] . '_' . $parts[1]);
}
// Plugins config in the plugin scope.
return array($this->componentname, 'search_' . $this->areaname);
}
/**
* Returns all the search area configuration.
*
* @return array
*/
public function get_config() {
list($componentname, $varname) = $this->get_config_var_name();
$config = [];
$settingnames = self::get_settingnames();
foreach ($settingnames as $name) {
$config[$varname . $name] = get_config($componentname, $varname . $name);
}
// Search areas are enabled by default.
if ($config[$varname . '_enabled'] === false) {
$config[$varname . '_enabled'] = 1;
}
return $config;
}
/**
* Return a list of all required setting names.
*
* @return array
*/
public static function get_settingnames() {
return array('_enabled', '_indexingstart', '_indexingend', '_lastindexrun',
'_docsignored', '_docsprocessed', '_recordsprocessed', '_partial');
}
/**
* Is the search component enabled by the system administrator?
*
* @return bool
*/
public function is_enabled() {
list($componentname, $varname) = $this->get_config_var_name();
$value = get_config($componentname, $varname . '_enabled');
// Search areas are enabled by default.
if ($value === false) {
$value = 1;
}
return (bool)$value;
}
public function set_enabled($isenabled) {
list($componentname, $varname) = $this->get_config_var_name();
return set_config($varname . '_enabled', $isenabled, $componentname);
}
/**
* Gets the length of time spent indexing this area (the last time it was indexed).
*
* @return int|bool Time in seconds spent indexing this area last time, false if never indexed
*/
public function get_last_indexing_duration() {
list($componentname, $varname) = $this->get_config_var_name();
$start = get_config($componentname, $varname . '_indexingstart');
$end = get_config($componentname, $varname . '_indexingend');
if ($start && $end) {
return $end - $start;
} else {
return false;
}
}
/**
* Returns true if this area uses file indexing.
*
* @return bool
*/
public function uses_file_indexing() {
return false;
}
/**
* 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.
*
* Since Moodle 3.4, subclasses should instead implement get_document_recordset, which has
* an additional context parameter. This function continues to work for implementations which
* haven't been updated, or where the context parameter is not required.
*
* @param int $modifiedfrom
* @return \moodle_recordset
*/
public function get_recordset_by_timestamp($modifiedfrom = 0) {
$result = $this->get_document_recordset($modifiedfrom);
if ($result === false) {
throw new \coding_exception(
'Search area must implement get_document_recordset or get_recordset_by_timestamp');
}
return $result;
}
/**
* Returns a recordset containing all items from this area, optionally within the given context,
* and including only items modifed from (>=) the specified time. The recordset must be ordered
* in ascending order of modified time.
*
* Each record can include any data self::get_document might need. It must include an 'id'
* field,a 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.
*
* The return value can be a recordset, null (if this area does not provide any results in the
* given context and there is no need to do a database query to find out), or false (if this
* facility is not currently supported by this search area).
*
* If this function returns false, then:
* - If indexing the entire system (no context restriction) the search indexer will try
* get_recordset_by_timestamp instead
* - If trying to index a context (e.g. when restoring a course), the search indexer will not
* index this area, so that restored content may not be indexed.
*
* The default implementation returns false, indicating that this facility is not supported and
* the older get_recordset_by_timestamp function should be used.
*
* This function must accept all possible values for the $context parameter. For example, if
* you are implementing this function for the forum module, it should still operate correctly
* if called with the context for a glossary module, or for the HTML block. (In these cases
* where it will not return any data, it may return null.)
*
* The $context parameter can also be null or the system context; both of these indicate that
* all data, without context restriction, should be returned.
*
* @param int $modifiedfrom Return only records modified after this date
* @param \context|null $context Context (null means no context restriction)
* @return \moodle_recordset|null|false Recordset / null if no results / false if not supported
* @since Moodle 3.4
*/
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
return false;
}
/**
* Checks if get_document_recordset is supported for this search area.
*
* For many uses you can simply call get_document_recordset and see if it returns false, but
* this function is useful when you don't want to actually call the function right away.
*/
public function supports_get_document_recordset() {
// Easiest way to check this is simply to see if the class has overridden the default
// function.
$method = new \ReflectionMethod($this, 'get_document_recordset');
return $method->getDeclaringClass()->getName() !== self::class;
}
/**
* 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 content_to_text}
*
* Valid keys for the options array are:
* indexfiles => File indexing is enabled if true.
* lastindexedtime => The last time this area was indexed. 0 if never indexed.
*
* The lastindexedtime value is not set if indexing a specific context rather than the whole
* system.
*
* @param \stdClass $record A record containing, at least, the indexed document id and a modified timestamp
* @param array $options Options for document creation
* @return \core_search\document
*/
abstract public function get_document($record, $options = array());
/**
* Returns the document title to display.
*
* Allow to customize the document title string to display.
*
* @param \core_search\document $doc
* @return string Document title to display in the search results page
*/
public function get_document_display_title(\core_search\document $doc) {
return $doc->get('title');
}
/**
* Return the context info required to index files for
* this search area.
*
* Should be onerridden by each search area.
*
* @return array
*/
public function get_search_fileareas() {
$fileareas = array();
return $fileareas;
}
/**
* Files related to the current document are attached,
* to the document object ready for indexing by
* Global Search.
*
* The default implementation retrieves all files for
* the file areas returned by get_search_fileareas().
* If you need to filter files to specific items per
* file area, you will need to override this method
* and explicitly provide the items.
*
* @param document $document The current document
* @return void
*/
public function attach_files($document) {
$fileareas = $this->get_search_fileareas();
$contextid = $document->get('contextid');
$component = $this->get_component_name();
$itemid = $document->get('itemid');
foreach ($fileareas as $filearea) {
$fs = get_file_storage();
$files = $fs->get_area_files($contextid, $component, $filearea, $itemid, '', false);
foreach ($files as $file) {
$document->add_stored_file($file);
}
}
}
/**
* Can the current user see the document.
*
* @param int $id The internal search area entity id.
* @return int manager:ACCESS_xx constant
*/
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);
/**
* Helper function that gets SQL useful for restricting a search query given a passed-in
* context, for data stored at course level.
*
* The SQL returned will be zero or more JOIN statements, surrounded by whitespace, which act
* as restrictions on the query based on the rows in a module table.
*
* You can pass in a null or system context, which will both return an empty string and no
* params.
*
* Returns an array with two nulls if there can be no results for a course within this context.
*
* If named parameters are used, these will be named gclcrs0, gclcrs1, etc. The table aliases
* used in SQL also all begin with gclcrs, to avoid conflicts.
*
* @param \context|null $context Context to restrict the query
* @param string $coursetable Name of alias for course table e.g. 'c'
* @param int $paramtype Type of SQL parameters to use (default question mark)
* @return array Array with SQL and parameters; both null if no need to query
* @throws \coding_exception If called with invalid params
*/
protected function get_course_level_context_restriction_sql(?\context $context,
$coursetable, $paramtype = SQL_PARAMS_QM) {
global $DB;
if (!$context) {
return ['', []];
}
switch ($paramtype) {
case SQL_PARAMS_QM:
$param1 = '?';
$param2 = '?';
$key1 = 0;
$key2 = 1;
break;
case SQL_PARAMS_NAMED:
$param1 = ':gclcrs0';
$param2 = ':gclcrs1';
$key1 = 'gclcrs0';
$key2 = 'gclcrs1';
break;
default:
throw new \coding_exception('Unexpected $paramtype: ' . $paramtype);
}
$params = [];
switch ($context->contextlevel) {
case CONTEXT_SYSTEM:
$sql = '';
break;
case CONTEXT_COURSECAT:
// Find all courses within the specified category or any sub-category.
$pathmatch = $DB->sql_like('gclcrscc2.path',
$DB->sql_concat('gclcrscc1.path', $param2));
$sql = " JOIN {course_categories} gclcrscc1 ON gclcrscc1.id = $param1
JOIN {course_categories} gclcrscc2 ON gclcrscc2.id = $coursetable.category
AND (gclcrscc2.id = gclcrscc1.id OR $pathmatch) ";
$params[$key1] = $context->instanceid;
// Note: This param is a bit annoying as it obviously never changes, but sql_like
// throws a debug warning if you pass it anything with quotes in, so it has to be
// a bound parameter.
$params[$key2] = '/%';
break;
case CONTEXT_COURSE:
// We just join again against the same course entry and confirm that it has the
// same id as the context.
$sql = " JOIN {course} gclcrsc ON gclcrsc.id = $coursetable.id
AND gclcrsc.id = $param1";
$params[$key1] = $context->instanceid;
break;
case CONTEXT_BLOCK:
case CONTEXT_MODULE:
case CONTEXT_USER:
// Context cannot contain any courses.
return [null, null];
default:
throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel);
}
return [$sql, $params];
}
/**
* Gets a list of all contexts to reindex when reindexing this search area. The list should be
* returned in an order that is likely to be suitable when reindexing, for example with newer
* contexts first.
*
* The default implementation simply returns the system context, which will result in
* reindexing everything in normal date order (oldest first).
*
* @return \Iterator Iterator of contexts to reindex
*/
public function get_contexts_to_reindex() {
return new \ArrayIterator([\context_system::instance()]);
}
/**
* Returns an icon instance for the document.
*
* @param \core_search\document $doc
* @return \core_search\document_icon
*/
public function get_doc_icon(document $doc): document_icon {
return new document_icon('i/empty');
}
/**
* Returns a list of category names associated with the area.
*
* @return array
*/
public function get_category_names() {
return [manager::SEARCH_AREA_CATEGORY_OTHER];
}
}
+244
View File
@@ -0,0 +1,244 @@
<?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;
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';
/**
* Activities with a time created field can overwrite this constant.
*/
const CREATED_FIELD_NAME = '';
/**
* The context levels the search area is working on.
* @var array
*/
protected static $levels = [CONTEXT_MODULE];
/** @var array activity data instance. */
public $activitiesdata = [];
/**
* Returns recordset containing all activities within the given context.
*
* @param \context|null $context Context
* @param int $modifiedfrom Return only records modified after this date
* @return \moodle_recordset|null Recordset, or null if no possible activities in given context
*/
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
list ($contextjoin, $contextparams) = $this->get_context_restriction_sql(
$context, $this->get_module_name(), 'modtable');
if ($contextjoin === null) {
return null;
}
return $DB->get_recordset_sql('SELECT modtable.* FROM {' . $this->get_module_name() .
'} modtable ' . $contextjoin . ' WHERE modtable.' . static::MODIFIED_FIELD_NAME .
' >= ? ORDER BY modtable.' . static::MODIFIED_FIELD_NAME . ' ASC',
array_merge($contextparams, [$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
* @param array $options
* @return \core_search\document
*/
public function get_document($record, $options = array()) {
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', content_to_text($record->name, false));
$doc->set('content', content_to_text($record->intro, $record->introformat));
$doc->set('contextid', $context->id);
$doc->set('courseid', $record->course);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $record->{static::MODIFIED_FIELD_NAME});
// Check if this document should be considered new.
if (isset($options['lastindexedtime'])) {
$createdfield = static::CREATED_FIELD_NAME;
if (!empty($createdfield) && ($options['lastindexedtime'] < $record->{$createdfield})) {
// If the document was created after the last index time, it must be new.
$doc->set_is_new(true);
}
}
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;
try {
$activity = $this->get_activity($id);
$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 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];
}
/**
* Return the context info required to index files for
* this search area.
*
* Should be onerridden by each search area.
*
* @return array
*/
public function get_search_fileareas() {
$fileareas = array(
'intro' // Fileareas.
);
return $fileareas;
}
/**
* Files related to the current document are attached,
* to the document object ready for indexing by
* Global Search.
*
* The default implementation retrieves all files for
* the file areas returned by get_search_fileareas().
* If you need to filter files to specific items per
* file area, you will need to override this method
* and explicitly provide the items.
*
* @param document $document The current document
* @return void
*/
public function attach_files($document) {
$fileareas = $this->get_search_fileareas();
if (!empty($fileareas)) {
$cm = $this->get_cm($this->get_module_name(), $document->get('itemid'), $document->get('courseid'));
$context = \context_module::instance($cm->id);
$contextid = $context->id;
$fs = get_file_storage();
$files = $fs->get_area_files($contextid, $this->get_component_name(), $fileareas, false, '', false);
foreach ($files as $file) {
$document->add_stored_file($file);
}
}
return;
}
}
+420
View File
@@ -0,0 +1,420 @@
<?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 blocks.
*
* Note: Only blocks within courses are supported.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search;
defined('MOODLE_INTERNAL') || die();
/**
* Search area base class for blocks.
*
* Note: Only blocks within courses are supported.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base_block extends base {
/** @var string Cache name used for block instances */
const CACHE_INSTANCES = 'base_block_instances';
/**
* 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_BLOCK];
/**
* Gets the block name only.
*
* @return string Block name e.g. 'html'
*/
public function get_block_name() {
// Remove 'block_' text.
return substr($this->get_component_name(), 6);
}
/**
* Returns restrictions on which block_instances rows to return. By default, excludes rows
* that have empty configdata.
*
* If no restriction is required, you could return ['', []].
*
* @return array 2-element array of SQL restriction and params for it
*/
protected function get_indexing_restrictions() {
global $DB;
// This includes completely empty configdata, and also three other values that are
// equivalent to empty:
// - A serialized completely empty object.
// - A serialized object with one field called '0' (string not int) set to boolean false
// (this can happen after backup and restore, at least historically).
// - A serialized null.
$stupidobject = (object)[];
$zero = '0';
$stupidobject->{$zero} = false;
return [$DB->sql_compare_text('bi.configdata') . " != ? AND " .
$DB->sql_compare_text('bi.configdata') . " != ? AND " .
$DB->sql_compare_text('bi.configdata') . " != ? AND " .
$DB->sql_compare_text('bi.configdata') . " != ?",
['', base64_encode(serialize((object)[])), base64_encode(serialize($stupidobject)),
base64_encode(serialize(null))]];
}
/**
* Gets recordset of all blocks of this type modified since given time within the given context.
*
* See base class for detailed requirements. This implementation includes the key fields
* from block_instances.
*
* This can be overridden to do something totally different if the block's data is stored in
* other tables.
*
* If there are certain instances of the block which should not be included in the search index
* then you can override get_indexing_restrictions; by default this excludes rows with empty
* configdata.
*
* @param int $modifiedfrom Return only records modified after this date
* @param \context|null $context Context to find blocks within
* @return false|\moodle_recordset|null
*/
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
// Get context restrictions.
list ($contextjoin, $contextparams) = $this->get_context_restriction_sql($context, 'bi');
// Get custom restrictions for block type.
list ($restrictions, $restrictionparams) = $this->get_indexing_restrictions();
if ($restrictions) {
$restrictions = 'AND ' . $restrictions;
}
// Query for all entries in block_instances for this type of block, within the specified
// context. The query is based on the one from get_recordset_by_timestamp and applies the
// same restrictions.
return $DB->get_recordset_sql("
SELECT bi.id, bi.timemodified, bi.timecreated, bi.configdata,
c.id AS courseid, x.id AS contextid
FROM {block_instances} bi
$contextjoin
JOIN {context} x ON x.instanceid = bi.id AND x.contextlevel = ?
JOIN {context} parent ON parent.id = bi.parentcontextid
LEFT JOIN {course_modules} cm ON cm.id = parent.instanceid AND parent.contextlevel = ?
JOIN {course} c ON c.id = cm.course
OR (c.id = parent.instanceid AND parent.contextlevel = ?)
WHERE bi.timemodified >= ?
AND bi.blockname = ?
AND (parent.contextlevel = ? AND (" . $DB->sql_like('bi.pagetypepattern', '?') . "
OR bi.pagetypepattern IN ('site-index', 'course-*', '*')))
$restrictions
ORDER BY bi.timemodified ASC",
array_merge($contextparams, [CONTEXT_BLOCK, CONTEXT_MODULE, CONTEXT_COURSE,
$modifiedfrom, $this->get_block_name(), CONTEXT_COURSE, 'course-view-%'],
$restrictionparams));
}
public function get_doc_url(\core_search\document $doc) {
// Load block instance and find cmid if there is one.
$blockinstanceid = preg_replace('~^.*-~', '', $doc->get('id'));
$instance = $this->get_block_instance($blockinstanceid);
$courseid = $doc->get('courseid');
$anchor = 'inst' . $blockinstanceid;
// Check if the block is at course or module level.
if ($instance->cmid) {
// No module-level page types are supported at present so the search system won't return
// them. But let's put some example code here to indicate how it could work.
debugging('Unexpected module-level page type for block ' . $blockinstanceid . ': ' .
$instance->pagetypepattern, DEBUG_DEVELOPER);
$modinfo = get_fast_modinfo($courseid);
$cm = $modinfo->get_cm($instance->cmid);
return new \moodle_url($cm->url, null, $anchor);
} else {
// The block is at course level. Let's check the page type, although in practice we
// currently only support the course main page.
if ($instance->pagetypepattern === '*' || $instance->pagetypepattern === 'course-*' ||
preg_match('~^course-view-(.*)$~', $instance->pagetypepattern)) {
return new \moodle_url('/course/view.php', ['id' => $courseid], $anchor);
} else if ($instance->pagetypepattern === 'site-index') {
return new \moodle_url('/', ['redirect' => 0], $anchor);
} else {
debugging('Unexpected page type for block ' . $blockinstanceid . ': ' .
$instance->pagetypepattern, DEBUG_DEVELOPER);
return new \moodle_url('/course/view.php', ['id' => $courseid], $anchor);
}
}
}
public function get_context_url(\core_search\document $doc) {
return $this->get_doc_url($doc);
}
/**
* Checks access for a document in this search area.
*
* If you override this function for a block, you should call this base class version first
* as it will check that the block is still visible to users in a supported location.
*
* @param int $id Document id
* @return int manager:ACCESS_xx constant
*/
public function check_access($id) {
$instance = $this->get_block_instance($id, IGNORE_MISSING);
if (!$instance) {
// This generally won't happen because if the block has been deleted then we won't have
// included its context in the search area list, but just in case.
return manager::ACCESS_DELETED;
}
// Check block has not been moved to an unsupported area since it was indexed. (At the
// moment, only blocks within site and course context are supported, also only certain
// page types.)
if (!$instance->courseid ||
!self::is_supported_page_type_at_course_context($instance->pagetypepattern)) {
return manager::ACCESS_DELETED;
}
// Note we do not need to check if the block was hidden or if the user has access to the
// context, because those checks are included in the list of search contexts user can access
// that is calculated in manager.php every time they do a query.
return manager::ACCESS_GRANTED;
}
/**
* Checks if a page type is supported for blocks when at course (or also site) context. This
* function should be consistent with the SQL in get_recordset_by_timestamp.
*
* @param string $pagetype Page type
* @return bool True if supported
*/
protected static function is_supported_page_type_at_course_context($pagetype) {
if (in_array($pagetype, ['site-index', 'course-*', '*'])) {
return true;
}
if (preg_match('~^course-view-~', $pagetype)) {
return true;
}
return false;
}
/**
* Gets a block instance with given id.
*
* Returns the fields id, pagetypepattern, subpagepattern from block_instances and also the
* cmid (if parent context is an activity module).
*
* @param int $id ID of block instance
* @param int $strictness MUST_EXIST or IGNORE_MISSING
* @return false|mixed Block instance data (may be false if strictness is IGNORE_MISSING)
*/
protected function get_block_instance($id, $strictness = MUST_EXIST) {
global $DB;
$cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_search',
self::CACHE_INSTANCES, [], ['simplekeys' => true]);
$id = (int)$id;
$instance = $cache->get($id);
if (!$instance) {
$instance = $DB->get_record_sql("
SELECT bi.id, bi.pagetypepattern, bi.subpagepattern,
c.id AS courseid, cm.id AS cmid
FROM {block_instances} bi
JOIN {context} parent ON parent.id = bi.parentcontextid
LEFT JOIN {course} c ON c.id = parent.instanceid AND parent.contextlevel = ?
LEFT JOIN {course_modules} cm ON cm.id = parent.instanceid AND parent.contextlevel = ?
WHERE bi.id = ?",
[CONTEXT_COURSE, CONTEXT_MODULE, $id], $strictness);
$cache->set($id, $instance);
}
return $instance;
}
/**
* Clears static cache. This function can be removed (with calls to it in the test script
* replaced with cache_helper::purge_all) if MDL-59427 is fixed.
*/
public static function clear_static() {
\cache::make_from_params(\cache_store::MODE_REQUEST, 'core_search',
self::CACHE_INSTANCES, [], ['simplekeys' => true])->purge();
}
/**
* Helper function that gets SQL useful for restricting a search query given a passed-in
* context.
*
* The SQL returned will be one or more JOIN statements, surrounded by whitespace, which act
* as restrictions on the query based on the rows in the block_instances table.
*
* We assume the block instances have already been restricted by blockname.
*
* Returns null if there can be no results for this block within this context.
*
* If named parameters are used, these will be named gcrs0, gcrs1, etc. The table aliases used
* in SQL also all begin with gcrs, to avoid conflicts.
*
* @param \context|null $context Context to restrict the query
* @param string $blocktable Alias of block_instances table
* @param int $paramtype Type of SQL parameters to use (default question mark)
* @return array Array with SQL and parameters
* @throws \coding_exception If called with invalid params
*/
protected function get_context_restriction_sql(\context $context = null, $blocktable = 'bi',
$paramtype = SQL_PARAMS_QM) {
global $DB;
if (!$context) {
return ['', []];
}
switch ($paramtype) {
case SQL_PARAMS_QM:
$param1 = '?';
$param2 = '?';
$key1 = 0;
$key2 = 1;
break;
case SQL_PARAMS_NAMED:
$param1 = ':gcrs0';
$param2 = ':gcrs1';
$key1 = 'gcrs0';
$key2 = 'gcrs1';
break;
default:
throw new \coding_exception('Unexpected $paramtype: ' . $paramtype);
}
$params = [];
switch ($context->contextlevel) {
case CONTEXT_SYSTEM:
$sql = '';
break;
case CONTEXT_COURSECAT:
case CONTEXT_COURSE:
case CONTEXT_MODULE:
case CONTEXT_USER:
// Find all blocks whose parent is within the specified context.
$sql = " JOIN {context} gcrsx ON gcrsx.id = $blocktable.parentcontextid
AND (gcrsx.id = $param1 OR " . $DB->sql_like('gcrsx.path', $param2) . ") ";
$params[$key1] = $context->id;
$params[$key2] = $context->path . '/%';
break;
case CONTEXT_BLOCK:
// Find only the specified block of this type. Since we are generating JOINs
// here, we do this by joining again to the block_instances table with the same ID.
$sql = " JOIN {block_instances} gcrsbi ON gcrsbi.id = $blocktable.id
AND gcrsbi.id = $param1 ";
$params[$key1] = $context->instanceid;
break;
default:
throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel);
}
return [$sql, $params];
}
/**
* This can be used in subclasses to change ordering within the get_contexts_to_reindex
* function.
*
* It returns 2 values:
* - Extra SQL joins (tables block_instances 'bi' and context 'x' already exist).
* - An ORDER BY value which must use aggregate functions, by default 'MAX(bi.timemodified) DESC'.
*
* Note the query already includes a GROUP BY on the context fields, so if your joins result
* in multiple rows, you can use aggregate functions in the ORDER BY. See forum for an example.
*
* @return string[] Array with 2 elements; extra joins for the query, and ORDER BY value
*/
protected function get_contexts_to_reindex_extra_sql() {
return ['', 'MAX(bi.timemodified) DESC'];
}
/**
* Gets a list of all contexts to reindex when reindexing this search area.
*
* For blocks, the default is to return all contexts for blocks of that type, that are on a
* course page, in order of time added (most recent first).
*
* @return \Iterator Iterator of contexts to reindex
* @throws \moodle_exception If any DB error
*/
public function get_contexts_to_reindex() {
global $DB;
list ($extrajoins, $dborder) = $this->get_contexts_to_reindex_extra_sql();
$contexts = [];
$selectcolumns = \context_helper::get_preload_record_columns_sql('x');
$groupbycolumns = '';
foreach (\context_helper::get_preload_record_columns('x') as $column => $thing) {
if ($groupbycolumns !== '') {
$groupbycolumns .= ',';
}
$groupbycolumns .= $column;
}
$rs = $DB->get_recordset_sql("
SELECT $selectcolumns
FROM {block_instances} bi
JOIN {context} x ON x.instanceid = bi.id AND x.contextlevel = ?
JOIN {context} parent ON parent.id = bi.parentcontextid
$extrajoins
WHERE bi.blockname = ? AND parent.contextlevel = ?
GROUP BY $groupbycolumns
ORDER BY $dborder", [CONTEXT_BLOCK, $this->get_block_name(), CONTEXT_COURSE]);
return new \core\dml\recordset_walk($rs, function($rec) {
$id = $rec->ctxid;
\context_helper::preload_from_record($rec);
return \context::instance_by_id($id);
});
}
/**
* Returns an icon instance for the document.
*
* @param \core_search\document $doc
* @return \core_search\document_icon
*/
public function get_doc_icon(document $doc): document_icon {
return new document_icon('e/anchor');
}
/**
* Returns a list of category names associated with the area.
*
* @return array
*/
public function get_category_names() {
return [manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
}
}
+308
View File
@@ -0,0 +1,308 @@
<?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;
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];
/**
* Returns the module name.
*
* @return string
*/
protected function get_module_name() {
return substr($this->componentname, 4);
}
/**
* 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);
}
/**
* Helper function that gets SQL useful for restricting a search query given a passed-in
* context.
*
* The SQL returned will be zero or more JOIN statements, surrounded by whitespace, which act
* as restrictions on the query based on the rows in a module table.
*
* You can pass in a null or system context, which will both return an empty string and no
* params.
*
* Returns an array with two nulls if there can be no results for the activity within this
* context (e.g. it is a block context).
*
* If named parameters are used, these will be named gcrs0, gcrs1, etc. The table aliases used
* in SQL also all begin with gcrs, to avoid conflicts.
*
* @param \context|null $context Context to restrict the query
* @param string $modname Name of module e.g. 'forum'
* @param string $modtable Alias of table containing module id
* @param int $paramtype Type of SQL parameters to use (default question mark)
* @return array Array with SQL and parameters; both null if no need to query
* @throws \coding_exception If called with invalid params
*/
protected function get_context_restriction_sql(?\context $context, $modname, $modtable,
$paramtype = SQL_PARAMS_QM) {
global $DB;
if (!$context) {
return ['', []];
}
switch ($paramtype) {
case SQL_PARAMS_QM:
$param1 = '?';
$param2 = '?';
$param3 = '?';
$key1 = 0;
$key2 = 1;
$key3 = 2;
break;
case SQL_PARAMS_NAMED:
$param1 = ':gcrs0';
$param2 = ':gcrs1';
$param3 = ':gcrs2';
$key1 = 'gcrs0';
$key2 = 'gcrs1';
$key3 = 'gcrs2';
break;
default:
throw new \coding_exception('Unexpected $paramtype: ' . $paramtype);
}
$params = [];
switch ($context->contextlevel) {
case CONTEXT_SYSTEM:
$sql = '';
break;
case CONTEXT_COURSECAT:
// Find all activities of this type within the specified category or any
// sub-category.
$pathmatch = $DB->sql_like('gcrscc2.path', $DB->sql_concat('gcrscc1.path', $param3));
$sql = " JOIN {course_modules} gcrscm ON gcrscm.instance = $modtable.id
AND gcrscm.module = (SELECT id FROM {modules} WHERE name = $param1)
JOIN {course} gcrsc ON gcrsc.id = gcrscm.course
JOIN {course_categories} gcrscc1 ON gcrscc1.id = $param2
JOIN {course_categories} gcrscc2 ON gcrscc2.id = gcrsc.category AND
(gcrscc2.id = gcrscc1.id OR $pathmatch) ";
$params[$key1] = $modname;
$params[$key2] = $context->instanceid;
// Note: This param is a bit annoying as it obviously never changes, but sql_like
// throws a debug warning if you pass it anything with quotes in, so it has to be
// a bound parameter.
$params[$key3] = '/%';
break;
case CONTEXT_COURSE:
// Find all activities of this type within the course.
$sql = " JOIN {course_modules} gcrscm ON gcrscm.instance = $modtable.id
AND gcrscm.course = $param1
AND gcrscm.module = (SELECT id FROM {modules} WHERE name = $param2) ";
$params[$key1] = $context->instanceid;
$params[$key2] = $modname;
break;
case CONTEXT_MODULE:
// Find only the specified activity of this type.
$sql = " JOIN {course_modules} gcrscm ON gcrscm.instance = $modtable.id
AND gcrscm.id = $param1
AND gcrscm.module = (SELECT id FROM {modules} WHERE name = $param2) ";
$params[$key1] = $context->instanceid;
$params[$key2] = $modname;
break;
case CONTEXT_BLOCK:
case CONTEXT_USER:
// These contexts cannot contain any activities, so return null.
return [null, null];
default:
throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel);
}
return [$sql, $params];
}
/**
* This can be used in subclasses to change ordering within the get_contexts_to_reindex
* function.
*
* It returns 2 values:
* - Extra SQL joins (tables course_modules 'cm' and context 'x' already exist).
* - An ORDER BY value which must use aggregate functions, by default 'MAX(cm.added) DESC'.
*
* Note the query already includes a GROUP BY on the context fields, so if your joins result
* in multiple rows, you can use aggregate functions in the ORDER BY. See forum for an example.
*
* @return string[] Array with 2 elements; extra joins for the query, and ORDER BY value
*/
protected function get_contexts_to_reindex_extra_sql() {
return ['', 'MAX(cm.added) DESC'];
}
/**
* Gets a list of all contexts to reindex when reindexing this search area.
*
* For modules, the default is to return all contexts for modules of that type, in order of
* time added (most recent first).
*
* @return \Iterator Iterator of contexts to reindex
* @throws \moodle_exception If any DB error
*/
public function get_contexts_to_reindex() {
global $DB;
list ($extrajoins, $dborder) = $this->get_contexts_to_reindex_extra_sql();
$contexts = [];
$selectcolumns = \context_helper::get_preload_record_columns_sql('x');
$groupbycolumns = '';
foreach (\context_helper::get_preload_record_columns('x') as $column => $thing) {
if ($groupbycolumns !== '') {
$groupbycolumns .= ',';
}
$groupbycolumns .= $column;
}
$rs = $DB->get_recordset_sql("
SELECT $selectcolumns
FROM {course_modules} cm
JOIN {context} x ON x.instanceid = cm.id AND x.contextlevel = ?
$extrajoins
WHERE cm.module = (SELECT id FROM {modules} WHERE name = ?)
GROUP BY $groupbycolumns
ORDER BY $dborder", [CONTEXT_MODULE, $this->get_module_name()]);
return new \core\dml\recordset_walk($rs, function($rec) {
$id = $rec->ctxid;
\context_helper::preload_from_record($rec);
return \context::instance_by_id($id);
});
}
/**
* Indicates whether this search area may restrict access by group.
*
* This should return true if the search area (sometimes) sets the 'groupid' schema field, and
* false if it never sets that field.
*
* (If this function returns false, but the field is set, then results may be restricted
* unintentionally.)
*
* If this returns true, the search engine will automatically apply group restrictions in some
* cases (by default, where a module is configured to use separate groups). See function
* restrict_cm_access_by_group().
*
* @return bool
*/
public function supports_group_restriction() {
return false;
}
/**
* Checks whether the content of this search area should be restricted by group for a
* specific module. Called at query time.
*
* The default behaviour simply checks if the effective group mode is SEPARATEGROUPS, which
* is probably correct for most cases.
*
* If restricted by group, the search query will (where supported by the engine) filter out
* results for groups the user does not belong to, unless the user has 'access all groups'
* for the activity. This affects only documents which set the 'groupid' field; results with no
* groupid will not be restricted.
*
* Even if you return true to this function, you may still need to do group access checks in
* check_access, because the search engine may not support group restrictions.
*
* @param \cm_info $cm
* @return bool True to restrict by group
*/
public function restrict_cm_access_by_group(\cm_info $cm) {
return $cm->effectivegroupmode == SEPARATEGROUPS;
}
/**
* Returns an icon instance for the document.
*
* @param \core_search\document $doc
* @return \core_search\document_icon
*/
public function get_doc_icon(document $doc): document_icon {
return new document_icon('monologo', $this->get_module_name());
}
/**
* Returns a list of category names associated with the area.
*
* @return array
*/
public function get_category_names() {
return [manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
}
}
+709
View File
@@ -0,0 +1,709 @@
<?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;
use context;
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 \core_search\document_icon Document icon instance.
*/
protected $docicon = null;
/**
* @var int|null The content field filearea.
*/
protected $contentfilearea = null;
/**
* @var int|null The content field itemid.
*/
protected $contentitemid = null;
/**
* @var bool Should be set to true if document hasn't been indexed before. False if unknown.
*/
protected $isnew = false;
/**
* @var \stored_file[] An array of stored files to attach to the document.
*/
protected $files = array();
/**
* Change list (for engine implementers):
* 2017091700 - add optional field groupid
*
* @var int Schema version number (update if any change)
*/
const SCHEMA_VERSION = 2017091700;
/**
* 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' => 'text',
'stored' => true,
'indexed' => true,
'mainquery' => true
),
'content' => array(
'type' => 'text',
'stored' => true,
'indexed' => true,
'mainquery' => 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' => true
),
'owneruserid' => array(
'type' => 'int',
'stored' => true,
'indexed' => true
),
'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' => true
),
'groupid' => array(
'type' => 'int',
'stored' => true,
'indexed' => true
),
'description1' => array(
'type' => 'text',
'stored' => true,
'indexed' => true,
'mainquery' => true
),
'description2' => array(
'type' => 'text',
'stored' => true,
'indexed' => true,
'mainquery' => true
)
);
/**
* Any fields that are engine specifc. These are fields that are solely used by a search engine plugin
* for internal purposes.
*
* Field names should be prefixed with engine name to avoid potential conflict with core fields.
*
* Uses same format as fields above.
*
* @var array
*/
protected static $enginefields = array();
/**
* 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);
}
/**
* Add a stored file to the document.
*
* @param \stored_file|int $file The file to add, or file id.
* @return void
*/
public function add_stored_file($file) {
if (is_numeric($file)) {
$this->files[$file] = $file;
} else {
$this->files[$file->get_id()] = $file;
}
}
/**
* Returns the array of attached files.
*
* @return \stored_file[]
*/
public function get_files() {
// The files array can contain stored file ids, so we need to get instances if asked.
foreach ($this->files as $id => $listfile) {
if (is_numeric($listfile)) {
$fs = get_file_storage();
if ($file = $fs->get_file_by_id($id)) {
$this->files[$id] = $file;
} else {
unset($this->files[$id]); // Index is out of date and referencing a file that does not exist.
}
}
}
return $this->files;
}
/**
* 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];
} else if (!empty(static::$enginefields[$fieldname])) {
$fielddata = static::$enginefields[$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 {
// Remove disallowed Unicode characters.
$value = \core_text::remove_unicode_non_characters($value);
// Replace all groups of line breaks and spaces by single spaces.
$this->data[$fieldname] = preg_replace("/\s+/u", " ", $value);
if ($this->data[$fieldname] === null) {
if (isset($this->data['id'])) {
$docid = $this->data['id'];
} else {
$docid = '(unknown)';
}
throw new \moodle_exception('error_indexing', 'search', '', null, '"' . $fieldname .
'" value causes preg_replace error (may be caused by unusual characters) ' .
'in document with id "' . $docid . '"');
}
}
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]));
}
/**
* Set if this is a new document. False if unknown.
*
* @param bool $new
*/
public function set_is_new($new) {
$this->isnew = (bool)$new;
}
/**
* Returns if the document is new. False if unknown.
*
* @return bool
*/
public function get_is_new() {
return $this->isnew;
}
/**
* Returns all default fields definitions.
*
* @return array
*/
public static function get_default_fields_definition() {
return static::$requiredfields + static::$optionalfields + static::$enginefields;
}
/**
* 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;
}
/**
* Formats a text 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 $text
* @return string
*/
public static function format_text_for_engine($text) {
return $text;
}
/**
* 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 + static::$enginefields;
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;
}
/**
* Sets document icon instance.
*
* @param \core_search\document_icon $docicon
*/
public function set_doc_icon(document_icon $docicon) {
$this->docicon = $docicon;
}
/**
* Gets document icon instance.
*
* @return \core_search\document_icon
*/
public function get_doc_icon() {
return $this->docicon;
}
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() {
// Set any unset defaults.
$this->apply_defaults();
// 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 string with the engine dependant format.
$data[$fieldname] = static::format_string_for_engine($data[$fieldname]);
} else if ($field['type'] === 'text') {
// Overwrite the text with the engine dependant format.
$data[$fieldname] = static::format_text_for_engine($data[$fieldname]);
}
}
$fields = static::$optionalfields + static::$enginefields;
foreach ($fields 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 string with the engine dependant format.
$data[$fieldname] = static::format_string_for_engine($data[$fieldname]);
} else if ($field['type'] === 'text') {
// Overwrite the text with the engine dependant format.
$data[$fieldname] = static::format_text_for_engine($data[$fieldname]);
}
}
return $data;
}
/**
* Apply any defaults to unset fields before export. Called after document building, but before export.
*
* Sub-classes of this should make sure to call parent::apply_defaults().
*/
protected function apply_defaults() {
// Set the default type, TYPE_TEXT.
if (!isset($this->data['type'])) {
$this->data['type'] = manager::TYPE_TEXT;
}
}
/**
* Export the document data to be used as a template context.
*
* Just delegates all the processing to export_doc_info, also used by external functions.
* Adding more info than the required one as people might be interested in extending the template.
*
* @param \renderer_base $output The renderer.
* @return array
*/
public function export_for_template(\renderer_base $output): array {
$docdata = $this->export_doc($output);
return $docdata;
}
/**
* Returns the current docuement information.
*
* Adding more info than the required one as themers and ws clients might be interested in showing more stuff.
*
* 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.
*
* SECURITY NOTE: It is the responsibility of the document to properly escape any text to be displayed.
* The renderer will output the content without any further cleaning.
*
* @param \renderer_base $output The renderer.
* @return array
*/
public function export_doc(\renderer_base $output): array {
global $USER, $CFG;
require_once($CFG->dirroot . '/course/lib.php');
list($componentname, $areaname) = \core_search\manager::extract_areaid_parts($this->get('areaid'));
$context = context::instance_by_id($this->get('contextid'));
$searcharea = \core_search\manager::get_search_area($this->data['areaid']);
$title = $this->is_set('title') ? $this->format_text($searcharea->get_document_display_title($this)) : '';
$data = [
'itemid' => $this->get('itemid'),
'componentname' => $componentname,
'areaname' => $areaname,
'courseurl' => (course_get_url($this->get('courseid')))->out(false),
'coursefullname' => format_string($this->get('coursefullname'), true, ['context' => $context->id]),
'modified' => userdate($this->get('modified')),
'timemodified' => $this->get('modified'),
'title' => ($title !== '') ? $title : get_string('notitle', 'search'),
'docurl' => ($this->get_doc_url())->out(false),
'content' => $this->is_set('content') ? $this->format_text($this->get('content')) : null,
'contextid' => $this->get('contextid'),
'contexturl' => ($this->get_context_url())->out(false),
'description1' => $this->is_set('description1') ? $this->format_text($this->get('description1')) : null,
'description2' => $this->is_set('description2') ? $this->format_text($this->get('description2')) : null,
];
// Now take any attached any files.
$files = $this->get_files();
if (!empty($files)) {
if (count($files) > 1) {
$filenames = [];
foreach ($files as $file) {
$filenames[] = format_string($file->get_filename(), true, ['context' => $context->id]);
}
$data['multiplefiles'] = true;
$data['filenames'] = $filenames;
} else {
$file = reset($files);
$data['filename'] = format_string($file->get_filename(), true, ['context' => $context->id]);
}
}
if ($this->is_set('userid')) {
if ($this->get('userid') == $USER->id ||
(has_capability('moodle/user:viewdetails', $context) &&
has_capability('moodle/course:viewparticipants', $context))) {
$data['userurl'] = (new \moodle_url(
'/user/view.php',
['id' => $this->get('userid'), 'course' => $this->get('courseid')]
))->out(false);
$data['userfullname'] = format_string($this->get('userfullname'), true, ['context' => $context->id]);
$data['userid'] = $this->get('userid');
}
}
if ($docicon = $this->get_doc_icon()) {
$data['icon'] = $output->image_url($docicon->get_name(), $docicon->get_component());
$data['iconurl'] = $data['icon']->out(false);
}
$data['textformat'] = $this->get_text_format();
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')));
}
}
+84
View File
@@ -0,0 +1,84 @@
<?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();
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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 icon class.
*
* @package core_search
* @copyright 2018 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search;
defined('MOODLE_INTERNAL') || die();
/**
* Represents a document icon.
*
* @package core_search
* @copyright 2018 Dmitrii Metelkin <dmitriim@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class document_icon {
/**
* Icon file name.
* @var string
*/
protected $name;
/** Icon file component.
* @var string
*/
protected $component;
/**
* Constructor.
*
* @param string $name Icon name.
* @param string $component Icon component.
*/
public function __construct($name, $component = 'moodle') {
$this->name = $name;
$this->component = $component;
}
/**
* Returns name of the icon file.
*
* @return string
*/
public function get_name() {
return $this->name;
}
/**
* Returns the component of the icon file.
*
* @return string
*/
public function get_component() {
return $this->component;
}
}
+781
View File
@@ -0,0 +1,781 @@
<?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;
/**
* @var bool If true, should skip schema validity check when checking the search engine is ready
*/
protected $skipschemacheck = false;
/**
* Initialises the search engine configuration.
*
* Search engine availability should be checked separately.
*
* The alternate configuration option is only used to construct a special second copy of the
* search engine object, as described in {@see has_alternate_configuration}.
*
* @param bool $alternateconfiguration If true, use alternate configuration settings
* @return void
*/
public function __construct(bool $alternateconfiguration = false) {
$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();
}
// For alternate configuration, automatically replace normal configuration values with
// those beginning with 'alternate'.
if ($alternateconfiguration) {
foreach ((array)$this->config as $key => $value) {
if (preg_match('~^alternate(.*)$~', $key, $matches)) {
$this->config->{$matches[1]} = $value;
}
}
}
// Flag just in case engine needs to know it is using the alternate configuration.
$this->config->alternateconfiguration = $alternateconfiguration;
}
/**
* 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])) {
$userfieldsapi = \core_user\fields::for_name();
$fields = $userfieldsapi->get_sql('', false, '', '', false)->selects;
self::$cachedusers[$userid] = $DB->get_record('user', array('id' => $userid), 'id, ' . $fields);
}
return self::$cachedusers[$userid];
}
/**
* Clears the users cache.
*
* @return null
*/
public static function clear_users_cache() {
self::$cachedusers = [];
}
/**
* Returns a search instance of the specified area checking internal caching.
*
* @param string $areaid Area id
* @return \core_search\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\base $searcharea
* @param array $docdata
* @return \core_search\document
*/
protected function to_document(\core_search\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));
$doc->set_doc_icon($searcharea->get_doc_icon($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;
}
/**
* Loop through given iterator of search documents
* and and have the search engine back end add them
* to the index.
*
* @param \iterator $iterator the iterator of documents to index
* @param base $searcharea the area for the documents to index
* @param array $options document indexing options
* @return array Processed document counts
*/
public function add_documents($iterator, $searcharea, $options) {
$numrecords = 0;
$numdocs = 0;
$numdocsignored = 0;
$numbatches = 0;
$lastindexeddoc = 0;
$firstindexeddoc = 0;
$partial = false;
$lastprogress = manager::get_current_time();
$batchmode = $this->supports_add_document_batch();
$currentbatch = [];
foreach ($iterator as $document) {
// Stop if we have exceeded the time limit (and there are still more items). Always
// do at least one second's worth of documents otherwise it will never make progress.
if ($lastindexeddoc !== $firstindexeddoc &&
!empty($options['stopat']) && manager::get_current_time() >= $options['stopat']) {
$partial = true;
break;
}
if (!$document instanceof \core_search\document) {
continue;
}
if (isset($options['lastindexedtime']) && $options['lastindexedtime'] == 0) {
// If we have never indexed this area before, it must be new.
$document->set_is_new(true);
}
if ($options['indexfiles']) {
// Attach files if we are indexing.
$searcharea->attach_files($document);
}
if ($batchmode && strlen($document->get('content')) <= $this->get_batch_max_content()) {
$currentbatch[] = $document;
if (count($currentbatch) >= $this->get_batch_max_documents()) {
[$processed, $failed, $batches] = $this->add_document_batch($currentbatch, $options['indexfiles']);
$numdocs += $processed;
$numdocsignored += $failed;
$numbatches += $batches;
$currentbatch = [];
}
} else {
if ($this->add_document($document, $options['indexfiles'])) {
$numdocs++;
} else {
$numdocsignored++;
}
$numbatches++;
}
$lastindexeddoc = $document->get('modified');
if (!$firstindexeddoc) {
$firstindexeddoc = $lastindexeddoc;
}
$numrecords++;
// If indexing the area takes a long time, periodically output progress information.
if (isset($options['progress'])) {
$now = manager::get_current_time();
if ($now - $lastprogress >= manager::DISPLAY_INDEXING_PROGRESS_EVERY) {
$lastprogress = $now;
// The first date format is the same used in \core\cron::trace_time_and_memory().
$options['progress']->output(date('H:i:s', (int)$now) . ': Done to ' . userdate(
$lastindexeddoc, get_string('strftimedatetimeshort', 'langconfig')), 1);
}
}
}
// Add remaining documents from batch.
if ($batchmode && $currentbatch) {
[$processed, $failed, $batches] = $this->add_document_batch($currentbatch, $options['indexfiles']);
$numdocs += $processed;
$numdocsignored += $failed;
$numbatches += $batches;
}
return [$numrecords, $numdocs, $numdocsignored, $lastindexeddoc, $partial, $numbatches];
}
/**
* 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;
}
/**
* Run any pre-indexing operations.
*
* Should be overwritten if the search engine needs to do any pre index preparation.
*
* @param bool $fullindex True if a full index will be performed
* @return void
*/
public function index_starting($fullindex = false) {
// Nothing by default.
}
/**
* Run any post indexing operations.
*
* Should be overwritten if the search engine needs to do any post index cleanup.
*
* @param int $numdocs The number of documents that were added to the index
* @param bool $fullindex True if a full index was performed
* @return void
*/
public function index_complete($numdocs = 0, $fullindex = false) {
// Nothing by default.
}
/**
* Do anything that may need to be done before an area is indexed.
*
* @param \core_search\base $searcharea The search area that was complete
* @param bool $fullindex True if a full index is being performed
* @return void
*/
public function area_index_starting($searcharea, $fullindex = false) {
// Nothing by default.
}
/**
* Do any area cleanup needed, and do anything to confirm contents.
*
* Return false to prevent the search area completed time and stats from being updated.
*
* @param \core_search\base $searcharea The search area that was complete
* @param int $numdocs The number of documents that were added to the index
* @param bool $fullindex True if a full index is being performed
* @return bool True means that data is considered indexed
*/
public function area_index_complete($searcharea, $numdocs = 0, $fullindex = false) {
return true;
}
/**
* Optimizes the search engine.
*
* Should be overwritten if the search engine can optimize its contents.
*
* @return void
*/
public function optimize() {
// Nothing by default.
mtrace('The ' . get_string('pluginname', $this->get_plugin_name()) .
' search engine does not require automatic optimization.');
}
/**
* 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;
}
/**
* Returns the total number of documents available for the most recent call to execute_query.
*
* This can be an estimate, but should get more accurate the higher the limited passed to execute_query is.
* To do that, the engine can use (actual result returned count + count of unchecked documents), or
* (total possible docs - docs that have been checked and rejected).
*
* Engine can limit to manager::MAX_RESULTS if there is cost to determining more.
* If this cannot be computed in a reasonable way, manager::MAX_RESULTS may be returned.
*
* @return int
*/
abstract public function get_query_total_count();
/**
* Return true if file indexing is supported and enabled. False otherwise.
*
* @return bool
*/
public function file_indexing_enabled() {
return false;
}
/**
* 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.
*
* If the function $this->should_skip_schema_check() returns true, then this function may leave
* out time-consuming checks that the schema is valid. (This allows for improved performance on
* critical pages such as the main search form.)
*
* @return true|string Returns true if all good or an error string.
*/
abstract function is_server_ready();
/**
* Tells the search engine to skip any time-consuming checks that it might do as part of the
* is_server_ready function, and only carry out a basic check that it can contact the server.
*
* This setting is not remembered and applies only to the current request.
*
* @since Moodle 3.5
* @param bool $skip True to skip the checks, false to start checking again
*/
public function skip_schema_check($skip = true) {
$this->skipschemacheck = $skip;
}
/**
* For use by subclasses. The engine can call this inside is_server_ready to check whether it
* should skip time-consuming schema checks.
*
* @since Moodle 3.5
* @return bool True if schema checks should be skipped
*/
protected function should_skip_schema_check() {
return $this->skipschemacheck;
}
/**
* Adds a document to the search engine.
*
* @param document $document
* @param bool $fileindexing True if file indexing is to be used
* @return bool False if the file was skipped or failed, true on success
*/
abstract function add_document($document, $fileindexing = false);
/**
* Adds multiple documents to the search engine.
*
* It should return the number successfully processed, and the number of batches they were
* processed in (for example if you add 100 documents and there is an error processing one of
* those documents, and it took 4 batches, it would return [99, 1, 4]).
*
* If the engine implements this, it should return true to {@see supports_add_document_batch}.
*
* The system will only call this function with up to {@see get_batch_max_documents} documents,
* and each document in the batch will have content no larger than specified by
* {@see get_batch_max_content}.
*
* @param document[] $documents Documents to add
* @param bool $fileindexing True if file indexing is to be used
* @return int[] Array of three elements, successfully processed, failed processed, batch count
*/
public function add_document_batch(array $documents, bool $fileindexing = false): array {
throw new \coding_exception('add_document_batch not supported by this engine');
}
/**
* Executes the query on the engine.
*
* Implementations of this function should check user context array to limit the results to contexts where the
* user have access. They should also limit the owneruserid field to manger::NO_OWNER_ID or the current user's id.
* Engines must use area->check_access() to confirm user access.
*
* Engines should reasonably attempt to fill up to limit with valid results if they are available.
*
* The $filters object may include the following fields (optional except q):
* - q: value of main search field; results should include this text
* - title: if included, title must match this search
* - areaids: array of search area id strings (only these areas will be searched)
* - courseids: array of course ids (only these courses will be searched)
* - groupids: array of group ids (only results specifically from these groupids will be
* searched) - this option will be ignored if the search engine doesn't support groups
*
* The $accessinfo parameter has two different values (for historical compatibility). If the
* engine returns false to supports_group_filtering then it is an array of user contexts, or
* true if the user can access all contexts. (This parameter used to be called $usercontexts.)
* If the engine returns true to supports_group_filtering then it will be an object containing
* these fields:
* - everything (true if admin is searching with no restrictions)
* - usercontexts (same as above)
* - separategroupscontexts (array of context ids where separate groups are used)
* - visiblegroupscontextsareas (array of subset of those where some areas use visible groups)
* - usergroups (array of relevant group ids that user belongs to)
*
* The engine should apply group restrictions to those contexts listed in the
* 'separategroupscontexts' array. In these contexts, it shouled only include results if the
* groupid is not set, or if the groupid matches one of the values in USER_GROUPS array, or
* if the search area is one of those listed in 'visiblegroupscontextsareas' for that context.
*
* @param \stdClass $filters Query and filters to apply.
* @param \stdClass $accessinfo Information about the contexts the user can access
* @param int $limit The maximum number of results to return. If empty, limit to manager::MAX_RESULTS.
* @return \core_search\document[] Results or false if no results
*/
abstract public function execute_query($filters, $accessinfo, $limit = 0);
/**
* Delete all documents.
*
* @param string $areaid To filter by area
* @return void
*/
abstract function delete($areaid = null);
/**
* Deletes information related to a specific context id. This should be used when the context
* itself is deleted from Moodle.
*
* This only deletes information for the specified context - not for any child contexts.
*
* This function is optional; if not supported it will return false and the information will
* not be deleted from the search index.
*
* If an engine implements this function it should also implement delete_index_for_course;
* otherwise, nothing will be deleted when users delete an entire course at once.
*
* @param int $oldcontextid ID of context that has been deleted
* @return bool True if implemented
* @throws \core_search\engine_exception Engines may throw this exception for any problem
*/
public function delete_index_for_context(int $oldcontextid) {
return false;
}
/**
* Deletes information related to a specific course id. This should be used when the course
* itself is deleted from Moodle.
*
* This deletes all information relating to that course from the index, including all child
* contexts.
*
* This function is optional; if not supported it will return false and the information will
* not be deleted from the search index.
*
* If an engine implements this function then, ideally, it should also implement
* delete_index_for_context so that deletion of single activities/blocks also works.
*
* @param int $oldcourseid ID of course that has been deleted
* @return bool True if implemented
* @throws \core_search\engine_exception Engines may throw this exception for any problem
*/
public function delete_index_for_course(int $oldcourseid) {
return false;
}
/**
* Checks that the schema is the latest version. If the version stored in config does not match
* the current, this function will attempt to upgrade the schema.
*
* @return bool|string True if schema is OK, a string if user needs to take action
*/
public function check_latest_schema() {
if (empty($this->config->schemaversion)) {
$currentversion = 0;
} else {
$currentversion = $this->config->schemaversion;
}
if ($currentversion < document::SCHEMA_VERSION) {
return $this->update_schema((int)$currentversion, (int)document::SCHEMA_VERSION);
} else {
return true;
}
}
/**
* Usually called by the engine; marks that the schema has been updated.
*
* @param int $version Records the schema version now applied
*/
public function record_applied_schema_version($version) {
set_config('schemaversion', $version, $this->pluginname);
}
/**
* Requests the search engine to upgrade the schema. The engine should update the schema if
* possible/necessary, and should ensure that record_applied_schema_version is called as a
* result.
*
* If it is not possible to upgrade the schema at the moment, it can do nothing and return; the
* function will be called again next time search is initialised.
*
* The default implementation just returns, with a DEBUG_DEVELOPER warning.
*
* @param int $oldversion Old schema version
* @param int $newversion New schema version
* @return bool|string True if schema is updated successfully, a string if it needs updating manually
*/
protected function update_schema($oldversion, $newversion) {
debugging('Unable to update search engine schema: ' . $this->pluginname, DEBUG_DEVELOPER);
return get_string('schemanotupdated', 'search');
}
/**
* Checks if this search engine supports groups.
*
* Note that returning true to this function causes the parameters to execute_query to be
* passed differently!
*
* In order to implement groups and return true to this function, the search engine should:
*
* 1. Handle the fields ->separategroupscontexts and ->usergroups in the $accessinfo parameter
* to execute_query (ideally, using these to automatically restrict search results).
* 2. Support the optional groupids parameter in the $filter parameter for execute_query to
* restrict results to only those where the stored groupid matches the given value.
*
* @return bool True if this engine supports searching by group id field
*/
public function supports_group_filtering() {
return false;
}
/**
* Obtain a list of results orders (and names for them) that are supported by this
* search engine in the given context.
*
* By default, engines sort by relevance only.
*
* @param \context $context Context that the user requested search from
* @return array Array from order name => display text
*/
public function get_supported_orders(\context $context) {
return ['relevance' => get_string('order_relevance', 'search')];
}
/**
* Checks if the search engine supports searching by user.
*
* If it returns true to this function, the search engine should support the 'userids' option
* in the $filters value passed to execute_query(), returning only items where the userid in
* the search document matches one of those user ids.
*
* @return bool True if the search engine supports searching by user
*/
public function supports_users() {
return false;
}
/**
* Checks if the search engine supports adding documents in a batch.
*
* If it returns true to this function, the search engine must implement the add_document_batch
* function.
*
* @return bool True if the search engine supports adding documents in a batch
*/
public function supports_add_document_batch(): bool {
return false;
}
/**
* Gets the maximum number of documents to send together in batch mode.
*
* Only relevant if the engine returns true to {@see supports_add_document_batch}.
*
* Can be overridden by search engine if required.
*
* @var int Number of documents to send together in batch mode, default 100.
*/
public function get_batch_max_documents(): int {
return 100;
}
/**
* Gets the maximum size of document content to be included in a shared batch (if the
* document is bigger then it will be sent on its own; batching does not provide a performance
* improvement for big documents anyway).
*
* Only relevant if the engine returns true to {@see supports_add_document_batch}.
*
* Can be overridden by search engine if required.
*
* @return int Max size in bytes, default 1MB
*/
public function get_batch_max_content(): int {
return 1024 * 1024;
}
/**
* Checks if the search engine has an alternate configuration.
*
* This is used where the same search engine class supports two different configurations,
* which are both shown on the settings screen. The alternate configuration is selected by
* passing 'true' parameter to the constructor.
*
* The feature is used when a different connection is in use for indexing vs. querying
* the search engine.
*
* This function should only return true if the engine supports an alternate configuration
* and the user has filled in the settings. (We do not need to test they are valid, that will
* happen as normal.)
*
* @return bool True if an alternate configuration is defined
*/
public function has_alternate_configuration(): bool {
return false;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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 {
}
+123
View File
@@ -0,0 +1,123 @@
<?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/>.
/**
* Handles external (web service) function calls related to search.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_user\external\user_summary_exporter;
/**
* Handles external (web service) function calls related to search.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class external extends \core_external\external_api {
/**
* Returns parameter types for get_relevant_users function.
*
* @return external_function_parameters Parameters
*/
public static function get_relevant_users_parameters() {
return new external_function_parameters([
'query' => new external_value(
PARAM_RAW,
'Query string (full or partial user full name or other details)'
),
'courseid' => new external_value(PARAM_INT, 'Course id (0 if none)'),
]);
}
/**
* Returns result type for get_relevant_users function.
*
* @return external_description Result type
*/
public static function get_relevant_users_returns() {
return new external_multiple_structure(
new external_single_structure([
'id' => new external_value(PARAM_INT, 'User id'),
'fullname' => new external_value(PARAM_RAW, 'Full name as text'),
'profileimageurlsmall' => new external_value(PARAM_URL, 'URL to small profile image')
])
);
}
/**
* Searches for users given a query, taking into account the current user's permissions and
* possibly a course to check within.
*
* @param string $query Query text
* @param int $courseid Course id or 0 if no restriction
* @return array Defined return structure
*/
public static function get_relevant_users($query, $courseid) {
global $CFG, $PAGE;
// Validate parameter.
[
'query' => $query,
'courseid' => $courseid,
] = self::validate_parameters(self::get_relevant_users_parameters(), [
'query' => $query,
'courseid' => $courseid,
]);
// Validate the context (search page is always system context).
$systemcontext = \context_system::instance();
self::validate_context($systemcontext);
// Get course object too.
if ($courseid) {
$coursecontext = \context_course::instance($courseid);
} else {
$coursecontext = null;
}
// If not logged in, can't see anyone when forceloginforprofiles is on.
if (!empty($CFG->forceloginforprofiles)) {
if (!isloggedin() || isguestuser()) {
return [];
}
}
$users = \core_user::search($query, $coursecontext);
$result = [];
foreach ($users as $user) {
// Get a standard exported user object.
$fulldetails = (new user_summary_exporter($user))->export($PAGE->get_renderer('core'));
// To avoid leaking private data to students, only include the specific information we
// are going to display (and not the email, idnumber, etc).
$result[] = (object)['id' => $fulldetails->id, 'fullname' => $fulldetails->fullname,
'profileimageurlsmall' => $fulldetails->profileimageurlsmall];
}
return $result;
}
}
+141
View File
@@ -0,0 +1,141 @@
<?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_search\external;
use core\external\exporter;
/**
* Contains related class for displaying information of a search result.
*
* @package core_search
* @since Moodle 4.3
*/
class document_exporter extends exporter {
/**
* Return the list of properties.
*
* @return array
*/
protected static function define_properties() {
return [
'itemid' => [
'type' => PARAM_INT,
'description' => 'unique id in the search area scope',
],
'componentname' => [
'type' => PARAM_ALPHANUMEXT,
'description' => 'component name',
],
'areaname' => [
'type' => PARAM_ALPHANUMEXT,
'description' => 'search area name',
],
'courseurl' => [
'type' => PARAM_URL,
'description' => 'result course url',
],
'coursefullname' => [
'type' => PARAM_RAW,
'description' => 'result course fullname',
],
'timemodified' => [
'type' => PARAM_INT,
'description' => 'result modified time',
],
'title' => [
'type' => PARAM_RAW,
'description' => 'result title',
],
'docurl' => [
'type' => PARAM_URL,
'description' => 'result url',
],
'iconurl' => [
'type' => PARAM_URL,
'description' => 'icon url',
'optional' => true,
'default' => '',
'null' => NULL_ALLOWED,
],
'content' => [
'type' => PARAM_RAW,
'description' => 'result contents',
'optional' => true,
'default' => '',
'null' => NULL_ALLOWED,
],
'contextid' => [
'type' => PARAM_INT,
'description' => 'result context id',
],
'contexturl' => [
'type' => PARAM_URL,
'description' => 'result context url',
],
'description1' => [
'type' => PARAM_RAW,
'description' => 'extra result contents, depends on the search area',
'optional' => true,
'default' => '',
'null' => NULL_ALLOWED,
],
'description2' => [
'type' => PARAM_RAW,
'description' => 'extra result contents, depends on the search area',
'optional' => true,
'default' => '',
'null' => NULL_ALLOWED,
],
'multiplefiles' => [
'type' => PARAM_INT,
'description' => 'whether multiple files are returned or not',
'optional' => true,
],
'filenames' => [
'type' => PARAM_RAW,
'description' => 'result file names if present',
'muultiple' => true,
'optional' => true,
],
'filename' => [
'type' => PARAM_RAW,
'description' => 'result file name if present',
'optional' => true,
],
'userid' => [
'type' => PARAM_INT,
'description' => 'user id',
'optional' => true,
],
'userurl' => [
'type' => PARAM_URL,
'description' => 'user url',
'optional' => true,
],
'userfullname' => [
'type' => PARAM_RAW,
'description' => 'user fullname',
'optional' => true,
],
'textformat' => [
'type' => PARAM_INT,
'description' => 'text fields format, it is the same for all of them',
]
];
}
}
+158
View File
@@ -0,0 +1,158 @@
<?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_search\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_multiple_structure;
use core_external\external_value;
use moodle_exception;
/**
* External function for retrieving search results.
*
* @package core_search
* @copyright 2023 David Monllao & Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.3
*/
class get_results extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'query' => new external_value(PARAM_NOTAGS, 'the search query'),
'filters' => new external_single_structure(
[
'title' => new external_value(PARAM_NOTAGS, 'result title', VALUE_OPTIONAL),
'areaids' => new external_multiple_structure(
new external_value(PARAM_ALPHANUMEXT, 'areaid'), 'restrict results to these areas', VALUE_DEFAULT, []
),
'courseids' => new external_multiple_structure(
new external_value(PARAM_INT, 'courseid'), 'restrict results to these courses', VALUE_DEFAULT, []
),
'contextids' => new external_multiple_structure(
new external_value(PARAM_INT, 'contextid'), 'restrict results to these contexts', VALUE_DEFAULT, []
),
'cat' => new external_value(PARAM_NOTAGS, 'category to filter areas', VALUE_DEFAULT, ''),
'userids' => new external_multiple_structure(
new external_value(PARAM_INT, 'userid'), 'restrict results to these users', VALUE_DEFAULT, []
),
'groupids' => new external_multiple_structure(
new external_value(PARAM_INT, 'groupid'), 'restrict results to these groups', VALUE_DEFAULT, []
),
'mycoursesonly' => new external_value(PARAM_BOOL, 'only results from my courses', VALUE_DEFAULT, false),
'order' => new external_value(PARAM_ALPHA, 'how to order', VALUE_DEFAULT, ''),
'timestart' => new external_value(PARAM_INT, 'docs modified after this date', VALUE_DEFAULT, 0),
'timeend' => new external_value(PARAM_INT, 'docs modified before this date', VALUE_DEFAULT, 0)
], 'filters to apply', VALUE_DEFAULT, []
),
'page' => new external_value(PARAM_INT, 'results page number starting from 0, defaults to the first page',
VALUE_DEFAULT, 0)
]
);
}
/**
* Gets global search results based on the provided query and filters.
*
* @param string $query the search query
* @param array $filters filters to apply
* @param int $page results page
* @return array search results
*/
public static function execute(string $query, array $filters = [], int $page = 0): array {
global $PAGE;
$params = self::validate_parameters(self::execute_parameters(),
[
'query' => $query,
'filters' => $filters,
'page' => $page,
]
);
$system = \context_system::instance();
external_api::validate_context($system);
require_capability('moodle/search:query', $system);
if (\core_search\manager::is_global_search_enabled() === false) {
throw new moodle_exception('globalsearchdisabled', 'search');
}
$search = \core_search\manager::instance();
$data = new \stdClass();
// First, mandatory parameters for consistency with web.
$data->q = $params['query'];
$data->title = $params['filters']['title'] ?? '';
$data->timestart = $params['filters']['timestart'] ?? 0;
$data->timeend = $params['filters']['timeend'] ?? 0;
$data->areaids = $params['filters']['areaids'] ?? [];
$data->courseids = $params['filters']['courseids'] ?? [];
$data->contextids = $params['filters']['contextids'] ?? [];
$data->userids = $params['filters']['userids'] ?? [];
$data->groupids = $params['filters']['groupids'] ?? [];
$cat = $params['filters']['cat'] ?? '';
if (\core_search\manager::is_search_area_categories_enabled()) {
$cat = \core_search\manager::get_search_area_category_by_name($cat);
}
if ($cat instanceof \core_search\area_category) {
$data->cat = $cat->get_name();
}
$docs = $search->paged_search($data, $page);
$return = [
'totalcount' => $docs->totalcount,
'warnings' => [],
'results' => []
];
// Convert results to simple data structures.
if ($docs) {
foreach ($docs->results as $doc) {
$return['results'][] = $doc->export_doc($PAGE->get_renderer('core'));
}
}
return $return;
}
/**
* Webservice returns.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure(
[
'totalcount' => new external_value(PARAM_INT, 'Total number of results'),
'results' => new external_multiple_structure(
\core_search\external\document_exporter::get_read_structure()
),
]
);
}
}
+117
View File
@@ -0,0 +1,117 @@
<?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_search\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_multiple_structure;
use core_external\external_value;
use core_external\external_warnings;
use \core_search\manager;
use moodle_exception;
/**
* External function for return the list of search areas.
*
* @package core_search
* @copyright 2023 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.3
*/
class get_search_areas_list extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'cat' => new external_value(PARAM_NOTAGS, 'category to filter areas', VALUE_DEFAULT, ''),
]
);
}
/**
* Return list of search areas.
*
* @param string $cat category to filter areas
* @return array search areas and warnings
*/
public static function execute(string $cat = ''): array {
$params = self::validate_parameters(self::execute_parameters(), ['cat' => $cat]);
$system = \context_system::instance();
external_api::validate_context($system);
require_capability('moodle/search:query', $system);
if (manager::is_global_search_enabled() === false) {
throw new moodle_exception('globalsearchdisabled', 'search');
}
$areas = [];
$allsearchareas = manager::get_search_area_categories();
$enabledsearchareas = manager::get_search_areas_list(true);
foreach ($allsearchareas as $categoryid => $searchareacategory) {
if (!empty($params['cat']) && $params['cat'] != $categoryid) {
continue;
}
$searchareas = $searchareacategory->get_areas();
$catname = $searchareacategory->get_visiblename();
foreach ($searchareas as $areaid => $searcharea) {
if (key_exists($areaid, $enabledsearchareas)) {
$name = $searcharea->get_visible_name();
$areas[$name] = ['id' => $areaid, 'name' => $name, 'categoryid' => $categoryid, 'categoryname' => $catname];
}
}
}
ksort($areas);
return ['areas' => $areas, 'warnings' => []];
}
/**
* Webservice returns.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure(
[
'areas' => new external_multiple_structure(
new external_single_structure(
[
'id' => new external_value(PARAM_ALPHANUMEXT, 'search area id'),
'categoryid' => new external_value(PARAM_NOTAGS, 'category id'),
'categoryname' => new external_value(PARAM_NOTAGS, 'category name'),
'name' => new external_value(PARAM_TEXT, 'search area name'),
], 'Search area'
), 'Search areas'
),
'warnings' => new external_warnings()
]
);
}
}
+131
View File
@@ -0,0 +1,131 @@
<?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_search\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_multiple_structure;
use moodle_exception;
/**
* External function for retrieving top search results.
*
* @package core_search
* @copyright 2023 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.3
*/
class get_top_results extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
$baseparameters = get_results::execute_parameters();
return new external_function_parameters(
[
'query' => $baseparameters->keys['query'],
'filters' => $baseparameters->keys['filters'],
]
);
}
/**
* Gets top search results based on the provided query and filters.
*
* @param string $query the search query
* @param array $filters filters to apply
* @return array search results
*/
public static function execute(string $query, array $filters = []): array {
global $PAGE;
$params = self::validate_parameters(self::execute_parameters(),
[
'query' => $query,
'filters' => $filters,
]
);
$system = \context_system::instance();
external_api::validate_context($system);
require_capability('moodle/search:query', $system);
if (\core_search\manager::is_global_search_enabled() === false) {
throw new moodle_exception('globalsearchdisabled', 'search');
}
$search = \core_search\manager::instance();
$data = new \stdClass();
// First, mandatory parameters for consistency with web.
$data->q = $params['query'];
$data->title = $params['filters']['title'] ?? '';
$data->timestart = $params['filters']['timestart'] ?? 0;
$data->timeend = $params['filters']['timeend'] ?? 0;
$data->areaids = $params['filters']['areaids'] ?? [];
$data->courseids = $params['filters']['courseids'] ?? [];
$data->contextids = $params['filters']['contextids'] ?? [];
$data->userids = $params['filters']['userids'] ?? [];
$data->groupids = $params['filters']['groupids'] ?? [];
$cat = $params['filters']['cat'] ?? '';
if (\core_search\manager::is_search_area_categories_enabled()) {
$cat = \core_search\manager::get_search_area_category_by_name($cat);
}
if ($cat instanceof \core_search\area_category) {
$data->cat = $cat->get_name();
}
$docs = $search->search_top($data);
$return = [
'warnings' => [],
'results' => []
];
// Convert results to simple data structures.
if ($docs) {
foreach ($docs as $doc) {
$return['results'][] = $doc->export_doc($PAGE->get_renderer('core'));
}
}
return $return;
}
/**
* Webservice returns.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure(
[
'results' => new external_multiple_structure(
\core_search\external\document_exporter::get_read_structure()
),
]
);
}
}
+125
View File
@@ -0,0 +1,125 @@
<?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_search\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_multiple_structure;
use core_external\external_value;
use core_external\external_warnings;
use moodle_exception;
/**
* External function for trigger view search results event.
*
* @package core_search
* @copyright 2023 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 4.3
*/
class view_results extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'query' => new external_value(PARAM_NOTAGS, 'the search query'),
'filters' => new external_single_structure(
[
'title' => new external_value(PARAM_NOTAGS, 'result title', VALUE_OPTIONAL),
'areaids' => new external_multiple_structure(
new external_value(PARAM_RAW, 'areaid'), 'restrict results to these areas', VALUE_DEFAULT, []
),
'courseids' => new external_multiple_structure(
new external_value(PARAM_INT, 'courseid'), 'restrict results to these courses', VALUE_DEFAULT, []
),
'timestart' => new external_value(PARAM_INT, 'docs modified after this date', VALUE_DEFAULT, 0),
'timeend' => new external_value(PARAM_INT, 'docs modified before this date', VALUE_DEFAULT, 0)
], 'filters to apply', VALUE_DEFAULT, []
),
'page' => new external_value(PARAM_INT, 'results page number starting from 0, defaults to the first page',
VALUE_DEFAULT, 0)
]
);
}
/**
* Trigger view results event.
*
* @param string $query the search query
* @param array $filters filters to apply
* @param int $page results page
* @return array status and warnings
*/
public static function execute(string $query, array $filters = [], int $page = 0): array {
$params = self::validate_parameters(self::execute_parameters(),
[
'query' => $query,
'filters' => $filters,
'page' => $page,
]
);
$system = \context_system::instance();
external_api::validate_context($system);
require_capability('moodle/search:query', $system);
if (\core_search\manager::is_global_search_enabled() === false) {
throw new moodle_exception('globalsearchdisabled', 'search');
}
$filters = new \stdClass();
$filters->title = $params['filters']['title'] ?? '';
$filters->timestart = $params['filters']['timestart'] ?? 0;
$filters->timeend = $params['filters']['timeend'] ?? 0;
$filters->areaids = $params['filters']['areaids'] ?? [];
$filters->courseids = $params['filters']['courseids'] ?? [];
\core_search\manager::trigger_search_results_viewed([
'q' => $params['query'],
'page' => $params['page'],
'title' => !empty($filters->title) ? $filters->title : '',
'areaids' => !empty($filters->areaids) ? $filters->areaids : [],
'courseids' => !empty($filters->courseids) ? $filters->courseids : [],
'timestart' => isset($filters->timestart) ? $filters->timestart : 0,
'timeend' => isset($filters->timeend) ? $filters->timeend : 0
]);
return ['status' => true, 'warnings' => []];
}
/**
* Webservice returns.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure(
[
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
'warnings' => new external_warnings()
]
);
}
}
File diff suppressed because it is too large Load Diff
+203
View File
@@ -0,0 +1,203 @@
<?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;
use core_search\manager;
defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir . '/formslib.php');
class search extends \moodleform {
/**
* Form definition.
*
* @return void
*/
function definition() {
global $USER, $DB, $OUTPUT;
$mform =& $this->_form;
if (\core_search\manager::is_search_area_categories_enabled() && !empty($this->_customdata['cat'])) {
$mform->addElement('hidden', 'cat');
$mform->setType('cat', PARAM_NOTAGS);
$mform->setDefault('cat', $this->_customdata['cat']);
}
$mform->disable_form_change_checker();
$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');
// Show the 'search within' option if the user came from a particular context.
if (!empty($this->_customdata['searchwithin'])) {
$mform->addElement('select', 'searchwithin', get_string('searchwithin', 'search'),
$this->_customdata['searchwithin']);
$mform->setDefault('searchwithin', '');
}
// If the search engine provides multiple ways to order results, show options.
if (!empty($this->_customdata['orderoptions']) &&
count($this->_customdata['orderoptions']) > 1) {
$mform->addElement('select', 'order', get_string('order', 'search'),
$this->_customdata['orderoptions']);
$mform->setDefault('order', 'relevance');
}
$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(true);
$enabledsearchareas = \core_search\manager::get_search_areas_list(true);
$areanames = array();
if (\core_search\manager::is_search_area_categories_enabled() && !empty($this->_customdata['cat'])) {
$searchareacategory = \core_search\manager::get_search_area_category_by_name($this->_customdata['cat']);
$searchareas = $searchareacategory->get_areas();
foreach ($searchareas as $areaid => $searcharea) {
if (key_exists($areaid, $enabledsearchareas)) {
$areanames[$areaid] = $searcharea->get_visible_name();
}
}
} else {
foreach ($enabledsearchareas as $areaid => $searcharea) {
$areanames[$areaid] = $searcharea->get_visible_name();
}
}
// Sort the array by the text.
\core_collator::asort($areanames);
$options = array(
'multiple' => true,
'noselectionstring' => get_string('allareas', 'search'),
);
$mform->addElement('autocomplete', 'areaids', get_string('searcharea', 'search'), $areanames, $options);
if (is_siteadmin()) {
$limittoenrolled = false;
} else {
$limittoenrolled = !manager::include_all_courses();
}
$options = array(
'multiple' => true,
'limittoenrolled' => $limittoenrolled,
'noselectionstring' => get_string('allcourses', 'search'),
);
$mform->addElement('course', 'courseids', get_string('courses', 'core'), $options);
$mform->setType('courseids', PARAM_INT);
if (manager::include_all_courses() || !empty(get_config('core', 'searchallavailablecourses'))) {
$mform->addElement('checkbox', 'mycoursesonly', get_string('mycoursesonly', 'search'));
$mform->setType('mycoursesonly', PARAM_INT);
}
// If the search engine can search by user, and the user is logged in (so we have
// permission to call the user-listing web service) then show the user selector.
if ($search->get_engine()->supports_users() && isloggedin()) {
$options = [
'ajax' => 'core_search/form-search-user-selector',
'multiple' => true,
'noselectionstring' => get_string('allusers', 'search'),
'valuehtmlcallback' => function($value) {
global $DB, $OUTPUT;
$user = $DB->get_record('user', ['id' => (int)$value], '*', IGNORE_MISSING);
if (!$user || !user_can_view_profile($user)) {
return false;
}
$details = user_get_user_details($user);
return $OUTPUT->render_from_template(
'core_search/form-user-selector-suggestion', $details);
}
];
if (!empty($this->_customdata['withincourseid'])) {
$options['withincourseid'] = $this->_customdata['withincourseid'];
}
$mform->addElement('autocomplete', 'userids', get_string('users'), [], $options);
}
if (!empty($this->_customdata['searchwithin'])) {
// Course options should be hidden if we choose to search within a specific location.
$mform->hideIf('courseids', 'searchwithin', 'ne', '');
// Get groups on course (we don't show group selector if there aren't any).
$courseid = $this->_customdata['withincourseid'];
$allgroups = groups_get_all_groups($courseid);
if ($allgroups && $search->get_engine()->supports_group_filtering()) {
$groupnames = [];
foreach ($allgroups as $group) {
$groupnames[$group->id] = $group->name;
}
// Create group autocomplete option.
$options = array(
'multiple' => true,
'noselectionstring' => get_string('allgroups'),
);
$mform->addElement('autocomplete', 'groupids', get_string('groups'), $groupnames, $options);
// Is the second 'search within' option a cm?
if (!empty($this->_customdata['withincmid'])) {
// Find out if the cm supports groups.
$modinfo = get_fast_modinfo($courseid);
$cm = $modinfo->get_cm($this->_customdata['withincmid']);
if ($cm->effectivegroupmode != NOGROUPS) {
// If it does, group ids are available when you have course or module selected.
$mform->hideIf('groupids', 'searchwithin', 'eq', '');
} else {
// Group ids are only available if you have course selected.
$mform->hideIf('groupids', 'searchwithin', 'ne', 'course');
}
} else {
$mform->hideIf('groupids', 'searchwithin', 'eq', '');
}
}
}
$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);
// Source context i.e. the page they came from when they clicked search.
$mform->addElement('hidden', 'context');
$mform->setType('context', PARAM_INT);
$this->add_action_buttons(false, get_string('search', 'search'));
}
}
+155
View File
@@ -0,0 +1,155 @@
<?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 Zero based page number.
* @param int $totalcount Total number of results available.
* @param \moodle_url $url
* @param \core_search\area_category|null $cat Selected search are category or null if category functionality is disabled.
* @return string HTML
*/
public function render_results($results, $page, $totalcount, $url, $cat = null) {
$content = '';
if (\core_search\manager::is_search_area_categories_enabled() && !empty($cat)) {
$toprow = [];
foreach (\core_search\manager::get_search_area_categories() as $category) {
$taburl = clone $url;
$taburl->param('cat', $category->get_name());
$taburl->param('page', 0);
$taburl->remove_params(['page', 'areaids']);
$toprow[$category->get_name()] = new \tabobject($category->get_name(), $taburl, $category->get_visiblename());
}
if (\core_search\manager::should_hide_all_results_category()) {
unset($toprow[\core_search\manager::SEARCH_AREA_CATEGORY_ALL]);
}
$content .= $this->tabtree($toprow, $cat->get_name());
}
// Paging bar.
$perpage = \core_search\manager::DISPLAY_RESULTS_PER_PAGE;
$content .= $this->output->paging_bar($totalcount, $page, $perpage, $url);
// Results.
$resultshtml = array();
foreach ($results 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($totalcount, $page, $perpage, $url);
return $content;
}
/**
* Top results content
*
* @param \core_search\document[] $results Search Results
* @return string content of the top result section
*/
public function render_top_results($results): string {
$content = $this->output->box_start('topresults');
$content .= $this->output->heading(get_string('topresults', 'core_search'));
$content .= \html_writer::tag('hr', '');
$resultshtml = array();
foreach ($results as $hit) {
$resultshtml[] = $this->render_result($hit);
}
$content .= \html_writer::tag('div', implode('<hr/>', $resultshtml), array('class' => 'search-results'));
$content .= $this->output->box_end();
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;
}
/**
* Returns information about queued index requests.
*
* @param \stdClass $info Info object from get_index_requests_info
* @return string HTML
* @throws \moodle_exception Any error with template
*/
public function render_index_requests_info(\stdClass $info) {
return $this->output->render_from_template('core_search/index_requests', $info);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for core_search.
*
* @package core_search
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for core_search implementing null_provider.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,108 @@
<?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/>.
/**
* Iterator for skipping search recordset documents that are in the future.
*
* @package core_search
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_search;
defined('MOODLE_INTERNAL') || die();
/**
* Iterator for skipping search recordset documents that are in the future.
*
* This iterator stops iterating if it receives a document that was modified later than the
* specified cut-off time (usually current time).
*
* This iterator assumes that its parent iterator returns documents in modified order (which is
* required to be the case for search indexing). This means we will stop retrieving data from the
* recordset
*
* @copyright 2017 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class skip_future_documents_iterator implements \Iterator {
/** @var \Iterator Parent iterator */
protected $parent;
/** @var int Cutoff time; anything later than this will cause the iterator to stop */
protected $cutoff;
/** @var mixed Current value of iterator */
protected $currentdoc;
/** @var bool True if current value is available */
protected $gotcurrent;
/**
* Constructor.
*
* @param \Iterator $parent Parent iterator, must return search documents in modified order
* @param int $cutoff Cut-off time, default is current time
*/
public function __construct(\Iterator $parent, $cutoff = 0) {
$this->parent = $parent;
if ($cutoff) {
$this->cutoff = $cutoff;
} else {
$this->cutoff = time();
}
}
#[\ReturnTypeWillChange]
public function current() {
if (!$this->gotcurrent) {
$this->currentdoc = $this->parent->current();
$this->gotcurrent = true;
}
return $this->currentdoc;
}
public function next(): void {
$this->parent->next();
$this->gotcurrent = false;
}
#[\ReturnTypeWillChange]
public function key() {
return $this->parent->key();
}
public function valid(): bool {
// Check that the parent is valid.
if (!$this->parent->valid()) {
return false;
}
if ($doc = $this->current()) {
// This document is valid if the modification date is before the cutoff.
return $doc->get('modified') <= $this->cutoff;
} else {
// If the document is false/null, allow iterator to continue.
return true;
}
}
public function rewind(): void {
$this->parent->rewind();
$this->gotcurrent = false;
}
}