Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 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   * This file contains the profile_define_base class.
  19   *
  20   * @package core_user
  21   * @copyright  2007 onwards Shane Elliot {@link http://pukunui.com}
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  /**
  26   * Class profile_define_base
  27   *
  28   * @copyright  2007 onwards Shane Elliot {@link http://pukunui.com}
  29   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  30   */
  31  class profile_define_base {
  32  
  33      /**
  34       * Prints out the form snippet for creating or editing a profile field
  35       * @param MoodleQuickForm $form instance of the moodleform class
  36       */
  37      public function define_form(&$form) {
  38          $form->addElement('header', '_commonsettings', get_string('profilecommonsettings', 'admin'));
  39          $this->define_form_common($form);
  40  
  41          $form->addElement('header', '_specificsettings', get_string('profilespecificsettings', 'admin'));
  42          $this->define_form_specific($form);
  43      }
  44  
  45      /**
  46       * Prints out the form snippet for the part of creating or editing a profile field common to all data types.
  47       *
  48       * @param MoodleQuickForm $form instance of the moodleform class
  49       */
  50      public function define_form_common(&$form) {
  51  
  52          $strrequired = get_string('required');
  53  
  54          // Accepted values for 'shortname' would follow [a-zA-Z0-9_] pattern,
  55          // but we are accepting any PARAM_TEXT value here,
  56          // and checking [a-zA-Z0-9_] pattern in define_validate_common() function to throw an error when needed.
  57          $form->addElement('text', 'shortname', get_string('profileshortname', 'admin'), 'maxlength="100" size="25"');
  58          $form->addRule('shortname', $strrequired, 'required', null, 'client');
  59          $form->setType('shortname', PARAM_TEXT);
  60  
  61          $form->addElement('text', 'name', get_string('profilename', 'admin'), 'size="50"');
  62          $form->addRule('name', $strrequired, 'required', null, 'client');
  63          $form->setType('name', PARAM_TEXT);
  64  
  65          $form->addElement('editor', 'description', get_string('profiledescription', 'admin'), null, null);
  66  
  67          $form->addElement('selectyesno', 'required', get_string('profilerequired', 'admin'));
  68  
  69          $form->addElement('selectyesno', 'locked', get_string('profilelocked', 'admin'));
  70  
  71          $form->addElement('selectyesno', 'forceunique', get_string('profileforceunique', 'admin'));
  72  
  73          $form->addElement('selectyesno', 'signup', get_string('profilesignup', 'admin'));
  74  
  75          $choices = array();
  76          $choices[PROFILE_VISIBLE_NONE]    = get_string('profilevisiblenone', 'admin');
  77          $choices[PROFILE_VISIBLE_PRIVATE] = get_string('profilevisibleprivate', 'admin');
  78          $choices[PROFILE_VISIBLE_TEACHERS] = get_string('profilevisibleteachers', 'admin');
  79          $choices[PROFILE_VISIBLE_ALL]     = get_string('profilevisibleall', 'admin');
  80  
  81          $form->addElement('select', 'visible', get_string('profilevisible', 'admin'), $choices);
  82          $form->addHelpButton('visible', 'profilevisible', 'admin');
  83          $form->setDefault('visible', PROFILE_VISIBLE_ALL);
  84  
  85          $choices = profile_list_categories();
  86          $form->addElement('select', 'categoryid', get_string('profilecategory', 'admin'), $choices);
  87      }
  88  
  89      /**
  90       * Prints out the form snippet for the part of creating or editing a profile field specific to the current data type.
  91       * @param MoodleQuickForm $form instance of the moodleform class
  92       */
  93      public function define_form_specific($form) {
  94          // Do nothing - overwrite if necessary.
  95      }
  96  
  97      /**
  98       * Validate the data from the add/edit profile field form.
  99       *
 100       * Generally this method should not be overwritten by child classes.
 101       *
 102       * @param stdClass|array $data from the add/edit profile field form
 103       * @param array $files
 104       * @return array associative array of error messages
 105       */
 106      public function define_validate($data, $files) {
 107  
 108          $data = (object)$data;
 109          $err = array();
 110  
 111          $err += $this->define_validate_common($data, $files);
 112          $err += $this->define_validate_specific($data, $files);
 113  
 114          return $err;
 115      }
 116  
 117      /**
 118       * Validate the data from the add/edit profile field form that is common to all data types.
 119       *
 120       * Generally this method should not be overwritten by child classes.
 121       *
 122       * @param stdClass|array $data from the add/edit profile field form
 123       * @param array $files
 124       * @return  array    associative array of error messages
 125       */
 126      public function define_validate_common($data, $files) {
 127          global $DB;
 128  
 129          $err = array();
 130  
 131          // Check the shortname was not truncated by cleaning.
 132          if (empty($data->shortname)) {
 133              $err['shortname'] = get_string('required');
 134  
 135          } else {
 136              // Check allowed pattern (numbers, letters and underscore).
 137              if (!preg_match('/^[a-zA-Z0-9_]+$/', $data->shortname)) {
 138                  $err['shortname'] = get_string('profileshortnameinvalid', 'admin');
 139              } else {
 140                  // Fetch field-record from DB.
 141                  $field = profile_get_custom_field_data_by_shortname($data->shortname);
 142                  // Check the shortname is unique.
 143                  if ($field and $field->id <> $data->id) {
 144                      $err['shortname'] = get_string('profileshortnamenotunique', 'admin');
 145                  }
 146                  // NOTE: since 2.0 the shortname may collide with existing fields in $USER because we load these fields into
 147                  // $USER->profile array instead.
 148              }
 149          }
 150  
 151          // No further checks necessary as the form class will take care of it.
 152          return $err;
 153      }
 154  
 155      /**
 156       * Validate the data from the add/edit profile field form
 157       * that is specific to the current data type
 158       * @param array $data
 159       * @param array $files
 160       * @return  array    associative array of error messages
 161       */
 162      public function define_validate_specific($data, $files) {
 163          // Do nothing - overwrite if necessary.
 164          return array();
 165      }
 166  
 167      /**
 168       * Alter form based on submitted or existing data
 169       * @param MoodleQuickForm $mform
 170       */
 171      public function define_after_data(&$mform) {
 172          // Do nothing - overwrite if necessary.
 173      }
 174  
 175      /**
 176       * Add a new profile field or save changes to current field
 177       * @param array|stdClass $data from the add/edit profile field form
 178       */
 179      public function define_save($data) {
 180          global $DB;
 181  
 182          $data = $this->define_save_preprocess($data); // Hook for child classes.
 183  
 184          $old = false;
 185          if (!empty($data->id)) {
 186              $old = $DB->get_record('user_info_field', array('id' => (int)$data->id));
 187          }
 188  
 189          // Check to see if the category has changed.
 190          if (!$old or $old->categoryid != $data->categoryid) {
 191              $data->sortorder = $DB->count_records('user_info_field', array('categoryid' => $data->categoryid)) + 1;
 192          }
 193  
 194          if (empty($data->id)) {
 195              unset($data->id);
 196              $data->id = $DB->insert_record('user_info_field', $data);
 197          } else {
 198              $DB->update_record('user_info_field', $data);
 199          }
 200  
 201          $field = $DB->get_record('user_info_field', array('id' => $data->id));
 202          if ($old) {
 203              \core\event\user_info_field_updated::create_from_field($field)->trigger();
 204          } else {
 205              \core\event\user_info_field_created::create_from_field($field)->trigger();
 206          }
 207          profile_purge_user_fields_cache();
 208      }
 209  
 210      /**
 211       * Preprocess data from the add/edit profile field form before it is saved.
 212       *
 213       * This method is a hook for the child classes to overwrite.
 214       *
 215       * @param array|stdClass $data from the add/edit profile field form
 216       * @return array|stdClass processed data object
 217       */
 218      public function define_save_preprocess($data) {
 219          // Do nothing - overwrite if necessary.
 220          return $data;
 221      }
 222  
 223      /**
 224       * Provides a method by which we can allow the default data in profile_define_* to use an editor
 225       *
 226       * This should return an array of editor names (which will need to be formatted/cleaned)
 227       *
 228       * @return array
 229       */
 230      public function define_editors() {
 231          return array();
 232      }
 233  }
 234  
 235  
 236  
 237  /**
 238   * Reorder the profile fields within a given category starting at the field at the given startorder.
 239   */
 240  function profile_reorder_fields() {
 241      global $DB;
 242  
 243      if ($categories = $DB->get_records('user_info_category')) {
 244          foreach ($categories as $category) {
 245              $i = 1;
 246              if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) {
 247                  foreach ($fields as $field) {
 248                      $f = new stdClass();
 249                      $f->id = $field->id;
 250                      $f->sortorder = $i++;
 251                      $DB->update_record('user_info_field', $f);
 252                  }
 253              }
 254          }
 255          profile_purge_user_fields_cache();
 256      }
 257  }
 258  
 259  /**
 260   * Reorder the profile categoriess starting at the category at the given startorder.
 261   */
 262  function profile_reorder_categories() {
 263      global $DB;
 264  
 265      $i = 1;
 266      if ($categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) {
 267          foreach ($categories as $cat) {
 268              $c = new stdClass();
 269              $c->id = $cat->id;
 270              $c->sortorder = $i++;
 271              $DB->update_record('user_info_category', $c);
 272          }
 273          profile_purge_user_fields_cache();
 274      }
 275  }
 276  
 277  /**
 278   * Delete a profile category
 279   * @param int $id of the category to be deleted
 280   * @return bool success of operation
 281   */
 282  function profile_delete_category($id) {
 283      global $DB;
 284  
 285      // Retrieve the category.
 286      if (!$category = $DB->get_record('user_info_category', array('id' => $id))) {
 287          throw new \moodle_exception('invalidcategoryid');
 288      }
 289  
 290      if (!$categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) {
 291          throw new \moodle_exception('nocate', 'debug');
 292      }
 293  
 294      unset($categories[$category->id]);
 295  
 296      if (!count($categories)) {
 297          return false; // We can not delete the last category.
 298      }
 299  
 300      // Does the category contain any fields.
 301      if ($DB->count_records('user_info_field', array('categoryid' => $category->id))) {
 302          if (array_key_exists($category->sortorder - 1, $categories)) {
 303              $newcategory = $categories[$category->sortorder - 1];
 304          } else if (array_key_exists($category->sortorder + 1, $categories)) {
 305              $newcategory = $categories[$category->sortorder + 1];
 306          } else {
 307              $newcategory = reset($categories); // Get first category if sortorder broken.
 308          }
 309  
 310          $sortorder = $DB->count_records('user_info_field', array('categoryid' => $newcategory->id)) + 1;
 311  
 312          if ($fields = $DB->get_records('user_info_field', array('categoryid' => $category->id), 'sortorder ASC')) {
 313              foreach ($fields as $field) {
 314                  $f = new stdClass();
 315                  $f->id = $field->id;
 316                  $f->sortorder = $sortorder++;
 317                  $f->categoryid = $newcategory->id;
 318                  if ($DB->update_record('user_info_field', $f)) {
 319                      $field->sortorder = $f->sortorder;
 320                      $field->categoryid = $f->categoryid;
 321                      \core\event\user_info_field_updated::create_from_field($field)->trigger();
 322                  }
 323              }
 324          }
 325      }
 326  
 327      // Finally we get to delete the category.
 328      $DB->delete_records('user_info_category', array('id' => $category->id));
 329      profile_reorder_categories();
 330  
 331      \core\event\user_info_category_deleted::create_from_category($category)->trigger();
 332      profile_purge_user_fields_cache();
 333  
 334      return true;
 335  }
 336  
 337  /**
 338   * Deletes a profile field.
 339   * @param int $id
 340   */
 341  function profile_delete_field($id) {
 342      global $DB;
 343  
 344      // Remove any user data associated with this field.
 345      if (!$DB->delete_records('user_info_data', array('fieldid' => $id))) {
 346          throw new \moodle_exception('cannotdeletecustomfield');
 347      }
 348  
 349      // Note: Any availability conditions that depend on this field will remain,
 350      // but show the field as missing until manually corrected to something else.
 351  
 352      // Need to rebuild course cache to update the info.
 353      rebuild_course_cache(0, true);
 354  
 355      // Prior to the delete, pull the record for the event.
 356      $field = $DB->get_record('user_info_field', array('id' => $id));
 357  
 358      // Try to remove the record from the database.
 359      $DB->delete_records('user_info_field', array('id' => $id));
 360  
 361      \core\event\user_info_field_deleted::create_from_field($field)->trigger();
 362      profile_purge_user_fields_cache();
 363  
 364      // Reorder the remaining fields in the same category.
 365      profile_reorder_fields();
 366  }
 367  
 368  /**
 369   * Change the sort order of a field
 370   *
 371   * @param int $id of the field
 372   * @param string $move direction of move
 373   * @return bool success of operation
 374   */
 375  function profile_move_field($id, $move) {
 376      global $DB;
 377  
 378      // Get the field object.
 379      if (!$field = $DB->get_record('user_info_field', array('id' => $id))) {
 380          return false;
 381      }
 382      // Count the number of fields in this category.
 383      $fieldcount = $DB->count_records('user_info_field', array('categoryid' => $field->categoryid));
 384  
 385      // Calculate the new sortorder.
 386      if ( ($move == 'up') and ($field->sortorder > 1)) {
 387          $neworder = $field->sortorder - 1;
 388      } else if (($move == 'down') and ($field->sortorder < $fieldcount)) {
 389          $neworder = $field->sortorder + 1;
 390      } else {
 391          return false;
 392      }
 393  
 394      // Retrieve the field object that is currently residing in the new position.
 395      $params = array('categoryid' => $field->categoryid, 'sortorder' => $neworder);
 396      if ($swapfield = $DB->get_record('user_info_field', $params)) {
 397  
 398          // Swap the sortorders.
 399          $swapfield->sortorder = $field->sortorder;
 400          $field->sortorder     = $neworder;
 401  
 402          // Update the field records.
 403          $DB->update_record('user_info_field', $field);
 404          $DB->update_record('user_info_field', $swapfield);
 405  
 406          \core\event\user_info_field_updated::create_from_field($field)->trigger();
 407          \core\event\user_info_field_updated::create_from_field($swapfield)->trigger();
 408      }
 409  
 410      profile_reorder_fields();
 411      return true;
 412  }
 413  
 414  /**
 415   * Change the sort order of a category.
 416   *
 417   * @param int $id of the category
 418   * @param string $move direction of move
 419   * @return bool success of operation
 420   */
 421  function profile_move_category($id, $move) {
 422      global $DB;
 423      // Get the category object.
 424      if (!($category = $DB->get_record('user_info_category', array('id' => $id)))) {
 425          return false;
 426      }
 427  
 428      // Count the number of categories.
 429      $categorycount = $DB->count_records('user_info_category');
 430  
 431      // Calculate the new sortorder.
 432      if (($move == 'up') and ($category->sortorder > 1)) {
 433          $neworder = $category->sortorder - 1;
 434      } else if (($move == 'down') and ($category->sortorder < $categorycount)) {
 435          $neworder = $category->sortorder + 1;
 436      } else {
 437          return false;
 438      }
 439  
 440      // Retrieve the category object that is currently residing in the new position.
 441      if ($swapcategory = $DB->get_record('user_info_category', array('sortorder' => $neworder))) {
 442  
 443          // Swap the sortorders.
 444          $swapcategory->sortorder = $category->sortorder;
 445          $category->sortorder     = $neworder;
 446  
 447          // Update the category records.
 448          $DB->update_record('user_info_category', $category);
 449          $DB->update_record('user_info_category', $swapcategory);
 450  
 451          \core\event\user_info_category_updated::create_from_category($category)->trigger();
 452          \core\event\user_info_category_updated::create_from_category($swapcategory)->trigger();
 453          profile_purge_user_fields_cache();
 454  
 455          return true;
 456      }
 457  
 458      return false;
 459  }
 460  
 461  /**
 462   * Retrieve a list of all the available data types
 463   * @return   array   a list of the datatypes suitable to use in a select statement
 464   */
 465  function profile_list_datatypes() {
 466      $datatypes = array();
 467  
 468      $plugins = core_component::get_plugin_list('profilefield');
 469      foreach ($plugins as $type => $unused) {
 470          $datatypes[$type] = get_string('pluginname', 'profilefield_'.$type);
 471      }
 472      asort($datatypes);
 473  
 474      return $datatypes;
 475  }
 476  
 477  /**
 478   * Retrieve a list of categories and ids suitable for use in a form
 479   * @return   array
 480   */
 481  function profile_list_categories() {
 482      global $DB;
 483      $categories = $DB->get_records_menu('user_info_category', null, 'sortorder ASC', 'id, name');
 484      return array_map('format_string', $categories);
 485  }
 486  
 487  /**
 488   * Create or update a profile category
 489   *
 490   * @param stdClass $data
 491   */
 492  function profile_save_category(stdClass $data): void {
 493      global $DB;
 494  
 495      if (empty($data->id)) {
 496          unset($data->id);
 497          $data->sortorder = $DB->count_records('user_info_category') + 1;
 498          $data->id = $DB->insert_record('user_info_category', $data, true);
 499  
 500          $createdcategory = $DB->get_record('user_info_category', array('id' => $data->id));
 501          \core\event\user_info_category_created::create_from_category($createdcategory)->trigger();
 502      } else {
 503          $DB->update_record('user_info_category', $data);
 504  
 505          $updatedcateogry = $DB->get_record('user_info_category', array('id' => $data->id));
 506          \core\event\user_info_category_updated::create_from_category($updatedcateogry)->trigger();
 507      }
 508      profile_reorder_categories();
 509      profile_purge_user_fields_cache();
 510  }
 511  
 512  /**
 513   * Edit a category
 514   *
 515   * @deprecated since Moodle 3.11 MDL-71051 - please do not use this function any more.
 516   * @todo MDL-71413 This will be deleted in Moodle 4.3.
 517   * @see profile_save_category()
 518   *
 519   * @param int $id
 520   * @param string $redirect
 521   */
 522  function profile_edit_category($id, $redirect) {
 523      global $DB, $OUTPUT, $CFG;
 524  
 525      debugging('Function profile_edit_category() is deprecated without replacement, see also profile_save_category()',
 526          DEBUG_DEVELOPER);
 527  
 528      $categoryform = new \core_user\form\profile_category_form();
 529  
 530      if ($category = $DB->get_record('user_info_category', array('id' => $id))) {
 531          $categoryform->set_data($category);
 532      }
 533  
 534      if ($categoryform->is_cancelled()) {
 535          redirect($redirect);
 536      } else {
 537          if ($data = $categoryform->get_data()) {
 538              profile_save_category($data);
 539              redirect($redirect);
 540          }
 541  
 542          if (empty($id)) {
 543              $strheading = get_string('profilecreatenewcategory', 'admin');
 544          } else {
 545              $strheading = get_string('profileeditcategory', 'admin', format_string($category->name));
 546          }
 547  
 548          // Print the page.
 549          echo $OUTPUT->header();
 550          echo $OUTPUT->heading($strheading);
 551          $categoryform->display();
 552          echo $OUTPUT->footer();
 553          die;
 554      }
 555  
 556  }
 557  
 558  /**
 559   * Save updated field definition or create a new field
 560   *
 561   * @param stdClass $data data from the form profile_field_form
 562   * @param array $editors editors for this form field type
 563   */
 564  function profile_save_field(stdClass $data, array $editors): void {
 565      global $CFG;
 566  
 567      require_once($CFG->dirroot.'/user/profile/field/'.$data->datatype.'/define.class.php');
 568      $newfield = 'profile_define_'.$data->datatype;
 569      /** @var profile_define_base $formfield */
 570      $formfield = new $newfield();
 571  
 572      // Collect the description and format back into the proper data structure from the editor.
 573      // Note: This field will ALWAYS be an editor.
 574      $data->descriptionformat = $data->description['format'];
 575      $data->description = $data->description['text'];
 576  
 577      // Check whether the default data is an editor, this is (currently) only the textarea field type.
 578      if (is_array($data->defaultdata) && array_key_exists('text', $data->defaultdata)) {
 579          // Collect the default data and format back into the proper data structure from the editor.
 580          $data->defaultdataformat = $data->defaultdata['format'];
 581          $data->defaultdata = $data->defaultdata['text'];
 582      }
 583  
 584      // Convert the data format for.
 585      if (is_array($editors)) {
 586          foreach ($editors as $editor) {
 587              if (isset($field->$editor)) {
 588                  $field->{$editor.'format'} = $field->{$editor}['format'];
 589                  $field->$editor = $field->{$editor}['text'];
 590              }
 591          }
 592      }
 593  
 594      $formfield->define_save($data);
 595      profile_reorder_fields();
 596      profile_reorder_categories();
 597  }
 598  
 599  /**
 600   * Edit a profile field.
 601   *
 602   * @deprecated since Moodle 3.11 MDL-71051 - please do not use this function any more.
 603   * @todo MDL-71413 This will be deleted in Moodle 4.3.
 604   * @see profile_save_field()
 605   *
 606   * @param int $id
 607   * @param string $datatype
 608   * @param string $redirect
 609   */
 610  function profile_edit_field($id, $datatype, $redirect) {
 611      global $OUTPUT, $PAGE;
 612  
 613      debugging('Function profile_edit_field() is deprecated without replacement, see also profile_save_field()',
 614          DEBUG_DEVELOPER);
 615  
 616      $fieldform = new \core_user\form\profile_field_form();
 617      $fieldform->set_data_for_dynamic_submission();
 618  
 619      if ($fieldform->is_cancelled()) {
 620          redirect($redirect);
 621  
 622      } else {
 623          if ($data = $fieldform->get_data()) {
 624              profile_save_field($data, $fieldform->editors());
 625              redirect($redirect);
 626          }
 627  
 628          $datatypes = profile_list_datatypes();
 629  
 630          if (empty($id)) {
 631              $strheading = get_string('profilecreatenewfield', 'admin', $datatypes[$datatype]);
 632          } else {
 633              $strheading = get_string('profileeditfield', 'admin', format_string($fieldform->get_field_record()->name));
 634          }
 635  
 636          // Print the page.
 637          $PAGE->navbar->add($strheading);
 638          echo $OUTPUT->header();
 639          echo $OUTPUT->heading($strheading);
 640          $fieldform->display();
 641          echo $OUTPUT->footer();
 642          die;
 643      }
 644  }
 645  
 646  /**
 647   * Purge the cache for the user profile fields
 648   */
 649  function profile_purge_user_fields_cache() {
 650      $cache = \cache::make_from_params(cache_store::MODE_REQUEST, 'core_profile', 'customfields',
 651          [], ['simplekeys' => true, 'simpledata' => true]);
 652      $cache->purge();
 653  }