MDL-68334 user: Display name in footer as elsewhere.
[moodle.git] / search / engine / solr / classes / schema.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Solr schema manipulation manager.
19  *
20  * @package   search_solr
21  * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace search_solr;
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/lib/filelib.php');
31 /**
32  * Schema class to interact with Solr schema.
33  *
34  * At the moment it only implements create which should be enough for a basic
35  * moodle configuration in Solr.
36  *
37  * @package   search_solr
38  * @copyright 2015 David Monllao {@link http://www.davidmonllao.com}
39  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class schema {
43     /**
44      * @var stdClass
45      */
46     protected $config = null;
48     /**
49      * cUrl instance.
50      * @var \curl
51      */
52     protected $curl = null;
54     /**
55      * An engine instance.
56      * @var engine
57      */
58     protected $engine = null;
60     /**
61      * Constructor.
62      *
63      * @throws \moodle_exception
64      * @return void
65      */
66     public function __construct() {
67         if (!$this->config = get_config('search_solr')) {
68             throw new \moodle_exception('missingconfig', 'search_solr');
69         }
71         if (empty($this->config->server_hostname) || empty($this->config->indexname)) {
72             throw new \moodle_exception('missingconfig', 'search_solr');
73         }
75         $this->engine = new engine();
76         $this->curl = $this->engine->get_curl_object();
78         // HTTP headers.
79         $this->curl->setHeader('Content-type: application/json');
80     }
82     /**
83      * Can setup be executed against the configured server.
84      *
85      * @return true|string True or error message.
86      */
87     public function can_setup_server() {
89         $status = $this->engine->is_server_configured();
90         if ($status !== true) {
91             return $status;
92         }
94         // At this stage we know that the server is properly configured with a valid host:port and indexname.
95         // We're not too concerned about repeating the SolrClient::system() call (already called in
96         // is_server_configured) because this is just a setup script.
97         if ($this->engine->get_solr_major_version() < 5) {
98             // Schema setup script only available for 5.0 onwards.
99             return get_string('schemasetupfromsolr5', 'search_solr');
100         }
102         return true;
103     }
105     /**
106      * Setup solr stuff required by moodle.
107      *
108      * @param  bool $checkexisting Whether to check if the fields already exist or not
109      * @return bool
110      */
111     public function setup($checkexisting = true) {
112         $fields = \search_solr\document::get_default_fields_definition();
114         // Field id is already there.
115         unset($fields['id']);
117         $this->check_index();
119         $return = $this->add_fields($fields, $checkexisting);
121         // Tell the engine we are now using the latest schema version.
122         $this->engine->record_applied_schema_version(document::SCHEMA_VERSION);
124         return $return;
125     }
127     /**
128      * Checks the schema is properly set up.
129      *
130      * @throws \moodle_exception
131      * @return void
132      */
133     public function validate_setup() {
134         $fields = \search_solr\document::get_default_fields_definition();
136         // Field id is already there.
137         unset($fields['id']);
139         $this->check_index();
140         $this->validate_fields($fields, true);
141     }
143     /**
144      * Checks if the index is ready, triggers an exception otherwise.
145      *
146      * @throws \moodle_exception
147      * @return void
148      */
149     protected function check_index() {
151         // Check that the server is available and the index exists.
152         $url = $this->engine->get_connection_url('/select?wt=json');
153         $result = $this->curl->get($url);
154         if ($this->curl->error) {
155             throw new \moodle_exception('connectionerror', 'search_solr');
156         }
157         if ($this->curl->info['http_code'] === 404) {
158             throw new \moodle_exception('connectionerror', 'search_solr');
159         }
160     }
162     /**
163      * Adds the provided fields to Solr schema.
164      *
165      * Intentionally separated from create(), it can be called to add extra fields.
166      * fields separately.
167      *
168      * @throws \coding_exception
169      * @throws \moodle_exception
170      * @param  array $fields \core_search\document::$requiredfields format
171      * @param  bool $checkexisting Whether to check if the fields already exist or not
172      * @return bool
173      */
174     protected function add_fields($fields, $checkexisting = true) {
176         if ($checkexisting) {
177             // Check that non of them exists.
178             $this->validate_fields($fields, false);
179         }
181         $url = $this->engine->get_connection_url('/schema');
183         // Add all fields.
184         foreach ($fields as $fieldname => $data) {
186             if (!isset($data['type']) || !isset($data['stored']) || !isset($data['indexed'])) {
187                 throw new \coding_exception($fieldname . ' does not define all required field params: type, stored and indexed.');
188             }
189             $type = $this->doc_field_to_solr_field($data['type']);
191             // Changing default multiValued value to false as we want to match values easily.
192             $params = array(
193                 'add-field' => array(
194                     'name' => $fieldname,
195                     'type' => $type,
196                     'stored' => $data['stored'],
197                     'multiValued' => false,
198                     'indexed' => $data['indexed']
199                 )
200             );
201             $results = $this->curl->post($url, json_encode($params));
203             // We only validate if we are interested on it.
204             if ($checkexisting) {
205                 if ($this->curl->error) {
206                     throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
207                 }
208                 $this->validate_add_field_result($results);
209             }
210         }
212         return true;
213     }
215     /**
216      * Checks if the schema existing fields are properly set, triggers an exception otherwise.
217      *
218      * @throws \moodle_exception
219      * @param array $fields
220      * @param bool $requireexisting Require the fields to exist, otherwise exception.
221      * @return void
222      */
223     protected function validate_fields(&$fields, $requireexisting = false) {
224         global $CFG;
226         foreach ($fields as $fieldname => $data) {
227             $url = $this->engine->get_connection_url('/schema/fields/' . $fieldname);
228             $results = $this->curl->get($url);
230             if ($this->curl->error) {
231                 throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $this->curl->error);
232             }
234             if (!$results) {
235                 throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
236             }
237             $results = json_decode($results);
239             if ($requireexisting && !empty($results->error) && $results->error->code === 404) {
240                 $a = new \stdClass();
241                 $a->fieldname = $fieldname;
242                 $a->setupurl = $CFG->wwwroot . '/search/engine/solr/setup_schema.php';
243                 throw new \moodle_exception('errorvalidatingschema', 'search_solr', '', $a);
244             }
246             // The field should not exist so we only accept 404 errors.
247             if (empty($results->error) || (!empty($results->error) && $results->error->code !== 404)) {
248                 if (!empty($results->error)) {
249                     throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error->msg);
250                 } else {
251                     // All these field attributes are set when fields are added through this script and should
252                     // be returned and match the defined field's values.
254                     $expectedsolrfield = $this->doc_field_to_solr_field($data['type']);
255                     if (empty($results->field) || !isset($results->field->type) ||
256                             !isset($results->field->multiValued) || !isset($results->field->indexed) ||
257                             !isset($results->field->stored)) {
259                         throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
260                             get_string('schemafieldautocreated', 'search_solr', $fieldname));
262                     } else if ($results->field->type !== $expectedsolrfield ||
263                             $results->field->multiValued !== false ||
264                             $results->field->indexed !== $data['indexed'] ||
265                             $results->field->stored !== $data['stored']) {
267                         throw new \moodle_exception('errorcreatingschema', 'search_solr', '',
268                             get_string('schemafieldautocreated', 'search_solr', $fieldname));
269                     } else {
270                         // The field already exists and it is properly defined, no need to create it.
271                         unset($fields[$fieldname]);
272                     }
273                 }
274             }
275         }
276     }
278     /**
279      * Checks that the field results do not contain errors.
280      *
281      * @throws \moodle_exception
282      * @param string $results curl response body
283      * @return void
284      */
285     protected function validate_add_field_result($result) {
287         if (!$result) {
288             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', get_string('nodatafromserver', 'search_solr'));
289         }
291         $results = json_decode($result);
292         if (!$results) {
293             if (is_scalar($result)) {
294                 $errormsg = $result;
295             } else {
296                 $errormsg = json_encode($result);
297             }
298             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errormsg);
299         }
301         // It comes as error when fetching fields data.
302         if (!empty($results->error)) {
303             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $results->error);
304         }
306         // It comes as errors when adding fields.
307         if (!empty($results->errors)) {
309             // We treat this error separately.
310             $errorstr = '';
311             foreach ($results->errors as $error) {
312                 $errorstr .= implode(', ', $error->errorMessages);
313             }
314             throw new \moodle_exception('errorcreatingschema', 'search_solr', '', $errorstr);
315         }
317     }
319     /**
320      * Returns the solr field type from the document field type string.
321      *
322      * @param string $datatype
323      * @return string
324      */
325     private function doc_field_to_solr_field($datatype) {
326         $type = $datatype;
328         $solrversion = $this->engine->get_solr_major_version();
330         switch($datatype) {
331             case 'text':
332                 $type = 'text_general';
333                 break;
334             case 'int':
335                 if ($solrversion >= 7) {
336                     $type = 'pint';
337                 }
338                 break;
339             case 'tdate':
340                 if ($solrversion >= 7) {
341                     $type = 'pdate';
342                 }
343                 break;
344         }
346         return $type;
347     }