Proyectos de Subversion Moodle

Rev

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