Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * 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
 */
24
 
25
namespace availability_profile;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
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';
39
 
40
    /** @var string Operator: field does not contain value */
41
    const OP_DOES_NOT_CONTAIN = 'doesnotcontain';
42
 
43
    /** @var string Operator: field equals value */
44
    const OP_IS_EQUAL_TO = 'isequalto';
45
 
46
    /** @var string Operator: field starts with value */
47
    const OP_STARTS_WITH = 'startswith';
48
 
49
    /** @var string Operator: field ends with value */
50
    const OP_ENDS_WITH = 'endswith';
51
 
52
    /** @var string Operator: field is empty */
53
    const OP_IS_EMPTY = 'isempty';
54
 
55
    /** @var string Operator: field is not empty */
56
    const OP_IS_NOT_EMPTY = 'isnotempty';
57
 
58
    /** @var array|null Array of custom profile fields (static cache within request) */
59
    protected static $customprofilefields = null;
60
 
61
    /** @var string Field name (for standard fields) or '' if custom field */
62
    protected $standardfield = '';
63
 
64
    /** @var int Field name (for custom fields) or '' if standard field */
65
    protected $customfield = '';
66
 
67
    /** @var string Operator type (OP_xx constant) */
68
    protected $operator;
69
 
70
    /** @var string Expected value for field */
71
    protected $value = '';
72
 
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
        }
88
 
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
        }
105
 
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
    }
126
 
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
    }
144
 
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
    }
177
 
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
    }
186
 
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
            $standardfields = self::get_standard_profile_fields();
201
            if (array_key_exists($this->standardfield, $standardfields)) {
202
                $translatedfieldname = $standardfields[$this->standardfield];
203
            } else {
204
                $translatedfieldname = get_string('missing', 'availability_profile', $this->standardfield);
205
            }
206
        }
207
        $a = new \stdClass();
208
        // Not safe to call format_string here; use the special function to call it later.
209
        $a->field = self::description_format_string($translatedfieldname);
210
        $a->value = s($this->value);
211
        if ($not) {
212
            // When doing NOT strings, we replace the operator with its inverse.
213
            // Some of them don't have inverses, so for those we use a new
214
            // identifier which is only used for this lang string.
215
            switch($this->operator) {
216
                case self::OP_CONTAINS:
217
                    $opname = self::OP_DOES_NOT_CONTAIN;
218
                    break;
219
                case self::OP_DOES_NOT_CONTAIN:
220
                    $opname = self::OP_CONTAINS;
221
                    break;
222
                case self::OP_ENDS_WITH:
223
                    $opname = 'notendswith';
224
                    break;
225
                case self::OP_IS_EMPTY:
226
                    $opname = self::OP_IS_NOT_EMPTY;
227
                    break;
228
                case self::OP_IS_EQUAL_TO:
229
                    $opname = 'notisequalto';
230
                    break;
231
                case self::OP_IS_NOT_EMPTY:
232
                    $opname = self::OP_IS_EMPTY;
233
                    break;
234
                case self::OP_STARTS_WITH:
235
                    $opname = 'notstartswith';
236
                    break;
237
                default:
238
                    throw new \coding_exception('Unexpected operator: ' . $this->operator);
239
            }
240
        } else {
241
            $opname = $this->operator;
242
        }
243
        return get_string('requires_' . $opname, 'availability_profile', $a);
244
    }
245
 
246
    protected function get_debug_string() {
247
        if ($this->customfield) {
248
            $out = '*' . $this->customfield;
249
        } else {
250
            $out = $this->standardfield;
251
        }
252
        $out .= ' ' . $this->operator;
253
        switch($this->operator) {
254
            case self::OP_IS_EMPTY:
255
            case self::OP_IS_NOT_EMPTY:
256
                break;
257
            default:
258
                $out .= ' ' . $this->value;
259
                break;
260
        }
261
        return $out;
262
    }
263
 
264
    /**
265
     * Returns true if a field meets the required conditions, false otherwise.
266
     *
267
     * @param string $operator the requirement/condition
268
     * @param string $uservalue the user's value
269
     * @param string $value the value required
270
     * @return boolean True if conditions are met
271
     */
