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
 * This plugin is used to access Google Drive.
19
 *
20
 * @since Moodle 2.0
21
 * @package    repository_googledocs
22
 * @copyright  2009 Dan Poltawski <talktodan@gmail.com>
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
defined('MOODLE_INTERNAL') || die();
27
 
28
require_once($CFG->dirroot . '/repository/lib.php');
29
require_once($CFG->libdir . '/filebrowser/file_browser.php');
30
 
31
use repository_googledocs\helper;
32
use repository_googledocs\googledocs_content_search;
33
 
34
/**
35
 * Google Docs Plugin
36
 *
37
 * @since Moodle 2.0
38
 * @package    repository_googledocs
39
 * @copyright  2009 Dan Poltawski <talktodan@gmail.com>
40
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41
 */
42
class repository_googledocs extends repository {
43
 
44
    /**
45
     * OAuth 2 client
46
     * @var \core\oauth2\client
47
     */
48
    private $client = null;
49
 
50
    /**
51
     * OAuth 2 Issuer
52
     * @var \core\oauth2\issuer
53
     */
54
    private $issuer = null;
55
 
56
    /**
57
     * Additional scopes required for drive.
58
     */
59
    const SCOPES = 'https://www.googleapis.com/auth/drive';
60
 
61
    /** @var string Defines the path node identifier for the repository root. */
62
    const REPOSITORY_ROOT_ID = 'repository_root';
63
 
64
    /** @var string Defines the path node identifier for the my drive root. */
65
    const MY_DRIVE_ROOT_ID = 'root';
66
 
67
    /** @var string Defines the path node identifier for the shared drives root. */
68
    const SHARED_DRIVES_ROOT_ID = 'shared_drives_root';
69
 
70
    /** @var string Defines the path node identifier for the content search root. */
71
    const SEARCH_ROOT_ID = 'search';
72
 
73
    /**
74
     * Constructor.
75
     *
76
     * @param int $repositoryid repository instance id.
77
     * @param int|stdClass $context a context id or context object.
78
     * @param array $options repository options.
79
     * @param int $readonly indicate this repo is readonly or not.
80
     * @return void
81
     */
82
    public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
83
        parent::__construct($repositoryid, $context, $options, $readonly = 0);
84
 
85
        try {
86
            $this->issuer = \core\oauth2\api::get_issuer(get_config('googledocs', 'issuerid'));
87
        } catch (dml_missing_record_exception $e) {
88
            $this->disabled = true;
89
        }
90
 
91
        if ($this->issuer && !$this->issuer->get('enabled')) {
92
            $this->disabled = true;
93
        }
94
    }
95
 
96
    /**
97
     * Get a cached user authenticated oauth client.
98
     *
99
     * @param moodle_url $overrideurl - Use this url instead of the repo callback.
100
     * @return \core\oauth2\client
101
     */
102
    protected function get_user_oauth_client($overrideurl = false) {
103
        if ($this->client) {
104
            return $this->client;
105
        }
106
        if ($overrideurl) {
107
            $returnurl = $overrideurl;
108
        } else {
109
            $returnurl = new moodle_url('/repository/repository_callback.php');
110
            $returnurl->param('callback', 'yes');
111
            $returnurl->param('repo_id', $this->id);
112
            $returnurl->param('sesskey', sesskey());
113
        }
114
 
115
        $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES, true);
116
 
117
        return $this->client;
118
    }
119
 
120
    /**
121
     * Checks whether the user is authenticate or not.
122
     *
123
     * @return bool true when logged in.
124
     */
125
    public function check_login() {
126
        $client = $this->get_user_oauth_client();
127
        return $client->is_logged_in();
128
    }
129
 
130
    /**
131
     * Print or return the login form.
132
     *
133
     * @return void|array for ajax.
134
     */
135
    public function print_login() {
136
        $client = $this->get_user_oauth_client();
137
        $url = $client->get_login_url();
138
 
139
        if ($this->options['ajax']) {
140
            $popup = new stdClass();
141
            $popup->type = 'popup';
142
            $popup->url = $url->out(false);
143
            return array('login' => array($popup));
144
        } else {
145
            echo '<a target="_blank" href="'.$url->out(false).'">'.get_string('login', 'repository').'</a>';
146
        }
147
    }
148
 
149
    /**
150
     * Print the login in a popup.
151
     *
152
     * @param array|null $attr Custom attributes to be applied to popup div.
153
     */
154
    public function print_login_popup($attr = null) {
155
        global $OUTPUT, $PAGE;
156
 
157
        $client = $this->get_user_oauth_client(false);
158
        $url = new moodle_url($client->get_login_url());
159
        $state = $url->get_param('state') . '&reloadparent=true';
160
        $url->param('state', $state);
161
 
162
        $PAGE->set_pagelayout('embedded');
163
        echo $OUTPUT->header();
164
 
165
        $repositoryname = get_string('pluginname', 'repository_googledocs');
166
 
167
        $button = new single_button(
168
            $url,
169
            get_string('logintoaccount', 'repository', $repositoryname),
170
            'post',
171
            single_button::BUTTON_PRIMARY
172
        );
173
        $button->add_action(new popup_action('click', $url, 'Login'));
174
        $button->class = 'mdl-align';
175
        $button = $OUTPUT->render($button);
176
        echo html_writer::div($button, '', $attr);
177
 
178
        echo $OUTPUT->footer();
179
    }
180
 
181
    /**
182
     * Build the breadcrumb from a path.
183
     *
184
     * @deprecated since Moodle 3.11.
185
     * @param string $path to create a breadcrumb from.
186
     * @return array containing name and path of each crumb.
187
     */
