Merge branch 'MDL-47481-master' of https://github.com/sammarshallou/moodle
[moodle.git] / availability / condition / profile / classes / condition.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  * User profile field condition.
19  *
20  * @package availability_profile
21  * @copyright 2014 The Open University
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace availability_profile;
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * User profile field condition.
31  *
32  * @package availability_profile
33  * @copyright 2014 The Open University
34  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
36 class condition extends \core_availability\condition {
37     /** @var string Operator: field contains value */
38     const OP_CONTAINS = 'contains';
40     /** @var string Operator: field does not contain value */
41     const OP_DOES_NOT_CONTAIN = 'doesnotcontain';
43     /** @var string Operator: field equals value */
44     const OP_IS_EQUAL_TO = 'isequalto';
46     /** @var string Operator: field starts with value */
47     const OP_STARTS_WITH = 'startswith';
49     /** @var string Operator: field ends with value */
50     const OP_ENDS_WITH = 'endswith';
52     /** @var string Operator: field is empty */
53     const OP_IS_EMPTY = 'isempty';
55     /** @var string Operator: field is not empty */
56     const OP_IS_NOT_EMPTY = 'isnotempty';
58     /** @var array|null Array of custom profile fields (static cache within request) */
59     protected static $customprofilefields = null;
61     /** @var string Field name (for standard fields) or '' if custom field */
62     protected $standardfield = '';
64     /** @var int Field name (for custom fields) or '' if standard field */
65     protected $customfield = '';
67     /** @var string Operator type (OP_xx constant) */
68     protected $operator;
70     /** @var string Expected value for field */
71     protected $value = '';
73     /**
74      * Constructor.
75      *
76      * @param \stdClass $structure Data structure from JSON decode
77      * @throws \coding_exception If invalid data structure.
78      */
79     public function __construct($structure) {
80         // Get operator.
81         if (isset($structure->op) && in_array($structure->op, array(self::OP_CONTAINS,
82                 self::OP_DOES_NOT_CONTAIN, self::OP_IS_EQUAL_TO, self::OP_STARTS_WITH,
83                 self::OP_ENDS_WITH, self::OP_IS_EMPTY, self::OP_IS_NOT_EMPTY), true)) {
84             $this->operator = $structure->op;
85         } else {
86             throw new \coding_exception('Missing or invalid ->op for profile condition');
87         }
89         // For operators other than the empty/not empty ones, require value.
90         switch($this->operator) {
91             case self::OP_IS_EMPTY:
92             case self::OP_IS_NOT_EMPTY:
93                 if (isset($structure->v)) {
94                     throw new \coding_exception('Unexpected ->v for non-value operator');
95                 }
96                 break;
97             default:
98                 if (isset($structure->v) && is_string($structure->v)) {
99                     $this->value = $structure->v;
100                 } else {
101                     throw new \coding_exception('Missing or invalid ->v for profile condition');
102                 }
103                 break;
104         }
106         // Get field type.
107         if (property_exists($structure, 'sf')) {
108             if (property_exists($structure, 'cf')) {
109                 throw new \coding_exception('Both ->sf and ->cf for profile condition');
110             }
111             if (is_string($structure->sf)) {
112                 $this->standardfield = $structure->sf;
113             } else {
114                 throw new \coding_exception('Invalid ->sf for profile condition');
115             }
116         } else if (property_exists($structure, 'cf')) {
117             if (is_string($structure->cf)) {
118                 $this->customfield = $structure->cf;
119             } else {
120                 throw new \coding_exception('Invalid ->cf for profile condition');
121             }
122         } else {
123             throw new \coding_exception('Missing ->sf or ->cf for profile condition');
124         }
125     }
127     public function save() {
128         $result = (object)array('type' => 'profile', 'op' => $this->operator);
129         if ($this->customfield) {
130             $result->cf = $this->customfield;
131         } else {
132             $result->sf = $this->standardfield;
133         }
134         switch($this->operator) {
135             case self::OP_IS_EMPTY:
136             case self::OP_IS_NOT_EMPTY:
137                 break;
138             default:
139                 $result->v = $this->value;
140                 break;
141         }
142         return $result;
143     }
145     /**
146      * Returns a JSON object which corresponds to a condition of this type.
147      *
148      * Intended for unit testing, as normally the JSON values are constructed
149      * by JavaScript code.
150      *
151      * @param bool $customfield True if this is a custom field
152      * @param string $fieldname Field name
153      * @param string $operator Operator name (OP_xx constant)
154      * @param string|null $value Value (not required for some operator types)
155      * @return stdClass Object representing condition
156      */
157     public static function get_json($customfield, $fieldname, $operator, $value = null) {
158         $result = (object)array('type' => 'profile', 'op' => $operator);
159         if ($customfield) {
160             $result->cf = $fieldname;
161         } else {
162             $result->sf = $fieldname;
163         }
164         switch ($operator) {
165             case self::OP_IS_EMPTY:
166             case self::OP_IS_NOT_EMPTY:
167                 break;
168             default:
169                 if (is_null($value)) {
170                     throw new \coding_exception('Operator requires value');
171                 }
172                 $result->v = $value;
173                 break;
174         }
175         return $result;
176     }
178     public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
179         $uservalue = $this->get_cached_user_profile_field($userid);
180         $allow = self::is_field_condition_met($this->operator, $uservalue, $this->value);
181         if ($not) {
182             $allow = !$allow;
183         }
184         return $allow;
185     }
187     public function get_description($full, $not, \core_availability\info $info) {
188         $course = $info->get_course();
189         // Display the fieldname into current lang.
190         if ($this->customfield) {
191             // Is a custom profile field (will use multilang).
192             $customfields = self::get_custom_profile_fields();
193             if (array_key_exists($this->customfield, $customfields)) {
194                 $translatedfieldname = $customfields[$this->customfield]->name;
195             } else {
196                 $translatedfieldname = get_string('missing', 'availability_profile',
197                         $this->customfield);
198             }
199         } else {
200             $translatedfieldname = get_user_field_name($this->standardfield);
201         }
202         $context = \context_course::instance($course->id);
203         $a = new \stdClass();
204         $a->field = format_string($translatedfieldname, true, array('context' => $context));
205         $a->value = s($this->value);
206         if ($not) {
207             // When doing NOT strings, we replace the operator with its inverse.
208             // Some of them don't have inverses, so for those we use a new
209             // identifier which is only used for this lang string.
210             switch($this->operator) {
211                 case self::OP_CONTAINS:
212                     $opname = self::OP_DOES_NOT_CONTAIN;
213                     break;
214                 case self::OP_DOES_NOT_CONTAIN:
215                     $opname = self::OP_CONTAINS;
216                     break;
217                 case self::OP_ENDS_WITH:
218                     $opname = 'notendswith';
219                     break;
220                 case self::OP_IS_EMPTY:
221                     $opname = self::OP_IS_NOT_EMPTY;
222                     break;
223                 case self::OP_IS_EQUAL_TO:
224                     $opname = 'notisequalto';
225                     break;
226                 case self::OP_IS_NOT_EMPTY:
227                     $opname = self::OP_IS_EMPTY;
228                     break;
229                 case self::OP_STARTS_WITH:
230                     $opname = 'notstartswith';
231                     break;
232                 default:
233                     throw new \coding_exception('Unexpected operator: ' . $this->operator);
234             }
235         } else {
236             $opname = $this->operator;
237         }
238         return get_string('requires_' . $opname, 'availability_profile', $a);
239     }
241     protected function get_debug_string() {
242         if ($this->customfield) {
243             $out = '*' . $this->customfield;
244         } else {
245             $out = $this->standardfield;
246         }
247         $out .= ' ' . $this->operator;
248         switch($this->operator) {
249             case self::OP_IS_EMPTY:
250             case self::OP_IS_NOT_EMPTY:
251                 break;
252             default:
253                 $out .= ' ' . $this->value;
254                 break;
255         }
256         return $out;
257     }
259     /**
260      * Returns true if a field meets the required conditions, false otherwise.
261      *
262      * @param string $operator the requirement/condition
263      * @param string $uservalue the user's value
264      * @param string $value the value required
265      * @return boolean True if conditions are met
266      */
267     protected static function is_field_condition_met($operator, $uservalue, $value) {
268         if ($uservalue === false) {
269             // If the user value is false this is an instant fail.
270             // All user values come from the database as either data or the default.
271             // They will always be a string.
272             return false;
273         }
274         $fieldconditionmet = true;
275         // Just to be doubly sure it is a string.
276         $uservalue = (string)$uservalue;
277         switch($operator) {
278             case self::OP_CONTAINS:
279                 $pos = strpos($uservalue, $value);
280                 if ($pos === false) {
281                     $fieldconditionmet = false;
282                 }
283                 break;
284             case self::OP_DOES_NOT_CONTAIN:
285                 if (!empty($value)) {
286                     $pos = strpos($uservalue, $value);
287                     if ($pos !== false) {
288                         $fieldconditionmet = false;
289                     }
290                 }
291                 break;
292             case self::OP_IS_EQUAL_TO:
293                 if ($value !== $uservalue) {
294                     $fieldconditionmet = false;
295                 }
296                 break;
297             case self::OP_STARTS_WITH:
298                 $length = strlen($value);
299                 if ((substr($uservalue, 0, $length) !== $value)) {
300                     $fieldconditionmet = false;
301                 }
302                 break;
303             case self::OP_ENDS_WITH:
304                 $length = strlen($value);
305                 $start = $length * -1;
306                 if (substr($uservalue, $start) !== $value) {
307                     $fieldconditionmet = false;
308                 }
309                 break;
310             case self::OP_IS_EMPTY:
311                 if (!empty($uservalue)) {
312                     $fieldconditionmet = false;
313                 }
314                 break;
315             case self::OP_IS_NOT_EMPTY:
316                 if (empty($uservalue)) {
317                     $fieldconditionmet = false;
318                 }
319                 break;
320         }
321         return $fieldconditionmet;
322     }
324     /**
325      * Gets data about custom profile fields. Cached statically in current
326      * request.
327      *
328      * This only includes fields which can be tested by the system (those whose
329      * data is cached in $USER object) - basically doesn't include textarea type
330      * fields.
331      *
332      * @return array Array of records indexed by shortname
333      */
334     public static function get_custom_profile_fields() {
335         global $DB, $CFG;
337         if (self::$customprofilefields === null) {
338             // Get fields and store them indexed by shortname.
339             require_once($CFG->dirroot . '/user/profile/lib.php');
340             $fields = profile_get_custom_fields(true);
341             self::$customprofilefields = array();
342             foreach ($fields as $field) {
343                 self::$customprofilefields[$field->shortname] = $field;
344             }
345         }
346         return self::$customprofilefields;
347     }
349     /**
350      * Wipes the static cache (for use in unit tests).
351      */
352     public static function wipe_static_cache() {
353         self::$customprofilefields = null;
354     }
356     /**
357      * Return the value for a user's profile field
358      *
359      * @param int $userid User ID
360      * @return string|bool Value, or false if user does not have a value for this field
361      */
362     protected function get_cached_user_profile_field($userid) {
363         global $USER, $DB, $CFG;
364         $iscurrentuser = $USER->id == $userid;
365         if (isguestuser($userid) || ($iscurrentuser && !isloggedin())) {
366             // Must be logged in and can't be the guest.
367             return false;
368         }
370         // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
371         $iscustomprofilefield = $this->customfield ? true : false;
372         if ($iscustomprofilefield) {
373             // As its a custom profile field we need to map the id back to the actual field.
374             // We'll also preload all of the other custom profile fields just in case and ensure we have the
375             // default value available as well.
376             if (!array_key_exists($this->customfield, self::get_custom_profile_fields())) {
377                 // No such field exists.
378                 // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
379                 // or when restoring a backup of a course with user profile field conditions.
380                 return false;
381             }
382             $field = $this->customfield;
383         } else {
384             $field = $this->standardfield;
385         }
387         // If its the current user than most likely we will be able to get this information from $USER.
388         // If its a regular profile field then it should already be available, if not then we have a mega problem.
389         // If its a custom profile field then it should be available but may not be. If it is then we use the value
390         // available, otherwise we load all custom profile fields into a temp object and refer to that.
391         // Noting its not going be great for performance if we have to use the temp object as it involves loading the
392         // custom profile field API and classes.
393         if ($iscurrentuser) {
394             if (!$iscustomprofilefield) {
395                 if (property_exists($USER, $field)) {
396                     return $USER->{$field};
397                 } else {
398                     // Unknown user field. This should not happen.
399                     throw new \coding_exception('Requested user profile field does not exist');
400                 }
401             }
402             // Checking if the custom profile fields are already available.
403             if (!isset($USER->profile)) {
404                 // Drat! they're not. We need to use a temp object and load them.
405                 // We don't use $USER as the profile fields are loaded into the object.
406                 $user = new \stdClass;
407                 $user->id = $USER->id;
408                 // This should ALWAYS be set, but just in case we check.
409                 require_once($CFG->dirroot . '/user/profile/lib.php');
410                 profile_load_custom_fields($user);
411                 if (array_key_exists($field, $user->profile)) {
412                     return $user->profile[$field];
413                 }
414             } else if (array_key_exists($field, $USER->profile)) {
415                 // Hurrah they're available, this is easy.
416                 return $USER->profile[$field];
417             }
418             // The profile field doesn't exist.
419             return false;
420         } else {
421             // Loading for another user.
422             if ($iscustomprofilefield) {
423                 // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
424                 // for us (this will likely be hit again).
425                 // We are able to do this because we've already pre-loaded the custom fields.
426                 $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid,
427                         'fieldid' => self::$customprofilefields[$field]->id), IGNORE_MISSING);
428                 // If we have data return that, otherwise return the default.
429                 if ($data !== false) {
430                     return $data;
431                 } else {
432                     return self::$customprofilefields[$field]->defaultdata;
433                 }
434             } else {
435                 // Its a standard field, retrieve it from the user.
436                 return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST);
437             }
438         }
439         return false;
440     }
442     public function is_applied_to_user_lists() {
443         // Profile conditions are assumed to be 'permanent', so they affect the
444         // display of user lists for activities.
445         return true;
446     }
448     public function filter_user_list(array $users, $not, \core_availability\info $info,
449             \core_availability\capability_checker $checker) {
450         global $CFG, $DB;
452         // If the array is empty already, just return it.
453         if (!$users) {
454             return $users;
455         }
457         // Get all users from the list who match the condition.
458         list ($sql, $params) = $DB->get_in_or_equal(array_keys($users));
460         if ($this->customfield) {
461             $customfields = self::get_custom_profile_fields();
462             if (!array_key_exists($this->customfield, $customfields)) {
463                 // If the field isn't found, nobody matches.
464                 return array();
465             }
466             $customfield = $customfields[$this->customfield];
468             // Fetch custom field value for all users.
469             $values = $DB->get_records_select('user_info_data', 'fieldid = ? AND userid ' . $sql,
470                     array_merge(array($customfield->id), $params),
471                     '', 'userid, data');
472             $valuefield = 'data';
473             $default = $customfield->defaultdata;
474         } else {
475             $values = $DB->get_records_select('user', 'id ' . $sql, $params,
476                     '', 'id, '. $this->standardfield);
477             $valuefield = $this->standardfield;
478             $default = '';
479         }
481         // Filter the user list.
482         $result = array();
483         foreach ($users as $id => $user) {
484             // Get value for user.
485             if (array_key_exists($id, $values)) {
486                 $value = $values[$id]->{$valuefield};
487             } else {
488                 $value = $default;
489             }
491             // Check value.
492             $allow = $this->is_field_condition_met($this->operator, $value, $this->value);
493             if ($not) {
494                 $allow = !$allow;
495             }
496             if ($allow) {
497                 $result[$id] = $user;
498             }
499         }
500         return $result;
501     }
503     /**
504      * Gets SQL to match a field against this condition. The second copy of the
505      * field is in case you're using variables for the field so that it needs
506      * to be two different ones.
507      *
508      * @param string $field Field name
509      * @param string $field2 Second copy of field name (default same).
510      * @return array Array of SQL and parameters
511      */
512     private function get_condition_sql($field, $field2 = null) {
513         global $DB;
514         if (is_null($field2)) {
515             $field2 = $field;
516         }
518         $params = array();
519         switch($this->operator) {
520             case self::OP_CONTAINS:
521                 $sql = $DB->sql_like($field, self::unique_sql_parameter(
522                         $params, '%' . $this->value . '%'));
523                 break;
524             case self::OP_DOES_NOT_CONTAIN:
525                 if (empty($this->value)) {
526                     // The 'does not contain nothing' expression matches everyone.
527                     return null;
528                 }
529                 $sql = $DB->sql_like($field, self::unique_sql_parameter(
530                         $params, '%' . $this->value . '%'), true, true, true);
531                 break;
532             case self::OP_IS_EQUAL_TO:
533                 $sql = $field . ' = ' . self::unique_sql_parameter(
534                         $params, $this->value);
535                 break;
536             case self::OP_STARTS_WITH:
537                 $sql = $DB->sql_like($field, self::unique_sql_parameter(
538                         $params, $this->value . '%'));
539                 break;
540             case self::OP_ENDS_WITH:
541                 $sql = $DB->sql_like($field, self::unique_sql_parameter(
542                         $params, '%' . $this->value));
543                 break;
544             case self::OP_IS_EMPTY:
545                 // Mimic PHP empty() behaviour for strings, '0' or ''.
546                 $sql = '(' . $field . " IN ('0', '') OR $field2 IS NULL)";
547                 break;
548             case self::OP_IS_NOT_EMPTY:
549                 $sql = '(' . $field . " NOT IN ('0', '') AND $field2 IS NOT NULL)";
550                 break;
551         }
552         return array($sql, $params);
553     }
555     public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) {
556         global $DB;
558         // Build suitable SQL depending on custom or standard field.
559         if ($this->customfield) {
560             $customfields = self::get_custom_profile_fields();
561             if (!array_key_exists($this->customfield, $customfields)) {
562                 // If the field isn't found, nobody matches.
563                 return array('SELECT id FROM {user} WHERE 0 = 1', array());
564             }
565             $customfield = $customfields[$this->customfield];
567             $mainparams = array();
568             $tablesql = "LEFT JOIN {user_info_data} uid ON uid.fieldid = " .
569                     self::unique_sql_parameter($mainparams, $customfield->id) .
570                     " AND uid.userid = userids.id";
571             list ($condition, $conditionparams) = $this->get_condition_sql('uid.data');
572             $mainparams = array_merge($mainparams, $conditionparams);
574             // If default is true, then allow that too.
575             if ($this->is_field_condition_met(
576                     $this->operator, $customfield->defaultdata, $this->value)) {
577                 $where = "((uid.data IS NOT NULL AND $condition) OR (uid.data IS NULL))";
578             } else {
579                 $where = "(uid.data IS NOT NULL AND $condition)";
580             }
581         } else {
582             $tablesql = "JOIN {user} u ON u.id = userids.id";
583             list ($where, $mainparams) = $this->get_condition_sql(
584                     'u.' . $this->standardfield);
585         }
587         // Handle NOT.
588         if ($not) {
589             $where = 'NOT (' . $where . ')';
590         }
592         // Get enrolled user SQL and combine with this query.
593         list ($enrolsql, $enrolparams) =
594                 get_enrolled_sql($info->get_context(), '', 0, $onlyactive);
595         $sql = "SELECT userids.id
596                   FROM ($enrolsql) userids
597                        $tablesql
598                  WHERE $where";
599         $params = array_merge($enrolparams, $mainparams);
600         return array($sql, $params);
601     }