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