272
    protected static function is_field_condition_met($operator, $uservalue, $value) {
273
        if ($uservalue === false) {
274
            // If the user value is false this is an instant fail.
275
            // All user values come from the database as either data or the default.
276
            // They will always be a string.
277
            return false;
278
        }
279
        $fieldconditionmet = true;
280
        // Just to be doubly sure it is a string.
281
        $uservalue = (string)$uservalue;
282
        switch($operator) {
283
            case self::OP_CONTAINS:
284
                $pos = strpos($uservalue, $value);
285
                if ($pos === false) {
286
                    $fieldconditionmet = false;
287
                }
288
                break;
289
            case self::OP_DOES_NOT_CONTAIN:
290
                if (!empty($value)) {
291
                    $pos = strpos($uservalue, $value);
292
                    if ($pos !== false) {
293
                        $fieldconditionmet = false;
294
                    }
295
                }
296
                break;
297
            case self::OP_IS_EQUAL_TO:
298
                if ($value !== $uservalue) {
299
                    $fieldconditionmet = false;
300
                }
301
                break;
302
            case self::OP_STARTS_WITH:
303
                $length = strlen($value);
304
                if ((substr($uservalue, 0, $length) !== $value)) {
305
                    $fieldconditionmet = false;
306
                }
307
                break;
308
            case self::OP_ENDS_WITH:
309
                $length = strlen($value);
310
                $start = $length * -1;
311
                if (substr($uservalue, $start) !== $value) {
312
                    $fieldconditionmet = false;
313
                }
314
                break;
315
            case self::OP_IS_EMPTY:
316
                if (!empty($uservalue)) {
317
                    $fieldconditionmet = false;
318
                }
319
                break;
320
            case self::OP_IS_NOT_EMPTY:
321
                if (empty($uservalue)) {
322
                    $fieldconditionmet = false;
323
                }
324
                break;
325
        }
326
        return $fieldconditionmet;
327
    }
328
 
329
    /**
330
     * Return list of standard user profile fields used by the condition
331
     *
332
     * @return string[]
333
     */
334
    public static function get_standard_profile_fields(): array {
335
        return [
336
            'firstname' => \core_user\fields::get_display_name('firstname'),
337
            'lastname' => \core_user\fields::get_display_name('lastname'),
338
            'email' => \core_user\fields::get_display_name('email'),
339
            'city' => \core_user\fields::get_display_name('city'),
340
            'country' => \core_user\fields::get_display_name('country'),
341
            'idnumber' => \core_user\fields::get_display_name('idnumber'),
342
            'institution' => \core_user\fields::get_display_name('institution'),
343
            'department' => \core_user\fields::get_display_name('department'),
344
            'phone1' => \core_user\fields::get_display_name('phone1'),
345
            'phone2' => \core_user\fields::get_display_name('phone2'),
346
            'address' => \core_user\fields::get_display_name('address'),
347
        ];
348
    }
349
 
350
    /**
351
     * Gets data about custom profile fields. Cached statically in current
352
     * request.
353
     *
354
     * This only includes fields which can be tested by the system (those whose
355
     * data is cached in $USER object) - basically doesn't include textarea type
356
     * fields.
357
     *
358
     * @return array Array of records indexed by shortname
359
     */
360
    public static function get_custom_profile_fields() {
361
        global $DB, $CFG;
362
 
363
        if (self::$customprofilefields === null) {
364
            // Get fields and store them indexed by shortname.
365
            require_once($CFG->dirroot . '/user/profile/lib.php');
366
            $fields = profile_get_custom_fields(true);
367
            self::$customprofilefields = array();
368
            foreach ($fields as $field) {
369
                self::$customprofilefields[$field->shortname] = $field;
370
            }
371
        }
372
        return self::$customprofilefields;
373
    }
374
 
375
    /**
376
     * Wipes the static cache (for use in unit tests).
377
     */
378
    public static function wipe_static_cache() {
379
        self::$customprofilefields = null;
380
    }
381
 
382
    /**
383
     * Return the value for a user's profile field
384
     *
385
     * @param int $userid User ID
386
     * @return string|bool Value, or false if user does not have a value for this field
387
     */
