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