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