188
    protected function build_breadcrumb($path) {
189
        debugging('The function build_breadcrumb() is deprecated, please use get_navigation() from the ' .
190
            'googledocs repository content classes instead.', DEBUG_DEVELOPER);
191
 
192
        $bread = explode('/', $path);
193
        $crumbtrail = '';
194
        foreach ($bread as $crumb) {
195
            list($id, $name) = $this->explode_node_path($crumb);
196
            $name = empty($name) ? $id : $name;
197
            $breadcrumb[] = array(
198
                'name' => $name,
199
                'path' => $this->build_node_path($id, $name, $crumbtrail)
200
            );
201
            $tmp = end($breadcrumb);
202
            $crumbtrail = $tmp['path'];
203
        }
204
        return $breadcrumb;
205
    }
206
 
207
    /**
208
     * Generates a safe path to a node.
209
     *
210
     * Typically, a node will be id|Name of the node.
211
     *
212
     * @deprecated since Moodle 3.11.
213
     * @param string $id of the node.
214
     * @param string $name of the node, will be URL encoded.
215
     * @param string $root to append the node on, must be a result of this function.
216
     * @return string path to the node.
217
     */
218
    protected function build_node_path($id, $name = '', $root = '') {
219
        debugging('The function build_node_path() is deprecated, please use ' .
220
            '\repository_googledocs\helper::build_node_path() instead.', DEBUG_DEVELOPER);
221
 
222
        $path = $id;
223
        if (!empty($name)) {
224
            $path .= '|' . urlencode($name);
225
        }
226
        if (!empty($root)) {
227
            $path = trim($root, '/') . '/' . $path;
228
        }
229
        return $path;
230
    }
231
 
232
    /**
233
     * Returns information about a node in a path.
234
     *
235
     * @deprecated since Moodle 3.11.
236
     * @see self::build_node_path()
237
     * @param string $node to extrat information from.
238
     * @return array about the node.
239
     */
240
    protected function explode_node_path($node) {
241
        debugging('The function explode_node_path() is deprecated, please use ' .
242
            '\repository_googledocs\helper::explode_node_path() instead.', DEBUG_DEVELOPER);
243
 
244
        if (strpos($node, '|') !== false) {
245
            list($id, $name) = explode('|', $node, 2);
246
            $name = urldecode($name);
247
        } else {
248
            $id = $node;
249
            $name = '';
250
        }
251
        $id = urldecode($id);
252
        return array(
253
 
254
            1 => $name,
255
            'id' => $id,
256
            'name' => $name
257
        );
258
    }
259
 
260
    /**
261
     * List the files and folders.
262
     *
263
     * @param  string $path path to browse.
264
     * @param  string $page page to browse.
265
     * @return array of result.
266
     */
267
    public function get_listing($path='', $page = '') {
268
        if (empty($path)) {
269
            $pluginname = get_string('pluginname', 'repository_googledocs');
270
            $path = helper::build_node_path('repository_root', $pluginname);
271
        }
272
 
273
        if (!$this->issuer->get('enabled')) {
274
            // Empty list of files for disabled repository.
275
            return [
276
                'dynload' => false,
277
                'list' => [],
278
                'nologin' => true,
279
            ];
280
        }
281
 
282
        // We analyse the path to extract what to browse.
283
        $trail = explode('/', $path);
284
        $uri = array_pop($trail);
285
        list($id, $name) = helper::explode_node_path($uri);
286
        $service = new repository_googledocs\rest($this->get_user_oauth_client());
287
 
288
        // Define the content class object and query which will be used to get the contents for this path.
289
        if ($id === self::SEARCH_ROOT_ID) {
290
            // The special keyword 'search' is the ID of the node. This is possible as we can set up a breadcrumb in
291
            // the search results. Therefore, we should use the content search object to get the results from the
292
            // previously performed search.
293
            $contentobj = new googledocs_content_search($service, $path);
294
            // We need to deconstruct the node name in order to obtain the search term and use it as a query.
295
            $query = str_replace(get_string('searchfor', 'repository_googledocs'), '', $name);
296
            $query = trim(str_replace("'", "", $query));
297
        } else {
298
            // Otherwise, return and use the appropriate (based on the path) content browser object.
299
            $contentobj = helper::get_browser($service, $path);
300
            // Use the node ID as a query.
301
            $query = $id;
302
        }
303
 
304
        return [
305
            'dynload' => true,
306
            'defaultreturntype' => $this->default_returntype(),
307
            'path' => $contentobj->get_navigation(),
308
            'list' => $contentobj->get_content_nodes($query, [$this, 'filter']),
309
            'manage' => 'https://drive.google.com/',
310
        ];
311
    }
312
 
313
    /**
314
     * Search throughout the Google Drive.
315
     *
316
     * @param string $searchtext text to search for.
317
     * @param int $page search page.
318
     * @return array of results.
319
     */
320
    public function search($searchtext, $page = 0) {
321
        // Construct the path to the repository root.
322
        $pluginname = get_string('pluginname', 'repository_googledocs');
323
        $rootpath = helper::build_node_path(self::REPOSITORY_ROOT_ID, $pluginname);
324
        // Construct the path to the search results node.
325
        // Currently, when constructing the search node name, the search term is concatenated to the lang string.
326
        // This was done deliberately so that we can easily and accurately obtain the search term from the search node
327
        // name later when navigating to the search results through the breadcrumb navigation.
328
        $name = get_string('searchfor', 'repository_googledocs') . " '{$searchtext}'";
329
        $path = helper::build_node_path(self::SEARCH_ROOT_ID, $name, $rootpath);
330
 
331
        $service = new repository_googledocs\rest($this->get_user_oauth_client());
332
        $searchobj = new googledocs_content_search($service, $path);
333
 
334
        return [
335
            'dynload' => true,
336
            'path' => $searchobj->get_navigation(),
337
            'list' => $searchobj->get_content_nodes($searchtext, [$this, 'filter']),
338
            'manage' => 'https://drive.google.com/',
339
        ];
340
    }
