Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 * Search area base class for blocks.
19
 *
20
 * Note: Only blocks within courses are supported.
21
 *
22
 * @package core_search
23
 * @copyright 2017 The Open University
24
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
namespace core_search;
28
 
29
defined('MOODLE_INTERNAL') || die();
30
 
31
/**
32
 * Search area base class for blocks.
33
 *
34
 * Note: Only blocks within courses are supported.
35
 *
36
 * @package core_search
37
 * @copyright 2017 The Open University
38
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
 */
40
abstract class base_block extends base {
41
    /** @var string Cache name used for block instances */
42
    const CACHE_INSTANCES = 'base_block_instances';
43
 
44
    /**
45
     * The context levels the search area is working on.
46
     *
47
     * This can be overwriten by the search area if it works at multiple
48
     * levels.
49
     *
50
     * @var array
51
     */
52
    protected static $levels = [CONTEXT_BLOCK];
53
 
54
    /**
55
     * Gets the block name only.
56
     *
57
     * @return string Block name e.g. 'html'
58
     */
59
    public function get_block_name() {
60
        // Remove 'block_' text.
61
        return substr($this->get_component_name(), 6);
62
    }
63
 
64
    /**
65
     * Returns restrictions on which block_instances rows to return. By default, excludes rows
66
     * that have empty configdata.
67
     *
68
     * If no restriction is required, you could return ['', []].
69
     *
70
     * @return array 2-element array of SQL restriction and params for it
71
     */
72
    protected function get_indexing_restrictions() {
73
        global $DB;
74
 
75
        // This includes completely empty configdata, and also three other values that are
76
        // equivalent to empty:
77
        // - A serialized completely empty object.
78
        // - A serialized object with one field called '0' (string not int) set to boolean false
79
        //   (this can happen after backup and restore, at least historically).
80
        // - A serialized null.
81
        $stupidobject = (object)[];
82
        $zero = '0';
83
        $stupidobject->{$zero} = false;
84
        return [$DB->sql_compare_text('bi.configdata') . " != ? AND " .
85
                $DB->sql_compare_text('bi.configdata') . " != ? AND " .
86
                $DB->sql_compare_text('bi.configdata') . " != ? AND " .
87
                $DB->sql_compare_text('bi.configdata') . " != ?",
88
                ['', base64_encode(serialize((object)[])), base64_encode(serialize($stupidobject)),
89
                base64_encode(serialize(null))]];
90
    }
91
 
92
    /**
93
     * Gets recordset of all blocks of this type modified since given time within the given context.
94
     *
95
     * See base class for detailed requirements. This implementation includes the key fields
96
     * from block_instances.
97
     *
98
     * This can be overridden to do something totally different if the block's data is stored in
99
     * other tables.
100
     *
101
     * If there are certain instances of the block which should not be included in the search index
102
     * then you can override get_indexing_restrictions; by default this excludes rows with empty
103
     * configdata.
104
     *
105
     * @param int $modifiedfrom Return only records modified after this date
106
     * @param \context|null $context Context to find blocks within
107
     * @return false|\moodle_recordset|null
108
     */
109
    public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
110
        global $DB;
111
 
112
        // Get context restrictions.
113
        list ($contextjoin, $contextparams) = $this->get_context_restriction_sql($context, 'bi');
114
 
115
        // Get custom restrictions for block type.
116
        list ($restrictions, $restrictionparams) = $this->get_indexing_restrictions();
117
        if ($restrictions) {
118
            $restrictions = 'AND ' . $restrictions;
119
        }
120
 
121
        // Query for all entries in block_instances for this type of block, within the specified
122
        // context. The query is based on the one from get_recordset_by_timestamp and applies the
123
        // same restrictions.
124
        return $DB->get_recordset_sql("
125
                SELECT bi.id, bi.timemodified, bi.timecreated, bi.configdata,
126
                       c.id AS courseid, x.id AS contextid
127
                  FROM {block_instances} bi
128
                       $contextjoin
129
                  JOIN {context} x ON x.instanceid = bi.id AND x.contextlevel = ?
130
                  JOIN {context} parent ON parent.id = bi.parentcontextid
131
             LEFT JOIN {course_modules} cm ON cm.id = parent.instanceid AND parent.contextlevel = ?
132
                  JOIN {course} c ON c.id = cm.course
133
                       OR (c.id = parent.instanceid AND parent.contextlevel = ?)
134
                 WHERE bi.timemodified >= ?
135
                       AND bi.blockname = ?
136
                       AND (parent.contextlevel = ? AND (" . $DB->sql_like('bi.pagetypepattern', '?') . "
137
                           OR bi.pagetypepattern IN ('site-index', 'course-*', '*')))
138
                       $restrictions
139
              ORDER BY bi.timemodified ASC",
140
                array_merge($contextparams, [CONTEXT_BLOCK, CONTEXT_MODULE, CONTEXT_COURSE,
141
                    $modifiedfrom, $this->get_block_name(), CONTEXT_COURSE, 'course-view-%'],
142
                $restrictionparams));
143
    }
