Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       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          print_error('invalidcategoryid');
     288      }
     289  
     290      if (!$categories = $DB->get_records('user_info_category', null, 'sortorder ASC')) {
     291          print_error('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          print_error('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  }