341
 
342
    /**
343
     * Query Google Drive for files and folders using a search query.
344
     *
345
     * Documentation about the query format can be found here:
346
     *   https://developers.google.com/drive/search-parameters
347
     *
348
     * This returns a list of files and folders with their details as they should be
349
     * formatted and returned by functions such as get_listing() or search().
350
     *
351
     * @deprecated since Moodle 3.11.
352
     * @param string $q search query as expected by the Google API.
353
     * @param string $path parent path of the current files, will not be used for the query.
354
     * @param int $page page.
355
     * @return array of files and folders.
356
     */
357
    protected function query($q, $path = null, $page = 0) {
358
        debugging('The function query() is deprecated, please use get_content_nodes() from the ' .
359
            'googledocs repository content classes instead.', DEBUG_DEVELOPER);
360
 
361
        global $OUTPUT;
362
 
363
        $files = array();
364
        $folders = array();
365
        $config = get_config('googledocs');
366
        $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)";
367
        $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive');
368
 
369
        try {
370
            // Retrieving files and folders.
371
            $client = $this->get_user_oauth_client();
372
            $service = new repository_googledocs\rest($client);
373
 
374
            $response = $service->call('list', $params);
375
        } catch (Exception $e) {
376
            if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
377
                // This is raised when the service Drive API has not been enabled on Google APIs control panel.
378
                throw new repository_exception('servicenotenabled', 'repository_googledocs');
379
            } else {
380
                throw $e;
381
            }
382
        }
383
 
384
        $gfiles = isset($response->files) ? $response->files : array();
385
        foreach ($gfiles as $gfile) {
386
            if ($gfile->mimeType == 'application/vnd.google-apps.folder') {
387
                // This is a folder.
388
                $folders[$gfile->name . $gfile->id] = array(
389
                    'title' => $gfile->name,
390
                    'path' => $this->build_node_path($gfile->id, $gfile->name, $path),
391
                    'date' => strtotime($gfile->modifiedTime),
392
                    'thumbnail' => $OUTPUT->image_url(file_folder_icon())->out(false),
393
                    'thumbnail_height' => 64,
394
                    'thumbnail_width' => 64,
395
                    'children' => array()
396
                );
397
            } else {
398
                // This is a file.
399
                $link = isset($gfile->webViewLink) ? $gfile->webViewLink : '';
400
                if (empty($link)) {
401
                    $link = isset($gfile->webContentLink) ? $gfile->webContentLink : '';
402
                }
403
                if (isset($gfile->fileExtension)) {
404
                    // The file has an extension, therefore we can download it.
405
                    $source = json_encode([
406
                        'id' => $gfile->id,
407
                        'name' => $gfile->name,
408
                        'exportformat' => 'download',
409
                        'link' => $link
410
                    ]);
411
                    $title = $gfile->name;
412
                } else {
413
                    // The file is probably a Google Doc file, we get the corresponding export link.
414
                    // This should be improved by allowing the user to select the type of export they'd like.
415
                    $type = str_replace('application/vnd.google-apps.', '', $gfile->mimeType);
416
                    $title = '';
417
                    $exporttype = '';
418
                    $types = get_mimetypes_array();
419
 
420
                    switch ($type){
421
                        case 'document':
422
                            $ext = $config->documentformat;
423
                            $title = $gfile->name . '.gdoc';
424
                            if ($ext === 'rtf') {
425
                                // Moodle user 'text/rtf' as the MIME type for RTF files.
426
                                // Google uses 'application/rtf' for the same type of file.
427
                                // See https://developers.google.com/drive/v3/web/manage-downloads.
428
                                $exporttype = 'application/rtf';
429
                            } else {
430
                                $exporttype = $types[$ext]['type'];
431
                            }
432
                            break;
433
                        case 'presentation':
434
                            $ext = $config->presentationformat;
435
                            $title = $gfile->name . '.gslides';
436
                            $exporttype = $types[$ext]['type'];
437
                            break;
438
                        case 'spreadsheet':
439
                            $ext = $config->spreadsheetformat;
440
                            $title = $gfile->name . '.gsheet';
441
                            $exporttype = $types[$ext]['type'];
442
                            break;
443
                        case 'drawing':
444
                            $ext = $config->drawingformat;
445
                            $title = $gfile->name . '.'. $ext;
446
                            $exporttype = $types[$ext]['type'];
447
                            break;
448
                    }
449
                    // Skips invalid/unknown types.
450
                    if (empty($title)) {
451
                        continue;
452
                    }
453
                    $source = json_encode([
454
                        'id' => $gfile->id,
455
                        'exportformat' => $exporttype,
456
                        'link' => $link,
457
                        'name' => $gfile->name
458
                    ]);
459
                }
460
                // Adds the file to the file list. Using the itemId along with the name as key
461
                // of the array because Google Drive allows files with identical names.
462
                $thumb = '';
463
                if (isset($gfile->thumbnailLink)) {
464
                    $thumb = $gfile->thumbnailLink;
465
                } else if (isset($gfile->iconLink)) {
466
                    $thumb = $gfile->iconLink;
467
                }
468
                $files[$title . $gfile->id] = array(
469
                    'title' => $title,
470
                    'source' => $source,
471
                    'date' => strtotime($gfile->modifiedTime),
472
                    'size' => isset($gfile->size) ? $gfile->size : null,
473
                    'thumbnail' => $thumb,
474
                    'thumbnail_height' => 64,
475
                    'thumbnail_width' => 64,
476
                );
477
            }
478
        }
