Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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   * Class for loading/storing oauth2 endpoints from the DB.
  19   *
  20   * @package    core
  21   * @copyright  2017 Damyon Wiese
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace core\oauth2;
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once($CFG->libdir . '/filelib.php');
  29  
  30  use context_system;
  31  use curl;
  32  use stdClass;
  33  use moodle_exception;
  34  use moodle_url;
  35  
  36  
  37  /**
  38   * Static list of api methods for system oauth2 configuration.
  39   *
  40   * @copyright  2017 Damyon Wiese
  41   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  42   */
  43  class api {
  44  
  45      /**
  46       * Build a google ready OAuth 2 service.
  47       * @return \core\oauth2\issuer
  48       */
  49      private static function init_google() {
  50          $record = (object) [
  51              'name' => 'Google',
  52              'image' => 'https://accounts.google.com/favicon.ico',
  53              'baseurl' => 'https://accounts.google.com/',
  54              'loginparamsoffline' => 'access_type=offline&prompt=consent',
  55              'showonloginpage' => true
  56          ];
  57  
  58          $issuer = new issuer(0, $record);
  59          return $issuer;
  60      }
  61  
  62      /**
  63       * Create endpoints for google issuers.
  64       * @param issuer $issuer issuer the endpoints should be created for.
  65       * @return mixed
  66       * @throws \coding_exception
  67       * @throws \core\invalid_persistent_exception
  68       */
  69      private static function create_endpoints_for_google($issuer) {
  70  
  71          $record = (object) [
  72              'issuerid' => $issuer->get('id'),
  73              'name' => 'discovery_endpoint',
  74              'url' => 'https://accounts.google.com/.well-known/openid-configuration'
  75          ];
  76          $endpoint = new endpoint(0, $record);
  77          $endpoint->create();
  78          return $issuer;
  79      }
  80  
  81      /**
  82       * Build a facebook ready OAuth 2 service.
  83       * @return \core\oauth2\issuer
  84       */
  85      private static function init_facebook() {
  86          // Facebook is a custom setup.
  87          $record = (object) [
  88              'name' => 'Facebook',
  89              'image' => 'https://facebookbrand.com/wp-content/uploads/2016/05/flogo_rgb_hex-brc-site-250.png',
  90              'baseurl' => '',
  91              'loginscopes' => 'public_profile email',
  92              'loginscopesoffline' => 'public_profile email',
  93              'showonloginpage' => true
  94          ];
  95  
  96          $issuer = new issuer(0, $record);
  97          return $issuer;
  98      }
  99  
 100      /**
 101       * Create endpoints for facebook issuers.
 102       * @param issuer $issuer issuer the endpoints should be created for.
 103       * @return mixed
 104       * @throws \coding_exception
 105       * @throws \core\invalid_persistent_exception
 106       */
 107      private static function create_endpoints_for_facebook($issuer) {
 108          // The Facebook API version.
 109          $apiversion = '2.12';
 110          // The Graph API URL.
 111          $graphurl = 'https://graph.facebook.com/v' . $apiversion;
 112          // User information fields that we want to fetch.
 113          $infofields = [
 114              'id',
 115              'first_name',
 116              'last_name',
 117              'link',
 118              'picture.type(large)',
 119              'name',
 120              'email',
 121          ];
 122          $endpoints = [
 123              'authorization_endpoint' => sprintf('https://www.facebook.com/v%s/dialog/oauth', $apiversion),
 124              'token_endpoint' => $graphurl . '/oauth/access_token',
 125              'userinfo_endpoint' => $graphurl . '/me?fields=' . implode(',', $infofields)
 126          ];
 127  
 128          foreach ($endpoints as $name => $url) {
 129              $record = (object) [
 130                  'issuerid' => $issuer->get('id'),
 131                  'name' => $name,
 132                  'url' => $url
 133              ];
 134              $endpoint = new endpoint(0, $record);
 135              $endpoint->create();
 136          }
 137  
 138          // Create the field mappings.
 139          $mapping = [
 140              'name' => 'alternatename',
 141              'last_name' => 'lastname',
 142              'email' => 'email',
 143              'first_name' => 'firstname',
 144              'picture-data-url' => 'picture',
 145              'link' => 'url',
 146          ];
 147          foreach ($mapping as $external => $internal) {
 148              $record = (object) [
 149                  'issuerid' => $issuer->get('id'),
 150                  'externalfield' => $external,
 151                  'internalfield' => $internal
 152              ];
 153              $userfieldmapping = new user_field_mapping(0, $record);
 154              $userfieldmapping->create();
 155          }
 156          return $issuer;
 157      }
 158  
 159      /**
 160       * Build a microsoft ready OAuth 2 service.
 161       * @return \core\oauth2\issuer
 162       */
 163      private static function init_microsoft() {
 164          // Microsoft is a custom setup.
 165          $record = (object) [
 166              'name' => 'Microsoft',
 167              'image' => 'https://www.microsoft.com/favicon.ico',
 168              'baseurl' => '',
 169              'loginscopes' => 'openid profile email user.read',
 170              'loginscopesoffline' => 'openid profile email user.read offline_access',
 171              'showonloginpage' => true
 172          ];
 173  
 174          $issuer = new issuer(0, $record);
 175          return $issuer;
 176      }
 177  
 178      /**
 179       * Create endpoints for microsoft issuers.
 180       * @param issuer $issuer issuer the endpoints should be created for.
 181       * @return mixed
 182       * @throws \coding_exception
 183       * @throws \core\invalid_persistent_exception
 184       */
 185      private static function create_endpoints_for_microsoft($issuer) {
 186  
 187          $endpoints = [
 188              'authorization_endpoint' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
 189              'token_endpoint' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
 190              'userinfo_endpoint' => 'https://graph.microsoft.com/v1.0/me/',
 191              'userpicture_endpoint' => 'https://graph.microsoft.com/v1.0/me/photo/$value',
 192          ];
 193  
 194          foreach ($endpoints as $name => $url) {
 195              $record = (object) [
 196                  'issuerid' => $issuer->get('id'),
 197                  'name' => $name,
 198                  'url' => $url
 199              ];
 200              $endpoint = new endpoint(0, $record);
 201              $endpoint->create();
 202          }
 203  
 204          // Create the field mappings.
 205          $mapping = [
 206              'givenName' => 'firstname',
 207              'surname' => 'lastname',
 208              'userPrincipalName' => 'email',
 209              'displayName' => 'alternatename',
 210              'officeLocation' => 'address',
 211              'mobilePhone' => 'phone1',
 212              'preferredLanguage' => 'lang'
 213          ];
 214          foreach ($mapping as $external => $internal) {
 215              $record = (object) [
 216                  'issuerid' => $issuer->get('id'),
 217                  'externalfield' => $external,
 218                  'internalfield' => $internal
 219              ];
 220              $userfieldmapping = new user_field_mapping(0, $record);
 221              $userfieldmapping->create();
 222          }
 223          return $issuer;
 224      }
 225  
 226      /**
 227       * Build a nextcloud ready OAuth 2 service.
 228       * @return \core\oauth2\issuer
 229       */
 230      private static function init_nextcloud() {
 231          // Nextcloud has a custom baseurl. Thus, the creation of endpoints has to be done later.
 232          $record = (object) [
 233              'name' => 'Nextcloud',
 234              'image' => 'https://nextcloud.com/wp-content/themes/next/assets/img/common/favicon.png?x16328',
 235              'basicauth' => 1,
 236          ];
 237  
 238          $issuer = new issuer(0, $record);
 239  
 240          return $issuer;
 241      }
 242  
 243      /**
 244       * Create endpoints for nextcloud issuers.
 245       * @param issuer $issuer issuer the endpoints should be created for.
 246       * @return mixed
 247       * @throws \coding_exception
 248       * @throws \core\invalid_persistent_exception
 249       */
 250      private static function create_endpoints_for_nextcloud($issuer) {
 251          $baseurl = $issuer->get('baseurl');
 252          // Add trailing slash to baseurl, if needed.
 253          if (substr($baseurl, -1) !== '/') {
 254              $baseurl .= '/';
 255          }
 256  
 257          $endpoints = [
 258              // Baseurl will be prepended later.
 259              'authorization_endpoint' => 'index.php/apps/oauth2/authorize',
 260              'token_endpoint' => 'index.php/apps/oauth2/api/v1/token',
 261              'userinfo_endpoint' => 'ocs/v2.php/cloud/user?format=json',
 262              'webdav_endpoint' => 'remote.php/webdav/',
 263              'ocs_endpoint' => 'ocs/v1.php/apps/files_sharing/api/v1/shares',
 264          ];
 265  
 266          foreach ($endpoints as $name => $url) {
 267              $record = (object) [
 268                  'issuerid' => $issuer->get('id'),
 269                  'name' => $name,
 270                  'url' => $baseurl . $url,
 271              ];
 272              $endpoint = new \core\oauth2\endpoint(0, $record);
 273              $endpoint->create();
 274          }
 275  
 276          // Create the field mappings.
 277          $mapping = [
 278              'ocs-data-email' => 'email',
 279              'ocs-data-id' => 'username',
 280          ];
 281          foreach ($mapping as $external => $internal) {
 282              $record = (object) [
 283                  'issuerid' => $issuer->get('id'),
 284                  'externalfield' => $external,
 285                  'internalfield' => $internal
 286              ];
 287              $userfieldmapping = new \core\oauth2\user_field_mapping(0, $record);
 288              $userfieldmapping->create();
 289          }
 290      }
 291  
 292      /**
 293       * Initializes a record for one of the standard issuers to be displayed in the settings.
 294       * The issuer is not yet created in the database.
 295       * @param string $type One of google, facebook, microsoft, nextcloud
 296       * @return \core\oauth2\issuer
 297       */
 298      public static function init_standard_issuer($type) {
 299          require_capability('moodle/site:config', context_system::instance());
 300          if ($type == 'google') {
 301              return self::init_google();
 302          } else if ($type == 'microsoft') {
 303              return self::init_microsoft();
 304          } else if ($type == 'facebook') {
 305              return self::init_facebook();
 306          } else if ($type == 'nextcloud') {
 307              return self::init_nextcloud();
 308          } else {
 309              throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
 310          }
 311      }
 312  
 313      /**
 314       * Create endpoints for standard issuers, based on the issuer created from submitted data.
 315       * @param string $type One of google, facebook, microsoft, nextcloud
 316       * @param issuer $issuer issuer the endpoints should be created for.
 317       * @return \core\oauth2\issuer
 318       */
 319      public static function create_endpoints_for_standard_issuer($type, $issuer) {
 320          require_capability('moodle/site:config', context_system::instance());
 321          if ($type == 'google') {
 322              $issuer = self::create_endpoints_for_google($issuer);
 323              self::discover_endpoints($issuer);
 324              return $issuer;
 325          } else if ($type == 'microsoft') {
 326              return self::create_endpoints_for_microsoft($issuer);
 327          } else if ($type == 'facebook') {
 328              return self::create_endpoints_for_facebook($issuer);
 329          } else if ($type == 'nextcloud') {
 330              return self::create_endpoints_for_nextcloud($issuer);
 331          } else {
 332              throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
 333          }
 334      }
 335  
 336      /**
 337       * Create one of the standard issuers.
 338       * @param string $type One of google, facebook, microsoft, or nextcloud
 339       * @param string|false $baseurl Baseurl (only required for nextcloud)
 340       * @return \core\oauth2\issuer
 341       */
 342      public static function create_standard_issuer($type, $baseurl = false) {
 343          require_capability('moodle/site:config', context_system::instance());
 344          if ($type == 'google') {
 345              $issuer = self::init_google();
 346              $issuer->create();
 347              return self::create_endpoints_for_google($issuer);
 348          } else if ($type == 'microsoft') {
 349              $issuer = self::init_microsoft();
 350              $issuer->create();
 351              return self::create_endpoints_for_microsoft($issuer);
 352          } else if ($type == 'facebook') {
 353              $issuer = self::init_facebook();
 354              $issuer->create();
 355              return self::create_endpoints_for_facebook($issuer);
 356          } else if ($type == 'nextcloud') {
 357              if (!$baseurl) {
 358                  throw new moodle_exception('Nextcloud service type requires the baseurl parameter.');
 359              }
 360              $issuer = self::init_nextcloud();
 361              $issuer->set('baseurl', $baseurl);
 362              $issuer->create();
 363              return self::create_endpoints_for_nextcloud($issuer);
 364          } else {
 365              throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
 366          }
 367      }
 368  
 369  
 370      /**
 371       * List all the issuers, ordered by the sortorder field
 372       * @return \core\oauth2\issuer[]
 373       */
 374      public static function get_all_issuers() {
 375          return issuer::get_records([], 'sortorder');
 376      }
 377  
 378      /**
 379       * Get a single issuer by id.
 380       *
 381       * @param int $id
 382       * @return \core\oauth2\issuer
 383       */
 384      public static function get_issuer($id) {
 385          return new issuer($id);
 386      }
 387  
 388      /**
 389       * Get a single endpoint by id.
 390       *
 391       * @param int $id
 392       * @return \core\oauth2\endpoint
 393       */
 394      public static function get_endpoint($id) {
 395          return new endpoint($id);
 396      }
 397  
 398      /**
 399       * Get a single user field mapping by id.
 400       *
 401       * @param int $id
 402       * @return \core\oauth2\user_field_mapping
 403       */
 404      public static function get_user_field_mapping($id) {
 405          return new user_field_mapping($id);
 406      }
 407  
 408      /**
 409       * Get the system account for an installed OAuth service.
 410       * Never ever ever expose this to a webservice because it contains the refresh token which grants API access.
 411       *
 412       * @param \core\oauth2\issuer $issuer
 413       * @return system_account|false
 414       */
 415      public static function get_system_account(issuer $issuer) {
 416          return system_account::get_record(['issuerid' => $issuer->get('id')]);
 417      }
 418  
 419      /**
 420       * Get the full list of system scopes required by an oauth issuer.
 421       * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins.
 422       *
 423       * @param \core\oauth2\issuer $issuer
 424       * @return string
 425       */
 426      public static function get_system_scopes_for_issuer($issuer) {
 427          $scopes = $issuer->get('loginscopesoffline');
 428  
 429          $pluginsfunction = get_plugins_with_function('oauth2_system_scopes', 'lib.php');
 430          foreach ($pluginsfunction as $plugintype => $plugins) {
 431              foreach ($plugins as $pluginfunction) {
 432                  // Get additional scopes from the plugin.
 433                  $pluginscopes = $pluginfunction($issuer);
 434                  if (empty($pluginscopes)) {
 435                      continue;
 436                  }
 437  
 438                  // Merge the additional scopes with the existing ones.
 439                  $additionalscopes = explode(' ', $pluginscopes);
 440  
 441                  foreach ($additionalscopes as $scope) {
 442                      if (!empty($scope)) {
 443                          if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) {
 444                              $scopes .= ' ' . $scope;
 445                          }
 446                      }
 447                  }
 448              }
 449          }
 450  
 451          return $scopes;
 452      }
 453  
 454      /**
 455       * Get an authenticated oauth2 client using the system account.
 456       * This call uses the refresh token to get an access token.
 457       *
 458       * @param \core\oauth2\issuer $issuer
 459       * @return \core\oauth2\client|false An authenticated client (or false if the token could not be upgraded)
 460       * @throws moodle_exception Request for token upgrade failed for technical reasons
 461       */
 462      public static function get_system_oauth_client(issuer $issuer) {
 463          $systemaccount = self::get_system_account($issuer);
 464          if (empty($systemaccount)) {
 465              return false;
 466          }
 467          // Get all the scopes!
 468          $scopes = self::get_system_scopes_for_issuer($issuer);
 469  
 470          $client = new \core\oauth2\client($issuer, null, $scopes, true);
 471  
 472          if (!$client->is_logged_in()) {
 473              if (!$client->upgrade_refresh_token($systemaccount)) {
 474                  return false;
 475              }
 476          }
 477          return $client;
 478      }
 479  
 480      /**
 481       * Get an authenticated oauth2 client using the current user account.
 482       * This call does the redirect dance back to the current page after authentication.
 483       *
 484       * @param \core\oauth2\issuer $issuer The desired OAuth issuer
 485       * @param moodle_url $currenturl The url to the current page.
 486       * @param string $additionalscopes The additional scopes required for authorization.
 487       * @return \core\oauth2\client
 488       */
 489      public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') {
 490          $client = new \core\oauth2\client($issuer, $currenturl, $additionalscopes);
 491  
 492          return $client;
 493      }
 494  
 495      /**
 496       * Get the list of defined endpoints for this OAuth issuer
 497       *
 498       * @param \core\oauth2\issuer $issuer The desired OAuth issuer
 499       * @return \core\oauth2\endpoint[]
 500       */
 501      public static function get_endpoints(issuer $issuer) {
 502          return endpoint::get_records(['issuerid' => $issuer->get('id')]);
 503      }
 504  
 505      /**
 506       * Get the list of defined mapping from OAuth user fields to moodle user fields.
 507       *
 508       * @param \core\oauth2\issuer $issuer The desired OAuth issuer
 509       * @return \core\oauth2\user_field_mapping[]
 510       */
 511      public static function get_user_field_mappings(issuer $issuer) {
 512          return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]);
 513      }
 514  
 515      /**
 516       * Guess an image from the discovery URL.
 517       *
 518       * @param \core\oauth2\issuer $issuer The desired OAuth issuer
 519       */
 520      protected static function guess_image($issuer) {
 521          if (empty($issuer->get('image')) && !empty($issuer->get('baseurl'))) {
 522              $baseurl = parse_url($issuer->get('baseurl'));
 523              $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico';
 524              $issuer->set('image', $imageurl);
 525              $issuer->update();
 526          }
 527      }
 528  
 529      /**
 530       * If the discovery endpoint exists for this issuer, try and determine the list of valid endpoints.
 531       *
 532       * @param issuer $issuer
 533       * @return int The number of discovered services.
 534       */
 535      protected static function discover_endpoints($issuer) {
 536          $curl = new curl();
 537  
 538          if (empty($issuer->get('baseurl'))) {
 539              return 0;
 540          }
 541  
 542          $url = $issuer->get_endpoint_url('discovery');
 543          if (!$url) {
 544              $url = $issuer->get('baseurl') . '/.well-known/openid-configuration';
 545          }
 546  
 547          if (!$json = $curl->get($url)) {
 548              $msg = 'Could not discover end points for identity issuer' . $issuer->get('name');
 549              throw new moodle_exception($msg);
 550          }
 551  
 552          if ($msg = $curl->error) {
 553              throw new moodle_exception('Could not discover service endpoints: ' . $msg);
 554          }
 555  
 556          $info = json_decode($json);
 557          if (empty($info)) {
 558              $msg = 'Could not discover end points for identity issuer' . $issuer->get('name');
 559              throw new moodle_exception($msg);
 560          }
 561  
 562          foreach (endpoint::get_records(['issuerid' => $issuer->get('id')]) as $endpoint) {
 563              if ($endpoint->get('name') != 'discovery_endpoint') {
 564                  $endpoint->delete();
 565              }
 566          }
 567  
 568          foreach ($info as $key => $value) {
 569              if (substr_compare($key, '_endpoint', - strlen('_endpoint')) === 0) {
 570                  $record = new stdClass();
 571                  $record->issuerid = $issuer->get('id');
 572                  $record->name = $key;
 573                  $record->url = $value;
 574  
 575                  $endpoint = new endpoint(0, $record);
 576                  $endpoint->create();
 577              }
 578  
 579              if ($key == 'scopes_supported') {
 580                  $issuer->set('scopessupported', implode(' ', $value));
 581                  $issuer->update();
 582              }
 583          }
 584  
 585          // We got to here - must be a decent OpenID connect service. Add the default user field mapping list.
 586          foreach (user_field_mapping::get_records(['issuerid' => $issuer->get('id')]) as $userfieldmapping) {
 587              $userfieldmapping->delete();
 588          }
 589  
 590          // Create the field mappings.
 591          $mapping = [
 592              'given_name' => 'firstname',
 593              'middle_name' => 'middlename',
 594              'family_name' => 'lastname',
 595              'email' => 'email',
 596              'website' => 'url',
 597              'nickname' => 'alternatename',
 598              'picture' => 'picture',
 599              'address' => 'address',
 600              'phone' => 'phone1',
 601              'locale' => 'lang'
 602          ];
 603          foreach ($mapping as $external => $internal) {
 604              $record = (object) [
 605                  'issuerid' => $issuer->get('id'),
 606                  'externalfield' => $external,
 607                  'internalfield' => $internal
 608              ];
 609              $userfieldmapping = new user_field_mapping(0, $record);
 610              $userfieldmapping->create();
 611          }
 612  
 613          return endpoint::count_records(['issuerid' => $issuer->get('id')]);
 614      }
 615  
 616      /**
 617       * Take the data from the mform and update the issuer.
 618       *
 619       * @param stdClass $data
 620       * @return \core\oauth2\issuer
 621       */
 622      public static function update_issuer($data) {
 623          require_capability('moodle/site:config', context_system::instance());
 624          $issuer = new issuer(0, $data);
 625  
 626          // Will throw exceptions on validation failures.
 627          $issuer->update();
 628  
 629          // Perform service discovery.
 630          self::discover_endpoints($issuer);
 631          self::guess_image($issuer);
 632          return $issuer;
 633      }
 634  
 635      /**
 636       * Take the data from the mform and create the issuer.
 637       *
 638       * @param stdClass $data
 639       * @return \core\oauth2\issuer
 640       */
 641      public static function create_issuer($data) {
 642          require_capability('moodle/site:config', context_system::instance());
 643          $issuer = new issuer(0, $data);
 644  
 645          // Will throw exceptions on validation failures.
 646          $issuer->create();
 647  
 648          // Perform service discovery.
 649          self::discover_endpoints($issuer);
 650          self::guess_image($issuer);
 651          return $issuer;
 652      }
 653  
 654      /**
 655       * Take the data from the mform and update the endpoint.
 656       *
 657       * @param stdClass $data
 658       * @return \core\oauth2\endpoint
 659       */
 660      public static function update_endpoint($data) {
 661          require_capability('moodle/site:config', context_system::instance());
 662          $endpoint = new endpoint(0, $data);
 663  
 664          // Will throw exceptions on validation failures.
 665          $endpoint->update();
 666  
 667          return $endpoint;
 668      }
 669  
 670      /**
 671       * Take the data from the mform and create the endpoint.
 672       *
 673       * @param stdClass $data
 674       * @return \core\oauth2\endpoint
 675       */
 676      public static function create_endpoint($data) {
 677          require_capability('moodle/site:config', context_system::instance());
 678          $endpoint = new endpoint(0, $data);
 679  
 680          // Will throw exceptions on validation failures.
 681          $endpoint->create();
 682          return $endpoint;
 683      }
 684  
 685      /**
 686       * Take the data from the mform and update the user field mapping.
 687       *
 688       * @param stdClass $data
 689       * @return \core\oauth2\user_field_mapping
 690       */
 691      public static function update_user_field_mapping($data) {
 692          require_capability('moodle/site:config', context_system::instance());
 693          $userfieldmapping = new user_field_mapping(0, $data);
 694  
 695          // Will throw exceptions on validation failures.
 696          $userfieldmapping->update();
 697  
 698          return $userfieldmapping;
 699      }
 700  
 701      /**
 702       * Take the data from the mform and create the user field mapping.
 703       *
 704       * @param stdClass $data
 705       * @return \core\oauth2\user_field_mapping
 706       */
 707      public static function create_user_field_mapping($data) {
 708          require_capability('moodle/site:config', context_system::instance());
 709          $userfieldmapping = new user_field_mapping(0, $data);
 710  
 711          // Will throw exceptions on validation failures.
 712          $userfieldmapping->create();
 713          return $userfieldmapping;
 714      }
 715  
 716      /**
 717       * Reorder this identity issuer.
 718       *
 719       * Requires moodle/site:config capability at the system context.
 720       *
 721       * @param int $id The id of the identity issuer to move.
 722       * @return boolean
 723       */
 724      public static function move_up_issuer($id) {
 725          require_capability('moodle/site:config', context_system::instance());
 726          $current = new issuer($id);
 727  
 728          $sortorder = $current->get('sortorder');
 729          if ($sortorder == 0) {
 730              return false;
 731          }
 732  
 733          $sortorder = $sortorder - 1;
 734          $current->set('sortorder', $sortorder);
 735  
 736          $filters = array('sortorder' => $sortorder);
 737          $children = issuer::get_records($filters, 'id');
 738          foreach ($children as $needtoswap) {
 739              $needtoswap->set('sortorder', $sortorder + 1);
 740              $needtoswap->update();
 741          }
 742  
 743          // OK - all set.
 744          $result = $current->update();
 745  
 746          return $result;
 747      }
 748  
 749      /**
 750       * Reorder this identity issuer.
 751       *
 752       * Requires moodle/site:config capability at the system context.
 753       *
 754       * @param int $id The id of the identity issuer to move.
 755       * @return boolean
 756       */
 757      public static function move_down_issuer($id) {
 758          require_capability('moodle/site:config', context_system::instance());
 759          $current = new issuer($id);
 760  
 761          $max = issuer::count_records();
 762          if ($max > 0) {
 763              $max--;
 764          }
 765  
 766          $sortorder = $current->get('sortorder');
 767          if ($sortorder >= $max) {
 768              return false;
 769          }
 770          $sortorder = $sortorder + 1;
 771          $current->set('sortorder', $sortorder);
 772  
 773          $filters = array('sortorder' => $sortorder);
 774          $children = issuer::get_records($filters);
 775          foreach ($children as $needtoswap) {
 776              $needtoswap->set('sortorder', $sortorder - 1);
 777              $needtoswap->update();
 778          }
 779  
 780          // OK - all set.
 781          $result = $current->update();
 782  
 783          return $result;
 784      }
 785  
 786      /**
 787       * Disable an identity issuer.
 788       *
 789       * Requires moodle/site:config capability at the system context.
 790       *
 791       * @param int $id The id of the identity issuer to disable.
 792       * @return boolean
 793       */
 794      public static function disable_issuer($id) {
 795          require_capability('moodle/site:config', context_system::instance());
 796          $issuer = new issuer($id);
 797  
 798          $issuer->set('enabled', 0);
 799          return $issuer->update();
 800      }
 801  
 802  
 803      /**
 804       * Enable an identity issuer.
 805       *
 806       * Requires moodle/site:config capability at the system context.
 807       *
 808       * @param int $id The id of the identity issuer to enable.
 809       * @return boolean
 810       */
 811      public static function enable_issuer($id) {
 812          require_capability('moodle/site:config', context_system::instance());
 813          $issuer = new issuer($id);
 814  
 815          $issuer->set('enabled', 1);
 816          return $issuer->update();
 817      }
 818  
 819      /**
 820       * Delete an identity issuer.
 821       *
 822       * Requires moodle/site:config capability at the system context.
 823       *
 824       * @param int $id The id of the identity issuer to delete.
 825       * @return boolean
 826       */
 827      public static function delete_issuer($id) {
 828          require_capability('moodle/site:config', context_system::instance());
 829          $issuer = new issuer($id);
 830  
 831          $systemaccount = self::get_system_account($issuer);
 832          if ($systemaccount) {
 833              $systemaccount->delete();
 834          }
 835          $endpoints = self::get_endpoints($issuer);
 836          if ($endpoints) {
 837              foreach ($endpoints as $endpoint) {
 838                  $endpoint->delete();
 839              }
 840          }
 841  
 842          // Will throw exceptions on validation failures.
 843          return $issuer->delete();
 844      }
 845  
 846      /**
 847       * Delete an endpoint.
 848       *
 849       * Requires moodle/site:config capability at the system context.
 850       *
 851       * @param int $id The id of the endpoint to delete.
 852       * @return boolean
 853       */
 854      public static function delete_endpoint($id) {
 855          require_capability('moodle/site:config', context_system::instance());
 856          $endpoint = new endpoint($id);
 857  
 858          // Will throw exceptions on validation failures.
 859          return $endpoint->delete();
 860      }
 861  
 862      /**
 863       * Delete a user_field_mapping.
 864       *
 865       * Requires moodle/site:config capability at the system context.
 866       *
 867       * @param int $id The id of the user_field_mapping to delete.
 868       * @return boolean
 869       */
 870      public static function delete_user_field_mapping($id) {
 871          require_capability('moodle/site:config', context_system::instance());
 872          $userfieldmapping = new user_field_mapping($id);
 873  
 874          // Will throw exceptions on validation failures.
 875          return $userfieldmapping->delete();
 876      }
 877  
 878      /**
 879       * Perform the OAuth dance and get a refresh token.
 880       *
 881       * Requires moodle/site:config capability at the system context.
 882       *
 883       * @param \core\oauth2\issuer $issuer
 884       * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication).
 885       * @return boolean
 886       */
 887      public static function connect_system_account($issuer, $returnurl) {
 888          require_capability('moodle/site:config', context_system::instance());
 889  
 890          // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access.
 891          $scopes = self::get_system_scopes_for_issuer($issuer);
 892  
 893          // Allow callbacks to inject non-standard scopes to the auth request.
 894  
 895          $client = new client($issuer, $returnurl, $scopes, true);
 896  
 897          if (!optional_param('response', false, PARAM_BOOL)) {
 898              $client->log_out();
 899          }
 900  
 901          if (optional_param('error', '', PARAM_RAW)) {
 902              return false;
 903          }
 904  
 905          if (!$client->is_logged_in()) {
 906              redirect($client->get_login_url());
 907          }
 908  
 909          $refreshtoken = $client->get_refresh_token();
 910          if (!$refreshtoken) {
 911              return false;
 912          }
 913  
 914          $systemaccount = self::get_system_account($issuer);
 915          if ($systemaccount) {
 916              $systemaccount->delete();
 917          }
 918  
 919          $userinfo = $client->get_userinfo();
 920  
 921          $record = new stdClass();
 922          $record->issuerid = $issuer->get('id');
 923          $record->refreshtoken = $refreshtoken;
 924          $record->grantedscopes = $scopes;
 925          $record->email = isset($userinfo['email']) ? $userinfo['email'] : '';
 926          $record->username = $userinfo['username'];
 927  
 928          $systemaccount = new system_account(0, $record);
 929  
 930          $systemaccount->create();
 931  
 932          $client->log_out();
 933          return true;
 934      }
 935  }