Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * 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       * Create a linkedin OAuth2 issuer
 294       *
 295       * @return issuer
 296       */
 297      private static function init_linkedin(): issuer {
 298          $record = (object) [
 299              'name' => 'LinkedIn',
 300              'image' => 'https://static.licdn.com/scds/common/u/images/logos/favicons/v1/favicon.ico',
 301              'baseurl' => 'https://api.linkedin.com/v2',
 302              'loginscopes' => 'r_liteprofile r_emailaddress',
 303              'loginscopesoffline' => 'r_liteprofile r_emailaddress',
 304              'showonloginpage' => true
 305          ];
 306  
 307          $issuer = new issuer(0, $record);
 308          return $issuer;
 309      }
 310  
 311      /**
 312       * Create endpoints for linkedin issuers.
 313       *
 314       * @param issuer $issuer
 315       * @throws \coding_exception
 316       * @throws \core\invalid_persistent_exception
 317       */
 318      private static function create_endpoints_for_linkedin(issuer $issuer) {
 319          $endpoints = [
 320              'authorization_endpoint' => 'https://www.linkedin.com/oauth/v2/authorization',
 321              'token_endpoint' => 'https://www.linkedin.com/oauth/v2/accessToken',
 322              'email_endpoint' => 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))',
 323              'userinfo_endpoint' => "https://api.linkedin.com/v2/me?projection=(localizedFirstName,localizedLastName,"
 324                  . "profilePicture(displayImage~digitalmediaAsset:playableStreams))",
 325          ];
 326          foreach ($endpoints as $name => $url) {
 327              $record = (object) [
 328                  'issuerid' => $issuer->get('id'),
 329                  'name' => $name,
 330                  'url' => $url
 331              ];
 332              $endpoint = new endpoint(0, $record);
 333              $endpoint->create();
 334          }
 335  
 336          // Create the field mappings.
 337          $mapping = [
 338              'localizedFirstName' => 'firstname',
 339              'localizedLastName' => 'lastname',
 340              'elements[0]-handle~-emailAddress' => 'email',
 341              'profilePicture-displayImage~-elements[0]-identifiers[0]-identifier' => 'picture'
 342          ];
 343          foreach ($mapping as $external => $internal) {
 344              $record = (object) [
 345                  'issuerid' => $issuer->get('id'),
 346                  'externalfield' => $external,
 347                  'internalfield' => $internal
 348              ];
 349              $userfieldmapping = new user_field_mapping(0, $record);
 350              $userfieldmapping->create();
 351          }
 352      }
 353  
 354      /**
 355       * Initializes a record for one of the standard issuers to be displayed in the settings.
 356       * The issuer is not yet created in the database.
 357       * @param string $type One of google, facebook, microsoft, nextcloud
 358       * @return \core\oauth2\issuer
 359       */
 360      public static function init_standard_issuer($type) {
 361          require_capability('moodle/site:config', context_system::instance());
 362          if ($type == 'google') {
 363              return self::init_google();
 364          } else if ($type == 'microsoft') {
 365              return self::init_microsoft();
 366          } else if ($type == 'facebook') {
 367              return self::init_facebook();
 368          } else if ($type == 'nextcloud') {
 369              return self::init_nextcloud();
 370          } else if ($type == 'linkedin') {
 371              return self::init_linkedin();
 372          } else {
 373              throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
 374          }
 375      }
 376  
 377      /**
 378       * Create endpoints for standard issuers, based on the issuer created from submitted data.
 379       * @param string $type One of google, facebook, microsoft, nextcloud
 380       * @param issuer $issuer issuer the endpoints should be created for.
 381       * @return \core\oauth2\issuer
 382       */
 383      public static function create_endpoints_for_standard_issuer($type, $issuer) {
 384          require_capability('moodle/site:config', context_system::instance());
 385          if ($type == 'google') {
 386              $issuer = self::create_endpoints_for_google($issuer);
 387              self::discover_endpoints($issuer);
 388              return $issuer;
 389          } else if ($type == 'microsoft') {
 390              return self::create_endpoints_for_microsoft($issuer);
 391          } else if ($type == 'facebook') {
 392              return self::create_endpoints_for_facebook($issuer);
 393          } else if ($type == 'nextcloud') {
 394              return self::create_endpoints_for_nextcloud($issuer);
 395          } else if ($type == 'linkedin') {
 396              return self::create_endpoints_for_linkedin($issuer);
 397          } else {
 398              throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
 399          }
 400      }
 401  
 402      /**
 403       * Create one of the standard issuers.
 404       * @param string $type One of google, facebook, microsoft, or nextcloud
 405       * @param string|false $baseurl Baseurl (only required for nextcloud)
 406       * @return \core\oauth2\issuer
 407       */
 408      public static function create_standard_issuer($type, $baseurl = false) {
 409          require_capability('moodle/site:config', context_system::instance());
 410          if ($type == 'google') {
 411              $issuer = self::init_google();
 412              $issuer->create();
 413              return self::create_endpoints_for_google($issuer);
 414          } else if ($type == 'microsoft') {
 415              $issuer = self::init_microsoft();
 416              $issuer->create();
 417              return self::create_endpoints_for_microsoft($issuer);
 418          } else if ($type == 'facebook') {
 419              $issuer = self::init_facebook();
 420              $issuer->create();
 421              return self::create_endpoints_for_facebook($issuer);
 422          } else if ($type == 'nextcloud') {
 423              if (!$baseurl) {
 424                  throw new moodle_exception('Nextcloud service type requires the baseurl parameter.');
 425              }
 426              $issuer = self::init_nextcloud();
 427              $issuer->set('baseurl', $baseurl);
 428              $issuer->create();
 429              return self::create_endpoints_for_nextcloud($issuer);
 430          } else {
 431              throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
 432          }
 433      }
 434  
 435  
 436      /**
 437       * List all the issuers, ordered by the sortorder field
 438       * @return \core\oauth2\issuer[]
 439       */
 440      public static function get_all_issuers() {
 441          return issuer::get_records([], 'sortorder');
 442      }
 443  
 444      /**
 445       * Get a single issuer by id.
 446       *
 447       * @param int $id
 448       * @return \core\oauth2\issuer
 449       */
 450      public static function get_issuer($id) {
 451          return new issuer($id);
 452      }
 453  
 454      /**
 455       * Get a single endpoint by id.
 456       *
 457       * @param int $id
 458       * @return \core\oauth2\endpoint
 459       */
 460      public static function get_endpoint($id) {
 461          return new endpoint($id);
 462      }
 463  
 464      /**
 465       * Get a single user field mapping by id.
 466       *
 467       * @param int $id
 468       * @return \core\oauth2\user_field_mapping
 469       */
 470      public static function get_user_field_mapping($id) {
 471          return new user_field_mapping($id);
 472      }
 473  
 474      /**
 475       * Get the system account for an installed OAuth service.
 476       * Never ever ever expose this to a webservice because it contains the refresh token which grants API access.
 477       *
 478       * @param \core\oauth2\issuer $issuer
 479       * @return system_account|false
 480       */
 481      public static function get_system_account(issuer $issuer) {
 482          return system_account::get_record(['issuerid' => $issuer->get('id')]);
 483      }
 484  
 485      /**
 486       * Get the full list of system scopes required by an oauth issuer.
 487       * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins.
 488       *
 489       * @param \core\oauth2\issuer $issuer
 490       * @return string
 491       */
 492      public static function get_system_scopes_for_issuer($issuer) {
 493          $scopes = $issuer->get('loginscopesoffline');
 494  
 495          $pluginsfunction = get_plugins_with_function('oauth2_system_scopes', 'lib.php');
 496          foreach ($pluginsfunction as $plugintype => $plugins) {
 497              foreach ($plugins as $pluginfunction) {
 498                  // Get additional scopes from the plugin.
 499                  $pluginscopes = $pluginfunction($issuer);
 500                  if (empty($pluginscopes)) {
 501                      continue;
 502                  }
 503  
 504                  // Merge the additional scopes with the existing ones.
 505                  $additionalscopes = explode(' ', $pluginscopes);
 506  
 507                  foreach ($additionalscopes as $scope) {
 508                      if (!empty($scope)) {
 509                          if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) {
 510                              $scopes .= ' ' . $scope;
 511                          }
 512                      }
 513                  }
 514              }
 515          }
 516  
 517          return $scopes;
 518      }
 519  
 520      /**
 521       * Get an authenticated oauth2 client using the system account.
 522       * This call uses the refresh token to get an access token.
 523       *
 524       * @param \core\oauth2\issuer $issuer
 525       * @return \core\oauth2\client|false An authenticated client (or false if the token could not be upgraded)
 526       * @throws moodle_exception Request for token upgrade failed for technical reasons
 527       */
 528      public static function get_system_oauth_client(issuer $issuer) {
 529          $systemaccount = self::get_system_account($issuer);
 530          if (empty($systemaccount)) {
 531              return false;
 532          }
 533          // Get all the scopes!
 534          $scopes = self::get_system_scopes_for_issuer($issuer);
 535          $class = self::get_client_classname($issuer->get('name'));
 536          $client = new $class($issuer, null, $scopes, true);
 537  
 538          if (!$client->is_logged_in()) {
 539              if (!$client->upgrade_refresh_token($systemaccount)) {
 540                  return false;
 541              }
 542          }
 543          return $client;
 544      }
 545  
 546      /**
 547       * Get an authenticated oauth2 client using the current user account.
 548       * This call does the redirect dance back to the current page after authentication.
 549       *
 550       * @param \core\oauth2\issuer $issuer The desired OAuth issuer
 551       * @param moodle_url $currenturl The url to the current page.
 552       * @param string $additionalscopes The additional scopes required for authorization.
 553       * @param bool $autorefresh Should the client support the use of refresh tokens to persist access across sessions.
 554       * @return \core\oauth2\client
 555       */
 556      public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '',
 557              $autorefresh = false) {
 558          $class = self::get_client_classname($issuer->get('name'));
 559          $client = new $class($issuer, $currenturl, $additionalscopes, false, $autorefresh);
 560  
 561          return $client;
 562      }
 563  
 564      /**
 565       * Get the client classname for an issuer.
 566       *
 567       * @param string $type The OAuth issuer name
 568       * @return string The classname for the custom client or core client class if the class for the defined type
 569       *                 doesn't exist or null type is defined.
 570       */
 571      protected static function get_client_classname(?string $type): string {
 572          // Default core client class.
 573          $classname = 'core\\oauth2\\client';
 574  
 575          if (strpos(strtolower($type), 'linkedin') !== false) {
 576              $classname = 'core\\oauth2\\client\\linkedin';
 577          }
 578  
 579          return $classname;
 580      }
 581  
 582      /**
 583       * Get the list of defined endpoints for this OAuth issuer
 584       *
 585       * @param \core\oauth2\issuer $issuer The desired OAuth issuer
 586       * @return \core\oauth2\endpoint[]
 587       */
 588      public static function get_endpoints(issuer $issuer) {
 589          return endpoint::get_records(['issuerid' => $issuer->get('id')]);
 590      }
 591  
 592      /**
 593       * Get the list of defined mapping from OAuth user fields to moodle user fields.
 594       *
 595       * @param \core\oauth2\issuer $issuer The desired OAuth issuer
 596       * @return \core\oauth2\user_field_mapping[]
 597       */
 598      public static function get_user_field_mappings(issuer $issuer) {
 599          return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]);
 600      }
 601  
 602      /**
 603       * Guess an image from the discovery URL.
 604       *
 605       * @param \core\oauth2\issuer $issuer The desired OAuth issuer
 606       */
 607      protected static function guess_image($issuer) {
 608          if (empty($issuer->get('image')) && !empty($issuer->get('baseurl'))) {
 609              $baseurl = parse_url($issuer->get('baseurl'));
 610              $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico';
 611              $issuer->set('image', $imageurl);
 612              $issuer->update();
 613          }
 614      }
 615  
 616      /**
 617       * If the discovery endpoint exists for this issuer, try and determine the list of valid endpoints.
 618       *
 619       * @param issuer $issuer
 620       * @return int The number of discovered services.
 621       */
 622      protected static function discover_endpoints($issuer) {
 623          $curl = new curl();
 624  
 625          if (empty($issuer->get('baseurl'))) {
 626              return 0;
 627          }
 628  
 629          $url = $issuer->get_endpoint_url('discovery');
 630          if (!$url) {
 631              $url = $issuer->get('baseurl') . '/.well-known/openid-configuration';
 632          }
 633  
 634          if (!$json = $curl->get($url)) {
 635              $msg = 'Could not discover end points for identity issuer' . $issuer->get('name');
 636              throw new moodle_exception($msg);
 637          }
 638  
 639          if ($msg = $curl->error) {
 640              throw new moodle_exception('Could not discover service endpoints: ' . $msg);
 641          }
 642  
 643          $info = json_decode($json);
 644          if (empty($info)) {
 645              $msg = 'Could not discover end points for identity issuer' . $issuer->get('name');
 646              throw new moodle_exception($msg);
 647          }
 648  
 649          foreach (endpoint::get_records(['issuerid' => $issuer->get('id')]) as $endpoint) {
 650              if ($endpoint->get('name') != 'discovery_endpoint') {
 651                  $endpoint->delete();
 652              }
 653          }
 654  
 655          foreach ($info as $key => $value) {
 656              if (substr_compare($key, '_endpoint', - strlen('_endpoint')) === 0) {
 657                  $record = new stdClass();
 658                  $record->issuerid = $issuer->get('id');
 659                  $record->name = $key;
 660                  $record->url = $value;
 661  
 662                  $endpoint = new endpoint(0, $record);
 663                  $endpoint->create();
 664              }
 665  
 666              if ($key == 'scopes_supported') {
 667                  $issuer->set('scopessupported', implode(' ', $value));
 668                  $issuer->update();
 669              }
 670          }
 671  
 672          // We got to here - must be a decent OpenID connect service. Add the default user field mapping list.
 673          foreach (user_field_mapping::get_records(['issuerid' => $issuer->get('id')]) as $userfieldmapping) {
 674              $userfieldmapping->delete();
 675          }
 676  
 677          // Create the field mappings.
 678          $mapping = [
 679              'given_name' => 'firstname',
 680              'middle_name' => 'middlename',
 681              'family_name' => 'lastname',
 682              'email' => 'email',
 683              'website' => 'url',
 684              'nickname' => 'alternatename',
 685              'picture' => 'picture',
 686              'address' => 'address',
 687              'phone' => 'phone1',
 688              'locale' => 'lang'
 689          ];
 690          foreach ($mapping as $external => $internal) {
 691              $record = (object) [
 692                  'issuerid' => $issuer->get('id'),
 693                  'externalfield' => $external,
 694                  'internalfield' => $internal
 695              ];
 696              $userfieldmapping = new user_field_mapping(0, $record);
 697              $userfieldmapping->create();
 698          }
 699  
 700          return endpoint::count_records(['issuerid' => $issuer->get('id')]);
 701      }
 702  
 703      /**
 704       * Take the data from the mform and update the issuer.
 705       *
 706       * @param stdClass $data
 707       * @return \core\oauth2\issuer
 708       */
 709      public static function update_issuer($data) {
 710          require_capability('moodle/site:config', context_system::instance());
 711          $issuer = new issuer(0, $data);
 712  
 713          // Will throw exceptions on validation failures.
 714          $issuer->update();
 715  
 716          // Perform service discovery.
 717          self::discover_endpoints($issuer);
 718          self::guess_image($issuer);
 719          return $issuer;
 720      }
 721  
 722      /**
 723       * Take the data from the mform and create the issuer.
 724       *
 725       * @param stdClass $data
 726       * @return \core\oauth2\issuer
 727       */
 728      public static function create_issuer($data) {
 729          require_capability('moodle/site:config', context_system::instance());
 730          $issuer = new issuer(0, $data);
 731  
 732          // Will throw exceptions on validation failures.
 733          $issuer->create();
 734  
 735          // Perform service discovery.
 736          self::discover_endpoints($issuer);
 737          self::guess_image($issuer);
 738          return $issuer;
 739      }
 740  
 741      /**
 742       * Take the data from the mform and update the endpoint.
 743       *
 744       * @param stdClass $data
 745       * @return \core\oauth2\endpoint
 746       */
 747      public static function update_endpoint($data) {
 748          require_capability('moodle/site:config', context_system::instance());
 749          $endpoint = new endpoint(0, $data);
 750  
 751          // Will throw exceptions on validation failures.
 752          $endpoint->update();
 753  
 754          return $endpoint;
 755      }
 756  
 757      /**
 758       * Take the data from the mform and create the endpoint.
 759       *
 760       * @param stdClass $data
 761       * @return \core\oauth2\endpoint
 762       */
 763      public static function create_endpoint($data) {
 764          require_capability('moodle/site:config', context_system::instance());
 765          $endpoint = new endpoint(0, $data);
 766  
 767          // Will throw exceptions on validation failures.
 768          $endpoint->create();
 769          return $endpoint;
 770      }
 771  
 772      /**
 773       * Take the data from the mform and update the user field mapping.
 774       *
 775       * @param stdClass $data
 776       * @return \core\oauth2\user_field_mapping
 777       */
 778      public static function update_user_field_mapping($data) {
 779          require_capability('moodle/site:config', context_system::instance());
 780          $userfieldmapping = new user_field_mapping(0, $data);
 781  
 782          // Will throw exceptions on validation failures.
 783          $userfieldmapping->update();
 784  
 785          return $userfieldmapping;
 786      }
 787  
 788      /**
 789       * Take the data from the mform and create the user field mapping.
 790       *
 791       * @param stdClass $data
 792       * @return \core\oauth2\user_field_mapping
 793       */
 794      public static function create_user_field_mapping($data) {
 795          require_capability('moodle/site:config', context_system::instance());
 796          $userfieldmapping = new user_field_mapping(0, $data);
 797  
 798          // Will throw exceptions on validation failures.
 799          $userfieldmapping->create();
 800          return $userfieldmapping;
 801      }
 802  
 803      /**
 804       * Reorder this 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 move.
 809       * @return boolean
 810       */
 811      public static function move_up_issuer($id) {
 812          require_capability('moodle/site:config', context_system::instance());
 813          $current = new issuer($id);
 814  
 815          $sortorder = $current->get('sortorder');
 816          if ($sortorder == 0) {
 817              return false;
 818          }
 819  
 820          $sortorder = $sortorder - 1;
 821          $current->set('sortorder', $sortorder);
 822  
 823          $filters = array('sortorder' => $sortorder);
 824          $children = issuer::get_records($filters, 'id');
 825          foreach ($children as $needtoswap) {
 826              $needtoswap->set('sortorder', $sortorder + 1);
 827              $needtoswap->update();
 828          }
 829  
 830          // OK - all set.
 831          $result = $current->update();
 832  
 833          return $result;
 834      }
 835  
 836      /**
 837       * Reorder this identity issuer.
 838       *
 839       * Requires moodle/site:config capability at the system context.
 840       *
 841       * @param int $id The id of the identity issuer to move.
 842       * @return boolean
 843       */
 844      public static function move_down_issuer($id) {
 845          require_capability('moodle/site:config', context_system::instance());
 846          $current = new issuer($id);
 847  
 848          $max = issuer::count_records();
 849          if ($max > 0) {
 850              $max--;
 851          }
 852  
 853          $sortorder = $current->get('sortorder');
 854          if ($sortorder >= $max) {
 855              return false;
 856          }
 857          $sortorder = $sortorder + 1;
 858          $current->set('sortorder', $sortorder);
 859  
 860          $filters = array('sortorder' => $sortorder);
 861          $children = issuer::get_records($filters);
 862          foreach ($children as $needtoswap) {
 863              $needtoswap->set('sortorder', $sortorder - 1);
 864              $needtoswap->update();
 865          }
 866  
 867          // OK - all set.
 868          $result = $current->update();
 869  
 870          return $result;
 871      }
 872  
 873      /**
 874       * Disable an identity issuer.
 875       *
 876       * Requires moodle/site:config capability at the system context.
 877       *
 878       * @param int $id The id of the identity issuer to disable.
 879       * @return boolean
 880       */
 881      public static function disable_issuer($id) {
 882          require_capability('moodle/site:config', context_system::instance());
 883          $issuer = new issuer($id);
 884  
 885          $issuer->set('enabled', 0);
 886          return $issuer->update();
 887      }
 888  
 889  
 890      /**
 891       * Enable an identity issuer.
 892       *
 893       * Requires moodle/site:config capability at the system context.
 894       *
 895       * @param int $id The id of the identity issuer to enable.
 896       * @return boolean
 897       */
 898      public static function enable_issuer($id) {
 899          require_capability('moodle/site:config', context_system::instance());
 900          $issuer = new issuer($id);
 901  
 902          $issuer->set('enabled', 1);
 903          return $issuer->update();
 904      }
 905  
 906      /**
 907       * Delete an identity issuer.
 908       *
 909       * Requires moodle/site:config capability at the system context.
 910       *
 911       * @param int $id The id of the identity issuer to delete.
 912       * @return boolean
 913       */
 914      public static function delete_issuer($id) {
 915          require_capability('moodle/site:config', context_system::instance());
 916          $issuer = new issuer($id);
 917  
 918          $systemaccount = self::get_system_account($issuer);
 919          if ($systemaccount) {
 920              $systemaccount->delete();
 921          }
 922          $endpoints = self::get_endpoints($issuer);
 923          if ($endpoints) {
 924              foreach ($endpoints as $endpoint) {
 925                  $endpoint->delete();
 926              }
 927          }
 928  
 929          // Will throw exceptions on validation failures.
 930          return $issuer->delete();
 931      }
 932  
 933      /**
 934       * Delete an endpoint.
 935       *
 936       * Requires moodle/site:config capability at the system context.
 937       *
 938       * @param int $id The id of the endpoint to delete.
 939       * @return boolean
 940       */
 941      public static function delete_endpoint($id) {
 942          require_capability('moodle/site:config', context_system::instance());
 943          $endpoint = new endpoint($id);
 944  
 945          // Will throw exceptions on validation failures.
 946          return $endpoint->delete();
 947      }
 948  
 949      /**
 950       * Delete a user_field_mapping.
 951       *
 952       * Requires moodle/site:config capability at the system context.
 953       *
 954       * @param int $id The id of the user_field_mapping to delete.
 955       * @return boolean
 956       */
 957      public static function delete_user_field_mapping($id) {
 958          require_capability('moodle/site:config', context_system::instance());
 959          $userfieldmapping = new user_field_mapping($id);
 960  
 961          // Will throw exceptions on validation failures.
 962          return $userfieldmapping->delete();
 963      }
 964  
 965      /**
 966       * Perform the OAuth dance and get a refresh token.
 967       *
 968       * Requires moodle/site:config capability at the system context.
 969       *
 970       * @param \core\oauth2\issuer $issuer
 971       * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication).
 972       * @return boolean
 973       */
 974      public static function connect_system_account($issuer, $returnurl) {
 975          require_capability('moodle/site:config', context_system::instance());
 976  
 977          // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access.
 978          $scopes = self::get_system_scopes_for_issuer($issuer);
 979  
 980          // Allow callbacks to inject non-standard scopes to the auth request.
 981          $class = self::get_client_classname($issuer->get('name'));
 982          $client = new $class($issuer, $returnurl, $scopes, true);
 983  
 984          if (!optional_param('response', false, PARAM_BOOL)) {
 985              $client->log_out();
 986          }
 987  
 988          if (optional_param('error', '', PARAM_RAW)) {
 989              return false;
 990          }
 991  
 992          if (!$client->is_logged_in()) {
 993              redirect($client->get_login_url());
 994          }
 995  
 996          $refreshtoken = $client->get_refresh_token();
 997          if (!$refreshtoken) {
 998              return false;
 999          }
1000  
1001          $systemaccount = self::get_system_account($issuer);
1002          if ($systemaccount) {
1003              $systemaccount->delete();
1004          }
1005  
1006          $userinfo = $client->get_userinfo();
1007  
1008          $record = new stdClass();
1009          $record->issuerid = $issuer->get('id');
1010          $record->refreshtoken = $refreshtoken;
1011          $record->grantedscopes = $scopes;
1012          $record->email = isset($userinfo['email']) ? $userinfo['email'] : '';
1013          $record->username = $userinfo['username'];
1014  
1015          $systemaccount = new system_account(0, $record);
1016  
1017          $systemaccount->create();
1018  
1019          $client->log_out();
1020          return true;
1021      }
1022  }