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