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