PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/search/engine/simpledb/classes/engine.php

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