479
 
480
        // Filter and order the results.
481
        $files = array_filter($files, array($this, 'filter'));
482
        core_collator::ksort($files, core_collator::SORT_NATURAL);
483
        core_collator::ksort($folders, core_collator::SORT_NATURAL);
484
        return array_merge(array_values($folders), array_values($files));
485
    }
486
 
487
    /**
488
     * Logout.
489
     *
490
     * @return string
491
     */
492
    public function logout() {
493
        $client = $this->get_user_oauth_client();
494
        $client->log_out();
495
        return parent::logout();
496
    }
497
 
498
    /**
499
     * Get a file.
500
     *
501
     * @param string $reference reference of the file.
502
     * @param string $file name to save the file to.
503
     * @return string JSON encoded array of information about the file.
504
     */
505
    public function get_file($reference, $filename = '') {
506
        global $CFG;
507
 
508
        if (!$this->issuer->get('enabled')) {
509
            throw new repository_exception('cannotdownload', 'repository');
510
        }
511
 
512
        $source = json_decode($reference);
513
 
514
        $client = null;
515
        if (!empty($source->usesystem)) {
516
            $client = \core\oauth2\api::get_system_oauth_client($this->issuer);
517
        } else {
518
            $client = $this->get_user_oauth_client();
519
        }
520
 
521
        $base = 'https://www.googleapis.com/drive/v3';
522
 
523
        $newfilename = false;
524
        if ($source->exportformat == 'download') {
525
            $params = ['alt' => 'media'];
526
            $sourceurl = new moodle_url($base . '/files/' . $source->id, $params);
527
            $source = $sourceurl->out(false);
528
        } else {
529
            $params = ['mimeType' => $source->exportformat];
530
            $sourceurl = new moodle_url($base . '/files/' . $source->id . '/export', $params);
531
            $types = get_mimetypes_array();
532
            $checktype = $source->exportformat;
533
            if ($checktype == 'application/rtf') {
534
                $checktype = 'text/rtf';
535
            }
536
            // Determine the relevant default import format config for the given file.
537
            switch ($source->googledoctype) {
538
                case 'document':
539
                    $importformatconfig = get_config('googledocs', 'documentformat');
540
                    break;
541
                case 'presentation':
542
                    $importformatconfig = get_config('googledocs', 'presentationformat');
543
                    break;
544
                case 'spreadsheet':
545
                    $importformatconfig = get_config('googledocs', 'spreadsheetformat');
546
                    break;
547
                case 'drawing':
548
                    $importformatconfig = get_config('googledocs', 'drawingformat');
549
                    break;
550
                default:
551
                    $importformatconfig = null;
552
            }
553
 
554
            foreach ($types as $extension => $info) {
555
                if ($info['type'] == $checktype && $extension === $importformatconfig) {
556
                    $newfilename = $source->name . '.' . $extension;
557
                    break;
558
                }
559
            }
560
            $source = $sourceurl->out(false);
561
        }
562
 
563
        // We use download_one and not the rest API because it has special timeouts etc.
564
        $path = $this->prepare_file($filename);
565
        $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
566
        $success = $client->download_one($source, null, $options);
567
 
568
        if ($success) {
569
            @chmod($path, $CFG->filepermissions);
570
 
571
            $result = [
572
                'path' => $path,
573
                'url' => $reference,
574
            ];
575
            if (!empty($newfilename)) {
576
                $result['newfilename'] = $newfilename;
577
            }
578
            return $result;
579
        }
580
        throw new repository_exception('cannotdownload', 'repository');
581
    }
582
 
583
    /**
584
     * Prepare file reference information.
585
     *
586
     * We are using this method to clean up the source to make sure that it
587
     * is a valid source.
588
     *
589
     * @param string $source of the file.
590
     * @return string file reference.
591
     */
592
    public function get_file_reference($source) {
593
        // We could do some magic upgrade code here.
594
        return $source;
595
    }
596
 
597
    /**
598
     * What kind of files will be in this repository?
599
     *
600
     * @return array return '*' means this repository support any files, otherwise
601
     *               return mimetypes of files, it can be an array
602
     */
603
    public function supported_filetypes() {
604
        return '*';
605
    }
606
 
607
    /**
608
     * Tells how the file can be picked from this repository.
609
     *
610
     * @return int
611
     */
612
    public function supported_returntypes() {
613
        // We can only support references if the system account is connected.
614
        if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) {
615
            $setting = get_config('googledocs', 'supportedreturntypes');
616
            if ($setting == 'internal') {
617
                return FILE_INTERNAL;
618
            } else if ($setting == 'external') {
619
                return FILE_CONTROLLED_LINK;
620
            } else {
621
                return FILE_CONTROLLED_LINK | FILE_INTERNAL;
622
            }
623
        } else {
624
            return FILE_INTERNAL;
625
        }
626
    }
627
 
628
    /**
629
     * Which return type should be selected by default.
630
     *
631
     * @return int
632
     */
633
    public function default_returntype() {
634
        $setting = get_config('googledocs', 'defaultreturntype');
635
        $supported = get_config('googledocs', 'supportedreturntypes');
636
        if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') {
637
            return FILE_INTERNAL;
638
        } else {
639
            return FILE_CONTROLLED_LINK;
640
        }