144
 
145
    public function get_doc_url(\core_search\document $doc) {
146
        // Load block instance and find cmid if there is one.
147
        $blockinstanceid = preg_replace('~^.*-~', '', $doc->get('id'));
148
        $instance = $this->get_block_instance($blockinstanceid);
149
        $courseid = $doc->get('courseid');
150
        $anchor = 'inst' . $blockinstanceid;
151
 
152
        // Check if the block is at course or module level.
153
        if ($instance->cmid) {
154
            // No module-level page types are supported at present so the search system won't return
155
            // them. But let's put some example code here to indicate how it could work.
156
            debugging('Unexpected module-level page type for block ' . $blockinstanceid . ': ' .
157
                    $instance->pagetypepattern, DEBUG_DEVELOPER);
158
            $modinfo = get_fast_modinfo($courseid);
159
            $cm = $modinfo->get_cm($instance->cmid);
160
            return new \moodle_url($cm->url, null, $anchor);
161
        } else {
162
            // The block is at course level. Let's check the page type, although in practice we
163
            // currently only support the course main page.
164
            if ($instance->pagetypepattern === '*' || $instance->pagetypepattern === 'course-*' ||
165
                    preg_match('~^course-view-(.*)$~', $instance->pagetypepattern)) {
166
                return new \moodle_url('/course/view.php', ['id' => $courseid], $anchor);
167
            } else if ($instance->pagetypepattern === 'site-index') {
168
                return new \moodle_url('/', ['redirect' => 0], $anchor);
169
            } else {
170
                debugging('Unexpected page type for block ' . $blockinstanceid . ': ' .
171
                        $instance->pagetypepattern, DEBUG_DEVELOPER);
172
                return new \moodle_url('/course/view.php', ['id' => $courseid], $anchor);
173
            }
174
        }
175
    }
176
 
177
    public function get_context_url(\core_search\document $doc) {
178
        return $this->get_doc_url($doc);
179
    }
180
 
181
    /**
182
     * Checks access for a document in this search area.
183
     *
184
     * If you override this function for a block, you should call this base class version first
185
     * as it will check that the block is still visible to users in a supported location.
186
     *
187
     * @param int $id Document id
188
     * @return int manager:ACCESS_xx constant
189
     */
190
    public function check_access($id) {
191
        $instance = $this->get_block_instance($id, IGNORE_MISSING);
192
        if (!$instance) {
193
            // This generally won't happen because if the block has been deleted then we won't have
194
            // included its context in the search area list, but just in case.
195
            return manager::ACCESS_DELETED;
196
        }
197
 
198
        // Check block has not been moved to an unsupported area since it was indexed. (At the
199
        // moment, only blocks within site and course context are supported, also only certain
200
        // page types.)
201
        if (!$instance->courseid ||
202
                !self::is_supported_page_type_at_course_context($instance->pagetypepattern)) {
203
            return manager::ACCESS_DELETED;
204
        }
205
 
206
        // Note we do not need to check if the block was hidden or if the user has access to the
207
        // context, because those checks are included in the list of search contexts user can access
208
        // that is calculated in manager.php every time they do a query.
209
        return manager::ACCESS_GRANTED;
210
    }
211
 
212
    /**
213
     * Checks if a page type is supported for blocks when at course (or also site) context. This
214
     * function should be consistent with the SQL in get_recordset_by_timestamp.
215
     *
216
     * @param string $pagetype Page type
217
     * @return bool True if supported
218
     */
