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 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   * Profile manager class
  19   *
  20   * @package    tool_moodlenet
  21   * @copyright  2020 Adrian Greeve <adrian@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_moodlenet;
  26  
  27  /**
  28   * Class for handling interaction with the moodlenet profile.
  29   *
  30   * @package    tool_moodlenet
  31   * @copyright  2020 Adrian Greeve <adrian@moodle.com>
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class profile_manager {
  35  
  36      /**
  37       * Get the mnet profile for a user.
  38       *
  39       * @param  int $userid The ID for the user to get the profile form
  40       * @return moodlenet_user_profile or null.
  41       */
  42      public static function get_moodlenet_user_profile(int $userid): ?moodlenet_user_profile {
  43          global $CFG;
  44          // Check for official profile.
  45          if (self::official_profile_exists()) {
  46              $user = \core_user::get_user($userid, 'moodlenetprofile');
  47              try {
  48                  $userprofile = $user->moodlenetprofile ? $user->moodlenetprofile : '';
  49                  return (isset($user)) ? new moodlenet_user_profile(s($userprofile), $userid) : null;
  50              } catch (\moodle_exception $e) {
  51                  // If an exception is thrown, means there isn't a valid profile set. No need to log exception.
  52                  return null;
  53              }
  54          }
  55          // Otherwise get hacked in user profile field.
  56          require_once($CFG->dirroot . '/user/profile/lib.php');
  57          $profilefields = profile_get_user_fields_with_data($userid);
  58          foreach ($profilefields as $key => $field) {
  59              if ($field->get_category_name() == self::get_category_name()
  60                      && $field->inputname == 'profile_field_mnetprofile') {
  61                  try {
  62                      return new moodlenet_user_profile(s($field->display_data()), $userid);
  63                  } catch (\moodle_exception $e) {
  64                      // If an exception is thrown, means there isn't a valid profile set. No need to log exception.
  65                      return null;
  66                  }
  67              }
  68          }
  69          return null;
  70      }
  71  
  72      /**
  73       * Save the moodlenet profile.
  74       *
  75       * @param moodlenet_user_profile $moodlenetprofile The moodlenet profile to save.
  76       */
  77      public static function save_moodlenet_user_profile(moodlenet_user_profile $moodlenetprofile): void {
  78          global $CFG, $DB;
  79          // Do some cursory checks first to see if saving is possible.
  80          if (self::official_profile_exists()) {
  81              // All good. Let's save.
  82              $user = \core_user::get_user($moodlenetprofile->get_userid());
  83              $user->moodlenetprofile = $moodlenetprofile->get_profile_name();
  84  
  85              require_once($CFG->dirroot . '/user/lib.php');
  86  
  87              \user_update_user($user, false, true);
  88              return;
  89          }
  90          $fielddata = self::get_user_profile_field();
  91          $fielddata = self::validate_and_fix_missing_profile_items($fielddata);
  92          // Everything should be back to normal. Let's save.
  93          require_once($CFG->dirroot . '/user/profile/lib.php');
  94          \profile_save_custom_fields($moodlenetprofile->get_userid(),
  95                  [$fielddata->shortname => $moodlenetprofile->get_profile_name()]);
  96      }
  97  
  98      /**
  99       * Checks to see if the required user profile fields and categories are in place. If not it regenerates them.
 100       *
 101       * @param  stdClass $fielddata The moodlenet profile field.
 102       * @return stdClass The same moodlenet profile field, with any necessary updates made.
 103       */
 104      private static function validate_and_fix_missing_profile_items(\stdClass $fielddata): \stdClass {
 105          global $DB;
 106  
 107          if (empty((array) $fielddata)) {
 108              // We need to regenerate the category and field to store this data.
 109              if (!self::check_profile_category()) {
 110                  $categoryid = self::create_user_profile_category();
 111                  self::create_user_profile_text_field($categoryid);
 112              } else {
 113                  // We need the category id.
 114                  $category = $DB->get_record('user_info_category', ['name' => self::get_category_name()]);
 115                  self::create_user_profile_text_field($category->id);
 116              }
 117              $fielddata = self::get_user_profile_field();
 118          } else {
 119              if (!self::check_profile_category($fielddata->categoryid)) {
 120                  $categoryid = self::create_user_profile_category();
 121                  // Update the field to put it back into this category.
 122                  $fielddata->categoryid = $categoryid;
 123                  $DB->update_record('user_info_field', $fielddata);
 124              }
 125          }
 126          return $fielddata;
 127      }
 128  
 129      /**
 130       * Returns the user profile field table object.
 131       *
 132       * @return stdClass the moodlenet profile table object. False if no record found.
 133       */
 134      private static function get_user_profile_field(): \stdClass {
 135          global $DB;
 136          $fieldname = self::get_profile_field_name();
 137          $record = $DB->get_record('user_info_field', ['shortname' => $fieldname]);
 138          return ($record) ? $record : (object) [];
 139      }
 140  
 141      /**
 142       * This reports back if the category has been deleted or the config value is different.
 143       *
 144       * @param  int $categoryid The category id to check against.
 145       * @return bool True is the category checks out, otherwise false.
 146       */
 147      private static function check_profile_category(int $categoryid = null): bool {
 148          global $DB;
 149          $categoryname = self::get_category_name();
 150          $categorydata = $DB->get_record('user_info_category', ['name' => $categoryname]);
 151          if (empty($categorydata)) {
 152              return false;
 153          }
 154          if (isset($categoryid) && $categorydata->id != $categoryid) {
 155              return false;
 156          }
 157          return true;
 158      }
 159  
 160      /**
 161       * Are we using the proper user profile field to hold the mnet profile?
 162       *
 163       * @return bool True if we are using a user table field for the mnet profile. False means we are using costom profile fields.
 164       */
 165      public static function official_profile_exists(): bool {
 166          global $DB;
 167  
 168          $usertablecolumns = $DB->get_columns('user', false);
 169          if (isset($usertablecolumns['moodlenetprofile'])) {
 170              return true;
 171          }
 172          return false;
 173      }
 174  
 175      /**
 176       * Gets the category name that is set for this site.
 177       *
 178       * @return string The category used to hold the moodle net profile field.
 179       */
 180      public static function get_category_name(): string {
 181          return get_config('tool_moodlenet', 'profile_category');
 182      }
 183  
 184      /**
 185       * Sets the a unique category to hold the moodle net user profile.
 186       *
 187       * @param string $categoryname The base category name to use.
 188       * @return string The actual name of the category to use.
 189       */
 190      private static function set_category_name(string $categoryname): string {
 191          global $DB;
 192  
 193          $attemptname = $categoryname;
 194  
 195          // Check if this category already exists.
 196          $foundcategoryname = false;
 197          $i = 0;
 198          do {
 199              $category = $DB->count_records('user_info_category', ['name' => $attemptname]);
 200              if ($category > 0) {
 201                  $i++;
 202                  $attemptname = $categoryname . $i;
 203              } else {
 204                  set_config('profile_category', $attemptname, 'tool_moodlenet');
 205                  $foundcategoryname = true;
 206              }
 207          } while (!$foundcategoryname);
 208          return $attemptname;
 209      }
 210  
 211      /**
 212       * Create a custom user profile category to hold our custom field.
 213       *
 214       * @return int The id of the created category.
 215       */
 216      public static function create_user_profile_category(): int {
 217          global $DB;
 218          // No nice API to do this, so direct DB calls it is.
 219          $data = new \stdClass();
 220          $data->sortorder = $DB->count_records('user_info_category') + 1;
 221          $data->name = self::set_category_name(get_string('pluginname', 'tool_moodlenet'));
 222          $data->id = $DB->insert_record('user_info_category', $data, true);
 223  
 224          $createdcategory = $DB->get_record('user_info_category', array('id' => $data->id));
 225          \core\event\user_info_category_created::create_from_category($createdcategory)->trigger();
 226          return $createdcategory->id;
 227      }
 228  
 229      /**
 230       * Sets a unique name to be used for the moodle net profile.
 231       *
 232       * @param string $fieldname The base fieldname to use.
 233       * @return string The actual profile field name.
 234       */
 235      private static function set_profile_field_name(string $fieldname): string {
 236          global $DB;
 237  
 238          $attemptname = $fieldname;
 239  
 240          // Check if this profilefield already exists.
 241          $foundfieldname = false;
 242          $i = 0;
 243          do {
 244              $profilefield = $DB->count_records('user_info_field', ['shortname' => $attemptname]);
 245              if ($profilefield > 0) {
 246                  $i++;
 247                  $attemptname = $fieldname . $i;
 248              } else {
 249                  set_config('profile_field_name', $attemptname, 'tool_moodlenet');
 250                  $foundfieldname = true;
 251              }
 252          } while (!$foundfieldname);
 253          return $attemptname;
 254      }
 255  
 256      /**
 257       * Gets the unique profile field used to hold the moodle net profile.
 258       *
 259       * @return string The profile field name being used on this site.
 260       */
 261      public static function get_profile_field_name(): string {
 262          return get_config('tool_moodlenet', 'profile_field_name');
 263      }
 264  
 265  
 266      /**
 267       * Create a user profile field to hold the moodlenet profile information.
 268       *
 269       * @param  int $categoryid The category to put this field into.
 270       */
 271      public static function create_user_profile_text_field(int $categoryid): void {
 272          global $CFG;
 273  
 274          require_once($CFG->dirroot . '/user/profile/definelib.php');
 275          require_once($CFG->dirroot . '/user/profile/field/text/define.class.php');
 276  
 277          // Add our moodlenet profile field.
 278          $profileclass = new \profile_define_text();
 279          $data = (object) [
 280              'shortname' => self::set_profile_field_name('mnetprofile'),
 281              'name' => get_string('mnetprofile', 'tool_moodlenet'),
 282              'datatype' => 'text',
 283              'description' => get_string('mnetprofiledesc', 'tool_moodlenet'),
 284              'descriptionformat' => 1,
 285              'categoryid' => $categoryid,
 286              'signup' => 1,
 287              'forceunique' => 1,
 288              'visible' => 2,
 289              'param1' => 30,
 290              'param2' => 2048
 291          ];
 292          $profileclass->define_save($data);
 293      }
 294  
 295      /**
 296       * Given our $moodlenetprofile let's cURL the domains' WebFinger endpoint
 297       *
 298       * @param moodlenet_user_profile $moodlenetprofile The moodlenet profile to get info from.
 299       * @return array [bool, text, raw]
 300       */
 301      public static function get_moodlenet_profile_link(moodlenet_user_profile $moodlenetprofile): array {
 302          $domain = $moodlenetprofile->get_domain();
 303          $username = $moodlenetprofile->get_username();
 304  
 305          // Assumption: All MoodleNet instance's will contain a WebFinger validation script.
 306          $url = "https://".$domain."/.well-known/webfinger?resource=acct:".$username."@".$domain;
 307  
 308          $curl = new \curl();
 309          $options = [
 310              'CURLOPT_HEADER' => 0,
 311          ];
 312          $content = $curl->get($url, null, $options);
 313          $info = $curl->get_info();
 314  
 315          // The base cURL seems fine, let's press on.
 316          if (!$curl->get_errno() && !$curl->error) {
 317              // WebFinger gave us a 404 back so the user has no droids here.
 318              if ($info['http_code'] >= 400) {
 319                  if ($info['http_code'] === 404) {
 320                      // User not found.
 321                      return [
 322                          'result' => false,
 323                          'message' => get_string('profilevalidationfail', 'tool_moodlenet'),
 324                      ];
 325                  } else {
 326                      // There was some other error that was not a missing account.
 327                      return [
 328                          'result' => false,
 329                          'message' => get_string('profilevalidationerror', 'tool_moodlenet'),
 330                      ];
 331                  }
 332              }
 333  
 334              // We must have a valid link so give it back to the user.
 335              $data = json_decode($content);
 336              return [
 337                  'result' => true,
 338                  'message' => get_string('profilevalidationpass', 'tool_moodlenet'),
 339                  'domain' => $data->aliases[0]
 340              ];
 341          } else {
 342              // There was some failure in curl so report it back.
 343              return [
 344                  'result' => false,
 345                  'message' => get_string('profilevalidationerror', 'tool_moodlenet'),
 346              ];
 347          }
 348      }
 349  }