641
    }
642
 
643
    /**
644
     * Return names of the general options.
645
     * By default: no general option name.
646
     *
647
     * @return array
648
     */
649
    public static function get_type_option_names() {
650
        return array('issuerid', 'pluginname',
651
            'documentformat', 'drawingformat',
652
            'presentationformat', 'spreadsheetformat',
653
            'defaultreturntype', 'supportedreturntypes');
654
    }
655
 
656
    /**
657
     * Store the access token.
658
     */
659
    public function callback() {
660
        $client = $this->get_user_oauth_client();
661
        // This will upgrade to an access token if we have an authorization code and save the access token in the session.
662
        $client->is_logged_in();
663
    }
664
 
665
    /**
666
     * Repository method to serve the referenced file
667
     *
668
     * @see send_stored_file
669
     *
670
     * @param stored_file $storedfile the file that contains the reference
671
     * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
672
     * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
673
     * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
674
     * @param array $options additional options affecting the file serving
675
     */
676
    public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
677
        if (!$this->issuer->get('enabled')) {
678
            throw new repository_exception('cannotdownload', 'repository');
679
        }
680
 
681
        $source = json_decode($storedfile->get_reference());
682
 
683
        $fb = get_file_browser();
684
        $context = context::instance_by_id($storedfile->get_contextid(), MUST_EXIST);
685
        $info = $fb->get_file_info($context,
686
                                   $storedfile->get_component(),
687
                                   $storedfile->get_filearea(),
688
                                   $storedfile->get_itemid(),
689
                                   $storedfile->get_filepath(),
690
                                   $storedfile->get_filename());
691
 
692
        if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
693
            // Add the current user as an OAuth writer.
694
            $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
695
 
696
            if ($systemauth === false) {
697
                $details = 'Cannot connect as system user';
698
                throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
699
            }
700
            $systemservice = new repository_googledocs\rest($systemauth);
701
 
702
            // Get the user oauth so we can get the account to add.
703
            $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(),
704
                                                   $storedfile->get_component(),
705
                                                   $storedfile->get_filearea(),
706
                                                   $storedfile->get_itemid(),
707
                                                   $storedfile->get_filepath(),
708
                                                   $storedfile->get_filename(),
709
                                                   $forcedownload);
710
            $url->param('sesskey', sesskey());
711
            $param = ($options['embed'] == true) ? false : $url;
712
            $userauth = $this->get_user_oauth_client($param);
713
            if (!$userauth->is_logged_in()) {
714
                if ($options['embed'] == true) {
715
                    // Due to Same-origin policy, we cannot redirect to googledocs login page.
716
                    // If the requested file is embed and the user is not logged in, add option to log in using a popup.
717
                    $this->print_login_popup(['style' => 'margin-top: 250px']);
718
                    exit;
719
                }
720
                redirect($userauth->get_login_url());
721
            }
722
            if ($userauth === false) {
723
                $details = 'Cannot connect as current user';
724
                throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
725
            }
726
            $userinfo = $userauth->get_userinfo();
727
            $useremail = $userinfo['email'];
728
 
729
            $this->add_temp_writer_to_file($systemservice, $source->id, $useremail);
730
        }
731
 
732
        if (!empty($options['offline'])) {
733
            $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename());
734
 
735
            $filename = $storedfile->get_filename();
736
            if (isset($downloaded['newfilename'])) {
737
                $filename = $downloaded['newfilename'];
738
            }
739
            send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options);
740
        } else if ($source->link) {
741
            // Do not use redirect() here because is not compatible with webservice/pluginfile.php.
742
            header('Location: ' . $source->link);
743
        } else {
744
            $details = 'File is missing source link';
745
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
746
        }
747
    }
748
 
749
    /**
750
     * See if a folder exists within a folder
751
     *
752
     * @param \repository_googledocs\rest $client Authenticated client.
753
     * @param string $foldername The folder we are looking for.
754
     * @param string $parentid The parent folder we are looking in.
755
     * @return string|boolean The file id if it exists or false.
756
     */
757
    protected function folder_exists_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) {
758
        $q = '\'' . addslashes($parentid) . '\' in parents and trashed = false and name = \'' . addslashes($foldername). '\'';
759
        $fields = 'files(id, name)';
760
        $params = [ 'q' => $q, 'fields' => $fields];
761
        $response = $client->call('list', $params);
762
        $missing = true;
763
        foreach ($response->files as $child) {
764
            if ($child->name == $foldername) {
765
                return $child->id;
766
            }
767
        }
768
        return false;
769
    }
770
 
771
    /**
772
     * Create a folder within a folder
773
     *
774
     * @param \repository_googledocs\rest $client Authenticated client.
775
     * @param string $foldername The folder we are creating.
776
     * @param string $parentid The parent folder we are creating in.
777
     *
778
     * @return string The file id of the new folder.
779
     */
780
    protected function create_folder_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) {
781
        $fields = 'id';
782
        $params = ['fields' => $fields];
783
        $folder = ['mimeType' => 'application/vnd.google-apps.folder', 'name' => $foldername, 'parents' => [$parentid]];
784
        $created = $client->call('create', $params, json_encode($folder));
785
        if (empty($created->id)) {
786
            $details = 'Cannot create folder:' . $foldername;
787
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
788
        }
789
        return $created->id;
790
    }
791
 
792
    /**
793
     * Get simple file info for humans.
794
     *
795
     * @param \repository_googledocs\rest $client Authenticated client.
796
     * @param string $fileid The file we are querying.
797
     *
798
     * @return stdClass
799
     */
