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
+395
View File
@@ -0,0 +1,395 @@
<?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/>.
/**
* Simple moodle database engine.
*
* @package search_simpledb
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace search_simpledb;
defined('MOODLE_INTERNAL') || die();
/**
* Simple moodle database engine.
*
* @package search_simpledb
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class engine extends \core_search\engine {
/**
* Total number of available results.
*
* @var null|int
*/
protected $totalresults = null;
/**
* Prepares a SQL query, applies filters and executes it returning its results.
*
* @throws \core_search\engine_exception
* @param stdClass $filters Containing query and filters.
* @param array $usercontexts Contexts where the user has access. True if the user can access all contexts.
* @param int $limit The maximum number of results to return.
* @return \core_search\document[] Results or false if no results
*/
public function execute_query($filters, $usercontexts, $limit = 0) {
global $DB, $USER;
$serverstatus = $this->is_server_ready();
if ($serverstatus !== true) {
throw new \core_search\engine_exception('engineserverstatus', 'search');
}
if (empty($limit)) {
$limit = \core_search\manager::MAX_RESULTS;
}
$params = array();
// To store all conditions we will add to where.
$ands = array();
// Get results only available for the current user.
$ands[] = '(owneruserid = ? OR owneruserid = ?)';
$params = array_merge($params, array(\core_search\manager::NO_OWNER_ID, $USER->id));
// Restrict it to the context where the user can access, we want this one cached.
// If the user can access all contexts $usercontexts value is just true, we don't need to filter
// in that case.
if ($usercontexts && is_array($usercontexts)) {
// Join all area contexts into a single array and implode.
$allcontexts = array();
foreach ($usercontexts as $areaid => $areacontexts) {
if (!empty($filters->areaids) && !in_array($areaid, $filters->areaids)) {
// Skip unused areas.
continue;
}
foreach ($areacontexts as $contextid) {
// Ensure they are unique.
$allcontexts[$contextid] = $contextid;
}
}
if (empty($allcontexts)) {
// This means there are no valid contexts for them, so they get no results.
return array();
}
list($contextsql, $contextparams) = $DB->get_in_or_equal($allcontexts);
$ands[] = 'contextid ' . $contextsql;
$params = array_merge($params, $contextparams);
}
// Course id filter.
if (!empty($filters->courseids)) {
list($conditionsql, $conditionparams) = $DB->get_in_or_equal($filters->courseids);
$ands[] = 'courseid ' . $conditionsql;
$params = array_merge($params, $conditionparams);
}
// Area id filter.
if (!empty($filters->areaids)) {
list($conditionsql, $conditionparams) = $DB->get_in_or_equal($filters->areaids);
$ands[] = 'areaid ' . $conditionsql;
$params = array_merge($params, $conditionparams);
}
if (!empty($filters->title)) {
$ands[] = $DB->sql_like('title', '?', false, false);
$params[] = $filters->title;
}
if (!empty($filters->timestart)) {
$ands[] = 'modified >= ?';
$params[] = $filters->timestart;
}
if (!empty($filters->timeend)) {
$ands[] = 'modified <= ?';
$params[] = $filters->timeend;
}
// And finally the main query after applying all AND filters.
if (!empty($filters->q)) {
switch ($DB->get_dbfamily()) {
case 'postgres':
$ands[] = "(" .
"to_tsvector('simple', title) @@ plainto_tsquery('simple', ?) OR ".
"to_tsvector('simple', content) @@ plainto_tsquery('simple', ?) OR ".
"to_tsvector('simple', description1) @@ plainto_tsquery('simple', ?) OR ".
"to_tsvector('simple', description2) @@ plainto_tsquery('simple', ?)".
")";
$params[] = $filters->q;
$params[] = $filters->q;
$params[] = $filters->q;
$params[] = $filters->q;
break;
case 'mysql':
if ($DB->is_fulltext_search_supported()) {
$ands[] = "MATCH (title, content, description1, description2) AGAINST (?)";
$params[] = $filters->q;
// Sorry for the hack, but it does not seem that we will have a solution for
// this soon (https://bugs.mysql.com/bug.php?id=78485).
if ($filters->q === '*') {
return array();
}
} else {
// Clumsy version for mysql versions with no fulltext support.
list($queryand, $queryparams) = $this->get_simple_query($filters->q);
$ands[] = $queryand;
$params = array_merge($params, $queryparams);
}
break;
case 'mssql':
if ($DB->is_fulltext_search_supported()) {
$ands[] = "CONTAINS ((title, content, description1, description2), ?)";
// Special treatment for double quotes:
// - Puntuation is ignored so we can get rid of them.
// - Phrases should be enclosed in double quotation marks.
$params[] = '"' . str_replace('"', '', $filters->q) . '"';
} else {
// Clumsy version for mysql versions with no fulltext support.
list($queryand, $queryparams) = $this->get_simple_query($filters->q);
$ands[] = $queryand;
$params = array_merge($params, $queryparams);
}
break;
default:
list($queryand, $queryparams) = $this->get_simple_query($filters->q);
$ands[] = $queryand;
$params = array_merge($params, $queryparams);
break;
}
}
// It is limited to $limit, no need to use recordsets.
$documents = $DB->get_records_select('search_simpledb_index', implode(' AND ', $ands), $params, 'docid', '*', 0, $limit);
// Hopefully database cached results as this applies the same filters than above.
$this->totalresults = $DB->count_records_select('search_simpledb_index', implode(' AND ', $ands), $params);
$numgranted = 0;
// Iterate through the results checking its availability and whether they are available for the user or not.
$docs = array();
foreach ($documents as $docdata) {
if ($docdata->owneruserid != \core_search\manager::NO_OWNER_ID && $docdata->owneruserid != $USER->id) {
// If owneruserid is set, no other user should be able to access this record.
continue;
}
if (!$searcharea = $this->get_search_area($docdata->areaid)) {
$this->totalresults--;
continue;
}
// Switch id back to the document id.
$docdata->id = $docdata->docid;
unset($docdata->docid);
$access = $searcharea->check_access($docdata->itemid);
switch ($access) {
case \core_search\manager::ACCESS_DELETED:
$this->delete_by_id($docdata->id);
$this->totalresults--;
break;
case \core_search\manager::ACCESS_DENIED:
$this->totalresults--;
break;
case \core_search\manager::ACCESS_GRANTED:
$numgranted++;
$docs[] = $this->to_document($searcharea, (array)$docdata);
break;
}
// This should never happen.
if ($numgranted >= $limit) {
$docs = array_slice($docs, 0, $limit, true);
break;
}
}
return $docs;
}
/**
* Adds a document to the search engine.
*
* This does not commit to the search engine.
*
* @param \core_search\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
*/
public function add_document($document, $fileindexing = false) {
global $DB;
$doc = (object)$document->export_for_engine();
// Moodle's ids using DML are always autoincremented.
$doc->docid = $doc->id;
unset($doc->id);
$id = $DB->get_field('search_simpledb_index', 'id', array('docid' => $doc->docid));
try {
if ($id) {
$doc->id = $id;
$DB->update_record('search_simpledb_index', $doc);
} else {
$DB->insert_record('search_simpledb_index', $doc);
}
} catch (\dml_exception $ex) {
debugging('dml error while trying to insert document with id ' . $doc->docid . ': ' . $ex->getMessage(),
DEBUG_DEVELOPER);
return false;
}
return true;
}
/**
* Deletes the specified document.
*
* @param string $id The document id to delete
* @return void
*/
public function delete_by_id($id) {
global $DB;
$DB->delete_records('search_simpledb_index', array('docid' => $id));
}
/**
* Delete all area's documents.
*
* @param string $areaid
* @return void
*/
public function delete($areaid = null) {
global $DB;
if ($areaid) {
$DB->delete_records('search_simpledb_index', array('areaid' => $areaid));
} else {
$DB->delete_records('search_simpledb_index');
}
}
/**
* Checks that the required table was installed.
*
* @return true|string Returns true if all good or an error string.
*/
public function is_server_ready() {
global $DB;
if (!$DB->get_manager()->table_exists('search_simpledb_index')) {
return 'search_simpledb_index table does not exist';
}
return true;
}
/**
* It is always installed.
*
* @return true
*/
public function is_installed() {
return true;
}
/**
* Returns the total results.
*
* Including skipped results.
*
* @return int
*/
public function get_query_total_count() {
if (is_null($this->totalresults)) {
// This is a just in case as we count total results in execute_query.
return \core_search\manager::MAX_RESULTS;
}
return $this->totalresults;
}
/**
* Returns the default query for db engines.
*
* @param string $q The query string
* @return array SQL string and params list
*/
protected function get_simple_query($q) {
global $DB;
$sql = '(' .
$DB->sql_like('title', '?', false, false) . ' OR ' .
$DB->sql_like('content', '?', false, false) . ' OR ' .
$DB->sql_like('description1', '?', false, false) . ' OR ' .
$DB->sql_like('description2', '?', false, false) .
')';
// Remove quotes from the query.
$q = str_replace('"', '', $q);
$params = [
'%' . $q . '%',
'%' . $q . '%',
'%' . $q . '%',
'%' . $q . '%'
];
return array($sql, $params);
}
/**
* Simpledb supports deleting the index for a context.
*
* @param int $oldcontextid Context that has been deleted
* @return bool True to indicate that any data was actually deleted
* @throws \core_search\engine_exception
*/
public function delete_index_for_context(int $oldcontextid) {
global $DB;
try {
$DB->delete_records('search_simpledb_index', ['contextid' => $oldcontextid]);
} catch (\dml_exception $e) {
throw new \core_search\engine_exception('dbupdatefailed');
}
return true;
}
/**
* Simpledb supports deleting the index for a course.
*
* @param int $oldcourseid
* @return bool True to indicate that any data was actually deleted
* @throws \core_search\engine_exception
*/
public function delete_index_for_course(int $oldcourseid) {
global $DB;
try {
$DB->delete_records('search_simpledb_index', ['courseid' => $oldcourseid]);
} catch (\dml_exception $e) {
throw new \core_search\engine_exception('dbupdatefailed');
}
return true;
}
}
@@ -0,0 +1,210 @@
<?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 class for requesting user data.
*
* @package search_simpledb
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace search_simpledb\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\local\metadata\collection;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
/**
* Provider for the search_simpledb plugin.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table(
'search_simpledb_index',
[
'docid' => 'privacy:metadata:index:docid',
'itemid' => 'privacy:metadata:index:itemid',
'title' => 'privacy:metadata:index:title',
'content' => 'privacy:metadata:index:content',
'contextid' => 'privacy:metadata:index:contextid',
'areaid' => 'privacy:metadata:index:areaid',
'type' => 'privacy:metadata:index:type',
'courseid' => 'privacy:metadata:index:courseid',
'owneruserid' => 'privacy:metadata:index:owneruserid',
'modified' => 'privacy:metadata:index:modified',
'userid' => 'privacy:metadata:index:userid',
'description1' => 'privacy:metadata:index:description1',
'description2' => 'privacy:metadata:index:description2',
],
'privacy:metadata:index'
);
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$contextlist = new \core_privacy\local\request\contextlist();
$params = ['userid' => $userid, 'owneruserid' => $userid];
$sql = "SELECT DISTINCT contextid
FROM {search_simpledb_index}
WHERE (userid = :userid OR owneruserid = :owneruserid)";
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
$params = [
'contextid' => $context->id,
];
$sql = "SELECT ssi.userid
FROM {search_simpledb_index} ssi
WHERE ssi.contextid = :contextid";
$userlist->add_from_sql('userid', $sql, $params);
$sql = "SELECT ssi.owneruserid AS userid
FROM {search_simpledb_index} ssi
WHERE ssi.contextid = :contextid";
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
// Plugin search_simpledb uses the default document object (core_search\document) which uses FORMAT_PLAIN.
$textformat = FORMAT_PLAIN;
$userid = $contextlist->get_user()->id;
$ctxfields = \context_helper::get_preload_record_columns_sql('ctx');
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$sql = "SELECT ssi.*, $ctxfields FROM {search_simpledb_index} ssi
JOIN {context} ctx ON ctx.id = ssi.contextid
WHERE ssi.contextid $contextsql AND (ssi.userid = :userid OR ssi.owneruserid = :owneruserid)";
$params = ['userid' => $userid, 'owneruserid' => $userid] + $contextparams;
$records = $DB->get_recordset_sql($sql, $params);
foreach ($records as $record) {
\context_helper::preload_from_record($record);
$context = \context::instance_by_id($record->contextid);
$document = (object)[
'title' => format_string($record->title, true, ['context' => $context]),
'content' => format_text($record->content, $textformat, ['context' => $context]),
'description1' => format_text($record->description1, $textformat, ['context' => $context]),
'description2' => format_text($record->description2, $textformat, ['context' => $context]),
'context' => $context->get_context_name(true, true),
'modified' => transform::datetime($record->modified),
];
$path = [get_string('search', 'search'), $record->docid];
writer::with_context($context)->export_data($path, $document);
}
$records->close();
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
$DB->delete_records('search_simpledb_index', ['contextid' => $context->id]);
if ($context->contextlevel == CONTEXT_USER) {
$select = "userid = :userid OR owneruserid = :owneruserid";
$params = ['userid' => $context->instanceid, 'owneruserid' => $context->instanceid];
$DB->delete_records_select('search_simpledb_index', $select, $params);
}
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
global $DB;
$userid = $contextlist->get_user()->id;
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$select = "contextid $contextsql AND (userid = :userid OR owneruserid = :owneruserid)";
$params = ['userid' => $userid, 'owneruserid' => $userid] + $contextparams;
$DB->delete_records_select('search_simpledb_index', $select, $params);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
$context = $userlist->get_context();
list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
list($ownersql, $ownerparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
$select = "contextid = :contextid AND (userid {$usersql} OR owneruserid {$ownersql})";
$params = ['contextid' => $context->id] + $userparams + $ownerparams;
$DB->delete_records_select('search_simpledb_index', $select, $params);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?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/>.
/**
* Post installation and migration code.
*
* @package search_simpledb
* @copyright 2016 Dan Poltawski <dan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
/**
* Post installation code.
*
* @package search_simpledb
* @copyright 2016 Dan Poltawski <dan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
function xmldb_search_simpledb_install() {
global $DB;
switch ($DB->get_dbfamily()) {
case 'postgres':
// There are a few other ways of doing this which avoid the need for individual indexes.
$DB->execute("CREATE INDEX {search_simpledb_title} ON {search_simpledb_index} " .
"USING gin(to_tsvector('simple', title))");
$DB->execute("CREATE INDEX {search_simpledb_content} ON {search_simpledb_index} " .
"USING gin(to_tsvector('simple', content))");
$DB->execute("CREATE INDEX {search_simpledb_description1} ON {search_simpledb_index} " .
"USING gin(to_tsvector('simple', description1))");
$DB->execute("CREATE INDEX {search_simpledb_description2} ON {search_simpledb_index} " .
"USING gin(to_tsvector('simple', description2))");
break;
case 'mysql':
if ($DB->is_fulltext_search_supported()) {
$DB->execute("CREATE FULLTEXT INDEX {search_simpledb_index_index}
ON {search_simpledb_index} (title, content, description1, description2)");
}
break;
case 'mssql':
if ($DB->is_fulltext_search_supported()) {
$catalogname = $DB->get_prefix() . 'search_simpledb_catalog';
if (!$DB->record_exists_sql('SELECT * FROM sys.fulltext_catalogs WHERE name = ?', array($catalogname))) {
$DB->execute("CREATE FULLTEXT CATALOG {search_simpledb_catalog} WITH ACCENT_SENSITIVITY=OFF");
}
if (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL) {
// We want manual tracking for phpunit because the fulltext index does get auto populated fast enough.
$changetracking = 'MANUAL';
} else {
$changetracking = 'AUTO';
}
$DB->execute("CREATE FULLTEXT INDEX ON {search_simpledb_index} (title, content, description1, description2)
KEY INDEX {searsimpinde_id_pk} ON {search_simpledb_catalog} WITH CHANGE_TRACKING $changetracking");
}
break;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="search/engine/simpledb/db" VERSION="20220504" COMMENT="XMLDB file for Moodle search/engine/simpledb"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="search_simpledb_index" COMMENT="search_simpledb table containing the index data.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="docid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="title" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="content" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="areaid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="type" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="courseid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="owneruserid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="modified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="description1" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="description2" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="docid" TYPE="unique" FIELDS="docid"/>
</KEYS>
<INDEXES>
<INDEX NAME="owneruserid-contextid" UNIQUE="false" FIELDS="owneruserid, contextid" COMMENT="Query filters if no extra search filters are applied"/>
<INDEX NAME="contextid" UNIQUE="false" FIELDS="contextid"/>
<INDEX NAME="courseid" UNIQUE="false" FIELDS="courseid"/>
<INDEX NAME="areaid" UNIQUE="false" FIELDS="areaid"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
+56
View File
@@ -0,0 +1,56 @@
<?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/>.
/**
* Uninstall code.
*
* @package search_simpledb
* @copyright 2016 Dan Poltawski <dan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
/**
* Plugin uninstall code.
*
* @package search_simpledb
* @copyright 2016 Dan Poltawski <dan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
function xmldb_search_simpledb_uninstall() {
global $DB;
switch ($DB->get_dbfamily()) {
case 'postgres':
$DB->execute("DROP INDEX {search_simpledb_title}");
$DB->execute("DROP INDEX {search_simpledb_content}");
$DB->execute("DROP INDEX {search_simpledb_description1}");
$DB->execute("DROP INDEX {search_simpledb_description2}");
break;
case 'mysql':
if ($DB->is_fulltext_search_supported()) {
$DB->execute("ALTER TABLE {search_simpledb_index} DROP INDEX {search_simpledb_index_index}");
}
break;
case 'mssql':
if ($DB->is_fulltext_search_supported()) {
$DB->execute("DROP FULLTEXT CATALOG {search_simpledb_catalog}");
}
break;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?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/>.
/**
* Calculated question type upgrade code.
*
* @package search_simpledb
* @copyright 2022 Renaud Lemaire {@link http://www.cblue.be}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Upgrade code for the simpledb search engine.
* @param int $oldversion the version we are upgrading from.
*/
function xmldb_search_simpledb_upgrade($oldversion = 0) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,41 @@
<?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/>.
/**
* Strings for component 'search_simpledb'.
*
* @package search_simpledb
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Simple search';
$string['privacy:metadata:index'] = 'Indexed contents';
$string['privacy:metadata:index:docid'] = 'Document ID (unique)';
$string['privacy:metadata:index:itemid'] = 'Item identifier (in search area scope)';
$string['privacy:metadata:index:title'] = 'Title';
$string['privacy:metadata:index:content'] = 'Contents';
$string['privacy:metadata:index:contextid'] = 'Document context ID';
$string['privacy:metadata:index:areaid'] = 'Search area ID';
$string['privacy:metadata:index:type'] = 'Document type';
$string['privacy:metadata:index:courseid'] = 'Course ID';
$string['privacy:metadata:index:owneruserid'] = 'Document owner user ID';
$string['privacy:metadata:index:modified'] = 'Last modification time';
$string['privacy:metadata:index:userid'] = 'Document user ID';
$string['privacy:metadata:index:description1'] = 'Extra description field';
$string['privacy:metadata:index:description2'] = 'Extra description field';
$string['searchinfo'] = 'Search queries';
$string['searchinfo_help'] = 'Enter the search query.';
@@ -0,0 +1,438 @@
<?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 search_simpledb;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
/**
* Simple search engine base unit tests.
*
* @package search_simpledb
* @category test
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class engine_test extends \advanced_testcase {
/**
* @var \core_search::manager
*/
protected $search = null;
/**
* @var \
*/
protected $engine = null;
/**
* @var \core_search_generator
*/
protected $generator = null;
/**
* Initial stuff.
*
* @return void
*/
public function setUp(): void {
$this->resetAfterTest();
if ($this->requires_manual_index_update()) {
// We need to update fulltext index manually, which requires an alter table statement.
$this->preventResetByRollback();
}
set_config('enableglobalsearch', true);
// Inject search_simpledb engine into the testable core search as we need to add the mock
// search component to it.
$this->engine = new \search_simpledb\engine();
$this->search = \testable_core_search::instance($this->engine);
$this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
$this->generator->setup();
$this->setAdminUser();
}
/**
* tearDown
*
* @return void
*/
public function tearDown(): void {
// For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
if ($this->generator) {
// Moodle DML freaks out if we don't teardown the temp table after each run.
$this->generator->teardown();
$this->generator = null;
}
}
/**
* Test indexing process.
*
* @return void
*/
public function test_index(): void {
global $DB;
$this->add_mock_search_area();
$record = new \stdClass();
$record->timemodified = time() - 1;
$this->generator->create_record($record);
// Data gets into the search engine.
$this->assertTrue($this->search->index());
// Not anymore as everything was already added.
sleep(1);
$this->assertFalse($this->search->index());
$this->generator->create_record();
// Indexing again once there is new data.
$this->assertTrue($this->search->index());
}
/**
* Test search filters.
*
* @return void
*/
public function test_search(): void {
global $USER, $DB;
$this->add_mock_search_area();
$this->generator->create_record();
$record = new \stdClass();
$record->title = "Special title";
$this->generator->create_record($record);
$this->search->index();
$this->update_index();
$querydata = new \stdClass();
$querydata->q = 'message';
$results = $this->search->search($querydata);
$this->assertCount(2, $results);
// Based on core_mocksearch\search\indexer.
$this->assertEquals($USER->id, $results[0]->get('userid'));
$this->assertEquals(\context_course::instance(SITEID)->id, $results[0]->get('contextid'));
// Do a test to make sure we aren't searching non-query fields, like areaid.
$querydata->q = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
$this->assertCount(0, $this->search->search($querydata));
$querydata->q = 'message';
sleep(1);
$beforeadding = time();
sleep(1);
$this->generator->create_record();
$this->search->index();
$this->update_index();
// Timestart.
$querydata->timestart = $beforeadding;
$this->assertCount(1, $this->search->search($querydata));
// Timeend.
unset($querydata->timestart);
$querydata->timeend = $beforeadding;
$this->assertCount(2, $this->search->search($querydata));
// Title.
unset($querydata->timeend);
$querydata->title = 'Special title';
$this->assertCount(1, $this->search->search($querydata));
// Course IDs.
unset($querydata->title);
$querydata->courseids = array(SITEID + 1);
$this->assertCount(0, $this->search->search($querydata));
$querydata->courseids = array(SITEID);
$this->assertCount(3, $this->search->search($querydata));
// Now try some area-id combinations.
unset($querydata->courseids);
$forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post');
$mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
$querydata->areaids = array($forumpostareaid);
$this->assertCount(0, $this->search->search($querydata));
$querydata->areaids = array($forumpostareaid, $mockareaid);
$this->assertCount(3, $this->search->search($querydata));
$querydata->areaids = array($mockareaid);
$this->assertCount(3, $this->search->search($querydata));
$querydata->areaids = array();
$this->assertCount(3, $this->search->search($querydata));
// Check that index contents get updated.
$this->generator->delete_all();
$this->search->index(true);
$this->update_index();
unset($querydata->title);
$querydata->q = '';
$this->assertCount(0, $this->search->search($querydata));
}
/**
* Test delete function
*
* @return void
*/
public function test_delete(): void {
$this->add_mock_search_area();
$this->generator->create_record();
$this->generator->create_record();
$this->search->index();
$this->update_index();
$querydata = new \stdClass();
$querydata->q = 'message';
$this->assertCount(2, $this->search->search($querydata));
$areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
$this->search->delete_index($areaid);
$this->update_index();
$this->assertCount(0, $this->search->search($querydata));
}
/**
* Test user is allowed.
*
* @return void
*/
public function test_alloweduserid(): void {
$this->add_mock_search_area();
$area = new \core_mocksearch\search\mock_search_area();
$record = $this->generator->create_record();
// Get the doc and insert the default doc.
$doc = $area->get_document($record);
$this->engine->add_document($doc);
$users = array();
$users[] = $this->getDataGenerator()->create_user();
$users[] = $this->getDataGenerator()->create_user();
$users[] = $this->getDataGenerator()->create_user();
// Add a record that only user 100 can see.
$originalid = $doc->get('id');
// Now add a custom doc for each user.
foreach ($users as $user) {
$doc = $area->get_document($record);
$doc->set('id', $originalid.'-'.$user->id);
$doc->set('owneruserid', $user->id);
$this->engine->add_document($doc);
}
$this->update_index();
$this->engine->area_index_complete($area->get_area_id());
$querydata = new \stdClass();
$querydata->q = 'message';
$querydata->title = $doc->get('title');
// We are going to go through each user and see if they get the original and the owned doc.
foreach ($users as $user) {
$this->setUser($user);
$results = $this->search->search($querydata);
$this->assertCount(2, $results);
$owned = 0;
$notowned = 0;
// We don't know what order we will get the results in, so we are doing this.
foreach ($results as $result) {
$owneruserid = $result->get('owneruserid');
if (empty($owneruserid)) {
$notowned++;
$this->assertEquals(0, $owneruserid);
$this->assertEquals($originalid, $result->get('id'));
} else {
$owned++;
$this->assertEquals($user->id, $owneruserid);
$this->assertEquals($originalid.'-'.$user->id, $result->get('id'));
}
}
$this->assertEquals(1, $owned);
$this->assertEquals(1, $notowned);
}
// Now test a user with no owned results.
$otheruser = $this->getDataGenerator()->create_user();
$this->setUser($otheruser);
$results = $this->search->search($querydata);
$this->assertCount(1, $results);
$this->assertEquals(0, $results[0]->get('owneruserid'));
$this->assertEquals($originalid, $results[0]->get('id'));
}
public function test_delete_by_id(): void {
$this->add_mock_search_area();
$this->generator->create_record();
$this->generator->create_record();
$this->search->index();
$this->update_index();
$querydata = new \stdClass();
// Then search to make sure they are there.
$querydata->q = 'message';
$results = $this->search->search($querydata);
$this->assertCount(2, $results);
$first = reset($results);
$deleteid = $first->get('id');
$this->engine->delete_by_id($deleteid);
$this->update_index();
// Check that we don't get a result for it anymore.
$results = $this->search->search($querydata);
$this->assertCount(1, $results);
$result = reset($results);
$this->assertNotEquals($deleteid, $result->get('id'));
}
/**
* Tries out deleting data for a context or a course.
*/
public function test_deleted_contexts_and_courses(): void {
// Create some courses and activities.
$generator = $this->getDataGenerator();
$course1 = $generator->create_course(['fullname' => 'C1', 'summary' => 'xyzzy']);
$course1page1 = $generator->create_module('page', ['course' => $course1, 'name' => 'C1P1', 'content' => 'xyzzy']);
$generator->create_module('page', ['course' => $course1, 'name' => 'C1P2', 'content' => 'xyzzy']);
$course2 = $generator->create_course(['fullname' => 'C2', 'summary' => 'xyzzy']);
$course2page = $generator->create_module('page', ['course' => $course2, 'name' => 'C2P', 'content' => 'xyzzy']);
$course2pagecontext = \context_module::instance($course2page->cmid);
$this->search->index();
// By default we have all data in the index.
$this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2', 'C2', 'C2P']);
// Say we delete the course2pagecontext...
$this->engine->delete_index_for_context($course2pagecontext->id);
$this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2', 'C2']);
// Now delete the second course...
$this->engine->delete_index_for_course($course2->id);
$this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2']);
// Finally let's delete using Moodle functions to check that works. Single context first.
course_delete_module($course1page1->cmid);
$this->assert_raw_index_contents('xyzzy', ['C1', 'C1P2']);
delete_course($course1, false);
$this->assert_raw_index_contents('xyzzy', []);
}
/**
* Check the contents of the index.
*
* @param string $searchword Word to match within the content field
* @param string[] $expected Array of expected result titles, in alphabetical order
*/
protected function assert_raw_index_contents(string $searchword, array $expected) {
global $DB;
$results = $DB->get_records_select('search_simpledb_index',
$DB->sql_like('content', '?'), ['%' . $searchword . '%'], "id, {$DB->sql_order_by_text('title')}");
$titles = array_map(function($x) {
return $x->title;
}, $results);
sort($titles);
$this->assertEquals($expected, $titles);
}
/**
* Adds a mock search area to the search system.
*/
protected function add_mock_search_area() {
$areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
$this->search->add_search_area($areaid, new \core_mocksearch\search\mock_search_area());
}
/**
* Updates mssql fulltext index if necessary.
*
* @return bool
*/
private function update_index() {
global $DB;
if (!$this->requires_manual_index_update()) {
return;
}
$DB->execute("ALTER FULLTEXT INDEX ON {search_simpledb_index} START UPDATE POPULATION");
$catalogname = $DB->get_prefix() . 'search_simpledb_catalog';
$retries = 0;
do {
// 0.2 seconds.
usleep(200000);
$record = $DB->get_record_sql("SELECT FULLTEXTCATALOGPROPERTY(cat.name, 'PopulateStatus') AS [PopulateStatus]
FROM sys.fulltext_catalogs AS cat
WHERE cat.name = ?", array($catalogname));
$retries++;
} while ($retries < 100 && $record->populatestatus != '0');
if ($retries === 100) {
// No update after 20 seconds...
$this->fail('Sorry, your SQL server fulltext search index is too slow.');
}
}
/**
* Mssql with fulltext support requires manual updates.
*
* @return bool
*/
private function requires_manual_index_update() {
global $DB;
return ($DB->get_dbfamily() === 'mssql' && $DB->is_fulltext_search_supported());
}
}
@@ -0,0 +1,310 @@
<?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/>.
/**
* Unit tests for privacy.
*
* @package search_simpledb
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace search_simpledb\privacy;
use search_simpledb\privacy\provider;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
/**
* Unit tests for privacy.
*
* @package search_simpledb
* @copyright 2018 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends \core_privacy\tests\provider_testcase {
/** @var \search_simpledb\engine database engine. */
protected $engine;
/** @var \testable_core_search core search class adapted to unit test. */
protected $search;
/** @var \core_search_generator core search generator class adapted to unit test. */
protected $generator = null;
/** @var \stdClass course record.*/
protected $c1;
/** @var \stdClass course record.*/
protected $c2;
/** @var \context_course context instance. */
protected $c1context;
/** @var \context_course context instance. */
protected $c2context;
/** @var \stdClass user record. */
protected $u1;
/** @var \stdClass user record. */
protected $u2;
public function setUp(): void {
global $DB;
if ($this->requires_manual_index_update()) {
// We need to update fulltext index manually, which requires an alter table statement.
$this->preventResetByRollback();
}
$this->resetAfterTest();
set_config('enableglobalsearch', true);
// Inject search_simpledb engine into the testable core search as we need to add the mock
// search component to it.
$this->engine = new \search_simpledb\engine();
$this->search = \testable_core_search::instance($this->engine);
$areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
$this->search->add_search_area($areaid, new \core_mocksearch\search\mock_search_area());
$this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
$this->generator->setup();
$this->c1 = $this->getDataGenerator()->create_course();
$this->c2 = $this->getDataGenerator()->create_course();
$this->c1context = \context_course::instance($this->c1->id);
$this->c2context = \context_course::instance($this->c2->id);
$this->u1 = $this->getDataGenerator()->create_user();
$this->u2 = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($this->u1->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u1->id, $this->c2->id, 'student');
$this->getDataGenerator()->enrol_user($this->u2->id, $this->c1->id, 'student');
$this->getDataGenerator()->enrol_user($this->u2->id, $this->c2->id, 'student');
$record = (object)[
'userid' => $this->u1->id,
'contextid' => $this->c1context->id,
'title' => 'vi',
'content' => 'va',
'description1' => 'san',
'description2' => 'jose'
];
$this->generator->create_record($record);
$this->generator->create_record((object)['userid' => $this->u1->id, 'contextid' => $this->c2context->id]);
$this->generator->create_record((object)['userid' => $this->u2->id, 'contextid' => $this->c2context->id]);
$this->generator->create_record((object)['userid' => $this->u2->id, 'contextid' => $this->c1context->id]);
$this->generator->create_record((object)['owneruserid' => $this->u1->id, 'contextid' => $this->c1context->id]);
$this->generator->create_record((object)['owneruserid' => $this->u1->id, 'contextid' => $this->c2context->id]);
$this->generator->create_record((object)['owneruserid' => $this->u2->id, 'contextid' => $this->c1context->id]);
$this->generator->create_record((object)['owneruserid' => $this->u2->id, 'contextid' => $this->c2context->id]);
$this->search->index();
$this->setAdminUser();
}
/**
* tearDown
*
* @return void
*/
public function tearDown(): void {
// Call parent tearDown() first.
parent::tearDown();
// For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
if ($this->generator) {
// Moodle DML freaks out if we don't teardown the temp table after each run.
$this->generator->teardown();
$this->generator = null;
}
}
/**
* Test fetching contexts for a given user ID.
*/
public function test_get_contexts_for_userid(): void {
// Ensure both contexts are found for both users.
$expected = [$this->c1context->id, $this->c2context->id];
sort($expected);
// User 1.
$contextlist = provider::get_contexts_for_userid($this->u1->id);
$this->assertCount(2, $contextlist);
$actual = $contextlist->get_contextids();
sort($actual);
$this->assertEquals($expected, $actual);
// User 2.
$contextlist = provider::get_contexts_for_userid($this->u2->id);
$this->assertCount(2, $contextlist);
$actual = $contextlist->get_contextids();
sort($actual);
$this->assertEquals($expected, $actual);
}
/**
* Test fetching user IDs for a given context.
*/
public function test_get_users_in_context(): void {
$component = 'search_simpledb';
// Ensure both users are found for both contexts.
$expected = [$this->u1->id, $this->u2->id];
sort($expected);
// User 1.
$userlist = new \core_privacy\local\request\userlist($this->c1context, $component);
provider::get_users_in_context($userlist);
$this->assertCount(2, $userlist);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
// User 2.
$userlist = new \core_privacy\local\request\userlist($this->c2context, $component);
provider::get_users_in_context($userlist);
$this->assertCount(2, $userlist);
$actual = $userlist->get_userids();
sort($actual);
$this->assertEquals($expected, $actual);
}
/**
* Test export user data.
*
* @return null
*/
public function test_export_user_data(): void {
global $DB;
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u1, 'search_simpledb',
[$this->c1context->id]);
provider::export_user_data($contextlist);
$writer = \core_privacy\local\request\writer::with_context($this->c1context);
$this->assertTrue($writer->has_any_data());
$u1c1 = $DB->get_record('search_simpledb_index', ['userid' => $this->u1->id, 'contextid' => $this->c1context->id]);
$data = $writer->get_data([get_string('search', 'search'), $u1c1->docid]);
$this->assertEquals($this->c1context->get_context_name(true, true), $data->context);
$this->assertEquals('vi', $data->title);
$this->assertEquals('va', $data->content);
$this->assertEquals('san', $data->description1);
$this->assertEquals('jose', $data->description2);
}
/**
* Test delete search for context.
*
* @return null
*/
public function test_delete_data_for_all_users(): void {
global $DB;
$this->assertEquals(8, $DB->count_records('search_simpledb_index'));
provider::delete_data_for_all_users_in_context($this->c1context);
$this->assertEquals(0, $DB->count_records('search_simpledb_index', ['contextid' => $this->c1context->id]));
$this->assertEquals(4, $DB->count_records('search_simpledb_index'));
$u2context = \context_user::instance($this->u2->id);
provider::delete_data_for_all_users_in_context($u2context);
$this->assertEquals(0, $DB->count_records('search_simpledb_index', ['contextid' => $u2context->id]));
$this->assertEquals(2, $DB->count_records('search_simpledb_index'));
}
/**
* Test delete search for user.
*
* @return null
*/
public function test_delete_data_for_user(): void {
global $DB;
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u1, 'search_simpledb',
[$this->c1context->id]);
provider::delete_data_for_user($contextlist);
$select = 'contextid = :contextid AND (owneruserid = :owneruserid OR userid = :userid)';
$params = ['contextid' => $this->c1context->id, 'owneruserid' => $this->u1->id, 'userid' => $this->u1->id];
$this->assertEquals(0, $DB->count_records_select('search_simpledb_index', $select, $params));
$this->assertEquals(2, $DB->count_records('search_simpledb_index', ['contextid' => $this->c1context->id]));
$this->assertEquals(6, $DB->count_records('search_simpledb_index'));
$contextlist = new \core_privacy\local\request\approved_contextlist($this->u2, 'search_simpledb',
[$this->c2context->id]);
provider::delete_data_for_user($contextlist);
$select = 'contextid = :contextid AND (owneruserid = :owneruserid OR userid = :userid)';
$params = ['contextid' => $this->c2context->id, 'owneruserid' => $this->u2->id, 'userid' => $this->u2->id];
$this->assertEquals(0, $DB->count_records_select('search_simpledb_index', $select, $params));
$this->assertEquals(2, $DB->count_records('search_simpledb_index', ['contextid' => $this->c2context->id]));
$this->assertEquals(4, $DB->count_records('search_simpledb_index'));
}
/**
* Test deleting data for an approved userlist.
*/
public function test_delete_data_for_users(): void {
global $DB;
$component = 'search_simpledb';
$select = 'contextid = :contextid AND (owneruserid = :owneruserid OR userid = :userid)';
// Ensure expected amount of data for both users exists in each context.
$this->assertEquals(4, $DB->count_records('search_simpledb_index', ['contextid' => $this->c1context->id]));
$this->assertEquals(4, $DB->count_records('search_simpledb_index', ['contextid' => $this->c2context->id]));
// Delete user 1's data in context 1.
$approveduserids = [$this->u1->id];
$approvedlist = new \core_privacy\local\request\approved_userlist($this->c1context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
$params = ['contextid' => $this->c1context->id, 'owneruserid' => $this->u1->id, 'userid' => $this->u1->id];
$this->assertEquals(0, $DB->count_records_select('search_simpledb_index', $select, $params));
// Ensure user 2's data in context 1 is retained.
$params = ['contextid' => $this->c1context->id, 'owneruserid' => $this->u2->id, 'userid' => $this->u2->id];
$this->assertEquals(2, $DB->count_records_select('search_simpledb_index', $select, $params));
// Ensure both users' data in context 2 is retained.
$params = ['contextid' => $this->c2context->id, 'owneruserid' => $this->u1->id, 'userid' => $this->u1->id];
$this->assertEquals(2, $DB->count_records_select('search_simpledb_index', $select, $params));
$params = ['contextid' => $this->c2context->id, 'owneruserid' => $this->u2->id, 'userid' => $this->u2->id];
$this->assertEquals(2, $DB->count_records_select('search_simpledb_index', $select, $params));
}
/**
* Mssql with fulltext support requires manual updates.
*
* @return bool
*/
private function requires_manual_index_update() {
global $DB;
return ($DB->get_dbfamily() === 'mssql' && $DB->is_fulltext_search_supported());
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Version info.
*
* @package search_simpledb
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'search_simpledb';
+208
View File
@@ -0,0 +1,208 @@
<?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 search_solr
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace search_solr;
defined('MOODLE_INTERNAL') || die();
/**
* Respresents a document to index.
*
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class document extends \core_search\document {
/**
* Indicates the file contents were not indexed due to an error.
*/
const INDEXED_FILE_ERROR = -1;
/**
* Indicates the file contents were not indexed due filtering/settings.
*/
const INDEXED_FILE_FALSE = 0;
/**
* Indicates the file contents are indexed with the record.
*/
const INDEXED_FILE_TRUE = 1;
/**
* Any fields that are engine specifc. These are fields that are solely used by a seach engine plugin
* for internal purposes.
*
* @var array
*/
protected static $enginefields = array(
'solr_filegroupingid' => array(
'type' => 'string',
'stored' => true,
'indexed' => true
),
'solr_fileid' => array(
'type' => 'string',
'stored' => true,
'indexed' => true
),
'solr_filecontenthash' => array(
'type' => 'string',
'stored' => true,
'indexed' => true
),
// Stores the status of file indexing.
'solr_fileindexstatus' => array(
'type' => 'int',
'stored' => true,
'indexed' => true
),
// Field to index, but not store, file contents.
'solr_filecontent' => array(
'type' => 'text',
'stored' => false,
'indexed' => true,
'mainquery' => true
)
);
/**
* Formats the timestamp according to the search engine needs.
*
* @param int $timestamp
* @return string
*/
public static function format_time_for_engine($timestamp) {
return gmdate(\search_solr\engine::DATE_FORMAT, $timestamp);
}
/**
* Formats the timestamp according to the search engine needs.
*
* @param int $timestamp
* @return string
*/
public static function format_string_for_engine($string) {
// 2^15 default. We could convert this to a setting as is possible to
// change the max in solr.
return \core_text::str_max_bytes($string, 32766);
}
/**
* Returns a timestamp from the value stored in the search engine.
*
* @param string $time
* @return int
*/
public static function import_time_from_engine($time) {
return strtotime($time);
}
/**
* Overwritten to use HTML (highlighting).
*
* @return int
*/
protected function get_text_format() {
return FORMAT_HTML;
}
/**
* Formats a text string coming from the search engine.
*
* Even if this is called through an external function it is fine to return HTML as
* HTML is considered solr's search engine text format. An external function can ask
* for raw text, but this just means that it will not pass through format_text, no that
* we can not add HTML.
*
* @param string $text Text to format
* @return string HTML text to be renderer
*/
protected function format_text($text) {
// Since we allow output for highlighting, we need to encode html entities.
// This ensures plaintext html chars don't become valid html.
$out = s($text);
$startcount = 0;
$endcount = 0;
// Remove end/start pairs that span a few common seperation characters. Allows us to highlight phrases instead of words.
$regex = '|'.engine::HIGHLIGHT_END.'([ .,-]{0,3})'.engine::HIGHLIGHT_START.'|';
$out = preg_replace($regex, '$1', $out);
// Now replace our start and end highlight markers.
$out = str_replace(engine::HIGHLIGHT_START, '<span class="highlight">', $out, $startcount);
$out = str_replace(engine::HIGHLIGHT_END, '</span>', $out, $endcount);
// This makes sure any highlight tags are balanced, incase truncation or the highlight text contained our markers.
while ($startcount > $endcount) {
$out .= '</span>';
$endcount++;
}
while ($startcount < $endcount) {
$out = '<span class="highlight">' . $out;
$endcount++;
}
return parent::format_text($out);
}
/**
* 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() {
parent::apply_defaults();
// We want to set the solr_filegroupingid to id if it isn't set.
if (!isset($this->data['solr_filegroupingid'])) {
$this->data['solr_filegroupingid'] = $this->data['id'];
}
}
/**
* Export the data for the given file in relation to this document.
*
* @param \stored_file $file The stored file we are talking about.
* @return array
*/
public function export_file_for_engine($file) {
$data = $this->export_for_engine();
// Content is index in the main document.
unset($data['content']);
unset($data['description1']);
unset($data['description2']);
// Going to append the fileid to give it a unique id.
$data['id'] = $data['id'].'-solrfile'.$file->get_id();
$data['type'] = \core_search\manager::TYPE_FILE;
$data['solr_fileid'] = $file->get_id();
$data['solr_filecontenthash'] = $file->get_contenthash();
$data['solr_fileindexstatus'] = self::INDEXED_FILE_TRUE;
$data['title'] = $file->get_filename();
$data['modified'] = self::format_time_for_engine($file->get_timemodified());
return $data;
}
}
File diff suppressed because it is too large Load Diff
@@ -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/>.
/**
* Privacy class for requesting user data.
*
* @package search_solr
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace search_solr\privacy;
defined('MOODLE_INTERNAL') || die();
use core_privacy\local\metadata\collection;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\userlist;
/**
* Provider for the search_solr plugin.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
// This search engine plugin does not store any data itself.
// It has no database tables, and it purely acts as a conduit, sending data externally.
// This plugin is capable of determining which users have data within it.
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
return $collection->add_external_location_link('solr', ['data' => 'privacy:metadata:data'],
'privacy:metadata');
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
return new contextlist();
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
}
}
+349
View File
@@ -0,0 +1,349 @@
<?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/>.
/**
* Solr schema manipulation manager.
*
* @package search_solr
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace search_solr;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/filelib.php');
/**
* Schema class to interact with Solr schema.
*
* At the moment it only implements create which should be enough for a basic
* moodle configuration in Solr.
*
* @package search_solr
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class schema {
/**
* @var stdClass
*/
protected $config = null;
/**
* cUrl instance.
* @var \curl
*/
protected $curl = null;
/**
* An engine instance.
* @var engine
*/
protected $engine = null;
/**
* Constructor.
*
* @param engine $engine Optional engine parameter, if not specified then one will be created
* @throws \moodle_exception
* @return void
*/
public function __construct(engine $engine = null) {
if (!$this->config = get_config('search_solr')) {
throw new \moodle_exception('missingconfig', 'search_solr');
}
if (empty($this->config->server_hostname) || empty($this->config->indexname)) {
throw new \moodle_exception('missingconfig', 'search_solr');
}
$this->engine = $engine ?? new engine();
$this->curl = $this->engine->get_curl_object();
// HTTP headers.
$this->curl->setHeader('Content-type: application/json');
}
/**
* Can setup be executed against the configured server.
*
* @return true|string True or error message.
*/
public function can_setup_server() {
$status = $this->engine->is_server_configured();
if ($status !== true) {
return $status;
}
// At this stage we know that the server is properly configured with a valid host:port and indexname.
// We're not too concerned about repeating the SolrClient::system() call (already called in
// is_server_configured) because this is just a setup script.
if ($this->engine->get_solr_major_version() < 5) {
// Schema setup script only available for 5.0 onwards.
return get_string('schemasetupfromsolr5', 'search_solr');
}
return true;
}
/**
* Setup solr stuff required by moodle.
*
* @param bool $checkexisting Whether to check if the fields already exist or not
* @return bool
*/
public function setup($checkexisting = true) {
$fields = \search_solr\document::get_default_fields_definition();
// Field id is already there.
unset($fields['id']);
$this->check_index();
$return = $this->add_fields($fields, $checkexisting);
// Tell the engine we are now using the latest schema version.
$this->engine->record_applied_schema_version(document::SCHEMA_VERSION);
return $return;
}
/**
* Checks the schema is properly set up.
*
* @throws \moodle_exception
* @return void
*/
public function validate_setup() {
$fields = \search_solr\document::get_default_fields_definition();
// Field id is already there.
unset($fields['id']);
$this->check_index();
$this->validate_fields($fields, true);
}
/**
* Checks if the index is ready, triggers an exception otherwise.
*
* @throws \moodle_exception
* @return void
*/
protected function check_index() {
// Check that the server is available and the index exists.
$url = $this->engine->get_connection_url('/select?wt=json');
$result = $this->curl->get($url);
if ($this->curl->error) {
throw new \moodle_exception('connectionerror', 'search_solr');
}
if ($this->curl->info['http_code'] === 404) {
throw new \moodle_exception('connectionerror', 'search_solr');
}
}
/**
* Adds the provided fields to Solr schema.
*
* Intentionally separated from create(), it can be called to add extra fields.
* fields separately.
*
* @throws \coding_exception
* @throws \moodle_exception
* @param array $fields \core_search\document::$requiredfields format
* @param bool $checkexisting Whether to check if the fields already exist or not
* @return bool
*/
protected function add_fields($fields, $checkexisting = true) {
if ($checkexisting) {
// Check that non of them exists.
$this->validate_fields($fields, false);
}
$url = $this->engine->get_connection_url('/schema');
// Add all fields.
foreach ($fields as $fieldname => $data) {
if (!isset($data['type']) || !isset($data['stored']) || !isset($data['indexed'])) {
throw new \coding_exception($fieldname . ' does not define all required field params: type, stored and indexed.');
}
$type = $this->doc_field_to_solr_field($data['type']);
// Changing default multiValued value to false as we want to match values easily.
$params = array(
'add-field' => array(
'name' => $fieldname,
'type' => $type,
'stored' => $data['stored'],
'multiValued' => false,
'indexed' => $data['indexed']
)
);
$results = $this->curl->post($url, json_encode($params));
// We only validate if we are interested on it.
if ($checkexisting) {
if ($this->curl->error) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
}
$this->validate_add_field_result($results);
}
}
return true;
}
/**
* Checks if the schema existing fields are properly set, triggers an exception otherwise.
*
* @throws \moodle_exception
* @param array $fields
* @param bool $requireexisting Require the fields to exist, otherwise exception.
* @return void
*/
protected function validate_fields(&$fields, $requireexisting = false) {
global $CFG;
foreach ($fields as $fieldname => $data) {
$url = $this->engine->get_connection_url('/schema/fields/' . $fieldname);
$results = $this->curl->get($url);
if ($this->curl->error) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
}
if (!$results) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
}
$results = json_decode($results);
if ($requireexisting && !empty($results->error) && $results->error->code === 404) {
$a = new \stdClass();
$a->fieldname = $fieldname;
$a->setupurl = $CFG->wwwroot . '/search/engine/solr/setup_schema.php';
throw new \moodle_exception('errorvalidatingschema', 'search_solr', '', $a);
}
// The field should not exist so we only accept 404 errors.
if (empty($results->error) || (!empty($results->error) && $results->error->code !== 404)) {
if (!empty($results->error)) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error->msg);
} else {
// All these field attributes are set when fields are added through this script and should
// be returned and match the defined field's values.
$expectedsolrfield = $this->doc_field_to_solr_field($data['type']);
if (empty($results->field) || !isset($results->field->type) ||
!isset($results->field->multiValued) || !isset($results->field->indexed) ||
!isset($results->field->stored)) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
get_string('schemafieldautocreated', 'search_solr', $fieldname));
} else if ($results->field->type !== $expectedsolrfield ||
$results->field->multiValued !== false ||
$results->field->indexed !== $data['indexed'] ||
$results->field->stored !== $data['stored']) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
get_string('schemafieldautocreated', 'search_solr', $fieldname));
} else {
// The field already exists and it is properly defined, no need to create it.
unset($fields[$fieldname]);
}
}
}
}
}
/**
* Checks that the field results do not contain errors.
*
* @throws \moodle_exception
* @param string $results curl response body
* @return void
*/
protected function validate_add_field_result($result) {
if (!$result) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
}
$results = json_decode($result);
if (!$results) {
if (is_scalar($result)) {
$errormsg = $result;
} else {
$errormsg = json_encode($result);
}
throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errormsg);
}
// It comes as error when fetching fields data.
if (!empty($results->error)) {
throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error);
}
// It comes as errors when adding fields.
if (!empty($results->errors)) {
// We treat this error separately.
$errorstr = '';
foreach ($results->errors as $error) {
$errorstr .= implode(', ', $error->errorMessages);
}
throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errorstr);
}
}
/**
* Returns the solr field type from the document field type string.
*
* @param string $datatype
* @return string
*/
private function doc_field_to_solr_field($datatype) {
$type = $datatype;
$solrversion = $this->engine->get_solr_major_version();
switch($datatype) {
case 'text':
$type = 'text_general';
break;
case 'int':
if ($solrversion >= 7) {
$type = 'pint';
}
break;
case 'tdate':
if ($solrversion >= 7) {
$type = 'pdate';
}
break;
}
return $type;
}
}
@@ -0,0 +1,74 @@
<?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/>.
/**
* Strings for component 'search_solr'.
*
* @package search_solr
* @copyright Prateek Sachan {@link http://prateeksachan.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['connectionerror'] = 'The specified Solr server is not available or the specified index does not exist';
$string['connectionsettings'] = 'Connection settings';
$string['errorcreatingschema'] = 'Error creating the Solr schema: {$a}';
$string['errorvalidatingschema'] = 'Error validating Solr schema: field {$a->fieldname} does not exist. Please <a href="{$a->setupurl}">follow this link</a> to set up the required fields.';
$string['errorsolr'] = 'The Solr search engine reported an error: {$a}';
$string['extensionerror'] = 'The Apache Solr PHP extension is not installed. Please check the documentation.';
$string['fileindexing'] = 'Enable file indexing';
$string['fileindexing_help'] = 'If your Solr install supports it, this feature allows Moodle to send files to be indexed.<br/>
You will need to reindex all site contents after enabling this option for all files to be added.';
$string['fileindexsettings'] = 'File indexing settings';
$string['maxindexfilekb'] = 'Maximum file size to index (kB)';
$string['maxindexfilekb_help'] = 'Files larger than this number of kilobytes will not be included in search indexing. If set to zero, files of any size will be indexed.';
$string['minimumsolr4'] = 'Solr 4.0 is the minimum version required for Moodle';
$string['missingconfig'] = 'Your Apache Solr server is not yet configured in Moodle.';
$string['multivaluedfield'] = 'Field "{$a}" returned an array instead of a scalar. Please delete the current index, create a new one and run setup_schema.php before indexing data in Solr.';
$string['nodatafromserver'] = 'No data from server';
$string['pluginname'] = 'Solr';
$string['privacy:metadata'] = 'This plugin sends data externally to a linked Solr search engine. It does not store data locally.';
$string['privacy:metadata:data'] = 'Personal data passed through from the search subsystem.';
$string['schemafieldautocreated'] = 'Field "{$a}" already exists in Solr schema. You probably forgot to run this script before indexing data and fields were autocreated by Solr. Please delete the current index, create a new one and run setup_schema.php again before indexing data in Solr.';
$string['schemasetupfromsolr5'] = 'Your Solr server version is lower than 5.0. This script can only set your schema if your Solr version is 5.0 or higher. You need to manually set the fields in your schema according to \\search_solr\\document::get_default_fields_definition().';
$string['searchinfo'] = 'Search queries';
$string['searchinfo_help'] = 'The field to be searched may be specified by prefixing the search query with \'title:\', \'content:\', \'name:\', or \'intro:\'. For example, searching for \'title:news\' would return results with the word \'news\' in the title.
Boolean operators (\'AND\', \'OR\', \'NOT\') may be used to combine or exclude keywords.
Wildcard characters (\'*\' or \'?\' ) may be used to represent characters in the search query.';
$string['setupok'] = 'The schema is ready to be used.';
$string['solrauthpassword'] = 'HTTP authentication password';
$string['solrauthuser'] = 'HTTP authentication username';
$string['solrindexname'] = 'Index name';
$string['solrhttpconnectionport'] = 'Port';
$string['solrhttpconnectiontimeout'] = 'Timeout';
$string['solrhttpconnectiontimeout_desc'] = 'The HTTP connection timeout is the maximum time in seconds allowed for the HTTP data transfer operation.';
$string['solrinfo'] = 'Solr';
$string['solrnotselected'] = 'Solr engine is not the configured search engine';
$string['solrserverhostname'] = 'Host name';
$string['solrserverhostname_desc'] = 'Domain name of the Solr server.';
$string['solrsecuremode'] = 'Secure mode';
$string['solrsetting'] = 'Solr settings';
$string['solrsslcainfo'] = 'SSL CA certificates name';
$string['solrsslcainfo_desc'] = 'File name holding one or more CA certificates to verify peer with';
$string['solrsslcapath'] = 'SSL CA certificates path';
$string['solrsslcapath_desc'] = 'Directory path holding multiple CA certificates to verify peer with';
$string['solrsslcert'] = 'SSL certificate';
$string['solrsslcert_desc'] = 'File name to a PEM-formatted private certificate';
$string['solrsslkey'] = 'SSL key';
$string['solrsslkey_desc'] = 'File name to a PEM-formatted private key';
$string['solrsslkeypassword'] = 'SSL key password';
$string['solrsslkeypassword_desc'] = 'Password for PEM-formatted private key file';
+98
View File
@@ -0,0 +1,98 @@
<?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/>.
/**
* Solr search engine settings.
*
* @package search_solr
* @copyright 2015 Daniel Neis
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if ($ADMIN->fulltree) {
if (!during_initial_install()) {
if (!function_exists('solr_get_version')) {
$settings->add(new admin_setting_heading('search_solr_settings', '', get_string('extensionerror', 'search_solr')));
} else {
$settings->add(new admin_setting_heading('search_solr_connection',
new lang_string('connectionsettings', 'search_solr'), ''));
$settings->add(new admin_setting_configtext('search_solr/server_hostname', new lang_string('solrserverhostname', 'search_solr'), new lang_string('solrserverhostname_desc', 'search_solr'), '127.0.0.1', PARAM_HOST));
$settings->add(new admin_setting_configtext('search_solr/indexname', new lang_string('solrindexname', 'search_solr'), '', '', PARAM_ALPHANUMEXT));
$settings->add(new admin_setting_configcheckbox('search_solr/secure', new lang_string('solrsecuremode', 'search_solr'), '', 0, 1, 0));
$secure = get_config('search_solr', 'secure');
$defaultport = !empty($secure) ? 8443 : 8983;
$settings->add(new admin_setting_configtext('search_solr/server_port', new lang_string('solrhttpconnectionport', 'search_solr'), '', $defaultport, PARAM_INT));
$settings->add(new admin_setting_configtext('search_solr/server_username', new lang_string('solrauthuser', 'search_solr'), '', '', PARAM_RAW));
$settings->add(new admin_setting_configpasswordunmask('search_solr/server_password', new lang_string('solrauthpassword', 'search_solr'), '', ''));
$settings->add(new admin_setting_configtext('search_solr/server_timeout', new lang_string('solrhttpconnectiontimeout', 'search_solr'), new lang_string('solrhttpconnectiontimeout_desc', 'search_solr'), 30, PARAM_INT));
$settings->add(new admin_setting_configtext('search_solr/ssl_cert', new lang_string('solrsslcert', 'search_solr'), new lang_string('solrsslcert_desc', 'search_solr'), '', PARAM_RAW));
$settings->add(new admin_setting_configtext('search_solr/ssl_key', new lang_string('solrsslkey', 'search_solr'), new lang_string('solrsslkey_desc', 'search_solr'), '', PARAM_RAW));
$settings->add(new admin_setting_configpasswordunmask('search_solr/ssl_keypassword', new lang_string('solrsslkeypassword', 'search_solr'), new lang_string('solrsslkeypassword_desc', 'search_solr'), ''));
$settings->add(new admin_setting_configtext('search_solr/ssl_cainfo', new lang_string('solrsslcainfo', 'search_solr'), new lang_string('solrsslcainfo_desc', 'search_solr'), '', PARAM_RAW));
$settings->add(new admin_setting_configtext('search_solr/ssl_capath', new lang_string('solrsslcapath', 'search_solr'), new lang_string('solrsslcapath_desc', 'search_solr'), '', PARAM_RAW));
$settings->add(new admin_setting_heading('search_solr_fileindexing',
new lang_string('fileindexsettings', 'search_solr'), ''));
$settings->add(new admin_setting_configcheckbox('search_solr/fileindexing',
new lang_string('fileindexing', 'search_solr'),
new lang_string('fileindexing_help', 'search_solr'), 1));
$settings->add(new admin_setting_configtext('search_solr/maxindexfilekb',
new lang_string('maxindexfilekb', 'search_solr'),
new lang_string('maxindexfilekb_help', 'search_solr'), '2097152', PARAM_INT));
// Alternate connection.
$settings->add(new admin_setting_heading('search_solr_alternatesettings',
new lang_string('searchalternatesettings', 'admin'),
new lang_string('searchalternatesettings_desc', 'admin')));
$settings->add(new admin_setting_configtext('search_solr/alternateserver_hostname',
new lang_string('solrserverhostname', 'search_solr'),
new lang_string('solrserverhostname_desc', 'search_solr'), '127.0.0.1', PARAM_HOST));
$settings->add(new admin_setting_configtext('search_solr/alternateindexname',
new lang_string('solrindexname', 'search_solr'), '', '', PARAM_ALPHANUMEXT));
$settings->add(new admin_setting_configcheckbox('search_solr/alternatesecure',
new lang_string('solrsecuremode', 'search_solr'), '', 0, 1, 0));
$secure = get_config('search_solr', 'alternatesecure');
$defaultport = !empty($secure) ? 8443 : 8983;
$settings->add(new admin_setting_configtext('search_solr/alternateserver_port',
new lang_string('solrhttpconnectionport', 'search_solr'), '', $defaultport, PARAM_INT));
$settings->add(new admin_setting_configtext('search_solr/alternateserver_username',
new lang_string('solrauthuser', 'search_solr'), '', '', PARAM_RAW));
$settings->add(new admin_setting_configpasswordunmask('search_solr/alternateserver_password',
new lang_string('solrauthpassword', 'search_solr'), '', ''));
$settings->add(new admin_setting_configtext('search_solr/alternatessl_cert',
new lang_string('solrsslcert', 'search_solr'),
new lang_string('solrsslcert_desc', 'search_solr'), '', PARAM_RAW));
$settings->add(new admin_setting_configtext('search_solr/alternatessl_key',
new lang_string('solrsslkey', 'search_solr'),
new lang_string('solrsslkey_desc', 'search_solr'), '', PARAM_RAW));
$settings->add(new admin_setting_configpasswordunmask('search_solr/alternatessl_keypassword',
new lang_string('solrsslkeypassword', 'search_solr'),
new lang_string('solrsslkeypassword_desc', 'search_solr'), ''));
$settings->add(new admin_setting_configtext('search_solr/alternatessl_cainfo',
new lang_string('solrsslcainfo', 'search_solr'),
new lang_string('solrsslcainfo_desc', 'search_solr'), '', PARAM_RAW));
$settings->add(new admin_setting_configtext('search_solr/alternatessl_capath',
new lang_string('solrsslcapath', 'search_solr'),
new lang_string('solrsslcapath_desc', 'search_solr'), '', PARAM_RAW));
}
}
}
+57
View File
@@ -0,0 +1,57 @@
<?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/>.
/**
* Adds moodle fields to solr schema.
*
* Schema REST API write actions are only available from Solr 4.4 onwards.
*
* The schema should be managed and mutable to allow this script
* to add new fields to the schema.
*
* @link https://cwiki.apache.org/confluence/display/solr/Managed+Schema+Definition+in+SolrConfig
* @package search_solr
* @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
require_login(null, false);
require_capability('moodle/site:config', context_system::instance());
$returnurl = new moodle_url('/admin/settings.php', array('section' => 'manageglobalsearch'));
$schema = new \search_solr\schema();
$status = $schema->can_setup_server();
if ($status !== true) {
$PAGE->set_context(context_system::instance());
$PAGE->set_url(new moodle_url('/search/engine/solr/setup_schema.php'));
echo $OUTPUT->header();
echo $OUTPUT->notification($status, \core\output\notification::NOTIFY_ERROR);
echo $OUTPUT->box($OUTPUT->action_link($returnurl, get_string('continue')), 'generalbox centerpara');
echo $OUTPUT->footer();
exit(1);
}
$schema->setup();
redirect($returnurl, get_string('setupok', 'search_solr'), 4);
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
<?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 search_solr;
/**
* Search engine for testing purposes.
*
* @package search_solr
* @category phpunit
* @copyright 2016 Eric Merrill {@link http://www.merrilldigital.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
class testable_engine extends \search_solr\engine {
/**
* Function that lets us update the internally cached config object of the engine.
*/
public function test_set_config($name, $value) {
$this->config->$name = $value;
}
/**
* Gets the search client (this function is usually protected) for testing.
*
* @return \SolrClient Solr client object
* @throws \core_search\engine_exception
*/
public function get_search_client_public(): \SolrClient {
return parent::get_search_client();
}
}
@@ -0,0 +1,47 @@
<?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 provider tests.
*
* @package search_solr
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace search_solr\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy provider tests class.
*
* @copyright 2018 David Monllao
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends \core_privacy\tests\provider_testcase {
/**
* Verify that a collection of metadata is returned for this component and that it just links to an external location.
*/
public function test_get_metadata(): void {
$collection = new \core_privacy\local\metadata\collection('search_solr');
$collection = \search_solr\privacy\provider::get_metadata($collection);
$this->assertNotEmpty($collection);
$items = $collection->get_collection();
$this->assertEquals(1, count($items));
$this->assertInstanceOf(\core_privacy\local\metadata\types\external_location::class, $items[0]);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Version info.
*
* @package search_solr
* @copyright 2015 Daniel Neis Araujo
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'search_solr';