388
    protected function get_cached_user_profile_field($userid) {
389
        global $USER, $DB, $CFG;
390
        $iscurrentuser = $USER->id == $userid;
391
        if (isguestuser($userid) || ($iscurrentuser && !isloggedin())) {
392
            // Must be logged in and can't be the guest.
393
            return false;
394
        }
395
 
396
        // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
397
        $iscustomprofilefield = $this->customfield ? true : false;
398
        if ($iscustomprofilefield) {
399
            // As its a custom profile field we need to map the id back to the actual field.
400
            // We'll also preload all of the other custom profile fields just in case and ensure we have the
401
            // default value available as well.
402
            if (!array_key_exists($this->customfield, self::get_custom_profile_fields())) {
403
                // No such field exists.
404
                // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
405
                // or when restoring a backup of a course with user profile field conditions.
406
                return false;
407
            }
408
            $field = $this->customfield;
409
        } else {
410
            $field = $this->standardfield;
411
        }
412
 
413
        // If its the current user than most likely we will be able to get this information from $USER.
414
        // If its a regular profile field then it should already be available, if not then we have a mega problem.
415
        // If its a custom profile field then it should be available but may not be. If it is then we use the value
416
        // available, otherwise we load all custom profile fields into a temp object and refer to that.
417
        // Noting its not going be great for performance if we have to use the temp object as it involves loading the
418
        // custom profile field API and classes.
419
        if ($iscurrentuser) {
420
            if (!$iscustomprofilefield) {
421
                if (property_exists($USER, $field)) {
422
                    return $USER->{$field};
423
                } else {
424
                    // Unknown user field. This should not happen.
425
                    throw new \coding_exception('Requested user profile field does not exist');
426
                }
427
            }
428
            // Checking if the custom profile fields are already available.
429
            if (!isset($USER->profile)) {
430
                // Drat! they're not. We need to use a temp object and load them.
431
                // We don't use $USER as the profile fields are loaded into the object.
432
                $user = new \stdClass;
433
                $user->id = $USER->id;
434
                // This should ALWAYS be set, but just in case we check.
435
                require_once($CFG->dirroot . '/user/profile/lib.php');
436
                profile_load_custom_fields($user);
437
                if (array_key_exists($field, $user->profile)) {
438
                    return $user->profile[$field];
439
                }
440
            } else if (array_key_exists($field, $USER->profile)) {
441
                // Hurrah they're available, this is easy.
442
                return $USER->profile[$field];
443
            }
444
            // The profile field doesn't exist.
445
            return false;
446
        } else {
447
            // Loading for another user.
448
            if ($iscustomprofilefield) {
449
                // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
450
                // for us (this will likely be hit again).
451
                // We are able to do this because we've already pre-loaded the custom fields.
452
                $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid,
453
                        'fieldid' => self::$customprofilefields[$field]->id), IGNORE_MISSING);
454
                // If we have data return that, otherwise return the default.
455
                if ($data !== false) {
456
                    return $data;
457
                } else {
458
                    return self::$customprofilefields[$field]->defaultdata;
459
                }
460
            } else {
461
                // Its a standard field, retrieve it from the user.
462
                return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST);
463
            }
464
        }
465
        return false;
466
    }
467
 
468
    public function is_applied_to_user_lists() {
469
        // Profile conditions are assumed to be 'permanent', so they affect the
470
        // display of user lists for activities.
471
        return true;
472
    }
473
 
474
    public function filter_user_list(array $users, $not, \core_availability\info $info,
475
            \core_availability\capability_checker $checker) {
476
        global $CFG, $DB;
477
 
478
        // If the array is empty already, just return it.
479
        if (!$users) {
480
            return $users;
481
        }
482
 
483
        // Get all users from the list who match the condition.
484
        list ($sql, $params) = $DB->get_in_or_equal(array_keys($users));
485
 
486
        if ($this->customfield) {
487
            $customfields = self::get_custom_profile_fields();
488
            if (!array_key_exists($this->customfield, $customfields)) {
489
                // If the field isn't found, nobody matches.
490
                return array();
491
            }
492
            $customfield = $customfields[$this->customfield];
493
 
494
            // Fetch custom field value for all users.
495
            $values = $DB->get_records_select('user_info_data', 'fieldid = ? AND userid ' . $sql,
496
                    array_merge(array($customfield->id), $params),
497
                    '', 'userid, data');
498
            $valuefield = 'data';
499
            $default = $customfield->defaultdata;
500
        } else {
501
            $standardfields = self::get_standard_profile_fields();
502
            if (!array_key_exists($this->standardfield, $standardfields)) {
503
                // If the field isn't found, nobody matches.
504
                return [];
505
            }
506
            $values = $DB->get_records_select('user', 'id ' . $sql, $params,
507
                    '', 'id, '. $this->standardfield);
508
            $valuefield = $this->standardfield;
509
            $default = '';
510
        }
511
 
512
        // Filter the user list.
513
        $result = array();
514
        foreach ($users as $id => $user) {
515
            // Get value for user.
516
            if (array_key_exists($id, $values)) {
517
                $value = $values[$id]->{$valuefield};
518
            } else {
519
                $value = $default;
520
            }
521
 
522
            // Check value.
523
            $allow = $this->is_field_condition_met($this->operator, $value, $this->value);
524
            if ($not) {
525
                $allow = !$allow;
526
            }
527
            if ($allow) {
528
                $result[$id] = $user;
529
            }
530
        }
531
        return $result;
532
    }
533
 
534
    /**
535
     * Gets SQL to match a field against this condition. The second copy of the
536
     * field is in case you're using variables for the field so that it needs
537
     * to be two different ones.
538
     *
539
     * @param string $field Field name
540
     * @param string $field2 Second copy of field name (default same).
541
     * @param boolean $istext Any of the fields correspond to a TEXT column in database (true) or not (false).
542
     * @return array Array of SQL and parameters
543
     */
