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