--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Document representation.
+ *
+ * @package 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 {
+
+ /**
+ * 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 substr($string, 0, 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 markdown format as we use markdown for solr highlighting.
+ *
+ * @return int
+ */
+ protected function get_text_format() {
+ return FORMAT_MARKDOWN;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Solr engine.
+ *
+ * @package search_solr
+ * @copyright 2015 Daniel Neis Araujo
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace search_solr;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Solr engine.
+ *
+ * @package search_solr
+ * @copyright 2015 Daniel Neis Araujo
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class engine extends \core_search\engine {
+
+ /**
+ * @var string The date format used by solr.
+ */
+ const DATE_FORMAT = 'Y-m-d\TH:i:s\Z';
+
+ /**
+ * @var int Commit documents interval (number of miliseconds).
+ */
+ const AUTOCOMMIT_WITHIN = 15000;
+
+ /**
+ * @var int Highlighting fragsize.
+ */
+ const FRAG_SIZE = 500;
+
+ /**
+ * @var \SolrClient
+ */
+ protected $client = null;
+
+ /**
+ * @var array Fields that can be highlighted.
+ */
+ protected $highlightfields = array('content', 'description1', 'description2');
+
+ /**
+ * Prepares a Solr 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.
+ * @return \core_search\document[] Results or false if no results
+ */
+ public function execute_query($filters, $usercontexts) {
+
+ // Let's keep these changes internal.
+ $data = clone $filters;
+
+ // If there is any problem we trigger the exception as soon as possible.
+ $this->client = $this->get_search_client();
+
+ $serverstatus = $this->is_server_ready();
+ if ($serverstatus !== true) {
+ throw new \core_search\engine_exception('engineserverstatus', 'search');
+ }
+
+ $query = new \SolrQuery();
+ $this->set_query($query, $data->q);
+ $this->add_fields($query);
+
+ // Search filters applied, we don't cache these filters as we don't want to pollute the cache with tmp filters
+ // we are really interested in caching contexts filters instead.
+ if (!empty($data->title)) {
+ $query->addFilterQuery('{!field cache=false f=title}' . $data->title);
+ }
+ if (!empty($data->areaid)) {
+ // Even if it is only supposed to contain PARAM_ALPHANUMEXT, better to prevent.
+ $query->addFilterQuery('{!field cache=false f=areaid}' . $data->areaid);
+ }
+
+ if (!empty($data->timestart) or !empty($data->timeend)) {
+ if (empty($data->timestart)) {
+ $data->timestart = '*';
+ } else {
+ $data->timestart = \search_solr\document::format_time_for_engine($data->timestart);
+ }
+ if (empty($data->timeend)) {
+ $data->timeend = '*';
+ } else {
+ $data->timeend = \search_solr\document::format_time_for_engine($data->timeend);
+ }
+
+ // No cache.
+ $query->addFilterQuery('{!cache=false}modified:[' . $data->timestart . ' TO ' . $data->timeend . ']');
+ }
+
+ // And finally 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)) {
+ if (!empty($data->areaid)) {
+ $query->addFilterQuery('contextid:(' . implode(' OR ', $usercontexts[$data->areaid]) . ')');
+ } else {
+ // Join all area contexts into a single array and implode.
+ $allcontexts = array();
+ foreach ($usercontexts as $areacontexts) {
+ foreach ($areacontexts as $contextid) {
+ // Ensure they are unique.
+ $allcontexts[$contextid] = $contextid;
+ }
+ }
+ $query->addFilterQuery('contextid:(' . implode(' OR ', $allcontexts) . ')');
+ }
+ }
+
+ try {
+ return $this->query_response($this->client->query($query));
+ } catch (\SolrClientException $ex) {
+ debugging('Error executing the provided query: ' . $ex->getMessage(), DEBUG_DEVELOPER);
+ $this->queryerror = $ex->getMessage();
+ return array();
+ } catch (\SolrServerException $ex) {
+ debugging('Error executing the provided query: ' . $ex->getMessage(), DEBUG_DEVELOPER);
+ $this->queryerror = $ex->getMessage();
+ return array();
+ }
+
+ }
+
+ /**
+ * Prepares a new query by setting the query, start offset and rows to return.
+ * @param SolrQuery $query
+ * @param object $q Containing query and filters.
+ */
+ protected function set_query($query, $q) {
+
+ // Set hightlighting.
+ $query->setHighlight(true);
+ foreach ($this->highlightfields as $field) {
+ $query->addHighlightField($field);
+ }
+ $query->setHighlightFragsize(static::FRAG_SIZE);
+ $query->setHighlightSimplePre('__');
+ $query->setHighlightSimplePost('__');
+
+ $query->setQuery($q);
+
+ // A reasonable max.
+ $query->setRows(\core_search\manager::MAX_RESULTS);
+ }
+
+ /**
+ * Sets fields to be returned in the result.
+ *
+ * @param SolrQuery $query object.
+ */
+ public function add_fields($query) {
+ $documentclass = $this->get_document_classname();
+ $fields = array_keys($documentclass::get_default_fields_definition());
+ foreach ($fields as $field) {
+ $query->addField($field);
+ }
+ }
+
+ /**
+ * Finds the key common to both highlighing and docs array returned from response.
+ * @param object $response containing results.
+ */
+ public function add_highlight_content($response) {
+ $highlightedobject = $response->highlighting;
+ foreach ($response->response->docs as $doc) {
+ $x = $doc->id;
+ $highlighteddoc = $highlightedobject->$x;
+ $this->merge_highlight_field_values($doc, $highlighteddoc);
+ }
+ }
+
+ /**
+ * Adds the highlighting array values to docs array values.
+ *
+ * @throws \core_search\engine_exception
+ * @param object $doc containing the results.
+ * @param object $highlighteddoc containing the highlighted results values.
+ */
+ public function merge_highlight_field_values($doc, $highlighteddoc) {
+
+ foreach ($this->highlightfields as $field) {
+ if (!empty($doc->$field)) {
+
+ // Check that the returned value is not an array. No way we can make this work with multivalued solr fields.
+ if (is_array($doc->{$field})) {
+ throw new \core_search\engine_exception('multivaluedfield', 'search_solr', '', $field);
+ }
+
+ if (!empty($highlighteddoc->$field)) {
+ // Replace by the highlighted result.
+ $doc->$field = reset($highlighteddoc->$field);
+ }
+ }
+ }
+ }
+
+ /**
+ * Filters the response on Moodle side.
+ *
+ * @param object $queryresponse containing the response return from solr server.
+ * @return array $results containing final results to be displayed.
+ */
+ public function query_response($queryresponse) {
+
+ $response = $queryresponse->getResponse();
+ $numgranted = 0;
+
+ if (!$docs = $response->response->docs) {
+ return array();
+ }
+
+ if (!empty($response->response->numFound)) {
+ $this->add_highlight_content($response);
+
+ // Iterate through the results checking its availability and whether they are available for the user or not.
+ foreach ($docs as $key => $docdata) {
+ if (!$searcharea = $this->get_search_area($docdata->areaid)) {
+ unset($docs[$key]);
+ continue;
+ }
+
+ $docdata = $this->standarize_solr_obj($docdata);
+
+ $access = $searcharea->check_access($docdata['itemid']);
+ switch ($access) {
+ case \core_search\manager::ACCESS_DELETED:
+ $this->delete_by_id($docdata['id']);
+ unset($docs[$key]);
+ break;
+ case \core_search\manager::ACCESS_DENIED:
+ unset($docs[$key]);
+ break;
+ case \core_search\manager::ACCESS_GRANTED:
+ $numgranted++;
+
+ // Add the doc.
+ $docs[$key] = $this->to_document($searcharea, $docdata);
+ break;
+ }
+
+ // This should never happen.
+ if ($numgranted >= \core_search\manager::MAX_RESULTS) {
+ $docs = array_slice($docs, 0, \core_search\manager::MAX_RESULTS, true);
+ break;
+ }
+ }
+ }
+
+ return $docs;
+ }
+
+ /**
+ * Returns a standard php array from a \SolrObject instance.
+ *
+ * @param \SolrObject $obj
+ * @return array The returned document as an array.
+ */
+ public function standarize_solr_obj(\SolrObject $obj) {
+ $properties = $obj->getPropertyNames();
+
+ $docdata = array();
+ foreach($properties as $name) {
+ // http://php.net/manual/en/solrobject.getpropertynames.php#98018.
+ $name = trim($name);
+ $docdata[$name] = $obj->offsetGet($name);
+ }
+ return $docdata;
+ }
+
+ /**
+ * Adds a document to the search engine.
+ *
+ * This does not commit to the search engine.
+ *
+ * @param array $doc
+ * @return void
+ */
+ public function add_document($doc) {
+
+ $solrdoc = new \SolrInputDocument();
+ foreach ($doc as $field => $value) {
+ $solrdoc->addField($field, $value);
+ }
+
+ try {
+ $result = $this->get_search_client()->addDocument($solrdoc, true, static::AUTOCOMMIT_WITHIN);
+ } catch (\SolrClientException $e) {
+ debugging('Solr client error adding document with id ' . $doc['id'] . ': ' . $e->getMessage(), DEBUG_DEVELOPER);
+ }
+ }
+
+ /**
+ * Commits all pending changes.
+ *
+ * @return void
+ */
+ public function commit() {
+ $this->get_search_client()->commit();
+ }
+
+ /**
+ * Defragments the index.
+ *
+ * @return void
+ */
+ public function optimize() {
+ $this->get_search_client()->optimize();
+ }
+
+ /**
+ * Deletes the specified document.
+ *
+ * @param string $id The document id to delete
+ * @return void
+ */
+ public function delete_by_id($id) {
+ $this->get_search_client()->deleteById($id);
+ }
+
+ /**
+ * Delete all area's documents.
+ *
+ * @param string $areaid
+ * @return void
+ */
+ public function delete($areaid = null) {
+ if ($areaid) {
+ $this->get_search_client()->deleteByQuery('areaid:' . $areaid);
+ } else {
+ $this->get_search_client()->deleteByQuery('*:*');
+ }
+ }
+
+ /**
+ * Pings the Solr server using search_solr config
+ *
+ * @return true|string Returns true if all good or an error string.
+ */
+ public function is_server_ready() {
+
+ if (empty($this->config->server_hostname) || empty($this->config->indexname)) {
+ return 'No solr configuration found';
+ }
+
+ if (!$this->client = $this->get_search_client(false)) {
+ return get_string('engineserverstatus', 'search');
+ }
+
+ try {
+ @$this->client->ping();
+ } catch (\SolrClientException $ex) {
+ return 'Solr client error: ' . $ex->getMessage();
+ } catch (\SolrServerException $ex) {
+ return 'Solr server error: ' . $ex->getMessage();
+ }
+
+ // Check that setup schema has already run.
+ try {
+ $schema = new \search_solr\schema();
+ $schema->validate_setup();
+ } catch (\moodle_exception $e) {
+ return $e->getMessage();
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the PHP Solr extension is available.
+ *
+ * @return bool
+ */
+ public function is_installed() {
+ return function_exists('solr_get_version');
+ }
+
+ /**
+ * Returns the solr client instance.
+ *
+ * @throws \core_search\engine_exception
+ * @param bool $triggerexception
+ * @return \SolrClient
+ */
+ protected function get_search_client($triggerexception = true) {
+
+ // Type comparison as it is set to false if not available.
+ if ($this->client !== null) {
+ return $this->client;
+ }
+
+ $options = array(
+ 'hostname' => $this->config->server_hostname,
+ 'path' => '/solr/' . $this->config->indexname,
+ 'login' => !empty($this->config->server_username) ? $this->config->server_username : '',
+ 'password' => !empty($this->config->server_password) ? $this->config->server_password : '',
+ 'port' => !empty($this->config->server_port) ? $this->config->server_port : '',
+ 'issecure' => !empty($this->config->secure) ? $this->config->secure : '',
+ 'ssl_cert' => !empty($this->config->ssl_cert) ? $this->config->ssl_cert : '',
+ 'ssl_cert_only' => !empty($this->config->ssl_cert_only) ? $this->config->ssl_cert_only : '',
+ 'ssl_key' => !empty($this->config->ssl_key) ? $this->config->ssl_key : '',
+ 'ssl_password' => !empty($this->config->ssl_keypassword) ? $this->config->ssl_keypassword : '',
+ 'ssl_cainfo' => !empty($this->config->ssl_cainfo) ? $this->config->ssl_cainfo : '',
+ 'ssl_capath' => !empty($this->config->ssl_capath) ? $this->config->ssl_capath : '',
+ );
+
+ $this->client = new \SolrClient($options);
+
+ if ($this->client === false && $triggerexception) {
+ throw new \core_search\engine_exception('engineserverstatus', 'search');
+ }
+
+ return $this->client;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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;
+
+ /**
+ * The URL.
+ * @var string
+ */
+ protected $url = null;
+
+ /**
+ * The schema URL.
+ * @var string
+ */
+ protected $schemaurl = null;
+
+ /**
+ * Constructor.
+ *
+ * @throws \moodle_exception
+ * @return void
+ */
+ public function __construct() {
+ 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->curl = new \curl();
+
+ // HTTP headers.
+ $this->curl->setHeader('Content-type: application/json');
+ if (!empty($this->config->server_username) && !empty($this->config->server_password)) {
+ $authorization = $this->config->server_username . ':' . $this->config->server_password;
+ $this->curl->setHeader('Authorization', 'Basic ' . base64_encode($authorization));
+ }
+
+ $this->url = rtrim($this->config->server_hostname, '/');
+ if (!empty($this->config->server_port)) {
+ $this->url .= ':' . $this->config->server_port;
+ }
+ $this->url .= '/solr/' . $this->config->indexname;
+ $this->schemaurl = $this->url . '/schema';
+
+
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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.
+ $result = $this->curl->get($this->url . '/select?wt=json');
+ 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);
+ }
+
+ // 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.');
+ }
+ // Changing default multiValued value to false as we want to match values easily.
+ $params = array(
+ 'add-field' => array(
+ 'name' => $fieldname,
+ 'type' => $data['type'],
+ 'stored' => $data['stored'],
+ 'multiValued' => false,
+ 'indexed' => $data['indexed']
+ )
+ );
+ $results = $this->curl->post($this->schemaurl, 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) {
+ $results = $this->curl->get($this->schemaurl . '/fields/' . $fieldname);
+
+ 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.
+
+ 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 !== $data['type'] ||
+ $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);
+ }
+
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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
+ */
+
+define('CLI_SCRIPT', true);
+
+require_once(__DIR__ . '/../../../../config.php');
+
+echo get_string('addingfields', 'search_solr') . "\n";
+
+if (!\core_search\manager::is_global_search_enabled()) {
+ throw new moodle_exception('globalsearchdisabled', 'search');
+}
+
+if ($CFG->searchengine !== 'solr') {
+ throw new moodle_exception('solrnotselected', 'search_solr');
+}
+
+$schema = new \search_solr\schema();
+$schema->setup();
+
+echo get_string('setupok', 'search_solr') . "\n";
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'search_solr'.
+ *
+ * @package core_search
+ * @copyright Prateek Sachan {@link http://prateeksachan.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['addingfields'] = 'Adding Moodle fields to the schema';
+$string['connectionerror'] = 'The specified Solr server is not available or the specified index does not exist';
+$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 setup the fields required by Moodle.';
+$string['extensionerror'] = 'The Apache Solr PHP extension is not installed. Please check the documentation.';
+$string['missingconfig'] = 'Your Apache Solr server is not yet configured in Moodle.';
+$string['multivaluedfield'] = 'Field "{$a}" returned an array instead of a scalar, the field is probably defined in Solr with "Multivalued" to true, this means that Solr autocreated the field for you when you indexed data because you forgot to run search/engine/solr/cli/setup_schema.php. 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['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['searchinfo'] = 'Search queries';
+$string['searchinfo_help'] = 'Features you can use while performing search queries:
+
+* Fields: You can specify which fields you want results from.<br/>
+ * title:(moodle + perth): returns all records that contains both "moodle" and "perth" in the title. Available fields: title, content, name, intro
+* Boolean Operators ("AND", "OR", "NOT"):<br/>
+ * (moodle AND perth) OR (moodle AND australia)
+* Wildcards ("*", "?"):<br/>
+ * "mo??l*" returns both "moodle" and "moodledata".
+* Proximity Searches ("~"):<br/>
+ * mood~2 returns "moodle". (2 alphabets away from "mood").
+ * moodle australia~3 returns results containing "moodle hq at perth australia" (the queried terms were within 3 words of each other)
+* Boosting Terms ("^"): To boost certain words/phrases:<br/>
+ * "perth australia"^5 "australia" will make results with the phrase "perth australia" more relevant.
+';
+$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 key & certificate';
+$string['solrsslcert_desc'] = 'File name to a PEM-formatted private key + private certificate (concatenated in that order)';
+$string['solrsslcertonly'] = 'SSL certificate';
+$string['solrsslcertonly_desc'] = 'File name to a PEM-formatted private certificate only';
+$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';
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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_configtext('search_solr/server_hostname', new lang_string('solrserverhostname', 'search_solr'), new lang_string('solrserverhostname_desc', 'search_solr'), '127.0.0.1', PARAM_TEXT));
+ $settings->add(new admin_setting_configtext('search_solr/indexname', new lang_string('solrindexname', 'search_solr'), '', 'moodle', PARAM_TEXT));
+ $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_configtext('search_solr/server_password', new lang_string('solrauthpassword', 'search_solr'), '', '', PARAM_RAW));
+ $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_cert_only', new lang_string('solrsslcertonly', 'search_solr'), new lang_string('solrsslcertonly_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_configtext('search_solr/ssl_keypassword', new lang_string('solrsslkeypassword', 'search_solr'), new lang_string('solrsslkeypassword_desc', 'search_solr'), '', PARAM_RAW));
+ $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));
+ }
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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_capability('moodle/site:config', context_system::instance());
+
+if (!\core_search\manager::is_global_search_enabled()) {
+ throw new moodle_exception('globalsearchdisabled', 'search');
+}
+
+if ($CFG->searchengine !== 'solr') {
+ throw new moodle_exception('solrnotselected', 'search_solr');
+}
+
+$schema = new \search_solr\schema();
+$schema->setup();
+
+$url = new moodle_url('/admin/settings.php', array('section' => 'searchsolr'));
+redirect($url, get_string('setupok', 'search_solr'), 4);
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Solr earch engine base unit tests.
+ *
+ * Required params:
+ * - define('TEST_SEARCH_SOLR_HOSTNAME', '127.0.0.1');
+ * - define('TEST_SEARCH_SOLR_PORT', '8983');
+ * - define('TEST_SEARCH_SOLR_INDEXNAME', 'unittest');
+ *
+ * Optional params:
+ * - define('TEST_SEARCH_SOLR_USERNAME', '');
+ * - define('TEST_SEARCH_SOLR_PASSWORD', '');
+ *
+ * @package core_search
+ * @category phpunit
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
+require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
+
+/**
+ * Solr search engine base unit tests.
+ *
+ * @package core_search
+ * @category phpunit
+ * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_solr_engine_testcase extends advanced_testcase {
+
+ /**
+ * @var \core_search::manager
+ */
+ protected $search = null;
+
+ public function setUp() {
+ $this->resetAfterTest();
+ set_config('enableglobalsearch', true);
+
+ if (!function_exists('solr_get_version')) {
+ $this->markTestSkipped('Solr extension is not loaded.');
+ }
+
+ if (!defined('TEST_SEARCH_SOLR_HOSTNAME') || !defined('TEST_SEARCH_SOLR_INDEXNAME') ||
+ !defined('TEST_SEARCH_SOLR_PORT')) {
+ $this->markTestSkipped('Solr extension test server not set.');
+ }
+
+ set_config('hostname', TEST_SEARCH_SOLR_HOSTNAME, 'search_solr');
+ set_config('port', TEST_SEARCH_SOLR_PORT, 'search_solr');
+ set_config('indexname', TEST_SEARCH_SOLR_INDEXNAME, 'search_solr');
+
+ if (defined('TEST_SEARCH_SOLR_USERNAME')) {
+ set_config('server_username', TEST_SEARCH_SOLR_USERNAME);
+ }
+
+ if (defined('TEST_SEARCH_SOLR_PASSWORD')) {
+ set_config('server_password', TEST_SEARCH_SOLR_PASSWORD);
+ }
+
+ // Inject search solr engine into the testable core search as we need to add the mock
+ // search component to it.
+ $searchengine = new \search_solr\engine();
+ $this->search = testable_core_search::instance($searchengine);
+ $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'role_capabilities');
+ $this->search->add_search_area($areaid, new core_mocksearch\search\role_capabilities());
+
+ $this->setAdminUser();
+
+ // Cleanup before doing anything on it as the index it is out of this test control.
+ $this->search->delete_index();
+
+ // Add moodle fields if they don't exist.
+ $schema = new \search_solr\schema();
+ $schema->setup(false);
+ }
+
+ public function test_connection() {
+ $this->assertTrue($this->search->get_engine()->is_server_ready());
+ }
+
+ public function test_index() {
+ global $DB;
+
+ $noneditingteacherid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
+
+ // 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());
+
+ assign_capability('moodle/course:renameroles', CAP_ALLOW, $noneditingteacherid, context_system::instance()->id);
+ accesslib_clear_all_caches_for_unit_testing();
+
+ // Indexing again once there is new data.
+ $this->assertTrue($this->search->index());
+ }
+
+ /**
+ * Better keep this not very strict about which or how many results are returned as may depend on solr engine config.
+ *
+ * @return void
+ */
+ public function test_search() {
+ global $USER, $DB;
+
+ $noneditingteacherid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
+
+ $this->search->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_system::instance()->id, $results[0]->get('contextid'));
+
+ // Testing filters we don't purge cache in between assertions because cache key depends on the whole filters set
+ // and they are different.
+ sleep(1);
+ $beforeadding = time();
+ sleep(1);
+ assign_capability('moodle/course:renameroles', CAP_ALLOW, $noneditingteacherid, context_system::instance()->id);
+ accesslib_clear_all_caches_for_unit_testing();
+ $this->search->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 = 'moodle/course:renameroles roleid 1';
+ $this->assertCount(1, $this->search->search($querydata));
+ }
+
+ public function test_delete() {
+ $this->search->index();
+
+ $querydata = new stdClass();
+ $querydata->q = 'message';
+
+ $this->assertCount(2, $this->search->search($querydata));
+
+ $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'role_capabilities');
+ $this->search->delete_index($areaid);
+ cache_helper::purge_by_definition('core', 'search_results');
+ $this->assertCount(0, $this->search->search($querydata));
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 = 2016012000;
+$plugin->requires = 2015111000;
+$plugin->component = 'search_solr';