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