Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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   * Microsoft Live Skydrive Repository Plugin
  19   *
  20   * @package    repository_onedrive
  21   * @copyright  2012 Lancaster University Network Services Ltd
  22   * @author     Dan Poltawski <dan.poltawski@luns.net.uk>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * Microsoft onedrive repository plugin.
  30   *
  31   * @package    repository_onedrive
  32   * @copyright  2012 Lancaster University Network Services Ltd
  33   * @author     Dan Poltawski <dan.poltawski@luns.net.uk>
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class repository_onedrive extends repository {
  37      /**
  38       * OAuth 2 client
  39       * @var \core\oauth2\client
  40       */
  41      private $client = null;
  42  
  43      /**
  44       * OAuth 2 Issuer
  45       * @var \core\oauth2\issuer
  46       */
  47      private $issuer = null;
  48  
  49      /**
  50       * Additional scopes required for drive.
  51       */
  52      const SCOPES = 'files.readwrite.all';
  53  
  54      /**
  55       * Constructor.
  56       *
  57       * @param int $repositoryid repository instance id.
  58       * @param int|stdClass $context a context id or context object.
  59       * @param array $options repository options.
  60       * @param int $readonly indicate this repo is readonly or not.
  61       * @return void
  62       */
  63      public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
  64          parent::__construct($repositoryid, $context, $options, $readonly = 0);
  65  
  66          try {
  67              $this->issuer = \core\oauth2\api::get_issuer(get_config('onedrive', 'issuerid'));
  68          } catch (dml_missing_record_exception $e) {
  69              $this->disabled = true;
  70          }
  71  
  72          if ($this->issuer && !$this->issuer->get('enabled')) {
  73              $this->disabled = true;
  74          }
  75      }
  76  
  77      /**
  78       * Get a cached user authenticated oauth client.
  79       *
  80       * @param moodle_url $overrideurl - Use this url instead of the repo callback.
  81       * @return \core\oauth2\client
  82       */
  83      protected function get_user_oauth_client($overrideurl = false) {
  84          if ($this->client) {
  85              return $this->client;
  86          }
  87          if ($overrideurl) {
  88              $returnurl = $overrideurl;
  89          } else {
  90              $returnurl = new moodle_url('/repository/repository_callback.php');
  91              $returnurl->param('callback', 'yes');
  92              $returnurl->param('repo_id', $this->id);
  93              $returnurl->param('sesskey', sesskey());
  94          }
  95  
  96          $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES, true);
  97  
  98          return $this->client;
  99      }
 100  
 101      /**
 102       * Checks whether the user is authenticate or not.
 103       *
 104       * @return bool true when logged in.
 105       */
 106      public function check_login() {
 107          $client = $this->get_user_oauth_client();
 108          return $client->is_logged_in();
 109      }
 110  
 111      /**
 112       * Print or return the login form.
 113       *
 114       * @return void|array for ajax.
 115       */
 116      public function print_login() {
 117          $client = $this->get_user_oauth_client();
 118          $url = $client->get_login_url();
 119  
 120          if ($this->options['ajax']) {
 121              $popup = new stdClass();
 122              $popup->type = 'popup';
 123              $popup->url = $url->out(false);
 124              return array('login' => array($popup));
 125          } else {
 126              echo '<a target="_blank" href="'.$url->out(false).'">'.get_string('login', 'repository').'</a>';
 127          }
 128      }
 129  
 130      /**
 131       * Print the login in a popup.
 132       *
 133       * @param array|null $attr Custom attributes to be applied to popup div.
 134       */
 135      public function print_login_popup($attr = null) {
 136          global $OUTPUT, $PAGE;
 137  
 138          $client = $this->get_user_oauth_client(false);
 139          $url = new moodle_url($client->get_login_url());
 140          $state = $url->get_param('state') . '&reloadparent=true';
 141          $url->param('state', $state);
 142  
 143          $PAGE->set_pagelayout('embedded');
 144          echo $OUTPUT->header();
 145  
 146          $repositoryname = get_string('pluginname', 'repository_onedrive');
 147  
 148          $button = new single_button(
 149              $url,
 150              get_string('logintoaccount', 'repository', $repositoryname),
 151              'post',
 152              single_button::BUTTON_PRIMARY
 153          );
 154          $button->add_action(new popup_action('click', $url, 'Login'));
 155          $button->class = 'mdl-align';
 156          $button = $OUTPUT->render($button);
 157          echo html_writer::div($button, '', $attr);
 158  
 159          echo $OUTPUT->footer();
 160      }
 161  
 162      /**
 163       * Build the breadcrumb from a path.
 164       *
 165       * @param string $path to create a breadcrumb from.
 166       * @return array containing name and path of each crumb.
 167       */
 168      protected function build_breadcrumb($path) {
 169          $bread = explode('/', $path);
 170          $crumbtrail = '';
 171          foreach ($bread as $crumb) {
 172              list($id, $name) = $this->explode_node_path($crumb);
 173              $name = empty($name) ? $id : $name;
 174              $breadcrumb[] = array(
 175                  'name' => $name,
 176                  'path' => $this->build_node_path($id, $name, $crumbtrail)
 177              );
 178              $tmp = end($breadcrumb);
 179              $crumbtrail = $tmp['path'];
 180          }
 181          return $breadcrumb;
 182      }
 183  
 184      /**
 185       * Generates a safe path to a node.
 186       *
 187       * Typically, a node will be id|Name of the node.
 188       *
 189       * @param string $id of the node.
 190       * @param string $name of the node, will be URL encoded.
 191       * @param string $root to append the node on, must be a result of this function.
 192       * @return string path to the node.
 193       */
 194      protected function build_node_path($id, $name = '', $root = '') {
 195          $path = $id;
 196          if (!empty($name)) {
 197              $path .= '|' . urlencode($name);
 198          }
 199          if (!empty($root)) {
 200              $path = trim($root, '/') . '/' . $path;
 201          }
 202          return $path;
 203      }
 204  
 205      /**
 206       * Returns information about a node in a path.
 207       *
 208       * @see self::build_node_path()
 209       * @param string $node to extrat information from.
 210       * @return array about the node.
 211       */
 212      protected function explode_node_path($node) {
 213          if (strpos($node, '|') !== false) {
 214              list($id, $name) = explode('|', $node, 2);
 215              $name = urldecode($name);
 216          } else {
 217              $id = $node;
 218              $name = '';
 219          }
 220          $id = urldecode($id);
 221          return array(
 222              0 => $id,
 223              1 => $name,
 224              'id' => $id,
 225              'name' => $name
 226          );
 227      }
 228  
 229      /**
 230       * List the files and folders.
 231       *
 232       * @param  string $path path to browse.
 233       * @param  string $page page to browse.
 234       * @return array of result.
 235       */
 236      public function get_listing($path='', $page = '') {
 237          if (empty($path)) {
 238              $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive'));
 239          }
 240  
 241          if ($this->disabled) {
 242              // Empty list of files for disabled repository.
 243              return ['dynload' => false, 'list' => [], 'nologin' => true];
 244          }
 245  
 246          // We analyse the path to extract what to browse.
 247          $trail = explode('/', $path);
 248          $uri = array_pop($trail);
 249          list($id, $name) = $this->explode_node_path($uri);
 250  
 251          // Handle the special keyword 'search', which we defined in self::search() so that
 252          // we could set up a breadcrumb in the search results. In any other case ID would be
 253          // 'root' which is a special keyword, or a parent (folder) ID.
 254          if ($id === 'search') {
 255              $q = $name;
 256              $id = 'root';
 257  
 258              // Append the active path for search.
 259              $str = get_string('searchfor', 'repository_onedrive', $searchtext);
 260              $path = $this->build_node_path('search', $str, $path);
 261          }
 262  
 263          // Query the Drive.
 264          $parent = $id;
 265          if ($parent != 'root') {
 266              $parent = 'items/' . $parent;
 267          }
 268          $q = '';
 269          $results = $this->query($q, $path, $parent);
 270  
 271          $ret = [];
 272          $ret['dynload'] = true;
 273          $ret['path'] = $this->build_breadcrumb($path);
 274          $ret['list'] = $results;
 275          $ret['manage'] = 'https://www.office.com/';
 276          return $ret;
 277      }
 278  
 279      /**
 280       * Search throughout the OneDrive
 281       *
 282       * @param string $searchtext text to search for.
 283       * @param int $page search page.
 284       * @return array of results.
 285       */
 286      public function search($searchtext, $page = 0) {
 287          $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive'));
 288          $str = get_string('searchfor', 'repository_onedrive', $searchtext);
 289          $path = $this->build_node_path('search', $str, $path);
 290  
 291          // Query the Drive.
 292          $parent = 'root';
 293          $results = $this->query($searchtext, $path, 'root');
 294  
 295          $ret = [];
 296          $ret['dynload'] = true;
 297          $ret['path'] = $this->build_breadcrumb($path);
 298          $ret['list'] = $results;
 299          $ret['manage'] = 'https://www.office.com/';
 300          return $ret;
 301      }
 302  
 303      /**
 304       * Query OneDrive for files and folders using a search query.
 305       *
 306       * Documentation about the query format can be found here:
 307       *   https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/driveitem
 308       *   https://developer.microsoft.com/en-us/graph/docs/overview/query_parameters
 309       *
 310       * This returns a list of files and folders with their details as they should be
 311       * formatted and returned by functions such as get_listing() or search().
 312       *
 313       * @param string $q search query as expected by the Graph API.
 314       * @param string $path parent path of the current files, will not be used for the query.
 315       * @param string $parent Parent id.
 316       * @param int $page page.
 317       * @return array of files and folders.
 318       * @throws Exception
 319       * @throws repository_exception
 320       */
 321      protected function query($q, $path = null, $parent = null, $page = 0) {
 322          global $OUTPUT;
 323  
 324          $files = [];
 325          $folders = [];
 326          $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,thumbnails";
 327          $params = ['$select' => $fields, '$expand' => 'thumbnails', 'parent' => $parent];
 328  
 329          try {
 330              // Retrieving files and folders.
 331              $client = $this->get_user_oauth_client();
 332              $service = new repository_onedrive\rest($client);
 333  
 334              if (!empty($q)) {
 335                  $params['search'] = urlencode($q);
 336  
 337                  // MS does not return thumbnails on a search.
 338                  unset($params['$expand']);
 339                  $response = $service->call('search', $params);
 340              } else {
 341                  $response = $service->call('list', $params);
 342              }
 343          } catch (Exception $e) {
 344              if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
 345                  throw new repository_exception('servicenotenabled', 'repository_onedrive');
 346              } else if (strpos($e->getMessage(), 'mysite not found') !== false) {
 347                  throw new repository_exception('mysitenotfound', 'repository_onedrive');
 348              }
 349          }
 350  
 351          $remotefiles = isset($response->value) ? $response->value : [];
 352          foreach ($remotefiles as $remotefile) {
 353              if (!empty($remotefile->folder)) {
 354                  // This is a folder.
 355                  $folders[$remotefile->id] = [
 356                      'title' => $remotefile->name,
 357                      'path' => $this->build_node_path($remotefile->id, $remotefile->name, $path),
 358                      'date' => strtotime($remotefile->lastModifiedDateTime),
 359                      'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false),
 360                      'thumbnail_height' => 64,
 361                      'thumbnail_width' => 64,
 362                      'children' => []
 363                  ];
 364              } else {
 365                  // We can download all other file types.
 366                  $title = $remotefile->name;
 367                  $source = json_encode([
 368                          'id' => $remotefile->id,
 369                          'name' => $remotefile->name,
 370                          'link' => $remotefile->webUrl
 371                      ]);
 372  
 373                  $thumb = '';
 374                  $thumbwidth = 0;
 375                  $thumbheight = 0;
 376                  $extendedinfoerr = false;
 377  
 378                  if (empty($remotefile->thumbnails)) {
 379                      // Try and get it directly from the item.
 380                      $params = ['fileid' => $remotefile->id, '$select' => $fields, '$expand' => 'thumbnails'];
 381                      try {
 382                          $response = $service->call('get', $params);
 383                          $remotefile = $response;
 384                      } catch (Exception $e) {
 385                          // This is not a failure condition - we just could not get extended info about the file.
 386                          $extendedinfoerr = true;
 387                      }
 388                  }
 389  
 390                  if (!empty($remotefile->thumbnails)) {
 391                      $thumbs = $remotefile->thumbnails;
 392                      if (count($thumbs)) {
 393                          $first = reset($thumbs);
 394                          if (!empty($first->medium) && !empty($first->medium->url)) {
 395                              $thumb = $first->medium->url;
 396                              $thumbwidth = min($first->medium->width, 64);
 397                              $thumbheight = min($first->medium->height, 64);
 398                          }
 399                      }
 400                  }
 401  
 402                  $files[$remotefile->id] = [
 403                      'title' => $title,
 404                      'source' => $source,
 405                      'date' => strtotime($remotefile->lastModifiedDateTime),
 406                      'size' => isset($remotefile->size) ? $remotefile->size : null,
 407                      'thumbnail' => $thumb,
 408                      'thumbnail_height' => $thumbwidth,
 409                      'thumbnail_width' => $thumbheight,
 410                  ];
 411              }
 412          }
 413  
 414          // Filter and order the results.
 415          $files = array_filter($files, [$this, 'filter']);
 416          core_collator::ksort($files, core_collator::SORT_NATURAL);
 417          core_collator::ksort($folders, core_collator::SORT_NATURAL);
 418          return array_merge(array_values($folders), array_values($files));
 419      }
 420  
 421      /**
 422       * Logout.
 423       *
 424       * @return string
 425       */
 426      public function logout() {
 427          $client = $this->get_user_oauth_client();
 428          $client->log_out();
 429          return parent::logout();
 430      }
 431  
 432      /**
 433       * Get a file.
 434       *
 435       * @param string $reference reference of the file.
 436       * @param string $filename filename to save the file to.
 437       * @return string JSON encoded array of information about the file.
 438       */
 439      public function get_file($reference, $filename = '') {
 440          global $CFG;
 441  
 442          if ($this->disabled) {
 443              throw new repository_exception('cannotdownload', 'repository');
 444          }
 445          $sourceinfo = json_decode($reference);
 446  
 447          $client = null;
 448          if (!empty($sourceinfo->usesystem)) {
 449              $client = \core\oauth2\api::get_system_oauth_client($this->issuer);
 450          } else {
 451              $client = $this->get_user_oauth_client();
 452          }
 453  
 454          $base = 'https://graph.microsoft.com/v1.0/';
 455  
 456          $sourceurl = new moodle_url($base . 'me/drive/items/' . $sourceinfo->id . '/content');
 457          $source = $sourceurl->out(false);
 458  
 459          // We use download_one and not the rest API because it has special timeouts etc.
 460          $path = $this->prepare_file($filename);
 461          $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
 462          $result = $client->download_one($source, null, $options);
 463  
 464          if ($result) {
 465              @chmod($path, $CFG->filepermissions);
 466              return array(
 467                  'path' => $path,
 468                  'url' => $reference
 469              );
 470          }
 471          throw new repository_exception('cannotdownload', 'repository');
 472      }
 473  
 474      /**
 475       * Prepare file reference information.
 476       *
 477       * We are using this method to clean up the source to make sure that it
 478       * is a valid source.
 479       *
 480       * @param string $source of the file.
 481       * @return string file reference.
 482       */
 483      public function get_file_reference($source) {
 484          // We could do some magic upgrade code here.
 485          return $source;
 486      }
 487  
 488      /**
 489       * What kind of files will be in this repository?
 490       *
 491       * @return array return '*' means this repository support any files, otherwise
 492       *               return mimetypes of files, it can be an array
 493       */
 494      public function supported_filetypes() {
 495          return '*';
 496      }
 497  
 498      /**
 499       * Tells how the file can be picked from this repository.
 500       *
 501       * @return int
 502       */
 503      public function supported_returntypes() {
 504          // We can only support references if the system account is connected.
 505          if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) {
 506              $setting = get_config('onedrive', 'supportedreturntypes');
 507              if ($setting == 'internal') {
 508                  return FILE_INTERNAL;
 509              } else if ($setting == 'external') {
 510                  return FILE_CONTROLLED_LINK;
 511              } else {
 512                  return FILE_CONTROLLED_LINK | FILE_INTERNAL;
 513              }
 514          } else {
 515              return FILE_INTERNAL;
 516          }
 517      }
 518  
 519      /**
 520       * Which return type should be selected by default.
 521       *
 522       * @return int
 523       */
 524      public function default_returntype() {
 525          $setting = get_config('onedrive', 'defaultreturntype');
 526          $supported = get_config('onedrive', 'supportedreturntypes');
 527          if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') {
 528              return FILE_INTERNAL;
 529          } else {
 530              return FILE_CONTROLLED_LINK;
 531          }
 532      }
 533  
 534      /**
 535       * Return names of the general options.
 536       * By default: no general option name.
 537       *
 538       * @return array
 539       */
 540      public static function get_type_option_names() {
 541          return array('issuerid', 'pluginname', 'defaultreturntype', 'supportedreturntypes');
 542      }
 543  
 544      /**
 545       * Store the access token.
 546       */
 547      public function callback() {
 548          $client = $this->get_user_oauth_client();
 549          // This will upgrade to an access token if we have an authorization code and save the access token in the session.
 550          $client->is_logged_in();
 551      }
 552  
 553      /**
 554       * Repository method to serve the referenced file
 555       *
 556       * @see send_stored_file
 557       *
 558       * @param stored_file $storedfile the file that contains the reference
 559       * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
 560       * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
 561       * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
 562       * @param array $options additional options affecting the file serving
 563       */
 564      public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
 565          if ($this->disabled) {
 566              throw new repository_exception('cannotdownload', 'repository');
 567          }
 568  
 569          $source = json_decode($storedfile->get_reference());
 570  
 571          $fb = get_file_browser();
 572          $context = context::instance_by_id($storedfile->get_contextid(), MUST_EXIST);
 573          $info = $fb->get_file_info($context,
 574                                     $storedfile->get_component(),
 575                                     $storedfile->get_filearea(),
 576                                     $storedfile->get_itemid(),
 577                                     $storedfile->get_filepath(),
 578                                     $storedfile->get_filename());
 579  
 580          if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
 581              // Add the current user as an OAuth writer.
 582              $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
 583  
 584              if ($systemauth === false) {
 585                  $details = 'Cannot connect as system user';
 586                  throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 587              }
 588              $systemservice = new repository_onedrive\rest($systemauth);
 589  
 590              // Get the user oauth so we can get the account to add.
 591              $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(),
 592                                                     $storedfile->get_component(),
 593                                                     $storedfile->get_filearea(),
 594                                                     $storedfile->get_itemid(),
 595                                                     $storedfile->get_filepath(),
 596                                                     $storedfile->get_filename(),
 597                                                     $forcedownload);
 598              $url->param('sesskey', sesskey());
 599              $param = ($options['embed'] == true) ? false : $url;
 600              $userauth = $this->get_user_oauth_client($param);
 601  
 602              if (!$userauth->is_logged_in()) {
 603                  if ($options['embed'] == true) {
 604                      // Due to Same-origin policy, we cannot redirect to onedrive login page.
 605                      // If the requested file is embed and the user is not logged in, add option to log in using a popup.
 606                      $this->print_login_popup(['style' => 'margin-top: 250px']);
 607                      exit;
 608                  }
 609                  redirect($userauth->get_login_url());
 610              }
 611              if ($userauth === false) {
 612                  $details = 'Cannot connect as current user';
 613                  throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 614              }
 615              $userinfo = $userauth->get_userinfo();
 616              $useremail = $userinfo['email'];
 617  
 618              $this->add_temp_writer_to_file($systemservice, $source->id, $useremail);
 619          }
 620  
 621          if (!empty($options['offline'])) {
 622              $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename());
 623              $filename = $storedfile->get_filename();
 624              send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options);
 625          } else if ($source->link) {
 626              // Do not use redirect() here because is not compatible with webservice/pluginfile.php.
 627              header('Location: ' . $source->link);
 628          } else {
 629              $details = 'File is missing source link';
 630              throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 631          }
 632      }
 633  
 634      /**
 635       * See if a folder exists within a folder
 636       *
 637       * @param \repository_onedrive\rest $client Authenticated client.
 638       * @param string $fullpath
 639       * @return string|boolean The file id if it exists or false.
 640       */
 641      protected function get_file_id_by_path(\repository_onedrive\rest $client, $fullpath) {
 642          $fields = "id";
 643          try {
 644              $response = $client->call('get_file_by_path', ['fullpath' => $fullpath, '$select' => $fields]);
 645          } catch (\core\oauth2\rest_exception $re) {
 646              return false;
 647          }
 648          return $response->id;
 649      }
 650  
 651      /**
 652       * Delete a file by full path.
 653       *
 654       * @param \repository_onedrive\rest $client Authenticated client.
 655       * @param string $fullpath
 656       * @return boolean
 657       */
 658      protected function delete_file_by_path(\repository_onedrive\rest $client, $fullpath) {
 659          try {
 660              $response = $client->call('delete_file_by_path', ['fullpath' => $fullpath]);
 661          } catch (\core\oauth2\rest_exception $re) {
 662              return false;
 663          }
 664          return true;
 665      }
 666  
 667      /**
 668       * Create a folder within a folder
 669       *
 670       * @param \repository_onedrive\rest $client Authenticated client.
 671       * @param string $foldername The folder we are creating.
 672       * @param string $parentid The parent folder we are creating in.
 673       *
 674       * @return string The file id of the new folder.
 675       */
 676      protected function create_folder_in_folder(\repository_onedrive\rest $client, $foldername, $parentid) {
 677          $params = ['parentid' => $parentid];
 678          $folder = [ 'name' => $foldername, 'folder' => [ 'childCount' => 0 ]];
 679          $created = $client->call('create_folder', $params, json_encode($folder));
 680          if (empty($created->id)) {
 681              $details = 'Cannot create folder:' . $foldername;
 682              throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 683          }
 684          return $created->id;
 685      }
 686  
 687      /**
 688       * Get simple file info for humans.
 689       *
 690       * @param \repository_onedrive\rest $client Authenticated client.
 691       * @param string $fileid The file we are querying.
 692       *
 693       * @return stdClass
 694       */
 695      protected function get_file_summary(\repository_onedrive\rest $client, $fileid) {
 696          $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser";
 697          $response = $client->call('get', ['fileid' => $fileid, '$select' => $fields]);
 698          return $response;
 699      }
 700  
 701      /**
 702       * Add a writer to the permissions on the file (temporary).
 703       *
 704       * @param \repository_onedrive\rest $client Authenticated client.
 705       * @param string $fileid The file we are updating.
 706       * @param string $email The email of the writer account to add.
 707       * @return boolean
 708       */
 709      protected function add_temp_writer_to_file(\repository_onedrive\rest $client, $fileid, $email) {
 710          // Expires in 7 days.
 711          $expires = new DateTime();
 712          $expires->add(new DateInterval("P7D"));
 713  
 714          $updateeditor = [
 715              'recipients' => [[ 'email' => $email ]],
 716              'roles' => ['write'],
 717              'requireSignIn' => true,
 718              'sendInvitation' => false
 719          ];
 720          $params = ['fileid' => $fileid];
 721          $response = $client->call('create_permission', $params, json_encode($updateeditor));
 722          if (empty($response->value[0]->id)) {
 723              $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid;
 724              throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 725          }
 726          // Store the permission id in the DB. Scheduled task will remove this permission after 7 days.
 727          if ($access = repository_onedrive\access::get_record(['permissionid' => $response->value[0]->id, 'itemid' => $fileid ])) {
 728              // Update the timemodified.
 729              $access->update();
 730          } else {
 731              $record = (object) [ 'permissionid' => $response->value[0]->id, 'itemid' => $fileid ];
 732              $access = new repository_onedrive\access(0, $record);
 733              $access->create();
 734          }
 735          return true;
 736      }
 737  
 738      /**
 739       * Allow anyone with the link to read the file.
 740       *
 741       * @param \repository_onedrive\rest $client Authenticated client.
 742       * @param string $fileid The file we are updating.
 743       * @return boolean
 744       */
 745      protected function set_file_sharing_anyone_with_link_can_read(\repository_onedrive\rest $client, $fileid) {
 746  
 747          $type = (isset($this->options['embed']) && $this->options['embed'] == true) ? 'embed' : 'view';
 748          $updateread = [
 749              'type' => $type,
 750              'scope' => 'anonymous'
 751          ];
 752          $params = ['fileid' => $fileid];
 753          $response = $client->call('create_link', $params, json_encode($updateread));
 754          if (empty($response->link)) {
 755              $details = 'Cannot update link sharing for the document: ' . $fileid;
 756              throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 757          }
 758          return $response->link->webUrl;
 759      }
 760  
 761      /**
 762       * Given a filename, use the core_filetypes registered types to guess a mimetype.
 763       *
 764       * If no mimetype is known, return 'application/unknown';
 765       *
 766       * @param string $filename
 767       * @return string $mimetype
 768       */
 769      protected function get_mimetype_from_filename($filename) {
 770          $mimetype = 'application/unknown';
 771          $types = core_filetypes::get_types();
 772          $fileextension = '.bin';
 773          if (strpos($filename, '.') !== false) {
 774              $fileextension = substr($filename, strrpos($filename, '.') + 1);
 775          }
 776  
 777          if (isset($types[$fileextension])) {
 778              $mimetype = $types[$fileextension]['type'];
 779          }
 780          return $mimetype;
 781      }
 782  
 783      /**
 784       * Upload a file to onedrive.
 785       *
 786       * @param \repository_onedrive\rest $service Authenticated client.
 787       * @param \curl $curl Curl client to perform the put operation (with no auth headers).
 788       * @param \curl $authcurl Curl client that will send authentication headers
 789       * @param string $filepath The local path to the file to upload
 790       * @param string $mimetype The new mimetype
 791       * @param string $parentid The folder to put it.
 792       * @param string $filename The name of the new file
 793       * @return string $fileid
 794       */
 795      protected function upload_file(\repository_onedrive\rest $service, \curl $curl, \curl $authcurl,
 796                                     $filepath, $mimetype, $parentid, $filename) {
 797          // Start an upload session.
 798          // Docs https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/item_createuploadsession link.
 799  
 800          $params = ['parentid' => $parentid, 'filename' => urlencode($filename)];
 801          $behaviour = [ 'item' => [ "@microsoft.graph.conflictBehavior" => "rename" ] ];
 802          $created = $service->call('create_upload', $params, json_encode($behaviour));
 803          if (empty($created->uploadUrl)) {
 804              $details = 'Cannot begin upload session:' . $parentid;
 805              throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 806          }
 807  
 808          $options = ['file' => $filepath];
 809  
 810          // Try each curl class in turn until we succeed.
 811          // First attempt an upload with no auth headers (will work for personal onedrive accounts).
 812          // If that fails, try an upload with the auth headers (will work for work onedrive accounts).
 813          $curls = [$curl, $authcurl];
 814          $response = null;
 815          foreach ($curls as $curlinstance) {
 816              $curlinstance->setHeader('Content-type: ' . $mimetype);
 817              $size = filesize($filepath);
 818              $curlinstance->setHeader('Content-Range: bytes 0-' . ($size - 1) . '/' . $size);
 819              $response = $curlinstance->put($created->uploadUrl, $options);
 820              if ($curlinstance->errno == 0) {
 821                  $response = json_decode($response);
 822              }
 823              if (!empty($response->id)) {
 824                  // We can stop now - there is a valid file returned.
 825                  break;
 826              }
 827          }
 828  
 829          if (empty($response->id)) {
 830              $details = 'File not created';
 831              throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 832          }
 833  
 834          return $response->id;
 835      }
 836  
 837  
 838      /**
 839       * Called when a file is selected as a "link".
 840       * Invoked at MOODLE/repository/repository_ajax.php
 841       *
 842       * What should happen here is that the file should be copied to a new file owned by the moodle system user.
 843       * It should be organised in a folder based on the file context.
 844       * It's sharing permissions should allow read access with the link.
 845       * The returned reference should point to the newly copied file - not the original.
 846       *
 847       * @param string $reference this reference is generated by
 848       *                          repository::get_file_reference()
 849       * @param context $context the target context for this new file.
 850       * @param string $component the target component for this new file.
 851       * @param string $filearea the target filearea for this new file.
 852       * @param string $itemid the target itemid for this new file.
 853       * @return string $modifiedreference (final one before saving to DB)
 854       */
 855      public function reference_file_selected($reference, $context, $component, $filearea, $itemid) {
 856          global $CFG, $SITE;
 857  
 858          // What we need to do here is transfer ownership to the system user (or copy)
 859          // then set the permissions so anyone with the share link can view,
 860          // finally update the reference to contain the share link if it was not
 861          // already there (and point to new file id if we copied).
 862          $source = json_decode($reference);
 863          if (!empty($source->usesystem)) {
 864              // If we already copied this file to the system account - we are done.
 865              return $reference;
 866          }
 867  
 868          // Get a system and a user oauth client.
 869          $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
 870  
 871          if ($systemauth === false) {
 872              $details = 'Cannot connect as system user';
 873              throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 874          }
 875  
 876          $userauth = $this->get_user_oauth_client();
 877          if ($userauth === false) {
 878              $details = 'Cannot connect as current user';
 879              throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
 880          }
 881  
 882          $systemservice = new repository_onedrive\rest($systemauth);
 883  
 884          // Download the file.
 885          $tmpfilename = clean_param($source->id, PARAM_PATH);
 886          $temppath = make_request_directory() . $tmpfilename;
 887  
 888          $options = ['filepath' => $temppath, 'timeout' => 60, 'followlocation' => true, 'maxredirs' => 5];
 889          $base = 'https://graph.microsoft.com/v1.0/';
 890          $sourceurl = new moodle_url($base . 'me/drive/items/' . $source->id . '/content');
 891          $sourceurl = $sourceurl->out(false);
 892  
 893          $result = $userauth->download_one($sourceurl, null, $options);
 894  
 895          if (!$result) {
 896              throw new repository_exception('cannotdownload', 'repository');
 897          }
 898  
 899          // Now copy it to a sensible folder.
 900          $contextlist = array_reverse($context->get_parent_contexts(true));
 901  
 902          $cache = cache::make('repository_onedrive', 'folder');
 903          $parentid = 'root';
 904          $fullpath = '';
 905          $allfolders = [];
 906          foreach ($contextlist as $context) {
 907              // Prepare human readable context folders names, making sure they are still unique within the site.
 908              $prevlang = force_current_language($CFG->lang);
 909              $foldername = $context->get_context_name();
 910              force_current_language($prevlang);
 911  
 912              if ($context->contextlevel == CONTEXT_SYSTEM) {
 913                  // Append the site short name to the root folder.
 914                  $foldername .= '_'.$SITE->shortname;
 915                  // Append the relevant object id.
 916              } else if ($context->instanceid) {
 917                  $foldername .= '_id_'.$context->instanceid;
 918              } else {
 919                  // This does not really happen but just in case.
 920                  $foldername .= '_ctx_'.$context->id;
 921              }
 922  
 923              $foldername = urlencode(clean_param($foldername, PARAM_PATH));
 924              $allfolders[] = $foldername;
 925          }
 926  
 927          $allfolders[] = urlencode(clean_param($component, PARAM_PATH));
 928          $allfolders[] = urlencode(clean_param($filearea, PARAM_PATH));
 929          $allfolders[] = urlencode(clean_param($itemid, PARAM_PATH));
 930  
 931          // Variable $allfolders now has the complete path we want to store the file in.
 932          // Create each folder in $allfolders under the system account.
 933          foreach ($allfolders as $foldername) {
 934              if ($fullpath) {
 935                  $fullpath .= '/';
 936              }
 937              $fullpath .= $foldername;
 938  
 939              $folderid = $cache->get($fullpath);
 940              if (empty($folderid)) {
 941                  $folderid = $this->get_file_id_by_path($systemservice, $fullpath);
 942              }
 943              if ($folderid !== false) {
 944                  $cache->set($fullpath, $folderid);
 945                  $parentid = $folderid;
 946              } else {
 947                  // Create it.
 948                  $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid);
 949                  $cache->set($fullpath, $parentid);
 950              }
 951          }
 952  
 953          // Delete any existing file at this path.
 954          $path = $fullpath . '/' . urlencode(clean_param($source->name, PARAM_PATH));
 955          $this->delete_file_by_path($systemservice, $path);
 956  
 957          // Upload the file.
 958          $safefilename = clean_param($source->name, PARAM_PATH);
 959          $mimetype = $this->get_mimetype_from_filename($safefilename);
 960          // We cannot send authorization headers in the upload or personal microsoft accounts will fail (what a joke!).
 961          $curl = new \curl();
 962          $fileid = $this->upload_file($systemservice, $curl, $systemauth, $temppath, $mimetype, $parentid, $safefilename);
 963  
 964          // Read with link.
 965          $link = $this->set_file_sharing_anyone_with_link_can_read($systemservice, $fileid);
 966  
 967          $summary = $this->get_file_summary($systemservice, $fileid);
 968  
 969          // Update the details in the file reference before it is saved.
 970          $source->id = $summary->id;
 971          $source->link = $link;
 972          $source->usesystem = true;
 973  
 974          $reference = json_encode($source);
 975  
 976          return $reference;
 977      }
 978  
 979      /**
 980       * Get human readable file info from the reference.
 981       *
 982       * @param string $reference
 983       * @param int $filestatus
 984       */
 985      public function get_reference_details($reference, $filestatus = 0) {
 986          if (empty($reference)) {
 987              return get_string('unknownsource', 'repository');
 988          }
 989          $source = json_decode($reference);
 990          if (empty($source->usesystem)) {
 991              return '';
 992          }
 993          $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
 994  
 995          if ($systemauth === false) {
 996              return '';
 997          }
 998          $systemservice = new repository_onedrive\rest($systemauth);
 999          $info = $this->get_file_summary($systemservice, $source->id);
1000  
1001          $owner = '';
1002          if (!empty($info->createdByUser->displayName)) {
1003              $owner = $info->createdByUser->displayName;
1004          }
1005          if ($owner) {
1006              return get_string('owner', 'repository_onedrive', $owner);
1007          } else {
1008              return $info->name;
1009          }
1010      }
1011  
1012      /**
1013       * Return true if any instances of the skydrive repo exist - and we can import them.
1014       *
1015       * @return bool
1016       * @deprecated since Moodle 4.0
1017       * @todo MDL-72620 This will be deleted in Moodle 4.4.
1018       */
1019      public static function can_import_skydrive_files() {
1020          global $DB;
1021  
1022          $skydrive = $DB->get_record('repository', ['type' => 'skydrive'], 'id', IGNORE_MISSING);
1023          $onedrive = $DB->get_record('repository', ['type' => 'onedrive'], 'id', IGNORE_MISSING);
1024  
1025          if (empty($skydrive) || empty($onedrive)) {
1026              return false;
1027          }
1028  
1029          $ready = true;
1030          try {
1031              $issuer = \core\oauth2\api::get_issuer(get_config('onedrive', 'issuerid'));
1032              if (!$issuer->get('enabled')) {
1033                  $ready = false;
1034              }
1035              if (!$issuer->is_configured()) {
1036                  $ready = false;
1037              }
1038          } catch (dml_missing_record_exception $e) {
1039              $ready = false;
1040          }
1041          if (!$ready) {
1042              return false;
1043          }
1044  
1045          $sql = "SELECT count('x')
1046                    FROM {repository_instances} i, {repository} r
1047                   WHERE r.type=:plugin AND r.id=i.typeid";
1048          $params = array('plugin' => 'skydrive');
1049          return $DB->count_records_sql($sql, $params) > 0;
1050      }
1051  
1052      /**
1053       * Import all the files that were created with the skydrive repo to this repo.
1054       *
1055       * @return bool
1056       * @deprecated since Moodle 4.0
1057       * @todo MDL-72620 This will be deleted in Moodle 4.4.
1058       */
1059      public static function import_skydrive_files() {
1060          global $DB;
1061  
1062          debugging('import_skydrive_files() is deprecated. Please migrate your files from repository_skydrive to ' .
1063              'repository_onedrive before it will be completely removed.', DEBUG_DEVELOPER);
1064  
1065          if (!self::can_import_skydrive_files()) {
1066              return false;
1067          }
1068          // Should only be one of each.
1069          $skydrivetype = repository::get_type_by_typename('skydrive');
1070  
1071          $skydriveinstances = repository::get_instances(['type' => 'skydrive']);
1072          $skydriveinstance = reset($skydriveinstances);
1073          $onedriveinstances = repository::get_instances(['type' => 'onedrive']);
1074          $onedriveinstance = reset($onedriveinstances);
1075  
1076          // Update all file references.
1077          $DB->set_field('files_reference', 'repositoryid', $onedriveinstance->id, ['repositoryid' => $skydriveinstance->id]);
1078  
1079          // Delete and disable the skydrive repo.
1080          $skydrivetype->delete();
1081          core_plugin_manager::reset_caches();
1082  
1083          $sql = "SELECT count('x')
1084                    FROM {repository_instances} i, {repository} r
1085                   WHERE r.type=:plugin AND r.id=i.typeid";
1086          $params = array('plugin' => 'skydrive');
1087          return $DB->count_records_sql($sql, $params) == 0;
1088      }
1089  
1090      /**
1091       * Edit/Create Admin Settings Moodle form.
1092       *
1093       * @param moodleform $mform Moodle form (passed by reference).
1094       * @param string $classname repository class name.
1095       */
1096      public static function type_config_form($mform, $classname = 'repository') {
1097          global $OUTPUT;
1098  
1099          $url = new moodle_url('/admin/tool/oauth2/issuers.php');
1100          $url = $url->out();
1101  
1102          $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_onedrive', $url));
1103  
1104          if (self::can_import_skydrive_files()) {
1105              debugging('can_import_skydrive_files() is deprecated. Please migrate your files from repository_skydrive to ' .
1106              'repository_onedrive before it will be completely removed.', DEBUG_DEVELOPER);
1107  
1108              $notice = get_string('skydrivefilesexist', 'repository_onedrive');
1109              $url = new moodle_url('/repository/onedrive/importskydrive.php');
1110              $attrs = ['class' => 'btn btn-primary'];
1111              $button = $OUTPUT->action_link($url, get_string('importskydrivefiles', 'repository_onedrive'), null, $attrs);
1112              $mform->addElement('static', null, '', $OUTPUT->notification($notice) . $button);
1113          }
1114  
1115          parent::type_config_form($mform);
1116          $options = [];
1117          $issuers = \core\oauth2\api::get_all_issuers();
1118  
1119          foreach ($issuers as $issuer) {
1120              $options[$issuer->get('id')] = s($issuer->get('name'));
1121          }
1122  
1123          $strrequired = get_string('required');
1124  
1125          $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_onedrive'), $options);
1126          $mform->addHelpButton('issuerid', 'issuer', 'repository_onedrive');
1127          $mform->addRule('issuerid', $strrequired, 'required', null, 'client');
1128  
1129          $mform->addElement('static', null, '', get_string('fileoptions', 'repository_onedrive'));
1130          $choices = [
1131              'internal' => get_string('internal', 'repository_onedrive'),
1132              'external' => get_string('external', 'repository_onedrive'),
1133              'both' => get_string('both', 'repository_onedrive')
1134          ];
1135          $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_onedrive'), $choices);
1136  
1137          $choices = [
1138              FILE_INTERNAL => get_string('internal', 'repository_onedrive'),
1139              FILE_CONTROLLED_LINK => get_string('external', 'repository_onedrive'),
1140          ];
1141          $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_onedrive'), $choices);
1142  
1143      }
1144  }
1145  
1146  /**
1147   * Callback to get the required scopes for system account.
1148   *
1149   * @param \core\oauth2\issuer $issuer
1150   * @return string
1151   */
1152  function repository_onedrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) {
1153      if ($issuer->get('id') == get_config('onedrive', 'issuerid')) {
1154          return repository_onedrive::SCOPES;
1155      }
1156      return '';
1157  }