weekly release 4.5dev
[moodle.git] / repository / onedrive / lib.php
blob417783e938ac24be7eb7f87466e2809e17431917
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.
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/>.
17 /**
18 * Microsoft Live Skydrive Repository Plugin
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
26 defined('MOODLE_INTERNAL') || die();
28 /**
29 * Microsoft onedrive repository plugin.
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
36 class repository_onedrive extends repository {
37 /**
38 * OAuth 2 client
39 * @var \core\oauth2\client
41 private $client = null;
43 /**
44 * OAuth 2 Issuer
45 * @var \core\oauth2\issuer
47 private $issuer = null;
49 /**
50 * Additional scopes required for drive.
52 const SCOPES = 'files.readwrite.all';
54 /**
55 * Constructor.
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
63 public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
64 parent::__construct($repositoryid, $context, $options, $readonly = 0);
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;
72 if ($this->issuer && !$this->issuer->get('enabled')) {
73 $this->disabled = true;
77 /**
78 * Get a cached user authenticated oauth client.
80 * @param moodle_url $overrideurl - Use this url instead of the repo callback.
81 * @return \core\oauth2\client
83 protected function get_user_oauth_client($overrideurl = false) {
84 if ($this->client) {
85 return $this->client;
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());
96 $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES, true);
98 return $this->client;
102 * Checks whether the user is authenticate or not.
104 * @return bool true when logged in.
106 public function check_login() {
107 $client = $this->get_user_oauth_client();
108 return $client->is_logged_in();
112 * Print or return the login form.
114 * @return void|array for ajax.
116 public function print_login() {
117 $client = $this->get_user_oauth_client();
118 $url = $client->get_login_url();
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>';
131 * Print the login in a popup.
133 * @param array|null $attr Custom attributes to be applied to popup div.
135 public function print_login_popup($attr = null) {
136 global $OUTPUT, $PAGE;
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);
143 $PAGE->set_pagelayout('embedded');
144 echo $OUTPUT->header();
146 $repositoryname = get_string('pluginname', 'repository_onedrive');
148 $button = new single_button(
149 $url,
150 get_string('logintoaccount', 'repository', $repositoryname),
151 'post',
152 single_button::BUTTON_PRIMARY
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);
159 echo $OUTPUT->footer();
163 * Build the breadcrumb from a path.
165 * @param string $path to create a breadcrumb from.
166 * @return array containing name and path of each crumb.
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)
178 $tmp = end($breadcrumb);
179 $crumbtrail = $tmp['path'];
181 return $breadcrumb;
185 * Generates a safe path to a node.
187 * Typically, a node will be id|Name of the node.
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.
194 protected function build_node_path($id, $name = '', $root = '') {
195 $path = $id;
196 if (!empty($name)) {
197 $path .= '|' . urlencode($name);
199 if (!empty($root)) {
200 $path = trim($root, '/') . '/' . $path;
202 return $path;
206 * Returns information about a node in a path.
208 * @see self::build_node_path()
209 * @param string $node to extrat information from.
210 * @return array about the node.
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 = '';
220 $id = urldecode($id);
221 return array(
222 0 => $id,
223 1 => $name,
224 'id' => $id,
225 'name' => $name
230 * List the files and folders.
232 * @param string $path path to browse.
233 * @param string $page page to browse.
234 * @return array of result.
236 public function get_listing($path='', $page = '') {
237 if (empty($path)) {
238 $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive'));
241 if ($this->disabled) {
242 // Empty list of files for disabled repository.
243 return ['dynload' => false, 'list' => [], 'nologin' => true];
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);
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';
258 // Append the active path for search.
259 $str = get_string('searchfor', 'repository_onedrive', $searchtext);
260 $path = $this->build_node_path('search', $str, $path);
263 // Query the Drive.
264 $parent = $id;
265 if ($parent != 'root') {
266 $parent = 'items/' . $parent;
268 $q = '';
269 $results = $this->query($q, $path, $parent);
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;
280 * Search throughout the OneDrive
282 * @param string $searchtext text to search for.
283 * @param int $page search page.
284 * @return array of results.
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);
291 // Query the Drive.
292 $parent = 'root';
293 $results = $this->query($searchtext, $path, 'root');
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;
304 * Query OneDrive for files and folders using a search query.
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
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().
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
321 protected function query($q, $path = null, $parent = null, $page = 0) {
322 global $OUTPUT;
324 $files = [];
325 $folders = [];
326 $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,thumbnails";
327 $params = ['$select' => $fields, '$expand' => 'thumbnails', 'parent' => $parent];
329 try {
330 // Retrieving files and folders.
331 $client = $this->get_user_oauth_client();
332 $service = new repository_onedrive\rest($client);
334 if (!empty($q)) {
335 $params['search'] = urlencode($q);
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);
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');
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' => []
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
373 $thumb = '';
374 $thumbwidth = 0;
375 $thumbheight = 0;
376 $extendedinfoerr = false;
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;
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);
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,
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));
422 * Logout.
424 * @return string
426 public function logout() {
427 $client = $this->get_user_oauth_client();
428 $client->log_out();
429 return parent::logout();
433 * Get a file.
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.
439 public function get_file($reference, $filename = '') {
440 global $CFG;
442 if ($this->disabled) {
443 throw new repository_exception('cannotdownload', 'repository');
445 $sourceinfo = json_decode($reference);
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();
454 $base = 'https://graph.microsoft.com/v1.0/';
456 $sourceurl = new moodle_url($base . 'me/drive/items/' . $sourceinfo->id . '/content');
457 $source = $sourceurl->out(false);
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);
464 if ($result) {
465 @chmod($path, $CFG->filepermissions);
466 return array(
467 'path' => $path,
468 'url' => $reference
471 throw new repository_exception('cannotdownload', 'repository');
475 * Prepare file reference information.
477 * We are using this method to clean up the source to make sure that it
478 * is a valid source.
480 * @param string $source of the file.
481 * @return string file reference.
483 public function get_file_reference($source) {
484 // We could do some magic upgrade code here.
485 return $source;
489 * What kind of files will be in this repository?
491 * @return array return '*' means this repository support any files, otherwise
492 * return mimetypes of files, it can be an array
494 public function supported_filetypes() {
495 return '*';
499 * Tells how the file can be picked from this repository.
501 * @return int
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;
514 } else {
515 return FILE_INTERNAL;
520 * Which return type should be selected by default.
522 * @return int
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;
535 * Return names of the general options.
536 * By default: no general option name.
538 * @return array
540 public static function get_type_option_names() {
541 return array('issuerid', 'pluginname', 'defaultreturntype', 'supportedreturntypes');
545 * Store the access token.
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();
554 * Repository method to serve the referenced file
556 * @see send_stored_file
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
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');
569 $source = json_decode($storedfile->get_reference());
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());
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);
584 if ($systemauth === false) {
585 $details = 'Cannot connect as system user';
586 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
588 $systemservice = new repository_onedrive\rest($systemauth);
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);
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;
609 redirect($userauth->get_login_url());
611 if ($userauth === false) {
612 $details = 'Cannot connect as current user';
613 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
615 $userinfo = $userauth->get_userinfo();
616 $useremail = $userinfo['email'];
618 $this->add_temp_writer_to_file($systemservice, $source->id, $useremail);
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);
635 * See if a folder exists within a folder
637 * @param \repository_onedrive\rest $client Authenticated client.
638 * @param string $fullpath
639 * @return string|boolean The file id if it exists or false.
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;
648 return $response->id;
652 * Delete a file by full path.
654 * @param \repository_onedrive\rest $client Authenticated client.
655 * @param string $fullpath
656 * @return boolean
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;
664 return true;
668 * Create a folder within a folder
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.
674 * @return string The file id of the new folder.
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);
684 return $created->id;
688 * Get simple file info for humans.
690 * @param \repository_onedrive\rest $client Authenticated client.
691 * @param string $fileid The file we are querying.
693 * @return stdClass
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;
702 * Add a writer to the permissions on the file (temporary).
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
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"));
714 $updateeditor = [
715 'recipients' => [[ 'email' => $email ]],
716 'roles' => ['write'],
717 'requireSignIn' => true,
718 'sendInvitation' => false
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);
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();
735 return true;
739 * Allow anyone with the link to read the file.
741 * @param \repository_onedrive\rest $client Authenticated client.
742 * @param string $fileid The file we are updating.
743 * @return boolean
745 protected function set_file_sharing_anyone_with_link_can_read(\repository_onedrive\rest $client, $fileid) {
747 $type = (isset($this->options['embed']) && $this->options['embed'] == true) ? 'embed' : 'view';
748 $updateread = [
749 'type' => $type,
750 'scope' => 'anonymous'
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);
758 return $response->link->webUrl;
762 * Given a filename, use the core_filetypes registered types to guess a mimetype.
764 * If no mimetype is known, return 'application/unknown';
766 * @param string $filename
767 * @return string $mimetype
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);
777 if (isset($types[$fileextension])) {
778 $mimetype = $types[$fileextension]['type'];
780 return $mimetype;
784 * Upload a file to onedrive.
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
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.
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);
808 $options = ['file' => $filepath];
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);
823 if (!empty($response->id)) {
824 // We can stop now - there is a valid file returned.
825 break;
829 if (empty($response->id)) {
830 $details = 'File not created';
831 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
834 return $response->id;
839 * Called when a file is selected as a "link".
840 * Invoked at MOODLE/repository/repository_ajax.php
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.
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)
855 public function reference_file_selected($reference, $context, $component, $filearea, $itemid) {
856 global $CFG, $SITE;
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;
868 // Get a system and a user oauth client.
869 $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
871 if ($systemauth === false) {
872 $details = 'Cannot connect as system user';
873 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
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);
882 $systemservice = new repository_onedrive\rest($systemauth);
884 // Download the file.
885 $tmpfilename = clean_param($source->id, PARAM_PATH);
886 $temppath = make_request_directory() . $tmpfilename;
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);
893 $result = $userauth->download_one($sourceurl, null, $options);
895 if (!$result) {
896 throw new repository_exception('cannotdownload', 'repository');
899 // Now copy it to a sensible folder.
900 $contextlist = array_reverse($context->get_parent_contexts(true));
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);
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;
923 $foldername = urlencode(clean_param($foldername, PARAM_PATH));
924 $allfolders[] = $foldername;
927 $allfolders[] = urlencode(clean_param($component, PARAM_PATH));
928 $allfolders[] = urlencode(clean_param($filearea, PARAM_PATH));
929 $allfolders[] = urlencode(clean_param($itemid, PARAM_PATH));
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 .= '/';
937 $fullpath .= $foldername;
939 $folderid = $cache->get($fullpath);
940 if (empty($folderid)) {
941 $folderid = $this->get_file_id_by_path($systemservice, $fullpath);
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);
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);
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);
964 // Read with link.
965 $link = $this->set_file_sharing_anyone_with_link_can_read($systemservice, $fileid);
967 $summary = $this->get_file_summary($systemservice, $fileid);
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;
974 $reference = json_encode($source);
976 return $reference;
980 * Get human readable file info from the reference.
982 * @param string $reference
983 * @param int $filestatus
985 public function get_reference_details($reference, $filestatus = 0) {
986 if (empty($reference)) {
987 return get_string('unknownsource', 'repository');
989 $source = json_decode($reference);
990 if (empty($source->usesystem)) {
991 return '';
993 $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
995 if ($systemauth === false) {
996 return '';
998 $systemservice = new repository_onedrive\rest($systemauth);
999 $info = $this->get_file_summary($systemservice, $source->id);
1001 $owner = '';
1002 if (!empty($info->createdByUser->displayName)) {
1003 $owner = $info->createdByUser->displayName;
1005 if ($owner) {
1006 return get_string('owner', 'repository_onedrive', $owner);
1007 } else {
1008 return $info->name;
1013 * Return true if any instances of the skydrive repo exist - and we can import them.
1015 * @return bool
1016 * @deprecated since Moodle 4.0
1017 * @todo MDL-72620 This will be deleted in Moodle 4.4.
1019 public static function can_import_skydrive_files() {
1020 global $DB;
1022 $skydrive = $DB->get_record('repository', ['type' => 'skydrive'], 'id', IGNORE_MISSING);
1023 $onedrive = $DB->get_record('repository', ['type' => 'onedrive'], 'id', IGNORE_MISSING);
1025 if (empty($skydrive) || empty($onedrive)) {
1026 return false;
1029 $ready = true;
1030 try {
1031 $issuer = \core\oauth2\api::get_issuer(get_config('onedrive', 'issuerid'));
1032 if (!$issuer->get('enabled')) {
1033 $ready = false;
1035 if (!$issuer->is_configured()) {
1036 $ready = false;
1038 } catch (dml_missing_record_exception $e) {
1039 $ready = false;
1041 if (!$ready) {
1042 return false;
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;
1053 * Import all the files that were created with the skydrive repo to this repo.
1055 * @return bool
1056 * @deprecated since Moodle 4.0
1057 * @todo MDL-72620 This will be deleted in Moodle 4.4.
1059 public static function import_skydrive_files() {
1060 global $DB;
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);
1065 if (!self::can_import_skydrive_files()) {
1066 return false;
1068 // Should only be one of each.
1069 $skydrivetype = repository::get_type_by_typename('skydrive');
1071 $skydriveinstances = repository::get_instances(['type' => 'skydrive']);
1072 $skydriveinstance = reset($skydriveinstances);
1073 $onedriveinstances = repository::get_instances(['type' => 'onedrive']);
1074 $onedriveinstance = reset($onedriveinstances);
1076 // Update all file references.
1077 $DB->set_field('files_reference', 'repositoryid', $onedriveinstance->id, ['repositoryid' => $skydriveinstance->id]);
1079 // Delete and disable the skydrive repo.
1080 $skydrivetype->delete();
1081 core_plugin_manager::reset_caches();
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;
1091 * Edit/Create Admin Settings Moodle form.
1093 * @param moodleform $mform Moodle form (passed by reference).
1094 * @param string $classname repository class name.
1096 public static function type_config_form($mform, $classname = 'repository') {
1097 global $OUTPUT;
1099 $url = new moodle_url('/admin/tool/oauth2/issuers.php');
1100 $url = $url->out();
1102 $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_onedrive', $url));
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);
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);
1115 parent::type_config_form($mform);
1116 $options = [];
1117 $issuers = \core\oauth2\api::get_all_issuers();
1119 foreach ($issuers as $issuer) {
1120 $options[$issuer->get('id')] = s($issuer->get('name'));
1123 $strrequired = get_string('required');
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');
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')
1135 $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_onedrive'), $choices);
1137 $choices = [
1138 FILE_INTERNAL => get_string('internal', 'repository_onedrive'),
1139 FILE_CONTROLLED_LINK => get_string('external', 'repository_onedrive'),
1141 $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_onedrive'), $choices);
1147 * Callback to get the required scopes for system account.
1149 * @param \core\oauth2\issuer $issuer
1150 * @return string
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;
1156 return '';