219
    protected static function is_supported_page_type_at_course_context($pagetype) {
220
        if (in_array($pagetype, ['site-index', 'course-*', '*'])) {
221
            return true;
222
        }
223
        if (preg_match('~^course-view-~', $pagetype)) {
224
            return true;
225
        }
226
        return false;
227
    }
228
 
229
    /**
230
     * Gets a block instance with given id.
231
     *
232
     * Returns the fields id, pagetypepattern, subpagepattern from block_instances and also the
233
     * cmid (if parent context is an activity module).
234
     *
235
     * @param int $id ID of block instance
236
     * @param int $strictness MUST_EXIST or IGNORE_MISSING
237
     * @return false|mixed Block instance data (may be false if strictness is IGNORE_MISSING)
238
     */
239
    protected function get_block_instance($id, $strictness = MUST_EXIST) {
240
        global $DB;
241
 
242
        $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_search',
243
                self::CACHE_INSTANCES, [], ['simplekeys' => true]);
244
        $id = (int)$id;
245
        $instance = $cache->get($id);
246
        if (!$instance) {
247
            $instance = $DB->get_record_sql("
248
                    SELECT bi.id, bi.pagetypepattern, bi.subpagepattern,
249
                           c.id AS courseid, cm.id AS cmid
250
                      FROM {block_instances} bi
251
                      JOIN {context} parent ON parent.id = bi.parentcontextid
252
                 LEFT JOIN {course} c ON c.id = parent.instanceid AND parent.contextlevel = ?
253
                 LEFT JOIN {course_modules} cm ON cm.id = parent.instanceid AND parent.contextlevel = ?
254
                     WHERE bi.id = ?",
255
                    [CONTEXT_COURSE, CONTEXT_MODULE, $id], $strictness);
256
            $cache->set($id, $instance);
257
        }
258
        return $instance;
259
    }
260
 
261
    /**
262
     * Clears static cache. This function can be removed (with calls to it in the test script
263
     * replaced with cache_helper::purge_all) if MDL-59427 is fixed.
264
     */
265
    public static function clear_static() {
266
        \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_search',
267
                self::CACHE_INSTANCES, [], ['simplekeys' => true])->purge();
268
    }
269
 
270
    /**
271
     * Helper function that gets SQL useful for restricting a search query given a passed-in
272
     * context.
273
     *
274
     * The SQL returned will be one or more JOIN statements, surrounded by whitespace, which act
275
     * as restrictions on the query based on the rows in the block_instances table.
276
     *
277
     * We assume the block instances have already been restricted by blockname.
278
     *
279
     * Returns null if there can be no results for this block within this context.
280
     *
281
     * If named parameters are used, these will be named gcrs0, gcrs1, etc. The table aliases used
282
     * in SQL also all begin with gcrs, to avoid conflicts.
283
     *
284
     * @param \context|null $context Context to restrict the query
285
     * @param string $blocktable Alias of block_instances table
286
     * @param int $paramtype Type of SQL parameters to use (default question mark)
287
     * @return array Array with SQL and parameters
288
     * @throws \coding_exception If called with invalid params
289
     */
