Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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              $translatedfieldname = get_user_field_name($this->standardfield);
 201          }
 202          $a = new \stdClass();
 203          // Not safe to call format_string here; use the special function to call it later.
 204          $a->field = self::description_format_string($translatedfieldname);
 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      }
 240  
 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      }
 258  
 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      }
 323  
 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;
 336  
 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      }
 348  
 349      /**
 350       * Wipes the static cache (for use in unit tests).
 351       */
 352      public static function wipe_static_cache() {
 353          self::$customprofilefields = null;
 354      }
 355  
 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          }
 369  
 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          }
 386  
 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      }
 441  
 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      }
 447  
 448      public function filter_user_list(array $users, $not, \core_availability\info $info,
 449              \core_availability\capability_checker $checker) {
 450          global $CFG, $DB;
 451  
 452          // If the array is empty already, just return it.
 453          if (!$users) {
 454              return $users;
 455          }
 456  
 457          // Get all users from the list who match the condition.
 458          list ($sql, $params) = $DB->get_in_or_equal(array_keys($users));
 459  
 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];
 467  
 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          }
 480  
 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              }
 490  
 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      }
 502  
 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       * @param boolean $istext Any of the fields correspond to a TEXT column in database (true) or not (false).
 511       * @return array Array of SQL and parameters
 512       */
 513      private function get_condition_sql($field, $field2 = null, $istext = false) {
 514          global $DB;
 515          if (is_null($field2)) {
 516              $field2 = $field;
 517          }
 518  
 519          $params = array();
 520          switch($this->operator) {
 521              case self::OP_CONTAINS:
 522                  $sql = $DB->sql_like($field, self::unique_sql_parameter(
 523                          $params, '%' . $this->value . '%'));
 524                  break;
 525              case self::OP_DOES_NOT_CONTAIN:
 526                  if (empty($this->value)) {
 527                      // The 'does not contain nothing' expression matches everyone.
 528                      return null;
 529                  }
 530                  $sql = $DB->sql_like($field, self::unique_sql_parameter(
 531                          $params, '%' . $this->value . '%'), true, true, true);
 532                  break;
 533              case self::OP_IS_EQUAL_TO:
 534                  if ($istext) {
 535                      $sql = $DB->sql_compare_text($field) . ' = ' . $DB->sql_compare_text(
 536                              self::unique_sql_parameter($params, $this->value));
 537                  } else {
 538                      $sql = $field . ' = ' . self::unique_sql_parameter(
 539                              $params, $this->value);
 540                  }
 541                  break;
 542              case self::OP_STARTS_WITH:
 543                  $sql = $DB->sql_like($field, self::unique_sql_parameter(
 544                          $params, $this->value . '%'));
 545                  break;
 546              case self::OP_ENDS_WITH:
 547                  $sql = $DB->sql_like($field, self::unique_sql_parameter(
 548                          $params, '%' . $this->value));
 549                  break;
 550              case self::OP_IS_EMPTY:
 551                  // Mimic PHP empty() behaviour for strings, '0' or ''.
 552                  $emptystring = self::unique_sql_parameter($params, '');
 553                  if ($istext) {
 554                      $sql = '(' . $DB->sql_compare_text($field) . " IN ('0', $emptystring) OR $field2 IS NULL)";
 555                  } else {
 556                      $sql = '(' . $field . " IN ('0', $emptystring) OR $field2 IS NULL)";
 557                  }
 558                  break;
 559              case self::OP_IS_NOT_EMPTY:
 560                  $emptystring = self::unique_sql_parameter($params, '');
 561                  if ($istext) {
 562                      $sql = '(' . $DB->sql_compare_text($field) . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)";
 563                  } else {
 564                      $sql = '(' . $field . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)";
 565                  }
 566                  break;
 567          }
 568          return array($sql, $params);
 569      }
 570  
 571      public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) {
 572          global $DB;
 573  
 574          // Build suitable SQL depending on custom or standard field.
 575          if ($this->customfield) {
 576              $customfields = self::get_custom_profile_fields();
 577              if (!array_key_exists($this->customfield, $customfields)) {
 578                  // If the field isn't found, nobody matches.
 579                  return array('SELECT id FROM {user} WHERE 0 = 1', array());
 580              }
 581              $customfield = $customfields[$this->customfield];
 582  
 583              $mainparams = array();
 584              $tablesql = "LEFT JOIN {user_info_data} ud ON ud.fieldid = " .
 585                      self::unique_sql_parameter($mainparams, $customfield->id) .
 586                      " AND ud.userid = userids.id";
 587              list ($condition, $conditionparams) = $this->get_condition_sql('ud.data', null, true);
 588              $mainparams = array_merge($mainparams, $conditionparams);
 589  
 590              // If default is true, then allow that too.
 591              if ($this->is_field_condition_met(
 592                      $this->operator, $customfield->defaultdata, $this->value)) {
 593                  $where = "((ud.data IS NOT NULL AND $condition) OR (ud.data IS NULL))";
 594              } else {
 595                  $where = "(ud.data IS NOT NULL AND $condition)";
 596              }
 597          } else {
 598              $tablesql = "JOIN {user} u ON u.id = userids.id";
 599              list ($where, $mainparams) = $this->get_condition_sql(
 600                      'u.' . $this->standardfield);
 601          }
 602  
 603          // Handle NOT.
 604          if ($not) {
 605              $where = 'NOT (' . $where . ')';
 606          }
 607  
 608          // Get enrolled user SQL and combine with this query.
 609          list ($enrolsql, $enrolparams) =
 610                  get_enrolled_sql($info->get_context(), '', 0, $onlyactive);
 611          $sql = "SELECT userids.id
 612                    FROM ($enrolsql) userids
 613                         $tablesql
 614                   WHERE $where";
 615          $params = array_merge($enrolparams, $mainparams);
 616          return array($sql, $params);
 617      }
 618  }