544
    private function get_condition_sql($field, $field2 = null, $istext = false) {
545
        global $DB;
546
        if (is_null($field2)) {
547
            $field2 = $field;
548
        }
549
 
550
        $params = array();
551
        switch($this->operator) {
552
            case self::OP_CONTAINS:
553
                $sql = $DB->sql_like($field, self::unique_sql_parameter(
554
                        $params, '%' . $this->value . '%'));
555
                break;
556
            case self::OP_DOES_NOT_CONTAIN:
557
                if (empty($this->value)) {
558
                    // The 'does not contain nothing' expression matches everyone.
559
                    return null;
560
                }
561
                $sql = $DB->sql_like($field, self::unique_sql_parameter(
562
                        $params, '%' . $this->value . '%'), true, true, true);
563
                break;
564
            case self::OP_IS_EQUAL_TO:
565
                if ($istext) {
566
                    $sql = $DB->sql_compare_text($field) . ' = ' . $DB->sql_compare_text(
567
                            self::unique_sql_parameter($params, $this->value));
568
                } else {
569
                    $sql = $field . ' = ' . self::unique_sql_parameter(
570
                            $params, $this->value);
571
                }
572
                break;
573
            case self::OP_STARTS_WITH:
574
                $sql = $DB->sql_like($field, self::unique_sql_parameter(
575
                        $params, $this->value . '%'));
576
                break;
577
            case self::OP_ENDS_WITH:
578
                $sql = $DB->sql_like($field, self::unique_sql_parameter(
579
                        $params, '%' . $this->value));
580
                break;
581
            case self::OP_IS_EMPTY:
582
                // Mimic PHP empty() behaviour for strings, '0' or ''.
583
                $emptystring = self::unique_sql_parameter($params, '');
584
                if ($istext) {
585
                    $sql = '(' . $DB->sql_compare_text($field) . " IN ('0', $emptystring) OR $field2 IS NULL)";
586
                } else {
587
                    $sql = '(' . $field . " IN ('0', $emptystring) OR $field2 IS NULL)";
588
                }
589
                break;
590
            case self::OP_IS_NOT_EMPTY:
591
                $emptystring = self::unique_sql_parameter($params, '');
592
                if ($istext) {
593
                    $sql = '(' . $DB->sql_compare_text($field) . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)";
594
                } else {
595
                    $sql = '(' . $field . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)";
596
                }
597
                break;
598
        }
599
        return array($sql, $params);
600
    }
601
 
602
    public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) {
603
        global $DB;
604
 
605
        // Build suitable SQL depending on custom or standard field.
606
        if ($this->customfield) {
607
            $customfields = self::get_custom_profile_fields();
608
            if (!array_key_exists($this->customfield, $customfields)) {
609
                // If the field isn't found, nobody matches.
610
                return array('SELECT id FROM {user} WHERE 0 = 1', array());
611
            }
612
            $customfield = $customfields[$this->customfield];
613
 
614
            $mainparams = array();
615
            $tablesql = "LEFT JOIN {user_info_data} ud ON ud.fieldid = " .
616
                    self::unique_sql_parameter($mainparams, $customfield->id) .
617
                    " AND ud.userid = userids.id";
618
            list ($condition, $conditionparams) = $this->get_condition_sql('ud.data', null, true);
619
            $mainparams = array_merge($mainparams, $conditionparams);
620
 
621
            // If default is true, then allow that too.
622
            if ($this->is_field_condition_met(
623
                    $this->operator, $customfield->defaultdata, $this->value)) {
624
                $where = "((ud.data IS NOT NULL AND $condition) OR (ud.data IS NULL))";
625
            } else {
626
                $where = "(ud.data IS NOT NULL AND $condition)";
627
            }
628
        } else {
629
            $standardfields = self::get_standard_profile_fields();
630
            if (!array_key_exists($this->standardfield, $standardfields)) {
631
                // If the field isn't found, nobody matches.
632
                return ['SELECT id FROM {user} WHERE 0 = 1', []];
633
            }
634
            $tablesql = "JOIN {user} u ON u.id = userids.id";
635
            list ($where, $mainparams) = $this->get_condition_sql(
636
                    'u.' . $this->standardfield);
637
        }
638
 
639
        // Handle NOT.
640
        if ($not) {
641
            $where = 'NOT (' . $where . ')';
642
        }
643
 
644
        // Get enrolled user SQL and combine with this query.
645
        list ($enrolsql, $enrolparams) =
646
                get_enrolled_sql($info->get_context(), '', 0, $onlyactive);
647
        $sql = "SELECT userids.id
648
                  FROM ($enrolsql) userids
649
                       $tablesql
650
                 WHERE $where";
651
        $params = array_merge($enrolparams, $mainparams);
652
        return array($sql, $params);
653
    }
654
}