See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 $context = \context_course::instance($course->id); 208 $a = new \stdClass(); 209 $a->field = format_string($translatedfieldname, true, array('context' => $context)); 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' => get_user_field_name('firstname'), 337 'lastname' => get_user_field_name('lastname'), 338 'email' => get_user_field_name('email'), 339 'city' => get_user_field_name('city'), 340 'country' => get_user_field_name('country'), 341 'url' => get_user_field_name('url'), 342 'icq' => get_user_field_name('icq'), 343 'skype' => get_user_field_name('skype'), 344 'aim' => get_user_field_name('aim'), 345 'yahoo' => get_user_field_name('yahoo'), 346 'msn' => get_user_field_name('msn'), 347 'idnumber' => get_user_field_name('idnumber'), 348 'institution' => get_user_field_name('institution'), 349 'department' => get_user_field_name('department'), 350 'phone1' => get_user_field_name('phone1'), 351 'phone2' => get_user_field_name('phone2'), 352 'address' => get_user_field_name('address') 353 ]; 354 } 355 356 /** 357 * Gets data about custom profile fields. Cached statically in current 358 * request. 359 * 360 * This only includes fields which can be tested by the system (those whose 361 * data is cached in $USER object) - basically doesn't include textarea type 362 * fields. 363 * 364 * @return array Array of records indexed by shortname 365 */ 366 public static function get_custom_profile_fields() { 367 global $DB, $CFG; 368 369 if (self::$customprofilefields === null) { 370 // Get fields and store them indexed by shortname. 371 require_once($CFG->dirroot . '/user/profile/lib.php'); 372 $fields = profile_get_custom_fields(true); 373 self::$customprofilefields = array(); 374 foreach ($fields as $field) { 375 self::$customprofilefields[$field->shortname] = $field; 376 } 377 } 378 return self::$customprofilefields; 379 } 380 381 /** 382 * Wipes the static cache (for use in unit tests). 383 */ 384 public static function wipe_static_cache() { 385 self::$customprofilefields = null; 386 } 387 388 /** 389 * Return the value for a user's profile field 390 * 391 * @param int $userid User ID 392 * @return string|bool Value, or false if user does not have a value for this field 393 */ 394 protected function get_cached_user_profile_field($userid) { 395 global $USER, $DB, $CFG; 396 $iscurrentuser = $USER->id == $userid; 397 if (isguestuser($userid) || ($iscurrentuser && !isloggedin())) { 398 // Must be logged in and can't be the guest. 399 return false; 400 } 401 402 // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem. 403 $iscustomprofilefield = $this->customfield ? true : false; 404 if ($iscustomprofilefield) { 405 // As its a custom profile field we need to map the id back to the actual field. 406 // We'll also preload all of the other custom profile fields just in case and ensure we have the 407 // default value available as well. 408 if (!array_key_exists($this->customfield, self::get_custom_profile_fields())) { 409 // No such field exists. 410 // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field 411 // or when restoring a backup of a course with user profile field conditions. 412 return false; 413 } 414 $field = $this->customfield; 415 } else { 416 $field = $this->standardfield; 417 } 418 419 // If its the current user than most likely we will be able to get this information from $USER. 420 // If its a regular profile field then it should already be available, if not then we have a mega problem. 421 // If its a custom profile field then it should be available but may not be. If it is then we use the value 422 // available, otherwise we load all custom profile fields into a temp object and refer to that. 423 // Noting its not going be great for performance if we have to use the temp object as it involves loading the 424 // custom profile field API and classes. 425 if ($iscurrentuser) { 426 if (!$iscustomprofilefield) { 427 if (property_exists($USER, $field)) { 428 return $USER->{$field}; 429 } else { 430 // Unknown user field. This should not happen. 431 throw new \coding_exception('Requested user profile field does not exist'); 432 } 433 } 434 // Checking if the custom profile fields are already available. 435 if (!isset($USER->profile)) { 436 // Drat! they're not. We need to use a temp object and load them. 437 // We don't use $USER as the profile fields are loaded into the object. 438 $user = new \stdClass; 439 $user->id = $USER->id; 440 // This should ALWAYS be set, but just in case we check. 441 require_once($CFG->dirroot . '/user/profile/lib.php'); 442 profile_load_custom_fields($user); 443 if (array_key_exists($field, $user->profile)) { 444 return $user->profile[$field]; 445 } 446 } else if (array_key_exists($field, $USER->profile)) { 447 // Hurrah they're available, this is easy. 448 return $USER->profile[$field]; 449 } 450 // The profile field doesn't exist. 451 return false; 452 } else { 453 // Loading for another user. 454 if ($iscustomprofilefield) { 455 // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance 456 // for us (this will likely be hit again). 457 // We are able to do this because we've already pre-loaded the custom fields. 458 $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid, 459 'fieldid' => self::$customprofilefields[$field]->id), IGNORE_MISSING); 460 // If we have data return that, otherwise return the default. 461 if ($data !== false) { 462 return $data; 463 } else { 464 return self::$customprofilefields[$field]->defaultdata; 465 } 466 } else { 467 // Its a standard field, retrieve it from the user. 468 return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST); 469 } 470 } 471 return false; 472 } 473 474 public function is_applied_to_user_lists() { 475 // Profile conditions are assumed to be 'permanent', so they affect the 476 // display of user lists for activities. 477 return true; 478 } 479 480 public function filter_user_list(array $users, $not, \core_availability\info $info, 481 \core_availability\capability_checker $checker) { 482 global $CFG, $DB; 483 484 // If the array is empty already, just return it. 485 if (!$users) { 486 return $users; 487 } 488 489 // Get all users from the list who match the condition. 490 list ($sql, $params) = $DB->get_in_or_equal(array_keys($users)); 491 492 if ($this->customfield) { 493 $customfields = self::get_custom_profile_fields(); 494 if (!array_key_exists($this->customfield, $customfields)) { 495 // If the field isn't found, nobody matches. 496 return array(); 497 } 498 $customfield = $customfields[$this->customfield]; 499 500 // Fetch custom field value for all users. 501 $values = $DB->get_records_select('user_info_data', 'fieldid = ? AND userid ' . $sql, 502 array_merge(array($customfield->id), $params), 503 '', 'userid, data'); 504 $valuefield = 'data'; 505 $default = $customfield->defaultdata; 506 } else { 507 $standardfields = self::get_standard_profile_fields(); 508 if (!array_key_exists($this->standardfield, $standardfields)) { 509 // If the field isn't found, nobody matches. 510 return []; 511 } 512 $values = $DB->get_records_select('user', 'id ' . $sql, $params, 513 '', 'id, '. $this->standardfield); 514 $valuefield = $this->standardfield; 515 $default = ''; 516 } 517 518 // Filter the user list. 519 $result = array(); 520 foreach ($users as $id => $user) { 521 // Get value for user. 522 if (array_key_exists($id, $values)) { 523 $value = $values[$id]->{$valuefield}; 524 } else { 525 $value = $default; 526 } 527 528 // Check value. 529 $allow = $this->is_field_condition_met($this->operator, $value, $this->value); 530 if ($not) { 531 $allow = !$allow; 532 } 533 if ($allow) { 534 $result[$id] = $user; 535 } 536 } 537 return $result; 538 } 539 540 /** 541 * Gets SQL to match a field against this condition. The second copy of the 542 * field is in case you're using variables for the field so that it needs 543 * to be two different ones. 544 * 545 * @param string $field Field name 546 * @param string $field2 Second copy of field name (default same). 547 * @param boolean $istext Any of the fields correspond to a TEXT column in database (true) or not (false). 548 * @return array Array of SQL and parameters 549 */ 550 private function get_condition_sql($field, $field2 = null, $istext = false) { 551 global $DB; 552 if (is_null($field2)) { 553 $field2 = $field; 554 } 555 556 $params = array(); 557 switch($this->operator) { 558 case self::OP_CONTAINS: 559 $sql = $DB->sql_like($field, self::unique_sql_parameter( 560 $params, '%' . $this->value . '%')); 561 break; 562 case self::OP_DOES_NOT_CONTAIN: 563 if (empty($this->value)) { 564 // The 'does not contain nothing' expression matches everyone. 565 return null; 566 } 567 $sql = $DB->sql_like($field, self::unique_sql_parameter( 568 $params, '%' . $this->value . '%'), true, true, true); 569 break; 570 case self::OP_IS_EQUAL_TO: 571 if ($istext) { 572 $sql = $DB->sql_compare_text($field) . ' = ' . $DB->sql_compare_text( 573 self::unique_sql_parameter($params, $this->value)); 574 } else { 575 $sql = $field . ' = ' . self::unique_sql_parameter( 576 $params, $this->value); 577 } 578 break; 579 case self::OP_STARTS_WITH: 580 $sql = $DB->sql_like($field, self::unique_sql_parameter( 581 $params, $this->value . '%')); 582 break; 583 case self::OP_ENDS_WITH: 584 $sql = $DB->sql_like($field, self::unique_sql_parameter( 585 $params, '%' . $this->value)); 586 break; 587 case self::OP_IS_EMPTY: 588 // Mimic PHP empty() behaviour for strings, '0' or ''. 589 $emptystring = self::unique_sql_parameter($params, ''); 590 if ($istext) { 591 $sql = '(' . $DB->sql_compare_text($field) . " IN ('0', $emptystring) OR $field2 IS NULL)"; 592 } else { 593 $sql = '(' . $field . " IN ('0', $emptystring) OR $field2 IS NULL)"; 594 } 595 break; 596 case self::OP_IS_NOT_EMPTY: 597 $emptystring = self::unique_sql_parameter($params, ''); 598 if ($istext) { 599 $sql = '(' . $DB->sql_compare_text($field) . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)"; 600 } else { 601 $sql = '(' . $field . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)"; 602 } 603 break; 604 } 605 return array($sql, $params); 606 } 607 608 public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { 609 global $DB; 610 611 // Build suitable SQL depending on custom or standard field. 612 if ($this->customfield) { 613 $customfields = self::get_custom_profile_fields(); 614 if (!array_key_exists($this->customfield, $customfields)) { 615 // If the field isn't found, nobody matches. 616 return array('SELECT id FROM {user} WHERE 0 = 1', array()); 617 } 618 $customfield = $customfields[$this->customfield]; 619 620 $mainparams = array(); 621 $tablesql = "LEFT JOIN {user_info_data} ud ON ud.fieldid = " . 622 self::unique_sql_parameter($mainparams, $customfield->id) . 623 " AND ud.userid = userids.id"; 624 list ($condition, $conditionparams) = $this->get_condition_sql('ud.data', null, true); 625 $mainparams = array_merge($mainparams, $conditionparams); 626 627 // If default is true, then allow that too. 628 if ($this->is_field_condition_met( 629 $this->operator, $customfield->defaultdata, $this->value)) { 630 $where = "((ud.data IS NOT NULL AND $condition) OR (ud.data IS NULL))"; 631 } else { 632 $where = "(ud.data IS NOT NULL AND $condition)"; 633 } 634 } else { 635 $standardfields = self::get_standard_profile_fields(); 636 if (!array_key_exists($this->standardfield, $standardfields)) { 637 // If the field isn't found, nobody matches. 638 return ['SELECT id FROM {user} WHERE 0 = 1', []]; 639 } 640 $tablesql = "JOIN {user} u ON u.id = userids.id"; 641 list ($where, $mainparams) = $this->get_condition_sql( 642 'u.' . $this->standardfield); 643 } 644 645 // Handle NOT. 646 if ($not) { 647 $where = 'NOT (' . $where . ')'; 648 } 649 650 // Get enrolled user SQL and combine with this query. 651 list ($enrolsql, $enrolparams) = 652 get_enrolled_sql($info->get_context(), '', 0, $onlyactive); 653 $sql = "SELECT userids.id 654 FROM ($enrolsql) userids 655 $tablesql 656 WHERE $where"; 657 $params = array_merge($enrolparams, $mainparams); 658 return array($sql, $params); 659 } 660 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body