3bf7a8f8f3be41ed24304094d8131e490d9042ea
[moodle.git] / lib / conditionlib.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  * Used for tracking conditions that apply before activities are displayed
19  * to students ('conditional availability').
20  *
21  * @package    core_condition
22  * @category   condition
23  * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * CONDITION_STUDENTVIEW_HIDE - The activity is not displayed to students at all when conditions aren't met.
31  */
32 define('CONDITION_STUDENTVIEW_HIDE', 0);
33 /**
34  * CONDITION_STUDENTVIEW_SHOW - The activity is displayed to students as a greyed-out name, with
35  * informational text that explains the conditions under which it will be available.
36  */
37 define('CONDITION_STUDENTVIEW_SHOW', 1);
39 /**
40  * CONDITION_MISSING_NOTHING - The $item variable is expected to contain all completion-related data
41  */
42 define('CONDITION_MISSING_NOTHING', 0);
43 /**
44  * CONDITION_MISSING_EXTRATABLE - The $item variable is expected to contain the fields from
45  * the relevant table (course_modules or course_sections) but not the _availability data
46  */
47 define('CONDITION_MISSING_EXTRATABLE', 1);
48 /**
49  * CONDITION_MISSING_EVERYTHING - The $item variable is expected to contain nothing except the ID
50  */
51 define('CONDITION_MISSING_EVERYTHING', 2);
53 /**
54  * OP_CONTAINS - comparison operator that determines whether a specified user field contains
55  * a provided variable
56  */
57 define('OP_CONTAINS', 'contains');
58 /**
59  * OP_DOES_NOT_CONTAIN - comparison operator that determines whether a specified user field does not
60  * contain a provided variable
61  */
62 define('OP_DOES_NOT_CONTAIN', 'doesnotcontain');
63 /**
64  * OP_IS_EQUAL_TO - comparison operator that determines whether a specified user field is equal to
65  * a provided variable
66  */
67 define('OP_IS_EQUAL_TO', 'isequalto');
68 /**
69  * OP_STARTS_WITH - comparison operator that determines whether a specified user field starts with
70  * a provided variable
71  */
72 define('OP_STARTS_WITH', 'startswith');
73 /**
74  * OP_ENDS_WITH - comparison operator that determines whether a specified user field ends with
75  * a provided variable
76  */
77 define('OP_ENDS_WITH', 'endswith');
78 /**
79  * OP_IS_EMPTY - comparison operator that determines whether a specified user field is empty
80  */
81 define('OP_IS_EMPTY', 'isempty');
82 /**
83  * OP_IS_NOT_EMPTY - comparison operator that determines whether a specified user field is not empty
84  */
85 define('OP_IS_NOT_EMPTY', 'isnotempty');
87 require_once($CFG->libdir.'/completionlib.php');
89 /**
90  * Core class to handle conditional activites
91  *
92  * @package   core_condition
93  * @category  condition
94  * @copyright 2012 The Open University
95  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
96  */
97 class condition_info extends condition_info_base {
98     /**
99      * Constructs with course-module details.
100      *
101      * @global moodle_database $DB
102      * @uses CONDITION_MISSING_NOTHING
103      * @param object $cm Moodle course-module object. May have extra fields
104      *   ->conditionsgrade, ->conditionscompletion which should come from
105      *   get_fast_modinfo. Should have ->availablefrom, ->availableuntil,
106      *   and ->showavailability, ->course, ->visible; but the only required
107      *   thing is ->id.
108      * @param int $expectingmissing Used to control whether or not a developer
109      *   debugging message (performance warning) will be displayed if some of
110      *   the above data is missing and needs to be retrieved; a
111      *   CONDITION_MISSING_xx constant
112      * @param bool $loaddata If you need a 'write-only' object, set this value
113      *   to false to prevent database access from constructor
114      */
115     public function __construct($cm, $expectingmissing = CONDITION_MISSING_NOTHING,
116         $loaddata=true) {
117         parent::__construct($cm, 'course_modules', 'coursemoduleid',
118                 $expectingmissing, $loaddata);
119     }
121     /**
122      * Adds the extra availability conditions (if any) into the given
123      * course-module (or section) object.
124      *
125      * This function may be called statically (for the editing form) or
126      * dynamically.
127      *
128      * @param object $cm Moodle course-module data object
129      */
130     public static function fill_availability_conditions($cm) {
131         parent::fill_availability_conditions_inner($cm, 'course_modules', 'coursemoduleid');
132     }
134     /**
135      * Gets the course-module object with full necessary data to determine availability.
136      * @return object Course-module object with full data
137      * @throws coding_exception If data was not supplied when constructing object
138      */
139     public function get_full_course_module() {
140         return $this->get_full_item();
141     }
143     /**
144      * Utility function called by modedit.php; updates the
145      * course_modules_availability table based on the module form data.
146      *
147      * @param object $cm Course-module with as much data as necessary, min id
148      * @param object $fromform Data from form
149      * @param bool $wipefirst If true, wipes existing conditions
150      */
151     public static function update_cm_from_form($cm, $fromform, $wipefirst=true) {
152         $ci = new condition_info($cm, CONDITION_MISSING_EVERYTHING, false);
153         parent::update_from_form($ci, $fromform, $wipefirst);
154     }
156     /**
157      * Used in course/lib.php because we need to disable the completion JS if
158      * a completion value affects a conditional activity.
159      *
160      * @global stdClass $CONDITIONLIB_PRIVATE
161      * @param object $course Moodle course object
162      * @param object $item Moodle course-module
163      * @return bool True if this is used in a condition, false otherwise
164      */
165     public static function completion_value_used_as_condition($course, $cm) {
166         // Have we already worked out a list of required completion values
167         // for this course? If so just use that
168         global $CONDITIONLIB_PRIVATE, $DB;
169         if (!array_key_exists($course->id, $CONDITIONLIB_PRIVATE->usedincondition)) {
170             // We don't have data for this course, build it
171             $modinfo = get_fast_modinfo($course);
172             $CONDITIONLIB_PRIVATE->usedincondition[$course->id] = array();
174             // Activities
175             foreach ($modinfo->cms as $othercm) {
176                 foreach ($othercm->conditionscompletion as $cmid => $expectedcompletion) {
177                     $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
178                 }
179             }
181             // Sections
182             foreach ($modinfo->get_section_info_all() as $section) {
183                 foreach ($section->conditionscompletion as $cmid => $expectedcompletion) {
184                     $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
185                 }
186             }
187         }
188         return array_key_exists($cm->id, $CONDITIONLIB_PRIVATE->usedincondition[$course->id]);
189     }
191     protected function get_context() {
192         return context_module::instance($this->item->id);
193     }
197 /**
198  * Handles conditional access to sections.
199  *
200  * @package   core_condition
201  * @category  condition
202  * @copyright 2012 The Open University
203  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
204  */
205 class condition_info_section extends condition_info_base {
206     /**
207      * Constructs with course-module details.
208      *
209      * @global moodle_database $DB
210      * @uses CONDITION_MISSING_NOTHING
211      * @param object $section Moodle section object. May have extra fields
212      *   ->conditionsgrade, ->conditionscompletion. Should have ->availablefrom,
213      *   ->availableuntil, and ->showavailability, ->course; but the only
214      *   required thing is ->id.
215      * @param int $expectingmissing Used to control whether or not a developer
216      *   debugging message (performance warning) will be displayed if some of
217      *   the above data is missing and needs to be retrieved; a
218      *   CONDITION_MISSING_xx constant
219      * @param bool $loaddata If you need a 'write-only' object, set this value
220      *   to false to prevent database access from constructor
221      */
222     public function __construct($section, $expectingmissing = CONDITION_MISSING_NOTHING,
223         $loaddata=true) {
224         parent::__construct($section, 'course_sections', 'coursesectionid',
225                 $expectingmissing, $loaddata);
226     }
228     /**
229      * Adds the extra availability conditions (if any) into the given
230      * course-module (or section) object.
231      *
232      * This function may be called statically (for the editing form) or
233      * dynamically.
234      *
235      * @param object $section Moodle section data object
236      */
237     public static function fill_availability_conditions($section) {
238         parent::fill_availability_conditions_inner($section, 'course_sections', 'coursesectionid');
239     }
241     /**
242      * Gets the section object with full necessary data to determine availability.
243      * @return object Section object with full data
244      * @throws coding_exception If data was not supplied when constructing object
245      */
246     public function get_full_section() {
247         return $this->get_full_item();
248     }
250     /**
251      * Gets list of required fields from main table.
252      * @return array Array of field names
253      */
254     protected function get_main_table_fields() {
255         return array_merge(parent::get_main_table_fields(), array('groupingid'));
256     }
258     /**
259      * Determines whether this particular section is currently available
260      * according to these criteria.
261      *
262      * - This does not include the 'visible' setting (i.e. this might return
263      *   true even if visible is false); visible is handled independently.
264      * - This does not take account of the viewhiddenactivities capability.
265      *   That should apply later.
266      *
267      * @global moodle_database $DB
268      * @global stdclass $USER
269      * @param string $information If the item has availability restrictions,
270      *   a string that describes the conditions will be stored in this variable;
271      *   if this variable is set blank, that means don't display anything
272      * @param bool $grabthelot Performance hint: if true, caches information
273      *   required for all course-modules, to make the front page and similar
274      *   pages work more quickly (works only for current user)
275      * @param int $userid If set, specifies a different user ID to check availability for
276      * @param object $modinfo Usually leave as null for default. Specify when
277      *   calling recursively from inside get_fast_modinfo. The value supplied
278      *   here must include list of all CMs with 'id' and 'name'
279      * @return bool True if this item is available to the user, false otherwise
280      */
281     public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
282         global $DB, $USER, $CONDITIONLIB_PRIVATE;
284         $available = parent::is_available($information, $grabthelot, $userid, $modinfo);
286         // test if user is enrolled to a grouping which has access to the section
287         if (!empty($this->item->groupingid)) {
288             // Get real user id
289             if (!$userid) {
290                 $userid = $USER->id;
291             }
292             $context = $this->get_context();
294             if ($userid != $USER->id) {
295                 // We are requesting for a non-current user so check it individually
296                 // (no cache). Do grouping check first, it's probably faster
297                 // than the capability check
298                 $gotit = $DB->record_exists_sql('
299                         SELECT
300                             1
301                         FROM
302                             {groupings} g
303                             JOIN {groupings_groups} gg ON g.id = gg.groupingid
304                             JOIN {groups_members} gm ON gg.groupid = gm.groupid
305                         WHERE
306                             g.id = ? AND gm.userid = ?',
307                         array($this->item->groupingid, $userid));
308                 if (!$gotit && !has_capability('moodle/site:accessallgroups', $context, $userid)) {
309                     $available = false;
310                     $information .= get_string('groupingnoaccess', 'condition');
311                 }
312             } else {
313                 // Request is for current user - use cache
314                 if( !array_key_exists($this->item->course, $CONDITIONLIB_PRIVATE->groupingscache)) {
315                     if (has_capability('moodle/site:accessallgroups', $context)) {
316                         $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course] = true;
317                     } else {
318                         $groupings = $DB->get_records_sql('
319                                 SELECT
320                                     g.id as gid
321                                 FROM
322                                     {groupings} g
323                                     JOIN {groupings_groups} gg ON g.id = gg.groupingid
324                                     JOIN {groups_members} gm ON gg.groupid = gm.groupid
325                                 WHERE
326                                     g.courseid = ? AND gm.userid = ?',
327                                 array($this->item->course, $userid));
328                         $list = array();
329                         foreach ($groupings as $grouping) {
330                             $list[$grouping->gid] = true;
331                         }
332                         $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course] = $list;
333                     }
334                 }
336                 $usergroupings = $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course];
337                 if ($usergroupings !== true && !array_key_exists($this->item->groupingid, $usergroupings)) {
338                     $available = false;
339                     $information .= get_string('groupingnoaccess', 'condition');
340                 }
341             }
342         }
344         $information = trim($information);
345         return $available;
346     }
348     /**
349      * Utility function called by modedit.php; updates the
350      * course_modules_availability table based on the module form data.
351      *
352      * @param object $section Section object, must at minimum contain id
353      * @param object $fromform Data from form
354      * @param bool $wipefirst If true, wipes existing conditions
355      */
356     public static function update_section_from_form($section, $fromform, $wipefirst=true) {
357         $ci = new condition_info_section($section, CONDITION_MISSING_EVERYTHING);
358         parent::update_from_form($ci, $fromform, $wipefirst);
359     }
361     protected function get_context() {
362         return context_course::instance($this->item->course);
363     }
367 /**
368  * Base class to handle conditional items of some kind (currently either
369  * course_modules or sections; they must have a corresponding _availability
370  * table).
371  *
372  * @package   core_condition
373  * @category  condition
374  * @copyright 2012 The Open University
375  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
376  */
377 abstract class condition_info_base {
378     protected $item;
379     /** @var bool */
380     protected $gotdata;
381     /** @var string */
382     protected $availtable;
383     /** @var string */
384     protected $availfieldtable;
385     /** @var string */
386     protected $idfieldname;
387     /** @var array */
388     protected $usergroupings;
389     /** @var array An array of custom profile field ids => to their shortname */
390     protected $customprofilefields = null;
391     /**
392      * Constructs with item details.
393      *
394      * @global moodle_database $DB
395      * @uses CONDITION_MISSING_NOTHING
396      * @uses CONDITION_MISSING_EVERYTHING
397      * @uses CONDITION_MISSING_EXTRATABLE
398      * @uses DEBUG_DEVELOPER
399      * @param object $item Object representing some kind of item (cm or section).
400      *   May have extra fields ->conditionsgrade, ->conditionscompletion.
401      *   Should have ->availablefrom, ->availableuntil, and ->showavailability,
402      *   ->course; but the only required thing is ->id.
403      * @param string $tableprefix Prefix for table used to store availability
404      *   data, e.g. 'course_modules' if we are going to look at
405      *   course_modules_availability.
406      * @param string $idfield Within this table, name of field used as item id
407      *   (e.g. 'coursemoduleid')
408      * @param int $expectingmissing Used to control whether or not a developer
409      *   debugging message (performance warning) will be displayed if some of
410      *   the above data is missing and needs to be retrieved; a
411      *   CONDITION_MISSING_xx constant
412      * @param bool $loaddata If you need a 'write-only' object, set this value
413      *   to false to prevent database access from constructor
414      * @return condition_info Object which can retrieve information about the
415      *   activity
416      */
417     public function __construct($item, $tableprefix, $idfield, $expectingmissing, $loaddata) {
418         global $DB;
420         // Check ID as otherwise we can't do the other queries
421         if (empty($item->id)) {
422             throw new coding_exception('Invalid parameters; item ID not included');
423         }
425         // DB table to store availability conditions
426         $this->availtable = $tableprefix . '_availability';
428         // DB table to store availability conditions for user fields
429         $this->availfieldtable = $tableprefix . '_avail_fields';
431         // Name of module/section ID field in DB
432         $this->idfieldname = $idfield;
434         // If not loading data, don't do anything else
435         if (!$loaddata) {
436             $this->item = (object)array('id' => $item->id);
437             $this->gotdata = false;
438             return;
439         }
441         // Missing basic data from course_modules
442         $basicfields = $this->get_main_table_fields();
443         $missingbasicfields = false;
444         foreach ($basicfields as $field) {
445             if (!isset($item->{$field})) {
446                 $missingbasicfields = true;
447                 break;
448             }
449         }
450         if ($missingbasicfields) {
451             if ($expectingmissing<CONDITION_MISSING_EVERYTHING) {
452                 debugging('Performance warning: condition_info constructor is ' .
453                         'faster if you pass in $item with at least basic fields ' .
454                         'from its table. '.
455                         '[This warning can be disabled, see phpdoc.]',
456                         DEBUG_DEVELOPER);
457             }
458             $item = $DB->get_record($tableprefix, array('id' => $item->id),
459                     implode(',', $basicfields), MUST_EXIST);
460         }
462         $this->item = clone($item);
463         $this->gotdata = true;
465         // Missing extra data
466         if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion) || !isset($item->conditionsfield)) {
467             if ($expectingmissing<CONDITION_MISSING_EXTRATABLE) {
468                 debugging('Performance warning: condition_info constructor is ' .
469                         'faster if you pass in a $item from get_fast_modinfo or ' .
470                         'the equivalent for sections. ' .
471                         '[This warning can be disabled, see phpdoc.]',
472                         DEBUG_DEVELOPER);
473             }
475             $this->fill_availability_conditions($this->item);
476         }
477     }
479     /**
480      * Gets list of required fields from main table.
481      *
482      * @return array Array of field names
483      */
484     protected function get_main_table_fields() {
485         return array('id', 'course', 'visible',
486                 'availablefrom', 'availableuntil', 'showavailability');
487     }
489     /**
490      * Fills availability conditions into the item object, if they are missing,
491      * otherwise does nothing. Called by subclass fill_availability_conditions.
492      * @param object $item Item object
493      * @param string $tableprefix Prefix of name for _availability table e.g. 'course_modules'
494      * @param string $idfield Name of field that contains id e.g. 'coursemoduleid'
495      * @throws coding_exception If item object doesn't have id field
496      */
497     protected static function fill_availability_conditions_inner($item, $tableprefix, $idfield) {
498         global $DB, $CFG;
499         if (empty($item->id)) {
500             throw new coding_exception('Invalid parameters; item ID not included');
501         }
503         // Does nothing if the variables are already present
504         if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion) || !isset($item->conditionsfield)) {
505             $item->conditionsgrade = array();
506             $item->conditionscompletion = array();
507             $item->conditionsfield = array();
509             $conditions = $DB->get_records_sql('
510                     SELECT
511                         a.id AS aid, gi.*, a.sourcecmid, a.requiredcompletion, a.gradeitemid,
512                         a.grademin as conditiongrademin, a.grademax as conditiongrademax
513                     FROM
514                         {' . $tableprefix . '_availability} a
515                         LEFT JOIN {grade_items} gi ON gi.id = a.gradeitemid
516                     WHERE ' . $idfield . ' = ?', array($item->id));
517             foreach ($conditions as $condition) {
518                 if (!is_null($condition->sourcecmid)) {
519                     $item->conditionscompletion[$condition->sourcecmid] =
520                         $condition->requiredcompletion;
521                 } else {
522                     $minmax = new stdClass;
523                     $minmax->min = $condition->conditiongrademin;
524                     $minmax->max = $condition->conditiongrademax;
525                     $minmax->name = self::get_grade_name($condition);
526                     $item->conditionsgrade[$condition->gradeitemid] = $minmax;
527                 }
528             }
530             // For user fields
531             $sql = "SELECT a.id as cmaid, a.*, uf.*
532                       FROM {" . $tableprefix . "_avail_fields} a
533                  LEFT JOIN {user_info_field} uf ON a.customfieldid =  uf.id
534                      WHERE " . $idfield . " = :itemid";
535             $conditions = $DB->get_records_sql($sql, array('itemid' => $item->id));
536             foreach ($conditions as $condition) {
537                 // If the custom field is not empty, then we have a custom profile field
538                 if (!empty($condition->customfieldid)) {
539                     $field = $condition->customfieldid;
540                     // Check if the profile field name is not empty, if it is
541                     // then the custom profile field no longer exists, so
542                     // display !missing instead.
543                     if (!empty($condition->name)) {
544                         $fieldname = $condition->name;
545                     } else {
546                         $fieldname = '!missing';
547                     }
548                 } else {
549                     $field = $condition->userfield;
550                     $fieldname = $condition->userfield;
551                 }
552                 $details = new stdClass;
553                 $details->fieldname = $fieldname;
554                 $details->operator = $condition->operator;
555                 $details->value = $condition->value;
556                 $item->conditionsfield[$field] = $details;
557             }
558         }
559     }
561     /**
562      * Obtains the name of a grade item.
563      *
564      * @global object $CFG
565      * @param object $gradeitemobj Object from get_record on grade_items table,
566      *     (can be empty if you want to just get !missing)
567      * @return string Name of item of !missing if it didn't exist
568      */
569     private static function get_grade_name($gradeitemobj) {
570         global $CFG;
571         if (isset($gradeitemobj->id)) {
572             require_once($CFG->libdir . '/gradelib.php');
573             $item = new grade_item;
574             grade_object::set_properties($item, $gradeitemobj);
575             return $item->get_name();
576         } else {
577             return '!missing'; // Ooops, missing grade
578         }
579     }
581     /**
582      * Gets the item object with full necessary data to determine availability.
583      * @return object Item object with full data
584      * @throws coding_exception If data was not supplied when constructing object
585      */
586     protected function get_full_item() {
587         $this->require_data();
588         return $this->item;
589     }
591     /**
592      * The operators that provide the relationship
593      * between a field and a value.
594      *
595      * @return array Associative array from operator constant to display name
596      */
597     public static function get_condition_user_field_operators() {
598         return array(
599             OP_CONTAINS => get_string('contains', 'condition'),
600             OP_DOES_NOT_CONTAIN => get_string('doesnotcontain', 'condition'),
601             OP_IS_EQUAL_TO => get_string('isequalto', 'condition'),
602             OP_STARTS_WITH => get_string('startswith', 'condition'),
603             OP_ENDS_WITH => get_string('endswith', 'condition'),
604             OP_IS_EMPTY => get_string('isempty', 'condition'),
605             OP_IS_NOT_EMPTY => get_string('isnotempty', 'condition'),
606         );
607     }
609     /**
610      * Returns list of user fields that can be compared.
611      *
612      * If you specify $formatoptions, then format_string will be called on the
613      * custom field names. This is necessary for multilang support to work so
614      * you should include this parameter unless you are going to format the
615      * text later.
616      *
617      * @param array $formatoptions Passed to format_string if provided
618      * @return array Associative array from user field constants to display name
619      */
620     public static function get_condition_user_fields($formatoptions = null) {
621         global $DB;
623         $userfields = array(
624             'firstname' => get_user_field_name('firstname'),
625             'lastname' => get_user_field_name('lastname'),
626             'email' => get_user_field_name('email'),
627             'city' => get_user_field_name('city'),
628             'country' => get_user_field_name('country'),
629             'url' => get_user_field_name('url'),
630             'icq' => get_user_field_name('icq'),
631             'skype' => get_user_field_name('skype'),
632             'aim' => get_user_field_name('aim'),
633             'yahoo' => get_user_field_name('yahoo'),
634             'msn' => get_user_field_name('msn'),
635             'idnumber' => get_user_field_name('idnumber'),
636             'institution' => get_user_field_name('institution'),
637             'department' => get_user_field_name('department'),
638             'phone1' => get_user_field_name('phone1'),
639             'phone2' => get_user_field_name('phone2'),
640             'address' => get_user_field_name('address')
641         );
643         // Go through the custom profile fields now
644         if ($user_info_fields = $DB->get_records('user_info_field')) {
645             foreach ($user_info_fields as $field) {
646                 if ($formatoptions) {
647                     $userfields[$field->id] = format_string($field->name, true, $formatoptions);
648                 } else {
649                     $userfields[$field->id] = $field->name;
650                 }
651             }
652         }
654         return $userfields;
655     }
657     /**
658      * Adds to the database a condition based on completion of another module.
659      *
660      * @global moodle_database $DB
661      * @param int $cmid ID of other module
662      * @param int $requiredcompletion COMPLETION_xx constant
663      */
664     public function add_completion_condition($cmid, $requiredcompletion) {
665         global $DB;
666         // Add to DB
667         $DB->insert_record($this->availtable, (object)array(
668                 $this->idfieldname => $this->item->id,
669                 'sourcecmid' => $cmid, 'requiredcompletion' => $requiredcompletion),
670                 false);
672         // Store in memory too
673         $this->item->conditionscompletion[$cmid] = $requiredcompletion;
674     }
676     /**
677      * Adds user fields condition
678      *
679      * @param mixed $field numeric if it is a user profile field, character
680      *                     if it is a column in the user table
681      * @param int $operator specifies the relationship between field and value
682      * @param char $value the value of the field
683      */
684     public function add_user_field_condition($field, $operator, $value) {
685         global $DB;
687         // Get the field name
688         $idfieldname = $this->idfieldname;
690         $objavailfield = new stdClass;
691         $objavailfield->$idfieldname = $this->item->id;
692         if (is_numeric($field)) { // If the condition field is numeric then it is a custom profile field
693             // Need to get the field name so we can add it to the cache
694             $ufield = $DB->get_field('user_info_field', 'name', array('id' => $field));
695             $objavailfield->fieldname = $ufield;
696             $objavailfield->customfieldid = $field;
697         } else {
698             $objavailfield->fieldname = $field;
699             $objavailfield->userfield = $field;
700         }
701         $objavailfield->operator = $operator;
702         $objavailfield->value = $value;
703         $DB->insert_record($this->availfieldtable, $objavailfield, false);
705         // Store in memory too
706         $this->item->conditionsfield[$field] = $objavailfield;
707     }
709     /**
710      * Adds to the database a condition based on the value of a grade item.
711      *
712      * @global moodle_database $DB
713      * @param int $gradeitemid ID of grade item
714      * @param float $min Minimum grade (>=), up to 5 decimal points, or null if none
715      * @param float $max Maximum grade (<), up to 5 decimal points, or null if none
716      * @param bool $updateinmemory If true, updates data in memory; otherwise,
717      *   memory version may be out of date (this has performance consequences,
718      *   so don't do it unless it really needs updating)
719      */
720     public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) {
721         global $DB;
722         // Normalise nulls
723         if ($min==='') {
724             $min = null;
725         }
726         if ($max==='') {
727             $max = null;
728         }
729         // Add to DB
730         $DB->insert_record($this->availtable, (object)array(
731                 $this->idfieldname => $this->item->id,
732                 'gradeitemid' => $gradeitemid, 'grademin' => $min, 'grademax' => $max),
733                 false);
735         // Store in memory too
736         if ($updateinmemory) {
737             $this->item->conditionsgrade[$gradeitemid] = (object) array(
738                     'min' => $min, 'max' => $max);
739             $this->item->conditionsgrade[$gradeitemid]->name = self::get_grade_name(
740                     $DB->get_record('grade_items', array('id'=>$gradeitemid)));
741         }
742     }
744      /**
745      * Erases from the database all conditions for this activity.
746      *
747      * @global moodle_database $DB
748      */
749     public function wipe_conditions() {
750         // Wipe from DB
751         global $DB;
753         $DB->delete_records($this->availtable, array($this->idfieldname => $this->item->id));
754         $DB->delete_records($this->availfieldtable, array($this->idfieldname => $this->item->id));
756         // And from memory
757         $this->item->conditionsgrade = array();
758         $this->item->conditionscompletion = array();
759         $this->item->conditionsfield = array();
760     }
762     /**
763      * Obtains a string describing all availability restrictions (even if
764      * they do not apply any more).
765      *
766      * @global stdClass $COURSE
767      * @global moodle_database $DB
768      * @param object $modinfo Usually leave as null for default. Specify when
769      *   calling recursively from inside get_fast_modinfo. The value supplied
770      *   here must include list of all CMs with 'id' and 'name'
771      * @return string Information string (for admin) about all restrictions on
772      *   this item
773      */
774     public function get_full_information($modinfo=null) {
775         global $COURSE, $DB;
776         $this->require_data();
778         $information = '';
781         // Completion conditions
782         if (count($this->item->conditionscompletion) > 0) {
783             if ($this->item->course == $COURSE->id) {
784                 $course = $COURSE;
785             } else {
786                 $course = $DB->get_record('course', array('id' => $this->item->course),
787                         'id, enablecompletion, modinfo, sectioncache', MUST_EXIST);
788             }
789             foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
790                 if (!$modinfo) {
791                     $modinfo = get_fast_modinfo($course);
792                 }
793                 if (empty($modinfo->cms[$cmid])) {
794                     continue;
795                 }
796                 $information .= html_writer::start_tag('li');
797                 $information .= get_string(
798                         'requires_completion_' . $expectedcompletion,
799                         'condition', $modinfo->cms[$cmid]->name) . ' ';
800                 $information .= html_writer::end_tag('li');
801             }
802         }
804         // Grade conditions
805         if (count($this->item->conditionsgrade) > 0) {
806             foreach ($this->item->conditionsgrade as $gradeitemid => $minmax) {
807                 // String depends on type of requirement. We are coy about
808                 // the actual numbers, in case grades aren't released to
809                 // students.
810                 if (is_null($minmax->min) && is_null($minmax->max)) {
811                     $string = 'any';
812                 } else if (is_null($minmax->max)) {
813                     $string = 'min';
814                 } else if (is_null($minmax->min)) {
815                     $string = 'max';
816                 } else {
817                     $string = 'range';
818                 }
819                 $information .= html_writer::start_tag('li');
820                 $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' ';
821                 $information .= html_writer::end_tag('li');
822             }
823         }
825         // User field conditions
826         if (count($this->item->conditionsfield) > 0) {
827             $context = $this->get_context();
828             // Need the array of operators
829             foreach ($this->item->conditionsfield as $field => $details) {
830                 $a = new stdclass;
831                 // Display the fieldname into current lang.
832                 if (is_numeric($field)) {
833                     $translatedfieldname = $details->fieldname;
834                 } else {
835                     $translatedfieldname = get_user_field_name($details->fieldname);
836                 }
837                 $a->field = format_string($translatedfieldname, true, array('context' => $context));
838                 $a->value = s($details->value);
839                 $information .= html_writer::start_tag('li');
840                 $information .= get_string('requires_user_field_'.$details->operator, 'condition', $a) . ' ';
841                 $information .= html_writer::end_tag('li');
842             }
843         }
845         // The date logic is complicated. The intention of this logic is:
846         // 1) display date without time where possible (whenever the date is
847         //    midnight)
848         // 2) when the 'until' date is e.g. 00:00 on the 14th, we display it as
849         //    'until the 13th' (experience at the OU showed that students are
850         //    likely to interpret 'until <date>' as 'until the end of <date>').
851         // 3) This behaviour becomes confusing for 'same-day' dates where there
852         //    are some exceptions.
853         // Users in different time zones will typically not get the 'abbreviated'
854         // behaviour but it should work OK for them aside from that.
856         // The following cases are possible:
857         // a) From 13:05 on 14 Oct until 12:10 on 17 Oct (exact, exact)
858         // b) From 14 Oct until 12:11 on 17 Oct (midnight, exact)
859         // c) From 13:05 on 14 Oct until 17 Oct (exact, midnight 18 Oct)
860         // d) From 14 Oct until 17 Oct (midnight 14 Oct, midnight 18 Oct)
861         // e) On 14 Oct (midnight 14 Oct, midnight 15 Oct)
862         // f) From 13:05 on 14 Oct until 0:00 on 15 Oct (exact, midnight, same day)
863         // g) From 0:00 on 14 Oct until 12:05 on 14 Oct (midnight, exact, same day)
864         // h) From 13:05 on 14 Oct (exact)
865         // i) From 14 Oct (midnight)
866         // j) Until 13:05 on 14 Oct (exact)
867         // k) Until 14 Oct (midnight 15 Oct)
869         // Check if start and end dates are 'midnights', if so we show in short form
870         $shortfrom = self::is_midnight($this->item->availablefrom);
871         $shortuntil = self::is_midnight($this->item->availableuntil);
873         // For some checks and for display, we need the previous day for the 'until'
874         // value, if we are going to display it in short form
875         if ($this->item->availableuntil) {
876             $daybeforeuntil = strtotime('-1 day', usergetmidnight($this->item->availableuntil));
877         }
879         // Special case for if one but not both are exact and they are within a day
880         if ($this->item->availablefrom && $this->item->availableuntil &&
881                 $shortfrom != $shortuntil && $daybeforeuntil < $this->item->availablefrom) {
882             // Don't use abbreviated version (see examples f, g above)
883             $shortfrom = false;
884             $shortuntil = false;
885         }
887         // When showing short end date, the display time is the 'day before' one
888         $displayuntil = $shortuntil ? $daybeforeuntil : $this->item->availableuntil;
890         if ($this->item->availablefrom && $this->item->availableuntil) {
891             if ($shortfrom && $shortuntil && $daybeforeuntil == $this->item->availablefrom) {
892                 $information .= html_writer::start_tag('li');
893                 $information .= get_string('requires_date_both_single_day', 'condition',
894                         self::show_time($this->item->availablefrom, true));
895                 $information .= html_writer::end_tag('li');
896             } else {
897                 $information .= html_writer::start_tag('li');
898                 $information .= get_string('requires_date_both', 'condition', (object)array(
899                          'from' => self::show_time($this->item->availablefrom, $shortfrom),
900                          'until' => self::show_time($displayuntil, $shortuntil)));
901                 $information .= html_writer::end_tag('li');
902             }
903         } else if ($this->item->availablefrom) {
904             $information .= html_writer::start_tag('li');
905             $information .= get_string('requires_date', 'condition',
906                 self::show_time($this->item->availablefrom, $shortfrom));
907             $information .= html_writer::end_tag('li');
908         } else if ($this->item->availableuntil) {
909             $information .= html_writer::start_tag('li');
910             $information .= get_string('requires_date_before', 'condition',
911                 self::show_time($displayuntil, $shortuntil));
912             $information .= html_writer::end_tag('li');
913         }
915         // The information is in <li> tags, but to avoid taking up more space
916         // if there is only a single item, we strip out the list tags so that it
917         // is plain text in that case.
918         if (!empty($information)) {
919             $li = strpos($information, '<li>', 4);
920             if ($li === false) {
921                 $information = preg_replace('~^<li>(.*)</li>$~', '$1', $information);
922             } else {
923                 $information = html_writer::tag('ul', $information);
924             }
925             $information = trim($information);
926         }
927         return $information;
928     }
930     /**
931      * Checks whether a given time refers exactly to midnight (in current user
932      * timezone).
933      *
934      * @param int $time Time
935      * @return bool True if time refers to midnight, false if it's some other
936      *   time or if it is set to zero
937      */
938     private static function is_midnight($time) {
939         return $time && usergetmidnight($time) == $time;
940     }
942     /**
943      * Determines whether this particular item is currently available
944      * according to these criteria.
945      *
946      * - This does not include the 'visible' setting (i.e. this might return
947      *   true even if visible is false); visible is handled independently.
948      * - This does not take account of the viewhiddenactivities capability.
949      *   That should apply later.
950      *
951      * @global stdClass $COURSE
952      * @global moodle_database $DB
953      * @uses COMPLETION_COMPLETE
954      * @uses COMPLETION_COMPLETE_FAIL
955      * @uses COMPLETION_COMPLETE_PASS
956      * @param string $information If the item has availability restrictions,
957      *   a string that describes the conditions will be stored in this variable;
958      *   if this variable is set blank, that means don't display anything
959      * @param bool $grabthelot Performance hint: if true, caches information
960      *   required for all course-modules, to make the front page and similar
961      *   pages work more quickly (works only for current user)
962      * @param int $userid If set, specifies a different user ID to check availability for
963      * @param object $modinfo Usually leave as null for default. Specify when
964      *   calling recursively from inside get_fast_modinfo. The value supplied
965      *   here must include list of all CMs with 'id' and 'name'
966      * @return bool True if this item is available to the user, false otherwise
967      */
968     public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
969         global $COURSE, $DB;
970         $this->require_data();
972         $available = true;
973         $information = '';
975         // Check each completion condition
976         if (count($this->item->conditionscompletion) > 0) {
977             if ($this->item->course == $COURSE->id) {
978                 $course = $COURSE;
979             } else {
980                 $course = $DB->get_record('course', array('id' => $this->item->course),
981                         'id, enablecompletion, modinfo, sectioncache', MUST_EXIST);
982             }
984             $completion = new completion_info($course);
985             foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
986                 // If this depends on a deleted module, handle that situation
987                 // gracefully.
988                 if (!$modinfo) {
989                     $modinfo = get_fast_modinfo($course);
990                 }
991                 if (empty($modinfo->cms[$cmid])) {
992                     global $PAGE;
993                     if (isset($PAGE) && strpos($PAGE->pagetype, 'course-view-')===0) {
994                         debugging("Warning: activity {$this->item->id} '{$this->item->name}' has condition " .
995                                 "on deleted activity $cmid (to get rid of this message, edit the named activity)");
996                     }
997                     continue;
998                 }
1000                 // The completion system caches its own data
1001                 $completiondata = $completion->get_data((object)array('id' => $cmid),
1002                         $grabthelot, $userid, $modinfo);
1004                 $thisisok = true;
1005                 if ($expectedcompletion==COMPLETION_COMPLETE) {
1006                     // 'Complete' also allows the pass, fail states
1007                     switch ($completiondata->completionstate) {
1008                         case COMPLETION_COMPLETE:
1009                         case COMPLETION_COMPLETE_FAIL:
1010                         case COMPLETION_COMPLETE_PASS:
1011                             break;
1012                         default:
1013                             $thisisok = false;
1014                     }
1015                 } else {
1016                     // Other values require exact match
1017                     if ($completiondata->completionstate!=$expectedcompletion) {
1018                         $thisisok = false;
1019                     }
1020                 }
1021                 if (!$thisisok) {
1022                     $available = false;
1023                     $information .= html_writer::start_tag('li');
1024                     $information .= get_string(
1025                         'requires_completion_' . $expectedcompletion,
1026                         'condition', $modinfo->cms[$cmid]->name) . ' ';
1027                     $information .= html_writer::end_tag('li');
1028                 }
1029             }
1030         }
1032         // Check each grade condition
1033         if (count($this->item->conditionsgrade)>0) {
1034             foreach ($this->item->conditionsgrade as $gradeitemid => $minmax) {
1035                 $score = $this->get_cached_grade_score($gradeitemid, $grabthelot, $userid);
1036                 if ($score===false ||
1037                         (!is_null($minmax->min) && $score<$minmax->min) ||
1038                         (!is_null($minmax->max) && $score>=$minmax->max)) {
1039                     // Grade fail
1040                     $available = false;
1041                     // String depends on type of requirement. We are coy about
1042                     // the actual numbers, in case grades aren't released to
1043                     // students.
1044                     if (is_null($minmax->min) && is_null($minmax->max)) {
1045                         $string = 'any';
1046                     } else if (is_null($minmax->max)) {
1047                         $string = 'min';
1048                     } else if (is_null($minmax->min)) {
1049                         $string = 'max';
1050                     } else {
1051                         $string = 'range';
1052                     }
1053                     $information .= html_writer::start_tag('li');
1054                     $information .= get_string('requires_grade_' . $string, 'condition', $minmax->name) . ' ';
1055                     $information .= html_writer::end_tag('li');
1056                 }
1057             }
1058         }
1060         // Check if user field condition
1061         if (count($this->item->conditionsfield) > 0) {
1062             $context = $this->get_context();
1063             foreach ($this->item->conditionsfield as $field => $details) {
1064                 $uservalue = $this->get_cached_user_profile_field($userid, $field);
1065                 if (!$this->is_field_condition_met($details->operator, $uservalue, $details->value)) {
1066                     // Set available to false
1067                     $available = false;
1068                     $a = new stdClass();
1069                     $a->field = format_string($details->fieldname, true, array('context' => $context));
1070                     $a->value = s($details->value);
1071                     $information .= html_writer::start_tag('li');
1072                     $information .= get_string('requires_user_field_'.$details->operator, 'condition', $a) . ' ';
1073                     $information .= html_writer::end_tag('li');
1074                 }
1075             }
1076         }
1078         // Test dates
1079         if ($this->item->availablefrom) {
1080             if (time() < $this->item->availablefrom) {
1081                 $available = false;
1083                 $information .= html_writer::start_tag('li');
1084                 $information .= get_string('requires_date', 'condition',
1085                         self::show_time($this->item->availablefrom,
1086                             self::is_midnight($this->item->availablefrom)));
1087                 $information .= html_writer::end_tag('li');
1088             }
1089         }
1091         if ($this->item->availableuntil) {
1092             if (time() >= $this->item->availableuntil) {
1093                 $available = false;
1094                 // But we don't display any information about this case. This is
1095                 // because the only reason to set a 'disappear' date is usually
1096                 // to get rid of outdated information/clutter in which case there
1097                 // is no point in showing it...
1099                 // Note it would be nice if we could make it so that the 'until'
1100                 // date appears below the item while the item is still accessible,
1101                 // unfortunately this is not possible in the current system. Maybe
1102                 // later, or if somebody else wants to add it.
1103             }
1104         }
1106         // If the item is marked as 'not visible' then we don't change the available
1107         // flag (visible/available are treated distinctly), but we remove any
1108         // availability info. If the item is hidden with the eye icon, it doesn't
1109         // make sense to show 'Available from <date>' or similar, because even
1110         // when that date arrives it will still not be available unless somebody
1111         // toggles the eye icon.
1112         if (!$this->item->visible) {
1113             $information = '';
1114         }
1116         // The information is in <li> tags, but to avoid taking up more space
1117         // if there is only a single item, we strip out the list tags so that it
1118         // is plain text in that case.
1119         if (!empty($information)) {
1120             $li = strpos($information, '<li>', 4);
1121             if ($li === false) {
1122                 $information = preg_replace('~^<li>(.*)</li>$~', '$1', $information);
1123             } else {
1124                 $information = html_writer::tag('ul', $information);
1125             }
1126             $information = trim($information);
1127         }
1128         return $available;
1129     }
1131     /**
1132      * Shows a time either as a date or a full date and time, according to
1133      * user's timezone.
1134      *
1135      * @param int $time Time
1136      * @param bool $dateonly If true, uses date only
1137      * @return string Date
1138      */
1139     private function show_time($time, $dateonly) {
1140         return userdate($time,
1141                 get_string($dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig'));
1142     }
1144     /**
1145      * Checks whether availability information should be shown to normal users.
1146      *
1147      * @return bool True if information about availability should be shown to
1148      *   normal users
1149      * @throws coding_exception If data wasn't loaded
1150      */
1151     public function show_availability() {
1152         $this->require_data();
1153         return $this->item->showavailability;
1154     }
1156     /**
1157      * Internal function cheks that data was loaded.
1158      *
1159      * @throws coding_exception If data wasn't loaded
1160      */
1161     private function require_data() {
1162         if (!$this->gotdata) {
1163             throw new coding_exception('Error: cannot call when info was ' .
1164                 'constructed without data');
1165         }
1166     }
1168     /**
1169      * Obtains a grade score. Note that this score should not be displayed to
1170      * the user, because gradebook rules might prohibit that. It may be a
1171      * non-final score subject to adjustment later.
1172      *
1173      * @global stdClass $USER
1174      * @global moodle_database $DB
1175      * @global stdClass $SESSION
1176      * @param int $gradeitemid Grade item ID we're interested in
1177      * @param bool $grabthelot If true, grabs all scores for current user on
1178      *   this course, so that later ones come from cache
1179      * @param int $userid Set if requesting grade for a different user (does
1180      *   not use cache)
1181      * @return float Grade score as a percentage in range 0-100 (e.g. 100.0
1182      *   or 37.21), or false if user does not have a grade yet
1183      */
1184     private function get_cached_grade_score($gradeitemid, $grabthelot=false, $userid=0) {
1185         global $USER, $DB, $SESSION;
1186         if ($userid==0 || $userid==$USER->id) {
1187             // For current user, go via cache in session
1188             if (empty($SESSION->gradescorecache) || $SESSION->gradescorecacheuserid!=$USER->id) {
1189                 $SESSION->gradescorecache = array();
1190                 $SESSION->gradescorecacheuserid = $USER->id;
1191             }
1192             if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
1193                 if ($grabthelot) {
1194                     // Get all grades for the current course
1195                     $rs = $DB->get_recordset_sql('
1196                             SELECT
1197                                 gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax
1198                             FROM
1199                                 {grade_items} gi
1200                                 LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=?
1201                             WHERE
1202                                 gi.courseid = ?', array($USER->id, $this->item->course));
1203                     foreach ($rs as $record) {
1204                         $SESSION->gradescorecache[$record->id] =
1205                             is_null($record->finalgrade)
1206                                 // No grade = false
1207                                 ? false
1208                                 // Otherwise convert grade to percentage
1209                                 : (($record->finalgrade - $record->rawgrademin) * 100) /
1210                                     ($record->rawgrademax - $record->rawgrademin);
1212                     }
1213                     $rs->close();
1214                     // And if it's still not set, well it doesn't exist (eg
1215                     // maybe the user set it as a condition, then deleted the
1216                     // grade item) so we call it false
1217                     if (!array_key_exists($gradeitemid, $SESSION->gradescorecache)) {
1218                         $SESSION->gradescorecache[$gradeitemid] = false;
1219                     }
1220                 } else {
1221                     // Just get current grade
1222                     $record = $DB->get_record('grade_grades', array(
1223                         'userid'=>$USER->id, 'itemid'=>$gradeitemid));
1224                     if ($record && !is_null($record->finalgrade)) {
1225                         $score = (($record->finalgrade - $record->rawgrademin) * 100) /
1226                             ($record->rawgrademax - $record->rawgrademin);
1227                     } else {
1228                         // Treat the case where row exists but is null, same as
1229                         // case where row doesn't exist
1230                         $score = false;
1231                     }
1232                     $SESSION->gradescorecache[$gradeitemid]=$score;
1233                 }
1234             }
1235             return $SESSION->gradescorecache[$gradeitemid];
1236         } else {
1237             // Not the current user, so request the score individually
1238             $record = $DB->get_record('grade_grades', array(
1239                 'userid'=>$userid, 'itemid'=>$gradeitemid));
1240             if ($record && !is_null($record->finalgrade)) {
1241                 $score = (($record->finalgrade - $record->rawgrademin) * 100) /
1242                     ($record->rawgrademax - $record->rawgrademin);
1243             } else {
1244                 // Treat the case where row exists but is null, same as
1245                 // case where row doesn't exist
1246                 $score = false;
1247             }
1248             return $score;
1249         }
1250     }
1252     /**
1253      * Returns true if a field meets the required conditions, false otherwise.
1254      *
1255      * @param string $operator the requirement/condition
1256      * @param string $uservalue the user's value
1257      * @param string $value the value required
1258      * @return boolean
1259      */
1260     private function is_field_condition_met($operator, $uservalue, $value) {
1261         if ($uservalue === false) {
1262             // If the user value is false this is an instant fail.
1263             // All user values come from the database as either data or the default.
1264             // They will always be a string.
1265             return false;
1266         }
1267         $fieldconditionmet = true;
1268         // Just to be doubly sure it is a string.
1269         $uservalue = (string)$uservalue;
1270         switch($operator) {
1271             case OP_CONTAINS: // contains
1272                 $pos = strpos($uservalue, $value);
1273                 if ($pos === false) {
1274                     $fieldconditionmet = false;
1275                 }
1276                 break;
1277             case OP_DOES_NOT_CONTAIN: // does not contain
1278                 if (!empty($value)) {
1279                     $pos = strpos($uservalue, $value);
1280                     if ($pos !== false) {
1281                         $fieldconditionmet = false;
1282                     }
1283                 }
1284                 break;
1285             case OP_IS_EQUAL_TO: // equal to
1286                 if ($value !== $uservalue) {
1287                     $fieldconditionmet = false;
1288                 }
1289                 break;
1290             case OP_STARTS_WITH: // starts with
1291                 $length = strlen($value);
1292                 if ((substr($uservalue, 0, $length) !== $value)) {
1293                     $fieldconditionmet = false;
1294                 }
1295                 break;
1296             case OP_ENDS_WITH: // ends with
1297                 $length = strlen($value);
1298                 $start  = $length * -1; // negative
1299                 if (substr($uservalue, $start) !== $value) {
1300                     $fieldconditionmet = false;
1301                 }
1302                 break;
1303             case OP_IS_EMPTY: // is empty
1304                 if (!empty($uservalue)) {
1305                     $fieldconditionmet = false;
1306                 }
1307                 break;
1308             case OP_IS_NOT_EMPTY: // is not empty
1309                 if (empty($uservalue)) {
1310                     $fieldconditionmet = false;
1311                 }
1312                 break;
1313         }
1314         return $fieldconditionmet;
1315     }
1317     /**
1318      * Return the value for a user's profile field
1319      *
1320      * @param int $userid set if requesting grade for a different user (does not use cache)
1321      * @param int $fieldid the user profile field id
1322      * @return string the user value, or false if user does not have a user field value yet
1323      */
1324     protected function get_cached_user_profile_field($userid, $fieldid) {
1325         global $USER, $DB, $CFG;
1327         if ($userid === 0) {
1328             // Map out userid = 0 to the current user
1329             $userid = $USER->id;
1330         }
1331         $iscurrentuser = $USER->id == $userid;
1333         if (isguestuser($userid) || ($iscurrentuser && !isloggedin())) {
1334             // Must be logged in and can't be the guest. (e.g. front page)
1335             return false;
1336         }
1338         // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
1339         $iscustomprofilefield = is_numeric($fieldid);
1340         if ($iscustomprofilefield) {
1341             // As its a custom profile field we need to map the id back to the actual field.
1342             // We'll also preload all of the other custom profile fields just in case and ensure we have the
1343             // default value available as well.
1344             if ($this->customprofilefields === null) {
1345                 $this->customprofilefields = $DB->get_records('user_info_field', null, 'sortorder ASC, id ASC', 'id, shortname, defaultdata');
1346             }
1347             if (!array_key_exists($fieldid, $this->customprofilefields)) {
1348                 // No such field exists.
1349                 // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
1350                 // or when restoring a backup of a course with user profile field conditions.
1351                 return false;
1352             }
1353             $field = $this->customprofilefields[$fieldid]->shortname;
1354         } else {
1355             $field = $fieldid;
1356         }
1358         // If its the current user than most likely we will be able to get this information from $USER.
1359         // If its a regular profile field then it should already be available, if not then we have a mega problem.
1360         // If its a custom profile field then it should be available but may not be. If it is then we use the value
1361         // available, otherwise we load all custom profile fields into a temp object and refer to that.
1362         // Noting its not going be great for performance if we have to use the temp object as it involves loading the
1363         // custom profile field API and classes.
1364         if ($iscurrentuser) {
1365             if (!$iscustomprofilefield) {
1366                 if (property_exists($USER, $field)) {
1367                     return $USER->{$field};
1368                 } else {
1369                     // Unknown user field. This should not happen.
1370                     throw new coding_exception('Requested user profile field does not exist');
1371                 }
1372             }
1373             // Checking if the custom profile fields are already available.
1374             if (!isset($USER->profile)) {
1375                 // Drat! they're not. We need to use a temp object and load them.
1376                 // We don't use $USER as the profile fields are loaded into the object.
1377                 $user = new stdClass;
1378                 $user->id = $USER->id;
1379                 // This should ALWAYS be set, but just in case we check.
1380                 require_once($CFG->dirroot.'/user/profile/lib.php');
1381                 profile_load_custom_fields($user);
1382                 if (array_key_exists($field, $user->profile)) {
1383                     return $user->profile[$field];
1384                 }
1385             } else if (array_key_exists($field, $USER->profile)) {
1386                 // Hurrah they're available, this is easy.
1387                 return $USER->profile[$field];
1388             }
1389             // The profile field doesn't exist.
1390             return false;
1391         } else {
1392             // Loading for another user.
1393             if ($iscustomprofilefield) {
1394                 // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
1395                 // for us (this will likely be hit again).
1396                 // We are able to do this because we've already pre-loaded the custom fields.
1397                 $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid, 'fieldid' => $fieldid), IGNORE_MISSING);
1398                 // If we have data return that, otherwise return the default.
1399                 if ($data !== false) {
1400                     return $data;
1401                 } else {
1402                     return $this->customprofilefields[$field]->defaultdata;
1403                 }
1404             } else {
1405                 // Its a standard field, retrieve it from the user.
1406                 return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST);
1407             }
1408         }
1409         return false;
1410     }
1412     /**
1413      * For testing only. Wipes information cached in user session.
1414      *
1415      * @global stdClass $SESSION
1416      */
1417     static function wipe_session_cache() {
1418         global $SESSION;
1419         unset($SESSION->gradescorecache);
1420         unset($SESSION->gradescorecacheuserid);
1421         unset($SESSION->userfieldcache);
1422         unset($SESSION->userfieldcacheuserid);
1423     }
1425     /**
1426      * Initialises the global cache
1427      * @global stdClass $CONDITIONLIB_PRIVATE
1428      */
1429     public static function init_global_cache() {
1430         global $CONDITIONLIB_PRIVATE;
1431         $CONDITIONLIB_PRIVATE = new stdClass;
1432         $CONDITIONLIB_PRIVATE->usedincondition = array();
1433         $CONDITIONLIB_PRIVATE->groupingscache = array();
1434     }
1436     /**
1437      * Utility function that resets grade/completion conditions in table based
1438      * in data from editing form.
1439      *
1440      * @param condition_info_base $ci Condition info
1441      * @param object $fromform Data from form
1442      * @param bool $wipefirst If true, wipes existing conditions
1443      */
1444     protected static function update_from_form(condition_info_base $ci, $fromform, $wipefirst) {
1445         if ($wipefirst) {
1446             $ci->wipe_conditions();
1447         }
1448         foreach ($fromform->conditiongradegroup as $record) {
1449             if($record['conditiongradeitemid']) {
1450                 $ci->add_grade_condition($record['conditiongradeitemid'],
1451                     unformat_float($record['conditiongrademin']), unformat_float($record['conditiongrademax']));
1452             }
1453         }
1454         foreach ($fromform->conditionfieldgroup as $record) {
1455             if($record['conditionfield']) {
1456                 $ci->add_user_field_condition($record['conditionfield'],
1457                         $record['conditionfieldoperator'],
1458                         $record['conditionfieldvalue']);
1459             }
1460         }
1461         if(isset ($fromform->conditioncompletiongroup)) {
1462             foreach($fromform->conditioncompletiongroup as $record) {
1463                 if($record['conditionsourcecmid']) {
1464                     $ci->add_completion_condition($record['conditionsourcecmid'],
1465                         $record['conditionrequiredcompletion']);
1466                 }
1467             }
1468         }
1469     }
1471     /**
1472      * Obtains context for any necessary checks.
1473      *
1474      * @return context Suitable context for the item
1475      */
1476     protected abstract function get_context();
1479 condition_info::init_global_cache();