800
    protected function get_file_summary(\repository_googledocs\rest $client, $fileid) {
801
        $fields = "id,name,owners,parents";
802
        $params = [
803
            'fileid' => $fileid,
804
            'fields' => $fields
805
        ];
806
        return $client->call('get', $params);
807
    }
808
 
809
    /**
810
     * Copy a file and return the new file details. A side effect of the copy
811
     * is that the owner will be the account authenticated with this oauth client.
812
     *
813
     * @param \repository_googledocs\rest $client Authenticated client.
814
     * @param string $fileid The file we are copying.
815
     * @param string $name The original filename (don't change it).
816
     *
817
     * @return stdClass file details.
818
     */
819
    protected function copy_file(\repository_googledocs\rest $client, $fileid, $name) {
820
        $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink";
821
        $params = [
822
            'fileid' => $fileid,
823
            'fields' => $fields,
824
        ];
825
        // Keep the original name (don't put copy at the end of it).
826
        $copyinfo = [];
827
        if (!empty($name)) {
828
            $copyinfo = [ 'name' => $name ];
829
        }
830
        $fileinfo = $client->call('copy', $params, json_encode($copyinfo));
831
        if (empty($fileinfo->id)) {
832
            $details = 'Cannot copy file:' . $fileid;
833
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
834
        }
835
        return $fileinfo;
836
    }
837
 
838
    /**
839
     * Add a writer to the permissions on the file (temporary).
840
     *
841
     * @param \repository_googledocs\rest $client Authenticated client.
842
     * @param string $fileid The file we are updating.
843
     * @param string $email The email of the writer account to add.
844
     * @return boolean
845
     */
846
    protected function add_temp_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
847
        // Expires in 7 days.
848
        $expires = new DateTime();
849
        $expires->add(new DateInterval("P7D"));
850
 
851
        $updateeditor = [
852
            'emailAddress' => $email,
853
            'role' => 'writer',
854
            'type' => 'user',
855
            'expirationTime' => $expires->format(DateTime::RFC3339)
856
        ];
857
        $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false'];
858
        $response = $client->call('create_permission', $params, json_encode($updateeditor));
859
        if (empty($response->id)) {
860
            $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid;
861
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
862
        }
863
        return true;
864
    }
865
 
866
 
867
    /**
868
     * Add a writer to the permissions on the file.
869
     *
870
     * @param \repository_googledocs\rest $client Authenticated client.
871
     * @param string $fileid The file we are updating.
872
     * @param string $email The email of the writer account to add.
873
     * @return boolean
874
     */
875
    protected function add_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
876
        $updateeditor = [
877
            'emailAddress' => $email,
878
            'role' => 'writer',
879
            'type' => 'user'
880
        ];
881
        $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false'];
882
        $response = $client->call('create_permission', $params, json_encode($updateeditor));
883
        if (empty($response->id)) {
884
            $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid;
885
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
886
        }
887
        return true;
888
    }
889
 
890
    /**
891
     * Move from root to folder
892
     *
893
     * @param \repository_googledocs\rest $client Authenticated client.
894
     * @param string $fileid The file we are updating.
895
     * @param string $folderid The id of the folder we are moving to
896
     * @return boolean
897
     */
898
    protected function move_file_from_root_to_folder(\repository_googledocs\rest $client, $fileid, $folderid) {
899
        // Set the parent.
900
        $params = [
901
            'fileid' => $fileid, 'addParents' => $folderid, 'removeParents' => 'root'
902
        ];
903
        $response = $client->call('update', $params, ' ');
904
        if (empty($response->id)) {
905
            $details = 'Cannot move the file to a folder: ' . $fileid;
906
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
907
        }
908
        return true;
909
    }
910
 
911
    /**
912
     * Prevent writers from sharing.
913
     *
914
     * @param \repository_googledocs\rest $client Authenticated client.
915
     * @param string $fileid The file we are updating.
916
     * @return boolean
917
     */
918
    protected function prevent_writers_from_sharing_file(\repository_googledocs\rest $client, $fileid) {
919
        // We don't want anyone but Moodle to change the sharing settings.
920
        $params = [
921
            'fileid' => $fileid
922
        ];
923
        $update = [
924
            'writersCanShare' => false
925
        ];
926
        $response = $client->call('update', $params, json_encode($update));
927
        if (empty($response->id)) {
928
            $details = 'Cannot prevent writers from sharing document: ' . $fileid;
929
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
930
        }
931
        return true;
932
    }
933
 
934
    /**
935
     * Allow anyone with the link to read the file.
936
     *
937
     * @param \repository_googledocs\rest $client Authenticated client.
938
     * @param string $fileid The file we are updating.
939
     * @return boolean
940
     */
941
    protected function set_file_sharing_anyone_with_link_can_read(\repository_googledocs\rest $client, $fileid) {
942
        $updateread = [
943
            'type' => 'anyone',
944
            'role' => 'reader',
945
            'allowFileDiscovery' => 'false'
946
        ];
947
        $params = ['fileid' => $fileid];
948
        $response = $client->call('create_permission', $params, json_encode($updateread));
949
        if (empty($response->id) || $response->id != 'anyoneWithLink') {
950
            $details = 'Cannot update link sharing for the document: ' . $fileid;
951
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
952
        }
953
        return true;
954
    }
955
 