290
    protected function get_context_restriction_sql(\context $context = null, $blocktable = 'bi',
291
            $paramtype = SQL_PARAMS_QM) {
292
        global $DB;
293
 
294
        if (!$context) {
295
            return ['', []];
296
        }
297
 
298
        switch ($paramtype) {
299
            case SQL_PARAMS_QM:
300
                $param1 = '?';
301
                $param2 = '?';
302
                $key1 = 0;
303
                $key2 = 1;
304
                break;
305
            case SQL_PARAMS_NAMED:
306
                $param1 = ':gcrs0';
307
                $param2 = ':gcrs1';
308
                $key1 = 'gcrs0';
309
                $key2 = 'gcrs1';
310
                break;
311
            default:
312
                throw new \coding_exception('Unexpected $paramtype: ' . $paramtype);
313
        }
314
 
315
        $params = [];
316
        switch ($context->contextlevel) {
317
            case CONTEXT_SYSTEM:
318
                $sql = '';
319
                break;
320
 
321
            case CONTEXT_COURSECAT:
322
            case CONTEXT_COURSE:
323
            case CONTEXT_MODULE:
324
            case CONTEXT_USER:
325
                // Find all blocks whose parent is within the specified context.
326
                $sql = " JOIN {context} gcrsx ON gcrsx.id = $blocktable.parentcontextid
327
                              AND (gcrsx.id = $param1 OR " . $DB->sql_like('gcrsx.path', $param2) . ") ";
328
                $params[$key1] = $context->id;
329
                $params[$key2] = $context->path . '/%';
330
                break;
331
 
332
            case CONTEXT_BLOCK:
333
                // Find only the specified block of this type. Since we are generating JOINs
334
                // here, we do this by joining again to the block_instances table with the same ID.
335
                $sql = " JOIN {block_instances} gcrsbi ON gcrsbi.id = $blocktable.id
336
                              AND gcrsbi.id = $param1 ";
337
                $params[$key1] = $context->instanceid;
338
                break;
339
 
340
            default:
341
                throw new \coding_exception('Unexpected contextlevel: ' . $context->contextlevel);
342
        }
343
 
344
        return [$sql, $params];
345
    }
346
 
347
    /**
348
     * This can be used in subclasses to change ordering within the get_contexts_to_reindex
349
     * function.
350
     *
351
     * It returns 2 values:
352
     * - Extra SQL joins (tables block_instances 'bi' and context 'x' already exist).
353
     * - An ORDER BY value which must use aggregate functions, by default 'MAX(bi.timemodified) DESC'.
354
     *
355
     * Note the query already includes a GROUP BY on the context fields, so if your joins result
356
     * in multiple rows, you can use aggregate functions in the ORDER BY. See forum for an example.
357
     *
358
     * @return string[] Array with 2 elements; extra joins for the query, and ORDER BY value
359
     */
360
    protected function get_contexts_to_reindex_extra_sql() {
361
        return ['', 'MAX(bi.timemodified) DESC'];
362
    }
363
 
364
    /**
365
     * Gets a list of all contexts to reindex when reindexing this search area.
366
     *
367
     * For blocks, the default is to return all contexts for blocks of that type, that are on a
368
     * course page, in order of time added (most recent first).
369
     *
370
     * @return \Iterator Iterator of contexts to reindex
371
     * @throws \moodle_exception If any DB error
372
     */
373
    public function get_contexts_to_reindex() {
374
        global $DB;
375
 
376
        list ($extrajoins, $dborder) = $this->get_contexts_to_reindex_extra_sql();
377
        $contexts = [];
378
        $selectcolumns = \context_helper::get_preload_record_columns_sql('x');
379
        $groupbycolumns = '';
380
        foreach (\context_helper::get_preload_record_columns('x') as $column => $thing) {
381
            if ($groupbycolumns !== '') {
382
                $groupbycolumns .= ',';
383
            }
384
            $groupbycolumns .= $column;
385
        }
386
        $rs = $DB->get_recordset_sql("
387
                SELECT $selectcolumns
388
                  FROM {block_instances} bi
389
                  JOIN {context} x ON x.instanceid = bi.id AND x.contextlevel = ?
390
                  JOIN {context} parent ON parent.id = bi.parentcontextid
391
                       $extrajoins
392
                 WHERE bi.blockname = ? AND parent.contextlevel = ?
393
              GROUP BY $groupbycolumns
394
              ORDER BY $dborder", [CONTEXT_BLOCK, $this->get_block_name(), CONTEXT_COURSE]);
395
        return new \core\dml\recordset_walk($rs, function($rec) {
396
            $id = $rec->ctxid;
397
            \context_helper::preload_from_record($rec);
398
            return \context::instance_by_id($id);
399
        });
400
    }
401
 
402
    /**
403
     * Returns an icon instance for the document.
404
     *
405
     * @param \core_search\document $doc
406
     * @return \core_search\document_icon
407
     */
408
    public function get_doc_icon(document $doc): document_icon {
409
        return new document_icon('e/anchor');
410
    }
411
 
412
    /**
413
     * Returns a list of category names associated with the area.
414
     *
415
     * @return array
416
     */
417
    public function get_category_names() {
418
        return [manager::SEARCH_AREA_CATEGORY_COURSE_CONTENT];
419
    }
420
}