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