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
 * Base class for conditional availability information (for module or section).
19
 *
20
 * @package core_availability
21
 * @copyright 2014 The Open University
22
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_availability;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Base class for conditional availability information (for module or section).
31
 *
32
 * @package core_availability
33
 * @copyright 2014 The Open University
34
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
abstract class info {
37
    /** @var \stdClass Course */
38
    protected $course;
39
 
40
    /** @var \course_modinfo Modinfo (available only during some functions) */
41
    protected $modinfo = null;
42
 
43
    /** @var bool Visibility flag (eye icon) */
44
    protected $visible;
45
 
46
    /** @var string Availability data as JSON string */
47
    protected $availability;
48
 
49
    /** @var tree Availability configuration, decoded from JSON; null if unset */
50
    protected $availabilitytree;
51
 
52
    /** @var array The groups each user belongs to. */
53
    protected $groups = [];
54
 
55
    /** @var array|null Array of information about current restore if any */
56
    protected static $restoreinfo = null;
57
 
58
    /**
59
     * Constructs with item details.
60
     *
61
     * @param \stdClass $course Course object
62
     * @param int $visible Value of visible flag (eye icon)
63
     * @param string $availability Availability definition (JSON format) or null
64
     */
65
    public function __construct($course, $visible, $availability) {
66
        // Set basic values.
67
        $this->course = $course;
68
        $this->visible = (bool)$visible;
69
        $this->availability = $availability;
70
    }
71
 
72
    /**
73
     * Obtains the course associated with this availability information.
74
     *
75
     * @return \stdClass Moodle course object
76
     */
77
    public function get_course() {
78
        return $this->course;
79
    }
80
 
81
    /**
82
     * Gets context used for checking capabilities for this item.
83
     *
84
     * @return \context Context for this item
85
     */
86
    abstract public function get_context();
87
 
88
    /**
89
     * Obtains the modinfo associated with this availability information.
90
     *
91
     * Note: This field is available ONLY for use by conditions when calculating
92
     * availability or information.
93
     *
94
     * @return \course_modinfo Modinfo
95
     * @throws \coding_exception If called at incorrect times
96
     */
97
    public function get_modinfo() {
98
        if (!$this->modinfo) {
99
            throw new \coding_exception(
100
                    'info::get_modinfo available only during condition checking');
101
        }
102
        return $this->modinfo;
103
    }
104
 
105
    /**
106
     * Gets the availability tree, decoding it if not already done.
107
     *
108
     * @return tree Availability tree
109
     */
110
    public function get_availability_tree() {
111
        if (is_null($this->availabilitytree)) {
112
            if (is_null($this->availability)) {
113
                throw new \coding_exception(
114
                        'Cannot call get_availability_tree with null availability');
115
            }
116
            $this->availabilitytree = $this->decode_availability($this->availability, true);
117
        }
118
        return $this->availabilitytree;
119
    }
120
 
121
    /**
122
     * Decodes availability data from JSON format.
123
     *
124
     * This function also validates the retrieved data as follows:
125
     * 1. Data that does not meet the API-defined structure causes a
126
     *    coding_exception (this should be impossible unless there is
127
     *    a system bug or somebody manually hacks the database).
128
     * 2. Data that meets the structure but cannot be implemented (e.g.
129
     *    reference to missing plugin or to module that doesn't exist) is
130
     *    either silently discarded (if $lax is true) or causes a
131
     *    coding_exception (if $lax is false).
132
     *
133
     * @param string $availability Availability string in JSON format
134
     * @param boolean $lax If true, throw exceptions only for invalid structure
135
     * @return tree Availability tree
136
     * @throws \coding_exception If data is not valid JSON format
137
     */
138
    protected function decode_availability($availability, $lax) {
139
        // Decode JSON data.
140
        $structure = json_decode($availability);
141
        if (is_null($structure)) {
142
            throw new \coding_exception('Invalid availability text', $availability);
143
        }
144
 
145
        // Recursively decode tree.
146
        return new tree($structure, $lax);
147
    }
148
 
149
    /**
150
     * Determines whether this particular item is currently available
151
     * according to the availability criteria.
152
     *
153
     * - This does not include the 'visible' setting (i.e. this might return
154
     *   true even if visible is false); visible is handled independently.
155
     * - This does not take account of the viewhiddenactivities capability.
156
     *   That should apply later.
157
     *
158
     * Depending on options selected, a description of the restrictions which
159
     * mean the student can't view it (in HTML format) may be stored in
160
     * $information. If there is nothing in $information and this function
161
     * returns false, then the activity should not be displayed at all.
162
     *
163
     * This function displays debugging() messages if the availability
164
     * information is invalid.
165
     *
166
     * @param string $information String describing restrictions in HTML format
167
     * @param bool $grabthelot Performance hint: if true, caches information
168
     *   required for all course-modules, to make the front page and similar
169
     *   pages work more quickly (works only for current user)
170
     * @param int $userid If set, specifies a different user ID to check availability for
171
     * @param \course_modinfo $modinfo Usually leave as null for default. Specify when
172
     *   calling recursively from inside get_fast_modinfo()
173
     * @return bool True if this item is available to the user, false otherwise
174
     */
175
    public function is_available(&$information, $grabthelot = false, $userid = 0,
176
            \course_modinfo $modinfo = null) {
177
        global $USER;
178
 
179
        // Default to no information.
180
        $information = '';
181
 
182
        // Do nothing if there are no availability restrictions.
183
        if (is_null($this->availability)) {
184
            return true;
185
        }
186
 
187
        // Resolve optional parameters.
188
        if (!$userid) {
189
            $userid = $USER->id;
190
        }
191
        if (!$modinfo) {
192
            $modinfo = get_fast_modinfo($this->course, $userid);
193
        }
194
        $this->modinfo = $modinfo;
195
 
196
        // Get availability from tree.
197
        try {
198
            $tree = $this->get_availability_tree();
199
            $result = $tree->check_available(false, $this, $grabthelot, $userid);
200
        } catch (\coding_exception $e) {
201
            $this->warn_about_invalid_availability($e);
202
            $this->modinfo = null;
203
            return false;
204
        }
205
 
206
        // See if there are any messages.
207
        if ($result->is_available()) {
208
            $this->modinfo = null;
209
            return true;
210
        } else {
211
            // If the item is marked as 'not visible' then we don't change the available
212
            // flag (visible/available are treated distinctly), but we remove any
213
            // availability info. If the item is hidden with the eye icon, it doesn't
214
            // make sense to show 'Available from <date>' or similar, because even
215
            // when that date arrives it will still not be available unless somebody
216
            // toggles the eye icon.
217
            if ($this->visible) {
218
                $information = $tree->get_result_information($this, $result);
219
            }
220
 
221
            $this->modinfo = null;
222
            return false;
223
        }
224
    }
225
 
226
    /**
227
     * Checks whether this activity is going to be available for all users.
228
     *
229
     * Normally, if there are any conditions, then it may be hidden depending
230
     * on the user. However in the case of date conditions there are some
231
     * conditions which will definitely not result in it being hidden for
232
     * anyone.
233
     *
234
     * @return bool True if activity is available for all
235
     */
236
    public function is_available_for_all() {
237
        global $CFG;
238
        if (is_null($this->availability) || empty($CFG->enableavailability)) {
239
            return true;
240
        } else {
241
            try {
242
                return $this->get_availability_tree()->is_available_for_all();
243
            } catch (\coding_exception $e) {
244
                $this->warn_about_invalid_availability($e);
245
                return false;
246
            }
247
        }
248
    }
249
 
250
    /**
251
     * Obtains a string describing all availability restrictions (even if
252
     * they do not apply any more). Used to display information for staff
253
     * editing the website.
254
     *
255
     * The modinfo parameter must be specified when it is called from inside
256
     * get_fast_modinfo, to avoid infinite recursion.
257
     *
258
     * This function displays debugging() messages if the availability
259
     * information is invalid.
260
     *
261
     * @param \course_modinfo $modinfo Usually leave as null for default
262
     * @return string Information string (for admin) about all restrictions on
263
     *   this item
264
     */
265
    public function get_full_information(\course_modinfo $modinfo = null) {
266
        // Do nothing if there are no availability restrictions.
267
        if (is_null($this->availability)) {
268
            return '';
269
        }
270
 
271
        // Resolve optional parameter.
272
        if (!$modinfo) {
273
            $modinfo = get_fast_modinfo($this->course);
274
        }
275
        $this->modinfo = $modinfo;
276
 
277
        try {
278
            $result = $this->get_availability_tree()->get_full_information($this);
279
            $this->modinfo = null;
280
            return $result;
281
        } catch (\coding_exception $e) {
282
            $this->warn_about_invalid_availability($e);
283
            return false;
284
        }
285
    }
286
 
287
    /**
288
     * In some places we catch coding_exception because if a bug happens, it
289
     * would be fatal for the course page GUI; instead we just show a developer
290
     * debug message.
291
     *
292
     * @param \coding_exception $e Exception that occurred
293
     */
294
    protected function warn_about_invalid_availability(\coding_exception $e) {
295
        $name = $this->get_thing_name();
296
        $htmlname = $this->format_info($name, $this->course);
297
        // Because we call format_info here, likely in the middle of building dynamic data for the
298
        // activity, there could be a chance that the name might not be available.
299
        if ($htmlname === '') {
300
            // So instead use the numbers (cmid) from the tag.
301
            $htmlname = preg_replace('~[^0-9]~', '', $name);
302
        }
303
        $htmlname = html_to_text($htmlname, 75, false);
304
        $info = 'Error processing availability data for &lsquo;' . $htmlname
305
                 . '&rsquo;: ' . s($e->a);
306
        debugging($info, DEBUG_DEVELOPER);
307
    }
308
 
309
    /**
310
     * Called during restore (near end of restore). Updates any necessary ids
311
     * and writes the updated tree to the database. May output warnings if
312
     * necessary (e.g. if a course-module cannot be found after restore).
313
     *
314
     * @param string $restoreid Restore identifier
315
     * @param int $courseid Target course id
316
     * @param \base_logger $logger Logger for any warnings
317
     * @param int $dateoffset Date offset to be added to any dates (0 = none)
318
     * @param \base_task $task Restore task
319
     */
320
    public function update_after_restore($restoreid, $courseid, \base_logger $logger,
321
            $dateoffset, \base_task $task) {
322
        $tree = $this->get_availability_tree();
323
        // Set static data for use by get_restore_date_offset function.
324
        self::$restoreinfo = array('restoreid' => $restoreid, 'dateoffset' => $dateoffset,
325
                'task' => $task);
326
        $changed = $tree->update_after_restore($restoreid, $courseid, $logger,
327
                $this->get_thing_name());
328
        if ($changed) {
329
            // Save modified data.
330
            if ($tree->is_empty()) {
331
                // If the tree is empty, but the tree has changed, remove this condition.
332
                $this->set_in_database(null);
333
            } else {
334
                $structure = $tree->save();
335
                $this->set_in_database(json_encode($structure));
336
            }
337
        }
338
    }
339
 
340
    /**
341
     * Gets the date offset (amount by which any date values should be
342
     * adjusted) for the current restore.
343
     *
344
     * @param string $restoreid Restore identifier
345
     * @return int Date offset (0 if none)
346
     * @throws coding_exception If not in a restore (or not in that restore)
347
     */
348
    public static function get_restore_date_offset($restoreid) {
349
        if (!self::$restoreinfo) {
350
            throw new coding_exception('Only valid during restore');
351
        }
352
        if (self::$restoreinfo['restoreid'] !== $restoreid) {
353
            throw new coding_exception('Data not available for that restore id');
354
        }
355
        return self::$restoreinfo['dateoffset'];
356
    }
357
 
358
    /**
359
     * Gets the restore task (specifically, the task that calls the
360
     * update_after_restore method) for the current restore.
361
     *
362
     * @param string $restoreid Restore identifier
363
     * @return \base_task Restore task
364
     * @throws coding_exception If not in a restore (or not in that restore)
365
     */
366
    public static function get_restore_task($restoreid) {
367
        if (!self::$restoreinfo) {
368
            throw new coding_exception('Only valid during restore');
369
        }
370
        if (self::$restoreinfo['restoreid'] !== $restoreid) {
371
            throw new coding_exception('Data not available for that restore id');
372
        }
373
        return self::$restoreinfo['task'];
374
    }
375
 
376
    /**
377
     * Obtains the name of the item (cm_info or section_info, at present) that
378
     * this is controlling availability of. Name should be formatted ready
379
     * for on-screen display.
380
     *
381
     * @return string Name of item
382
     */
383
    abstract protected function get_thing_name();
384
 
385
    /**
386
     * Stores an updated availability tree JSON structure into the relevant
387
     * database table.
388
     *
389
     * @param string $availabilty New JSON value
390
     */
391
    abstract protected function set_in_database($availabilty);
392
 
393
    /**
394
     * In rare cases the system may want to change all references to one ID
395
     * (e.g. one course-module ID) to another one, within a course. This
396
     * function does that for the conditional availability data for all
397
     * modules and sections on the course.
398
     *
399
     * @param int|\stdClass $courseorid Course id or object
400
     * @param string $table Table name e.g. 'course_modules'
401
     * @param int $oldid Previous ID
402
     * @param int $newid New ID
403
     * @return bool True if anything changed, otherwise false
404
     */
405
    public static function update_dependency_id_across_course(
406
            $courseorid, $table, $oldid, $newid) {
407
        global $DB;
408
        $transaction = $DB->start_delegated_transaction();
409
        $modinfo = get_fast_modinfo($courseorid);
410
        $anychanged = false;
411
        foreach ($modinfo->get_cms() as $cm) {
412
            $info = new info_module($cm);
413
            $changed = $info->update_dependency_id($table, $oldid, $newid);
414
            $anychanged = $anychanged || $changed;
415
        }
416
        foreach ($modinfo->get_section_info_all() as $section) {
417
            $info = new info_section($section);
418
            $changed = $info->update_dependency_id($table, $oldid, $newid);
419
            $anychanged = $anychanged || $changed;
420
        }
421
        $transaction->allow_commit();
422
        if ($anychanged) {
423
            get_fast_modinfo($courseorid, 0, true);
424
        }
425
        return $anychanged;
426
    }
427
 
428
    /**
429
     * Called on a single item. If necessary, updates availability data where
430
     * it has a dependency on an item with a particular id.
431
     *
432
     * @param string $table Table name e.g. 'course_modules'
433
     * @param int $oldid Previous ID
434
     * @param int $newid New ID
435
     * @return bool True if it changed, otherwise false
436
     */
437
    protected function update_dependency_id($table, $oldid, $newid) {
438
        // Do nothing if there are no availability restrictions.
439
        if (is_null($this->availability)) {
440
            return false;
441
        }
442
        // Pass requirement on to tree object.
443
        $tree = $this->get_availability_tree();
444
        $changed = $tree->update_dependency_id($table, $oldid, $newid);
445
        if ($changed) {
446
            // Save modified data.
447
            $structure = $tree->save();
448
            $this->set_in_database(json_encode($structure));
449
        }
450
        return $changed;
451
    }
452
 
453
    /**
454
     * Converts legacy data from fields (if provided) into the new availability
455
     * syntax.
456
     *
457
     * Supported fields: availablefrom, availableuntil, showavailability
458
     * (and groupingid for sections).
459
     *
460
     * It also supports the groupmembersonly field for modules. This part was
461
     * optional in 2.7 but now always runs (because groupmembersonly has been
462
     * removed).
463
     *
464
     * @param \stdClass $rec Object possibly containing legacy fields
465
     * @param bool $section True if this is a section
466
     * @param bool $modgroupmembersonlyignored Ignored option, previously used
467
     * @return string|null New availability value or null if none
468
     */
469
    public static function convert_legacy_fields($rec, $section, $modgroupmembersonlyignored = false) {
470
        // Do nothing if the fields are not set.
471
        if (empty($rec->availablefrom) && empty($rec->availableuntil) &&
472
                (empty($rec->groupmembersonly)) &&
473
                (!$section || empty($rec->groupingid))) {
474
            return null;
475
        }
476
 
477
        // Handle legacy availability data.
478
        $conditions = array();
479
        $shows = array();
480
 
481
        // Groupmembersonly condition (if enabled) for modules, groupingid for
482
        // sections.
483
        if (!empty($rec->groupmembersonly) ||
484
                (!empty($rec->groupingid) && $section)) {
485
            if (!empty($rec->groupingid)) {
486
                $conditions[] = '{"type":"grouping"' .
487
                        ($rec->groupingid ? ',"id":' . $rec->groupingid : '') . '}';
488
            } else {
489
                // No grouping specified, so allow any group.
490
                $conditions[] = '{"type":"group"}';
491
            }
492
            // Group members only condition was not displayed to students.
493
            $shows[] = 'false';
494
        }
495
 
496
        // Date conditions.
497
        if (!empty($rec->availablefrom)) {
498
            $conditions[] = '{"type":"date","d":">=","t":' . $rec->availablefrom . '}';
499
            $shows[] = !empty($rec->showavailability) ? 'true' : 'false';
500
        }
501
        if (!empty($rec->availableuntil)) {
502
            $conditions[] = '{"type":"date","d":"<","t":' . $rec->availableuntil . '}';
503
            // Until dates never showed to students.
504
            $shows[] = 'false';
505
        }
506
 
507
        // If there are some conditions, return them.
508
        if ($conditions) {
509
            return '{"op":"&","showc":[' . implode(',', $shows) . '],' .
510
                    '"c":[' . implode(',', $conditions) . ']}';
511
        } else {
512
            return null;
513
        }
514
    }
515
 
516
    /**
517
     * Adds a condition from the legacy availability condition.
518
     *
519
     * (For use during restore only.)
520
     *
521
     * This function assumes that the activity either has no conditions, or
522
     * that it has an AND tree with one or more conditions.
523
     *
524
     * @param string|null $availability Current availability conditions
525
     * @param \stdClass $rec Object containing information from old table
526
     * @param bool $show True if 'show' option should be enabled
527
     * @return string New availability conditions
528
     */
529
    public static function add_legacy_availability_condition($availability, $rec, $show) {
530
        if (!empty($rec->sourcecmid)) {
531
            // Completion condition.
532
            $condition = '{"type":"completion","cm":' . $rec->sourcecmid .
533
                    ',"e":' . $rec->requiredcompletion . '}';
534
        } else {
535
            // Grade condition.
536
            $minmax = '';
537
            if (!empty($rec->grademin)) {
538
                $minmax .= ',"min":' . sprintf('%.5f', $rec->grademin);
539
            }
540
            if (!empty($rec->grademax)) {
541
                $minmax .= ',"max":' . sprintf('%.5f', $rec->grademax);
542
            }
543
            $condition = '{"type":"grade","id":' . $rec->gradeitemid . $minmax . '}';
544
        }
545
 
546
        return self::add_legacy_condition($availability, $condition, $show);
547
    }
548
 
549
    /**
550
     * Adds a condition from the legacy availability field condition.
551
     *
552
     * (For use during restore only.)
553
     *
554
     * This function assumes that the activity either has no conditions, or
555
     * that it has an AND tree with one or more conditions.
556
     *
557
     * @param string|null $availability Current availability conditions
558
     * @param \stdClass $rec Object containing information from old table
559
     * @param bool $show True if 'show' option should be enabled
560
     * @return string New availability conditions
561
     */
562
    public static function add_legacy_availability_field_condition($availability, $rec, $show) {
563
        if (isset($rec->userfield)) {
564
            // Standard field.
565
            $fieldbit = ',"sf":' . json_encode($rec->userfield);
566
        } else {
567
            // Custom field.
568
            $fieldbit = ',"cf":' . json_encode($rec->shortname);
569
        }
570
        // Value is not included for certain operators.
571
        switch($rec->operator) {
572
            case 'isempty':
573
            case 'isnotempty':
574
                $valuebit = '';
575
                break;
576
 
577
            default:
578
                $valuebit = ',"v":' . json_encode($rec->value);
579
                break;
580
        }
581
        $condition = '{"type":"profile","op":"' . $rec->operator . '"' .
582
                $fieldbit . $valuebit . '}';
583
 
584
        return self::add_legacy_condition($availability, $condition, $show);
585
    }
586
 
587
    /**
588
     * Adds a condition to an AND group.
589
     *
590
     * (For use during restore only.)
591
     *
592
     * This function assumes that the activity either has no conditions, or
593
     * that it has only conditions added by this function.
594
     *
595
     * @param string|null $availability Current availability conditions
596
     * @param string $condition Condition text '{...}'
597
     * @param bool $show True if 'show' option should be enabled
598
     * @return string New availability conditions
599
     */
600
    protected static function add_legacy_condition($availability, $condition, $show) {
601
        $showtext = ($show ? 'true' : 'false');
602
        if (is_null($availability)) {
603
            $availability = '{"op":"&","showc":[' . $showtext .
604
                    '],"c":[' . $condition . ']}';
605
        } else {
606
            $matches = array();
607
            if (!preg_match('~^({"op":"&","showc":\[(?:true|false)(?:,(?:true|false))*)' .
608
                    '(\],"c":\[.*)(\]})$~', $availability, $matches)) {
609
                throw new \coding_exception('Unexpected availability value');
610
            }
611
            $availability = $matches[1] . ',' . $showtext . $matches[2] .
612
                    ',' . $condition . $matches[3];
613
        }
614
        return $availability;
615
    }
616
 
617
    /**
618
     * Tests against a user list. Users who cannot access the activity due to
619
     * availability restrictions will be removed from the list.
620
     *
621
     * Note this only includes availability restrictions (those handled within
622
     * this API) and not other ways of restricting access.
623
     *
624
     * This test ONLY includes conditions which are marked as being applied to
625
     * user lists. For example, group conditions are included but date
626
     * conditions are not included.
627
     *
628
     * The function operates reasonably efficiently i.e. should not do per-user
629
     * database queries. It is however likely to be fairly slow.
630
     *
631
     * @param array $users Array of userid => object
632
     * @return array Filtered version of input array
633
     */
634
    public function filter_user_list(array $users) {
635
        global $CFG;
636
        if (is_null($this->availability) || !$CFG->enableavailability) {
637
            return $users;
638
        }
639
        $tree = $this->get_availability_tree();
640
        $checker = new capability_checker($this->get_context());
641
 
642
        // Filter using availability tree.
643
        $this->modinfo = get_fast_modinfo($this->get_course());
644
        $filtered = $tree->filter_user_list($users, false, $this, $checker);
645
        $this->modinfo = null;
646
 
647
        // Include users in the result if they're either in the filtered list,
648
        // or they have viewhidden. This logic preserves ordering of the
649
        // passed users array.
650
        $result = array();
651
        $canviewhidden = $checker->get_users_by_capability($this->get_view_hidden_capability());
652
        foreach ($users as $userid => $data) {
653
            if (array_key_exists($userid, $filtered) || array_key_exists($userid, $canviewhidden)) {
654
                $result[$userid] = $users[$userid];
655
            }
656
        }
657
 
658
        return $result;
659
    }
660
 
661
    /**
662
     * Gets the capability used to view hidden activities/sections (as
663
     * appropriate).
664
     *
665
     * @return string Name of capability used to view hidden items of this type
666
     */
667
    abstract protected function get_view_hidden_capability();
668
 
669
    /**
670
     * Obtains SQL that returns a list of enrolled users that has been filtered
671
     * by the conditions applied in the availability API, similar to calling
672
     * get_enrolled_users and then filter_user_list. As for filter_user_list,
673
     * this ONLY filters out users with conditions that are marked as applying
674
     * to user lists. For example, group conditions are included but date
675
     * conditions are not included.
676
     *
677
     * The returned SQL is a query that returns a list of user IDs. It does not
678
     * include brackets, so you neeed to add these to make it into a subquery.
679
     * You would normally use it in an SQL phrase like "WHERE u.id IN ($sql)".
680
     *
681
     * The function returns an array with '' and an empty array, if there are
682
     * no restrictions on users from these conditions.
683
     *
684
     * The SQL will be complex and may be slow. It uses named parameters (sorry,
685
     * I know they are annoying, but it was unavoidable here).
686
     *
687
     * @param bool $onlyactive True if including only active enrolments
688
     * @return array Array of SQL code (may be empty) and params
689
     */
690
    public function get_user_list_sql($onlyactive) {
691
        global $CFG;
692
        if (is_null($this->availability) || !$CFG->enableavailability) {
693
            return array('', array());
694
        }
695
 
696
        // Get SQL for the availability filter.
697
        $tree = $this->get_availability_tree();
698
        list ($filtersql, $filterparams) = $tree->get_user_list_sql(false, $this, $onlyactive);
699
        if ($filtersql === '') {
700
            // No restrictions, so return empty query.
701
            return array('', array());
702
        }
703
 
704
        // Get SQL for the view hidden list.
705
        list ($viewhiddensql, $viewhiddenparams) = get_enrolled_sql(
706
                $this->get_context(), $this->get_view_hidden_capability(), 0, $onlyactive);
707
 
708
        // Result is a union of the two.
709
        return array('(' . $filtersql . ') UNION (' . $viewhiddensql . ')',
710
                array_merge($filterparams, $viewhiddenparams));
711
    }
712
 
713
    /**
714
     * Formats the $cm->availableinfo string for display. This includes
715
     * filling in the names of any course-modules that might be mentioned.
716
     * Should be called immediately prior to display, or at least somewhere
717
     * that we can guarantee does not happen from within building the modinfo
718
     * object.
719
     *
720
     * @param \renderable|string $inforenderable Info string or renderable
721
     * @param int|\stdClass $courseorid
722
     * @return string Correctly formatted info string
723
     */
724
    public static function format_info($inforenderable, $courseorid) {
725
        global $PAGE, $OUTPUT;
726
 
727
        // Use renderer if required.
728
        if (is_string($inforenderable)) {
729
            $info = $inforenderable;
730
        } else {
731
            $renderable = new \core_availability\output\availability_info($inforenderable);
732
            $info = $OUTPUT->render($renderable);
733
        }
734
 
735
        // Don't waste time if there are no special tags.
736
        if (strpos($info, '<AVAILABILITY_') === false) {
737
            return $info;
738
        }
739
 
740
        // Handle CMNAME tags.
741
        $modinfo = get_fast_modinfo($courseorid);
742
        $context = \context_course::instance($modinfo->courseid);
743
        $info = preg_replace_callback('~<AVAILABILITY_CMNAME_([0-9]+)/>~',
744
                function($matches) use($modinfo, $context) {
745
                    $cm = $modinfo->get_cm($matches[1]);
746
                    $modulename = format_string($cm->get_name(), true, ['context' => $context]);
747
                    // We make sure that we add a data attribute to the name so we can change it later if the
748
                    // original module name changes.
749
                    if ($cm->has_view() && $cm->get_user_visible()) {
750
                        // Help student by providing a link to the module which is preventing availability.
751
                        return \html_writer::link($cm->get_url(), $modulename, ['data-cm-name-for' => $cm->id]);
752
                    } else {
753
                        return \html_writer::span($modulename, '', ['data-cm-name-for' => $cm->id]);
754
                    }
755
                }, $info);
756
        $info = preg_replace_callback('~<AVAILABILITY_FORMAT_STRING>(.*?)</AVAILABILITY_FORMAT_STRING>~s',
757
                function($matches) use ($context) {
758
                    $decoded = htmlspecialchars_decode($matches[1], ENT_NOQUOTES);
759
                    return format_string($decoded, true, ['context' => $context]);
760
                }, $info);
761
        $info = preg_replace_callback('~<AVAILABILITY_CALLBACK type="([a-z0-9_]+)">(.*?)</AVAILABILITY_CALLBACK>~s',
762
                function($matches) use ($modinfo, $context) {
763
                    // Find the class, it must have already been loaded by now.
764
                    $fullclassname = 'availability_' . $matches[1] . '\condition';
765
                    if (!class_exists($fullclassname, false)) {
766
                        return '<!-- Error finding class ' . $fullclassname .' -->';
767
                    }
768
                    // Load the parameters.
769
                    $params = [];
770
                    $encodedparams = preg_split('~<P/>~', $matches[2], 0);
771
                    foreach ($encodedparams as $encodedparam) {
772
                        $params[] = htmlspecialchars_decode($encodedparam, ENT_NOQUOTES);
773
                    }
774
                    return $fullclassname::get_description_callback_value($modinfo, $context, $params);
775
                }, $info);
776
 
777
        return $info;
778
    }
779
 
780
    /**
781
     * Used in course/lib.php because we need to disable the completion tickbox
782
     * JS (using the non-JS version instead, which causes a page reload) if a
783
     * completion tickbox value may affect a conditional activity.
784
     *
785
     * @param \stdClass $course Moodle course object
786
     * @param int $cmid Course-module id
787
     * @return bool True if this is used in a condition, false otherwise
788
     */
789
    public static function completion_value_used($course, $cmid) {
790
        // Access all plugins. Normally only the completion plugin is going
791
        // to affect this value, but it's potentially possible that some other
792
        // plugin could also rely on the completion plugin.
793
        $pluginmanager = \core_plugin_manager::instance();
794
        $enabled = $pluginmanager->get_enabled_plugins('availability');
795
        $componentparams = new \stdClass();
796
        foreach ($enabled as $plugin => $info) {
797
            // Use the static method.
798
            $class = '\availability_' . $plugin . '\condition';
799
            if ($class::completion_value_used($course, $cmid)) {
800
                return true;
801
            }
802
        }
803
        return false;
804
    }
805
 
806
    /**
807
     * Returns groups that the given user belongs to on the course. Note: If not already
808
     * available, this may make a database query.
809
     *
810
     * This will include groups the user is not allowed to see themselves, so check visibility
811
     * before displaying groups to the user.
812
     *
813
     * @param int $groupingid Grouping ID or 0 (default) for all groups
814
     * @param int $userid User ID or 0 (default) for current user
815
     * @return int[] Array of int (group id) => int (same group id again); empty array if none
816
     */
817
    public function get_groups(int $groupingid = 0, int $userid = 0): array {
818
        global $USER;
819
        if (empty($userid)) {
820
            $userid = $USER->id;
821
        }
822
        if (!array_key_exists($userid, $this->groups)) {
823
            $allgroups = groups_get_user_groups($this->course->id, $userid, true);
824
            $this->groups[$userid] = $allgroups;
825
        } else {
826
            $allgroups = $this->groups[$userid];
827
        }
828
        if (!isset($allgroups[$groupingid])) {
829
            return [];
830
        }
831
        return $allgroups[$groupingid];
832
    }
833
}