Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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