MDL-58538 repository_onedrive: Use image_url() instead of pix_url()
[moodle.git] / repository / onedrive / lib.php
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/>.
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  */
26 defined('MOODLE_INTERNAL') || die();
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;
43     /**
44      * OAuth 2 Issuer
45      * @var \core\oauth2\issuer
46      */
47     private $issuer = null;
49     /**
50      * Additional scopes required for drive.
51      */
52     const SCOPES = 'files.readwrite.all';
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);
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         }
72         if ($this->issuer && !$this->issuer->get('enabled')) {
73             $this->disabled = true;
74         }
75     }
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         }
96         $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES);
98         return $this->client;
99     }
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     }
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();
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     }
130     /**
131      * Build the breadcrumb from a path.
132      *
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     }
152     /**
153      * Generates a safe path to a node.
154      *
155      * Typically, a node will be id|Name of the node.
156      *
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     }
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     }
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.
203      */
204     public function get_listing($path='', $page = '') {
205         if (empty($path)) {
206             $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive'));
207         }
209         if ($this->disabled) {
210             // Empty list of files for disabled repository.
211             return ['dynload' => false, 'list' => [], 'nologin' => true];
212         }
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);
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';
226             // Append the active path for search.
227             $str = get_string('searchfor', 'repository_onedrive', $searchtext);
228             $path = $this->build_node_path('search', $str, $path);
229         }
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);
239         $ret = [];
240         $ret['dynload'] = true;
241         $ret['path'] = $this->build_breadcrumb($path);
242         $ret['list'] = $results;
243         $ret['manage'] = 'https://www.office.com/';
244         return $ret;
245     }
247     /**
248      * Search throughout the OneDrive
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) {
255         $path = $this->build_node_path('root', get_string('pluginname', 'repository_onedrive'));
256         $str = get_string('searchfor', 'repository_onedrive', $searchtext);
257         $path = $this->build_node_path('search', $str, $path);
259         // Query the Drive.
260         $parent = 'root';
261         $results = $this->query($searchtext, $path, 'root');
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     }
271     /**
272      * Query OneDrive for files and folders using a search query.
273      *
274      * Documentation about the query format can be found here:
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
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      *
281      * @param string $q search query as expected by the Graph API.
282      * @param string $path parent path of the current files, will not be used for the query.
283      * @param string $parent Parent id.
284      * @param int $page page.
285      * @return array of files and folders.
286      * @throws Exception
287      * @throws repository_exception
288      */
289     protected function query($q, $path = null, $parent = null, $page = 0) {
290         global $OUTPUT;
292         $files = [];
293         $folders = [];
294         $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,thumbnails";
295         $params = ['$select' => $fields, '$expand' => 'thumbnails', 'parent' => $parent];
297         try {
298             // Retrieving files and folders.
299             $client = $this->get_user_oauth_client();
300             $service = new repository_onedrive\rest($client);
302             if (!empty($q)) {
303                 $params['search'] = urlencode($q);
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) {
313                 throw new repository_exception('servicenotenabled', 'repository_onedrive');
314             } else {
315                 throw $e;
316             }
317         }
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),
327                     'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false),
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                     ]);
341                 $thumb = '';
342                 $thumbwidth = 0;
343                 $thumbheight = 0;
344                 $extendedinfoerr = false;
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                 }
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                     }
368                 }
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                 ];
379             }
380         }
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     }
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();
398     }
400     /**
401      * Get a file.
402      *
403      * @param string $reference reference of the file.
404      * @param string $filename filename to save the file to.
405      * @return string JSON encoded array of information about the file.
406      */
407     public function get_file($reference, $filename = '') {
408         global $CFG;
410         if ($this->disabled) {
411             throw new repository_exception('cannotdownload', 'repository');
412         }
414         $client = $this->get_user_oauth_client();
415         $base = 'https://graph.microsoft.com/v1.0/';
417         $sourceinfo = json_decode($reference);
418         $sourceurl = new moodle_url($base . 'me/drive/items/' . $sourceinfo->id . '/content');
419         $source = $sourceurl->out(false);
421         // We use download_one and not the rest API because it has special timeouts etc.
422         $path = $this->prepare_file($filename);
423         $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
424         $result = $client->download_one($source, null, $options);
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     }
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     }
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 '*';
458     }
460     /**
461      * Tells how the file can be picked from this repository.
462      *
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()) {
468             $setting = get_config('onedrive', 'supportedreturntypes');
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     }
481     /**
482      * Which return type should be selected by default.
483      *
484      * @return int
485      */
486     public function default_returntype() {
487         $setting = get_config('onedrive', 'defaultreturntype');
488         $supported = get_config('onedrive', 'supportedreturntypes');
489         if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') {
490             return FILE_INTERNAL;
491         } else {
492             return FILE_CONTROLLED_LINK;
493         }
494     }
496     /**
497      * Return names of the general options.
498      * By default: no general option name.
499      *
500      * @return array
501      */
502     public static function get_type_option_names() {
503         return array('issuerid', 'pluginname', 'defaultreturntype', 'supportedreturntypes');
504     }
506     /**
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     }
515     /**
516      * Repository method to serve the referenced file
517      *
518      * @see send_stored_file
519      *
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
525      */
526     public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
527         if ($this->disabled) {
528             throw new repository_exception('cannotdownload', 'repository');
529         }
531         $source = json_decode($storedfile->get_reference());
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());
542         if (empty($options['offline']) && !empty($info) && $info->is_writable()) {
543             // Add the current user as an OAuth writer.
544             $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
546             if ($systemauth === false) {
547                 $details = 'Cannot connect as system user';
548                 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
549             }
550             $systemservice = new repository_onedrive\rest($systemauth);
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'];
572             $this->add_temp_writer_to_file($systemservice, $source->id, $useremail);
573         }
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) {
580             redirect($source->link);
581         } else {
582             $details = 'File is missing source link';
583             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
584         }
585     }
587     /**
588      * List the permissions on a file.
589      *
590      * @param \repository_onedrive\rest $client Authenticated client.
591      * @param string $fileid The id of the file.
592      * @return array
593      */
594     protected function list_file_permissions(\repository_onedrive\rest $client, $fileid) {
595         $fields = "id,roles,link,grantedTo";
596         return $client->call('list_permissions', ['fileid' => $fileid, '$select' => $fields]);
597     }
599     /**
600      * See if a folder exists within a folder
601      *
602      * @param \repository_onedrive\rest $client Authenticated client.
603      * @param string $fullpath
604      * @return string|boolean The file id if it exists or false.
605      */
606     protected function get_file_id_by_path(\repository_onedrive\rest $client, $fullpath) {
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;
614     }
616     /**
617      * Delete a file by full path.
618      *
619      * @param \repository_onedrive\rest $client Authenticated client.
620      * @param string $fullpath
621      * @return boolean
622      */
623     protected function delete_file_by_path(\repository_onedrive\rest $client, $fullpath) {
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;
630     }
633     /**
634      * Get a file summary by full path.
635      *
636      * @param \repository_onedrive\rest $client Authenticated client.
637      * @param string $fullpath
638      * @return stdClass
639      */
640     protected function get_file_summary_by_path(\repository_onedrive\rest $client, $fullpath) {
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     }
650     /**
651      * Create a folder within a folder
652      *
653      * @param \repository_onedrive\rest $client Authenticated client.
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      */
659     protected function create_folder_in_folder(\repository_onedrive\rest $client, $foldername, $parentid) {
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     }
670     /**
671      * Get simple file info for humans.
672      *
673      * @param \repository_onedrive\rest $client Authenticated client.
674      * @param string $fileid The file we are querying.
675      *
676      * @return stdClass
677      */
678     protected function get_file_summary(\repository_onedrive\rest $client, $fileid) {
679         $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser";
680         $response = $client->call('get', ['fileid' => $fileid, '$select' => $fields]);
681         return $response;
682     }
684     /**
685      * Get the id of this users root drive.
686      *
687      * @param \repository_onedrive\rest $client Authenticated client.
688      *
689      * @return string id
690      */
691     protected function get_root_drive_id(\repository_onedrive\rest $client) {
692         $response = $client->call('get_drive', []);
694         if (empty($response->id)) {
695             $details = 'Cannot get driveid';
696             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
697         }
698         return $response->id;
699     }
701     /**
702      * Add a writer to the permissions on the file (temporary).
703      *
704      * @param \repository_onedrive\rest $client Authenticated client.
705      * @param string $fileid The file we are updating.
706      * @param string $email The email of the writer account to add.
707      * @return boolean
708      */
709     protected function add_temp_writer_to_file(\repository_onedrive\rest $client, $fileid, $email) {
710         // Expires in 7 days.
711         $expires = new DateTime();
712         $expires->add(new DateInterval("P7D"));
714         $updateeditor = [
715             'recipients' => [[ 'email' => $email ]],
716             'roles' => ['write'],
717             'requireSignIn' => true,
718             'sendInvitation' => false
719         ];
720         $params = ['fileid' => $fileid];
721         $response = $client->call('create_permission', $params, json_encode($updateeditor));
722         if (empty($response->value[0]->id)) {
723             $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid;
724             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
725         }
726         // Store the permission id in the DB. Scheduled task will remove this permission after 7 days.
727         if ($access = repository_onedrive\access::get_record(['permissionid' => $response->value[0]->id, 'itemid' => $fileid ])) {
728             // Update the timemodified.
729             $access->update();
730         } else {
731             $record = (object) [ 'permissionid' => $response->value[0]->id, 'itemid' => $fileid ];
732             $access = new repository_onedrive\access(0, $record);
733             $access->create();
734         }
735         return true;
736     }
738     /**
739      * Add a writer to the permissions on the file.
740      *
741      * @param \repository_onedrive\rest $client Authenticated client.
742      * @param string $fileid The file we are updating.
743      * @param string $useremail The user email of the writer account to add.
744      * @return boolean
745      */
746     protected function add_writer_to_file(\repository_onedrive\rest $client, $fileid, $useremail) {
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     }
762     /**
763      * Allow anyone with the link to read the file.
764      *
765      * @param \repository_onedrive\rest $client Authenticated client.
766      * @param string $fileid The file we are updating.
767      * @return boolean
768      */
769     protected function set_file_sharing_anyone_with_link_can_read(\repository_onedrive\rest $client, $fileid) {
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     }
783     /**
784      * Copy a shared file to a new folder.
785      *
786      * @param \repository_onedrive\rest $client Authenticated client.
787      * @param string $sharetoken The share we are querying.
788      * @param string $newdrive Id of the drive to copy to.
789      * @param string $parentid Id of the folder to copy to.
790      * @return stdClass
791      */
792     protected function copy_share(\repository_onedrive\rest $client, $sharetoken, $newdrive, $parentid) {
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     }
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      *
812      * @param string $shareurl
813      * @return string The sharing token
814      */
815     protected function get_share_token($shareurl) {
816         return 'u!' . str_replace(['/', '+'], ['_', '-'], rtrim(base64_encode($shareurl), '='));
817     }
819     /**
820      * Called when a file is selected as a "link".
821      * Invoked at MOODLE/repository/repository_ajax.php
822      *
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      *
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).
842         // Get a system and a user oauth client.
843         $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
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'];
852         $source = json_decode($reference);
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'];
862         $userservice = new repository_onedrive\rest($userauth);
863         $systemservice = new repository_onedrive\rest($systemauth);
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);
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         }
882         // Add Moodle as writer.
883         $this->add_writer_to_file($userservice, $source->id, $systemuseremail);
885         // Now copy it to a sensible folder.
886         $contextlist = array_reverse($context->get_parent_contexts(true));
888         $cache = cache::make('repository_onedrive', 'folder');
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         }
898         $allfolders[] = urlencode(clean_param($component, PARAM_PATH));
899         $allfolders[] = urlencode(clean_param($filearea, PARAM_PATH));
900         $allfolders[] = urlencode(clean_param($itemid, PARAM_PATH));
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.
904         foreach ($allfolders as $foldername) {
905             if ($fullpath) {
906                 $fullpath .= '/';
907             }
908             $fullpath .= $foldername;
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         }
924         // Get the users drive id.
925         $newdrive = $this->get_root_drive_id($systemservice);
927         if ($readshareupdaterequired) {
928             $response = $this->set_file_sharing_anyone_with_link_can_read($userservice, $source->id);
929             $shareurl = $response->value->webUrl;
930         }
932         // Turn the share url into a sharing token.
933         $sharetoken = $this->get_share_token($shareurl);
935         // Delete any existing file at this path.
936         $path = $fullpath . '/' . $source->name;
937         $this->delete_file_by_path($systemservice, $path);
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.
942         $this->copy_share($systemservice, $sharetoken, $newdrive, $parentid);
944         $summary = $this->get_file_summary_by_path($systemservice, $path);
946         // Update the details in the file reference before it is saved.
947         $source->id = $summary->id;
948         $source->link = $summary->webUrl;
950         $reference = json_encode($source);
952         return $reference;
953     }
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);
968         if ($systemauth === false) {
969             return '';
970         }
971         $systemservice = new repository_onedrive\rest($systemauth);
972         $info = $this->get_file_summary($systemservice, $source->id);
974         $owner = '';
975         if (!empty($info->createdByUser->displayName)) {
976             $owner = $info->createdByUser->displayName;
977         }
978         if ($owner) {
979             return get_string('owner', 'repository_onedrive', $owner);
980         } else {
981             return $info->name;
982         }
983     }
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;
993         $skydrive = $DB->get_record('repository', ['type' => 'skydrive'], 'id', IGNORE_MISSING);
994         $onedrive = $DB->get_record('repository', ['type' => 'onedrive'], 'id', IGNORE_MISSING);
996         if (empty($skydrive) || empty($onedrive)) {
997             return false;
998         }
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         }
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     }
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;
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');
1037         $skydriveinstances = repository::get_instances(['type' => 'skydrive']);
1038         $skydriveinstance = reset($skydriveinstances);
1039         $onedriveinstances = repository::get_instances(['type' => 'onedrive']);
1040         $onedriveinstance = reset($onedriveinstances);
1042         // Update all file references.
1043         $DB->set_field('files_reference', 'repositoryid', $onedriveinstance->id, ['repositoryid' => $skydriveinstance->id]);
1045         // Delete and disable the skydrive repo.
1046         $skydrivetype->delete();
1047         core_plugin_manager::reset_caches();
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     }
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') {
1063         global $OUTPUT;
1065         $url = new moodle_url('/admin/tool/oauth2/issuers.php');
1066         $url = $url->out();
1068         $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_onedrive', $url));
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         }
1078         parent::type_config_form($mform);
1079         $options = [];
1080         $issuers = \core\oauth2\api::get_all_issuers();
1082         foreach ($issuers as $issuer) {
1083             $options[$issuer->get('id')] = s($issuer->get('name'));
1084         }
1086         $strrequired = get_string('required');
1088         $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_onedrive'), $options);
1089         $mform->addHelpButton('issuerid', 'issuer', 'repository_onedrive');
1090         $mform->addRule('issuerid', $strrequired, 'required', null, 'client');
1092         $mform->addElement('static', null, '', get_string('fileoptions', 'repository_onedrive'));
1093         $choices = [
1094             'internal' => get_string('internal', 'repository_onedrive'),
1095             'external' => get_string('external', 'repository_onedrive'),
1096             'both' => get_string('both', 'repository_onedrive')
1097         ];
1098         $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_onedrive'), $choices);
1100         $choices = [
1101             FILE_INTERNAL => get_string('internal', 'repository_onedrive'),
1102             FILE_CONTROLLED_LINK => get_string('external', 'repository_onedrive'),
1103         ];
1104         $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_onedrive'), $choices);
1106     }
1109 /**
1110  * Callback to get the required scopes for system account.
1111  *
1112  * @param \core\oauth2\issuer $issuer
1113  * @return string
1114  */
1115 function repository_onedrive_oauth2_system_scopes(\core\oauth2\issuer $issuer) {
1116     if ($issuer->get('id') == get_config('onedrive', 'issuerid')) {
1117         return repository_onedrive::SCOPES;
1118     }
1119     return '';