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