956
    /**
957
     * Called when a file is selected as a "link".
958
     * Invoked at MOODLE/repository/repository_ajax.php
959
     *
960
     * This is called at the point the reference files are being copied from the draft area to the real area
961
     * (when the file has really really been selected.
962
     *
963
     * @param string $reference this reference is generated by
964
     *                          repository::get_file_reference()
965
     * @param context $context the target context for this new file.
966
     * @param string $component the target component for this new file.
967
     * @param string $filearea the target filearea for this new file.
968
     * @param string $itemid the target itemid for this new file.
969
     * @return string updated reference (final one before it's saved to db).
970
     */
971
    public function reference_file_selected($reference, $context, $component, $filearea, $itemid) {
972
        global $CFG, $SITE;
973
 
974
        // What we need to do here is transfer ownership to the system user (or copy)
975
        // then set the permissions so anyone with the share link can view,
976
        // finally update the reference to contain the share link if it was not
977
        // already there (and point to new file id if we copied).
978
 
979
        // Get the details from the reference.
980
        $source = json_decode($reference);
981
        if (!empty($source->usesystem)) {
982
            // If we already copied this file to the system account - we are done.
983
            return $reference;
984
        }
985
 
986
        // Check this issuer is enabled.
987
        if ($this->disabled) {
988
            throw new repository_exception('cannotdownload', 'repository');
989
        }
990
 
991
        // Get a system oauth client and a user oauth client.
992
        $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
993
 
994
        if ($systemauth === false) {
995
            $details = 'Cannot connect as system user';
996
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
997
        }
998
        // Get the system user email so we can share the file with this user.
999
        $systemuserinfo = $systemauth->get_userinfo();
1000
        $systemuseremail = $systemuserinfo['email'];
1001
 
1002
        $userauth = $this->get_user_oauth_client();
1003
        if ($userauth === false) {
1004
            $details = 'Cannot connect as current user';
1005
            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
1006
        }
1007
 
1008
        $userservice = new repository_googledocs\rest($userauth);
1009
        $systemservice = new repository_googledocs\rest($systemauth);
1010
 
1011
        // Add Moodle as writer.
1012
        $this->add_writer_to_file($userservice, $source->id, $systemuseremail);
1013
 
1014
        // Now move it to a sensible folder.
1015
        $contextlist = array_reverse($context->get_parent_contexts(true));
1016
 
1017
        $cache = cache::make('repository_googledocs', 'folder');
1018
        $parentid = 'root';
1019
        $fullpath = 'root';
1020
        $allfolders = [];
1021
        foreach ($contextlist as $context) {
1022
            // Prepare human readable context folders names, making sure they are still unique within the site.
1023
            $prevlang = force_current_language($CFG->lang);
1024
            $foldername = $context->get_context_name();
1025
            force_current_language($prevlang);
1026
 
1027
            if ($context->contextlevel == CONTEXT_SYSTEM) {
1028
                // Append the site short name to the root folder.
1029
                $foldername .= ' ('.$SITE->shortname.')';
1030
                // Append the relevant object id.
1031
            } else if ($context->instanceid) {
1032
                $foldername .= ' (id '.$context->instanceid.')';
1033
            } else {
1034
                // This does not really happen but just in case.
1035
                $foldername .= ' (ctx '.$context->id.')';
1036
            }
1037
 
1038
            $foldername = clean_param($foldername, PARAM_PATH);
1039
            $allfolders[] = $foldername;
1040
        }
1041
 
1042
        $allfolders[] = clean_param($component, PARAM_PATH);
1043
        $allfolders[] = clean_param($filearea, PARAM_PATH);
1044
        $allfolders[] = clean_param($itemid, PARAM_PATH);
1045
 
1046
        // Variable $allfolders is the full path we want to put the file in - so walk it and create each folder.
1047
 
1048
        foreach ($allfolders as $foldername) {
1049
            // Make sure a folder exists here.
1050
            $fullpath .= '/' . $foldername;
1051
 
1052
            $folderid = $cache->get($fullpath);
1053
            if (empty($folderid)) {
1054
                $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid);
1055
            }
1056
            if ($folderid !== false) {
1057
                $cache->set($fullpath, $folderid);
1058
                $parentid = $folderid;
1059
            } else {
1060
                // Create it.
1061
                $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid);
1062
                $cache->set($fullpath, $parentid);
1063
            }
1064
        }
1065
 
1066
        // Copy the file so we get a snapshot file owned by Moodle.
1067
        $newsource = $this->copy_file($systemservice, $source->id, $source->name);
1068
        // Move the copied file to the correct folder.
1069
        $this->move_file_from_root_to_folder($systemservice, $newsource->id, $parentid);
1070
 
1071
        // Set the sharing options.
1072
        $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id);
1073
        $this->prevent_writers_from_sharing_file($systemservice, $newsource->id);
1074
 
1075
        // Update the returned reference so that the stored_file in moodle points to the newly copied file.
1076
        $source->id = $newsource->id;
1077
        $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : '';
1078
        $source->usesystem = true;
1079
        if (empty($source->link)) {
1080
            $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : '';
1081
        }
1082
        $reference = json_encode($source);
1083
 
1084
        return $reference;
1085
    }
1086
 
1087
    /**
1088
     * Get human readable file info from a the reference.
1089
     *
1090
     * @param string $reference
1091
     * @param int $filestatus
1092
     */
