Search moodle.org's
Developer Documentation

See Release Notes

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

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

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