See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 403]
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/>. 16 17 /** 18 * Simple moodle database engine. 19 * 20 * @package search_simpledb 21 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace search_simpledb; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * Simple moodle database engine. 31 * 32 * @package search_simpledb 33 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class engine extends \core_search\engine { 37 38 /** 39 * Total number of available results. 40 * 41 * @var null|int 42 */ 43 protected $totalresults = null; 44 45 /** 46 * Prepares a SQL query, applies filters and executes it returning its results. 47 * 48 * @throws \core_search\engine_exception 49 * @param stdClass $filters Containing query and filters. 50 * @param array $usercontexts Contexts where the user has access. True if the user can access all contexts. 51 * @param int $limit The maximum number of results to return. 52 * @return \core_search\document[] Results or false if no results 53 */ 54 public function execute_query($filters, $usercontexts, $limit = 0) { 55 global $DB, $USER; 56 57 $serverstatus = $this->is_server_ready(); 58 if ($serverstatus !== true) { 59 throw new \core_search\engine_exception('engineserverstatus', 'search'); 60 } 61 62 if (empty($limit)) { 63 $limit = \core_search\manager::MAX_RESULTS; 64 } 65 66 $params = array(); 67 68 // To store all conditions we will add to where. 69 $ands = array(); 70 71 // Get results only available for the current user. 72 $ands[] = '(owneruserid = ? OR owneruserid = ?)'; 73 $params = array_merge($params, array(\core_search\manager::NO_OWNER_ID, $USER->id)); 74 75 // Restrict it to the context where the user can access, we want this one cached. 76 // If the user can access all contexts $usercontexts value is just true, we don't need to filter 77 // in that case. 78 if ($usercontexts && is_array($usercontexts)) { 79 // Join all area contexts into a single array and implode. 80 $allcontexts = array(); 81 foreach ($usercontexts as $areaid => $areacontexts) { 82 if (!empty($filters->areaids) && !in_array($areaid, $filters->areaids)) { 83 // Skip unused areas. 84 continue; 85 } 86 foreach ($areacontexts as $contextid) { 87 // Ensure they are unique. 88 $allcontexts[$contextid] = $contextid; 89 } 90 } 91 if (empty($allcontexts)) { 92 // This means there are no valid contexts for them, so they get no results. 93 return array(); 94 } 95 96 list($contextsql, $contextparams) = $DB->get_in_or_equal($allcontexts); 97 $ands[] = 'contextid ' . $contextsql; 98 $params = array_merge($params, $contextparams); 99 } 100 101 // Course id filter. 102 if (!empty($filters->courseids)) { 103 list($conditionsql, $conditionparams) = $DB->get_in_or_equal($filters->courseids); 104 $ands[] = 'courseid ' . $conditionsql; 105 $params = array_merge($params, $conditionparams); 106 } 107 108 // Area id filter. 109 if (!empty($filters->areaids)) { 110 list($conditionsql, $conditionparams) = $DB->get_in_or_equal($filters->areaids); 111 $ands[] = 'areaid ' . $conditionsql; 112 $params = array_merge($params, $conditionparams); 113 } 114 115 if (!empty($filters->title)) { 116 $ands[] = $DB->sql_like('title', '?', false, false); 117 $params[] = $filters->title; 118 } 119 120 if (!empty($filters->timestart)) { 121 $ands[] = 'modified >= ?'; 122 $params[] = $filters->timestart; 123 } 124 if (!empty($filters->timeend)) { 125 $ands[] = 'modified <= ?'; 126 $params[] = $filters->timeend; 127 } 128 129 // And finally the main query after applying all AND filters. 130 if (!empty($filters->q)) { 131 switch ($DB->get_dbfamily()) { 132 case 'postgres': 133 $ands[] = "(" . 134 "to_tsvector('simple', title) @@ plainto_tsquery('simple', ?) OR ". 135 "to_tsvector('simple', content) @@ plainto_tsquery('simple', ?) OR ". 136 "to_tsvector('simple', description1) @@ plainto_tsquery('simple', ?) OR ". 137 "to_tsvector('simple', description2) @@ plainto_tsquery('simple', ?)". 138 ")"; 139 $params[] = $filters->q; 140 $params[] = $filters->q; 141 $params[] = $filters->q; 142 $params[] = $filters->q; 143 break; 144 case 'mysql': 145 if ($DB->is_fulltext_search_supported()) { 146 $ands[] = "MATCH (title, content, description1, description2) AGAINST (?)"; 147 $params[] = $filters->q; 148 149 // Sorry for the hack, but it does not seem that we will have a solution for 150 // this soon (https://bugs.mysql.com/bug.php?id=78485). 151 if ($filters->q === '*') { 152 return array(); 153 } 154 } else { 155 // Clumsy version for mysql versions with no fulltext support. 156 list($queryand, $queryparams) = $this->get_simple_query($filters->q); 157 $ands[] = $queryand; 158 $params = array_merge($params, $queryparams); 159 } 160 break; 161 case 'mssql': 162 if ($DB->is_fulltext_search_supported()) { 163 $ands[] = "CONTAINS ((title, content, description1, description2), ?)"; 164 // Special treatment for double quotes: 165 // - Puntuation is ignored so we can get rid of them. 166 // - Phrases should be enclosed in double quotation marks. 167 $params[] = '"' . str_replace('"', '', $filters->q) . '"'; 168 } else { 169 // Clumsy version for mysql versions with no fulltext support. 170 list($queryand, $queryparams) = $this->get_simple_query($filters->q); 171 $ands[] = $queryand; 172 $params = array_merge($params, $queryparams); 173 } 174 break; 175 default: 176 list($queryand, $queryparams) = $this->get_simple_query($filters->q); 177 $ands[] = $queryand; 178 $params = array_merge($params, $queryparams); 179 break; 180 } 181 } 182 183 // It is limited to $limit, no need to use recordsets. 184 $documents = $DB->get_records_select('search_simpledb_index', implode(' AND ', $ands), $params, 'docid', '*', 0, $limit); 185 186 // Hopefully database cached results as this applies the same filters than above. 187 $this->totalresults = $DB->count_records_select('search_simpledb_index', implode(' AND ', $ands), $params); 188 189 $numgranted = 0; 190 191 // Iterate through the results checking its availability and whether they are available for the user or not. 192 $docs = array(); 193 foreach ($documents as $docdata) { 194 if ($docdata->owneruserid != \core_search\manager::NO_OWNER_ID && $docdata->owneruserid != $USER->id) { 195 // If owneruserid is set, no other user should be able to access this record. 196 continue; 197 } 198 199 if (!$searcharea = $this->get_search_area($docdata->areaid)) { 200 $this->totalresults--; 201 continue; 202 } 203 204 // Switch id back to the document id. 205 $docdata->id = $docdata->docid; 206 unset($docdata->docid); 207 208 $access = $searcharea->check_access($docdata->itemid); 209 switch ($access) { 210 case \core_search\manager::ACCESS_DELETED: 211 $this->delete_by_id($docdata->id); 212 $this->totalresults--; 213 break; 214 case \core_search\manager::ACCESS_DENIED: 215 $this->totalresults--; 216 break; 217 case \core_search\manager::ACCESS_GRANTED: 218 $numgranted++; 219 $docs[] = $this->to_document($searcharea, (array)$docdata); 220 break; 221 } 222 223 // This should never happen. 224 if ($numgranted >= $limit) { 225 $docs = array_slice($docs, 0, $limit, true); 226 break; 227 } 228 } 229 230 return $docs; 231 } 232 233 /** 234 * Adds a document to the search engine. 235 * 236 * This does not commit to the search engine. 237 * 238 * @param \core_search\document $document 239 * @param bool $fileindexing True if file indexing is to be used 240 * @return bool False if the file was skipped or failed, true on success 241 */ 242 public function add_document($document, $fileindexing = false) { 243 global $DB; 244 245 $doc = (object)$document->export_for_engine(); 246 247 // Moodle's ids using DML are always autoincremented. 248 $doc->docid = $doc->id; 249 unset($doc->id); 250 251 $id = $DB->get_field('search_simpledb_index', 'id', array('docid' => $doc->docid)); 252 try { 253 if ($id) { 254 $doc->id = $id; 255 $DB->update_record('search_simpledb_index', $doc); 256 } else { 257 $DB->insert_record('search_simpledb_index', $doc); 258 } 259 260 } catch (\dml_exception $ex) { 261 debugging('dml error while trying to insert document with id ' . $doc->docid . ': ' . $ex->getMessage(), 262 DEBUG_DEVELOPER); 263 return false; 264 } 265 266 return true; 267 } 268 269 /** 270 * Deletes the specified document. 271 * 272 * @param string $id The document id to delete 273 * @return void 274 */ 275 public function delete_by_id($id) { 276 global $DB; 277 $DB->delete_records('search_simpledb_index', array('docid' => $id)); 278 } 279 280 /** 281 * Delete all area's documents. 282 * 283 * @param string $areaid 284 * @return void 285 */ 286 public function delete($areaid = null) { 287 global $DB; 288 if ($areaid) { 289 $DB->delete_records('search_simpledb_index', array('areaid' => $areaid)); 290 } else { 291 $DB->delete_records('search_simpledb_index'); 292 } 293 } 294 295 /** 296 * Checks that the required table was installed. 297 * 298 * @return true|string Returns true if all good or an error string. 299 */ 300 public function is_server_ready() { 301 global $DB; 302 if (!$DB->get_manager()->table_exists('search_simpledb_index')) { 303 return 'search_simpledb_index table does not exist'; 304 } 305 306 return true; 307 } 308 309 /** 310 * It is always installed. 311 * 312 * @return true 313 */ 314 public function is_installed() { 315 return true; 316 } 317 318 /** 319 * Returns the total results. 320 * 321 * Including skipped results. 322 * 323 * @return int 324 */ 325 public function get_query_total_count() { 326 if (!is_null($this->totalresults)) { 327 // This is a just in case as we count total results in execute_query. 328 return \core_search\manager::MAX_RESULTS; 329 } 330 331 return $this->totalresults; 332 } 333 334 /** 335 * Returns the default query for db engines. 336 * 337 * @param string $q The query string 338 * @return array SQL string and params list 339 */ 340 protected function get_simple_query($q) { 341 global $DB; 342 343 $sql = '(' . 344 $DB->sql_like('title', '?', false, false) . ' OR ' . 345 $DB->sql_like('content', '?', false, false) . ' OR ' . 346 $DB->sql_like('description1', '?', false, false) . ' OR ' . 347 $DB->sql_like('description2', '?', false, false) . 348 ')'; 349 350 // Remove quotes from the query. 351 $q = str_replace('"', '', $q); 352 $params = [ 353 '%' . $q . '%', 354 '%' . $q . '%', 355 '%' . $q . '%', 356 '%' . $q . '%' 357 ]; 358 359 return array($sql, $params); 360 } 361 362 /** 363 * Simpledb supports deleting the index for a context. 364 * 365 * @param int $oldcontextid Context that has been deleted 366 * @return bool True to indicate that any data was actually deleted 367 * @throws \core_search\engine_exception 368 */ 369 public function delete_index_for_context(int $oldcontextid) { 370 global $DB; 371 try { 372 $DB->delete_records('search_simpledb_index', ['contextid' => $oldcontextid]); 373 } catch (\dml_exception $e) { 374 throw new \core_search\engine_exception('dbupdatefailed'); 375 } 376 return true; 377 } 378 379 /** 380 * Simpledb supports deleting the index for a course. 381 * 382 * @param int $oldcourseid 383 * @return bool True to indicate that any data was actually deleted 384 * @throws \core_search\engine_exception 385 */ 386 public function delete_index_for_course(int $oldcourseid) { 387 global $DB; 388 try { 389 $DB->delete_records('search_simpledb_index', ['courseid' => $oldcourseid]); 390 } catch (\dml_exception $e) { 391 throw new \core_search\engine_exception('dbupdatefailed'); 392 } 393 return true; 394 } 395 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body