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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body