MDL-60913 search: add search area categories
[moodle.git] / search / classes / base_mod.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  * Search area base class for areas working at module level.
19  *
20  * @package    core_search
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 core_search;
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * Base implementation for search areas working at module level.
31  *
32  * Even if the search area works at multiple levels, if module is one of these levels
33  * it should extend this class, as this class provides helper methods for module level search management.
34  *
35  * @package    core_search
36  * @copyright  2015 David Monllao {@link http://www.davidmonllao.com}
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 abstract class base_mod extends base {
41     /**
42      * The context levels the search area is working on.
43      *
44      * This can be overwriten by the search area if it works at multiple
45      * levels.
46      *
47      * @var array
48      */
49     protected static $levels = [CONTEXT_MODULE];
51     /**
52      * Returns the module name.
53      *
54      * @return string
55      */
56     protected function get_module_name() {
57         return substr($this->componentname, 4);
58     }
60     /**
61      * Gets the course module for the required instanceid + modulename.
62      *
63      * The returned data depends on the logged user, when calling this through
64      * self::get_document the admin user is used so everything would be returned.
65      *
66      * No need more internal caching here, modinfo is already cached.
67      *
68      * @throws \dml_missing_record_exception
69      * @param string $modulename The module name
70      * @param int $instanceid Module instance id (depends on the module)
71      * @param int $courseid Helps speeding up things
72      * @return \cm_info
73      */
74     protected function get_cm($modulename, $instanceid, $courseid) {
75         $modinfo = get_fast_modinfo($courseid);
77         // Hopefully not many, they are indexed by cmid.
78         $instances = $modinfo->get_instances_of($modulename);
79         foreach ($instances as $cminfo) {
80             if ($cminfo->instance == $instanceid) {
81                 return $cminfo;
82             }
83         }
85         // Nothing found.
86         throw new \dml_missing_record_exception($modulename);
87     }
89     /**
90      * Helper function that gets SQL useful for restricting a search query given a passed-in
91      * context.
92      *
93      * The SQL returned will be zero or more JOIN statements, surrounded by whitespace, which act
94      * as restrictions on the query based on the rows in a module table.
95      *
96      * You can pass in a null or system context, which will both return an empty string and no
97      * params.
98      *
99      * Returns an array with two nulls if there can be no results for the activity within this
100      * context (e.g. it is a block context).
101      *
102      * If named parameters are used, these will be named gcrs0, gcrs1, etc. The table aliases used
103      * in SQL also all begin with gcrs, to avoid conflicts.
104      *
105      * @param \context|null $context Context to restrict the query
106      * @param string $modname Name of module e.g. 'forum'
107      * @param string $modtable Alias of table containing module id
108      * @param int $paramtype Type of SQL parameters to use (default question mark)
109      * @return array Array with SQL and parameters; both null if no need to query
110      * @throws \coding_exception If called with invalid params
111      */
112     protected function get_context_restriction_sql(\context $context = null, $modname, $modtable,
113             $paramtype = SQL_PARAMS_QM) {
114         global $DB;
116         if (!$context) {
117             return ['', []];
118         }
120         switch ($paramtype) {
121             case SQL_PARAMS_QM:
122                 $param1 = '?';
123                 $param2 = '?';
124                 $param3 = '?';
125                 $key1 = 0;
126                 $key2 = 1;
127                 $key3 = 2;
128                 break;
129             case SQL_PARAMS_NAMED:
130                 $param1 = ':gcrs0';
131                 $param2 = ':gcrs1';
132                 $param3 = ':gcrs2';
133                 $key1 = 'gcrs0';
134                 $key2 = 'gcrs1';
135                 $key3 = 'gcrs2';
136                 break;
137             default:
138                 throw new \coding_exception('Unexpected $paramtype: ' . $paramtype);
139         }
141         $params = [];
142         switch ($context->contextlevel) {
143             case CONTEXT_SYSTEM:
144                 $sql = '';
145                 break;
147             case CONTEXT_COURSECAT:
148                 // Find all activities of this type within the specified category or any
149                 // sub-category.
150                 $pathmatch = $DB->sql_like('gcrscc2.path', $DB->sql_concat('gcrscc1.path', $param3));
151                 $sql = " JOIN {course_modules} gcrscm ON gcrscm.instance = $modtable.id
152                               AND gcrscm.module = (SELECT id FROM {modules} WHERE name = $param1)
153                          JOIN {course} gcrsc ON gcrsc.id = gcrscm.course
154                          JOIN {course_categories} gcrscc1 ON gcrscc1.id = $param2
155                          JOIN {course_categories} gcrscc2 ON gcrscc2.id = gcrsc.category AND
156                               (gcrscc2.id = gcrscc1.id OR $pathmatch) ";
157                 $params[$key1] = $modname;
158                 $params[$key2] = $context->instanceid;
159                 // Note: This param is a bit annoying as it obviously never changes, but sql_like
160                 // throws a debug warning if you pass it anything with quotes in, so it has to be
161                 // a bound parameter.
162                 $params[$key3] = '/%';
163                 break;
165             case CONTEXT_COURSE:
166                 // Find all activities of this type within the course.
167                 $sql = " JOIN {course_modules} gcrscm ON gcrscm.instance = $modtable.id
168                               AND gcrscm.course = $param1
169                               AND gcrscm.module = (SELECT id FROM {modules} WHERE name = $param2) ";
170                 $params[$key1] = $context->instanceid;
171                 $params[$key2] = $modname;
172                 break;
174             case CONTEXT_MODULE:
175                 // Find only the specified activity of this type.
176                 $sql = " JOIN {course_modules} gcrscm ON gcrscm.instance = $modtable.id
177                               AND gcrscm.id = $param1
178                               AND gcrscm.module = (SELECT id FROM {modules} WHERE name = $param2) ";
179                 $params[$key1] = $context->instanceid;
180                 $params[$key2] = $modname;
181                 break;
183             case CONTEXT_BLOCK:
184             case CONTEXT_USER:
185                 // These contexts cannot contain any activities, so return null.
186                 return [null, null];
188             default:
189                 throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel);
190         }
192         return [$sql, $params];
193     }
195     /**
196      * This can be used in subclasses to change ordering within the get_contexts_to_reindex
197      * function.
198      *
199      * It returns 2 values:
200      * - Extra SQL joins (tables course_modules 'cm' and context 'x' already exist).
201      * - An ORDER BY value which must use aggregate functions, by default 'MAX(cm.added) DESC'.
202      *
203      * Note the query already includes a GROUP BY on the context fields, so if your joins result
204      * in multiple rows, you can use aggregate functions in the ORDER BY. See forum for an example.
205      *
206      * @return string[] Array with 2 elements; extra joins for the query, and ORDER BY value
207      */
208     protected function get_contexts_to_reindex_extra_sql() {
209         return ['', 'MAX(cm.added) DESC'];
210     }
212     /**
213      * Gets a list of all contexts to reindex when reindexing this search area.
214      *
215      * For modules, the default is to return all contexts for modules of that type, in order of
216      * time added (most recent first).
217      *
218      * @return \Iterator Iterator of contexts to reindex
219      * @throws \moodle_exception If any DB error
220      */
221     public function get_contexts_to_reindex() {
222         global $DB;
224         list ($extrajoins, $dborder) = $this->get_contexts_to_reindex_extra_sql();
225         $contexts = [];
226         $selectcolumns = \context_helper::get_preload_record_columns_sql('x');
227         $groupbycolumns = '';
228         foreach (\context_helper::get_preload_record_columns('x') as $column => $thing) {
229             if ($groupbycolumns !== '') {
230                 $groupbycolumns .= ',';
231             }
232             $groupbycolumns .= $column;
233         }
234         $rs = $DB->get_recordset_sql("
235                 SELECT $selectcolumns
236                   FROM {course_modules} cm
237                   JOIN {context} x ON x.instanceid = cm.id AND x.contextlevel = ?
238                        $extrajoins
239                  WHERE cm.module = (SELECT id FROM {modules} WHERE name = ?)
240               GROUP BY $groupbycolumns
241               ORDER BY $dborder", [CONTEXT_MODULE, $this->get_module_name()]);
242         return new \core\dml\recordset_walk($rs, function($rec) {
243             $id = $rec->ctxid;
244             \context_helper::preload_from_record($rec);
245             return \context::instance_by_id($id);
246         });
247     }
249     /**
250      * Indicates whether this search area may restrict access by group.
251      *
252      * This should return true if the search area (sometimes) sets the 'groupid' schema field, and
253      * false if it never sets that field.
254      *
255      * (If this function returns false, but the field is set, then results may be restricted
256      * unintentionally.)
257      *
258      * If this returns true, the search engine will automatically apply group restrictions in some
259      * cases (by default, where a module is configured to use separate groups). See function
260      * restrict_cm_access_by_group().
261      *
262      * @return bool
263      */
264     public function supports_group_restriction() {
265         return false;
266     }
268     /**
269      * Checks whether the content of this search area should be restricted by group for a
270      * specific module. Called at query time.
271      *
272      * The default behaviour simply checks if the effective group mode is SEPARATEGROUPS, which
273      * is probably correct for most cases.
274      *
275      * If restricted by group, the search query will (where supported by the engine) filter out
276      * results for groups the user does not belong to, unless the user has 'access all groups'
277      * for the activity. This affects only documents which set the 'groupid' field; results with no
278      * groupid will not be restricted.
279      *
280      * Even if you return true to this function, you may still need to do group access checks in
281      * check_access, because the search engine may not support group restrictions.
282      *
283      * @param \cm_info $cm
284      * @return bool True to restrict by group
285      */
286     public function restrict_cm_access_by_group(\cm_info $cm) {
287         return $cm->effectivegroupmode == SEPARATEGROUPS;
288     }
290     /**
291      * Returns an icon instance for the document.
292      *
293      * @param \core_search\document $doc
294      * @return \core_search\document_icon
295      */
296     public function get_doc_icon(document $doc) : document_icon {
297         return new document_icon('icon', $this->get_module_name());
298     }
300     /**
301      * Returns a list of category names associated with the area.
302      *
303      * @return array
304      */
305     public function get_category_names() {
306         return [manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
307     }