See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body