Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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