1093
    public function get_reference_details($reference, $filestatus = 0) {
1094
        if ($this->disabled) {
1095
            throw new repository_exception('cannotdownload', 'repository');
1096
        }
1097
        if (empty($reference)) {
1098
            return get_string('unknownsource', 'repository');
1099
        }
1100
        $source = json_decode($reference);
1101
        if (empty($source->usesystem)) {
1102
            return '';
1103
        }
1104
        $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
1105
 
1106
        if ($systemauth === false) {
1107
            return '';
1108
        }
1109
        $systemservice = new repository_googledocs\rest($systemauth);
1110
        $info = $this->get_file_summary($systemservice, $source->id);
1111
 
1112
        $owner = '';
1113
        if (!empty($info->owners[0]->displayName)) {
1114
            $owner = $info->owners[0]->displayName;
1115
        }
1116
        if ($owner) {
1117
            return get_string('owner', 'repository_googledocs', $owner);
1118
        } else {
1119
            return $info->name;
1120
        }
1121
    }
1122
 
1123
    /**
1124
     * Edit/Create Admin Settings Moodle form.
1125
     *
1126
     * @param moodleform $mform Moodle form (passed by reference).
1127
     * @param string $classname repository class name.
1128
     */
1129
    public static function type_config_form($mform, $classname = 'repository') {
1130
        $url = new moodle_url('/admin/tool/oauth2/issuers.php');
1131
        $url = $url->out();
1132
 
1133
        $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url));
1134
 
1135
        parent::type_config_form($mform);
1136
        $options = [];
1137
        $issuers = \core\oauth2\api::get_all_issuers();
1138
 
1139
        foreach ($issuers as $issuer) {
1140
            $options[$issuer->get('id')] = s($issuer->get('name'));
1141
        }
1142
 
1143
        $strrequired = get_string('required');
1144
 
1145
        $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_googledocs'), $options);
1146
        $mform->addHelpButton('issuerid', 'issuer', 'repository_googledocs');
1147
        $mform->addRule('issuerid', $strrequired, 'required', null, 'client');
1148
 
1149
        $mform->addElement('static', null, '', get_string('fileoptions', 'repository_googledocs'));
1150
        $choices = [
1151
            'internal' => get_string('internal', 'repository_googledocs'),
1152
            'external' => get_string('external', 'repository_googledocs'),
1153
            'both' => get_string('both', 'repository_googledocs')
1154
        ];
1155
        $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_googledocs'), $choices);
1156
 
1157
        $choices = [
1158
            FILE_INTERNAL => get_string('internal', 'repository_googledocs'),
1159
            FILE_CONTROLLED_LINK => get_string('external', 'repository_googledocs'),
1160
        ];
1161
        $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices);
1162
 
1163
        $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs'));
1164
 
1165
        // Documents.
1166
        $docsformat = array();
1167
        $docsformat['html'] = 'html';
1168
        $docsformat['docx'] = 'docx';
1169
        $docsformat['odt'] = 'odt';
1170
        $docsformat['pdf'] = 'pdf';
1171
        $docsformat['rtf'] = 'rtf';
1172
        $docsformat['txt'] = 'txt';
1173
        core_collator::ksort($docsformat, core_collator::SORT_NATURAL);
1174
 
1175
        $mform->addElement('select', 'documentformat', get_string('docsformat', 'repository_googledocs'), $docsformat);
1176
        $mform->setDefault('documentformat', $docsformat['rtf']);
1177
        $mform->setType('documentformat', PARAM_ALPHANUM);
1178
 
1179
        // Drawing.
1180
        $drawingformat = array();
1181
        $drawingformat['jpeg'] = 'jpeg';
1182
        $drawingformat['png'] = 'png';
1183
        $drawingformat['svg'] = 'svg';
1184
        $drawingformat['pdf'] = 'pdf';
1185
        core_collator::ksort($drawingformat, core_collator::SORT_NATURAL);
1186
 
1187
        $mform->addElement('select', 'drawingformat', get_string('drawingformat', 'repository_googledocs'), $drawingformat);
1188
        $mform->setDefault('drawingformat', $drawingformat['pdf']);
1189
        $mform->setType('drawingformat', PARAM_ALPHANUM);
1190
 
1191
        // Presentation.
1192
        $presentationformat = array();
1193
        $presentationformat['pdf'] = 'pdf';
1194
        $presentationformat['pptx'] = 'pptx';
1195
        $presentationformat['txt'] = 'txt';
1196
        core_collator::ksort($presentationformat, core_collator::SORT_NATURAL);
1197
 
1198
        $str = get_string('presentationformat', 'repository_googledocs');
1199
        $mform->addElement('select', 'presentationformat', $str, $presentationformat);
1200
        $mform->setDefault('presentationformat', $presentationformat['pptx']);
1201
        $mform->setType('presentationformat', PARAM_ALPHANUM);
1202
 
1203
        // Spreadsheet.
1204
        $spreadsheetformat = array();
1205
        $spreadsheetformat['csv'] = 'csv';
1206
        $spreadsheetformat['ods'] = 'ods';
1207
        $spreadsheetformat['pdf'] = 'pdf';
1208
        $spreadsheetformat['xlsx'] = 'xlsx';
1209
        core_collator::ksort($spreadsheetformat, core_collator::SORT_NATURAL);
1210
 
1211
        $str = get_string('spreadsheetformat', 'repository_googledocs');
1212
        $mform->addElement('select', 'spreadsheetformat', $str, $spreadsheetformat);
1213
        $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']);
1214
        $mform->setType('spreadsheetformat', PARAM_ALPHANUM);
1215
    }
1216
}
1217
 
1218
/**
1219
 * Callback to get the required scopes for system account.
1220
 *
1221
 * @param \core\oauth2\issuer $issuer
1222
 * @return string
1223
 */
1224
function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) {
1225
    if ($issuer->get('id') == get_config('googledocs', 'issuerid')) {
1226
        return 'https://www.googleapis.com/auth/drive';
1227
    }
1228
    return '';
1229
}