ff4d1370147e06653b398429c235e5668e8cfa74
[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      * Build the breadcrumb from a path.
136      *
137      * @param string $path to create a breadcrumb from.
138      * @return array containing name and path of each crumb.
139      */
140     protected function build_breadcrumb($path) {
141         $bread = explode('/', $path);
142         $crumbtrail = '';
143         foreach ($bread as $crumb) {
144             list($id, $name) = $this->explode_node_path($crumb);
145             $name = empty($name) ? $id : $name;
146             $breadcrumb[] = array(
147                 'name' => $name,
148                 'path' => $this->build_node_path($id, $name, $crumbtrail)
149             );
150             $tmp = end($breadcrumb);
151             $crumbtrail = $tmp['path'];
152         }
153         return $breadcrumb;
154     }
156     /**
157      * Generates a safe path to a node.
158      *
159      * Typically, a node will be id|Name of the node.
160      *
161      * @param string $id of the node.
162      * @param string $name of the node, will be URL encoded.
163      * @param string $root to append the node on, must be a result of this function.
164      * @return string path to the node.
165      */
166     protected function build_node_path($id, $name = '', $root = '') {
167         $path = $id;
168         if (!empty($name)) {
169             $path .= '|' . urlencode($name);
170         }
171         if (!empty($root)) {
172             $path = trim($root, '/') . '/' . $path;
173         }
174         return $path;
175     }
177     /**
178      * Returns information about a node in a path.
179      *
180      * @see self::build_node_path()
181      * @param string $node to extrat information from.
182      * @return array about the node.
183      */
184     protected function explode_node_path($node) {
185         if (strpos($node, '|') !== false) {
186             list($id, $name) = explode('|', $node, 2);
187             $name = urldecode($name);
188         } else {
189             $id = $node;
190             $name = '';
191         }
192         $id = urldecode($id);
193         return array(
194             0 => $id,
195             1 => $name,
196             'id' => $id,
197             'name' => $name
198         );
199     }
201     /**
202      * List the files and folders.
203      *
204      * @param  string $path path to browse.
205      * @param  string $page page to browse.
206      * @return array of result.
207      */
208     public function get_listing($path='', $page = '') {
209         if (empty($path)) {
210             $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
211         }
212         if (!$this->issuer->get('enabled')) {
213             // Empty list of files for disabled repository.
214             return ['dynload' => false, 'list' => [], 'nologin' => true];
215         }
217         // We analyse the path to extract what to browse.
218         $trail = explode('/', $path);
219         $uri = array_pop($trail);
220         list($id, $name) = $this->explode_node_path($uri);
222         // Handle the special keyword 'search', which we defined in self::search() so that
223         // we could set up a breadcrumb in the search results. In any other case ID would be
224         // 'root' which is a special keyword set up by Google, or a parent (folder) ID.
225         if ($id === 'search') {
226             return $this->search($name);
227         }
229         // Query the Drive.
230         $q = "'" . str_replace("'", "\'", $id) . "' in parents";
231         $q .= ' AND trashed = false';
232         $results = $this->query($q, $path);
234         $ret = array();
235         $ret['dynload'] = true;
236         $ret['defaultreturntype'] = $this->default_returntype();
237         $ret['path'] = $this->build_breadcrumb($path);
238         $ret['list'] = $results;
239         $ret['manage'] = 'https://drive.google.com/';
241         return $ret;
242     }
244     /**
245      * Search throughout the Google Drive.
246      *
247      * @param string $searchtext text to search for.
248      * @param int $page search page.
249      * @return array of results.
250      */
251     public function search($searchtext, $page = 0) {
252         $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
253         $str = get_string('searchfor', 'repository_googledocs', $searchtext);
254         $path = $this->build_node_path('search', $str, $path);
256         // Query the Drive.
257         $q = "fullText contains '" . str_replace("'", "\'", $searchtext) . "'";
258         $q .= ' AND trashed = false';
259         $results = $this->query($q, $path);
261         $ret = array();
262         $ret['dynload'] = true;
263         $ret['path'] = $this->build_breadcrumb($path);
264         $ret['list'] = $results;
265         $ret['manage'] = 'https://drive.google.com/';
266         return $ret;
267     }
269     /**
270      * Query Google Drive for files and folders using a search query.
271      *
272      * Documentation about the query format can be found here:
273      *   https://developers.google.com/drive/search-parameters
274      *
275      * This returns a list of files and folders with their details as they should be
276      * formatted and returned by functions such as get_listing() or search().
277      *
278      * @param string $q search query as expected by the Google API.
279      * @param string $path parent path of the current files, will not be used for the query.
280      * @param int $page page.
281      * @return array of files and folders.
282      */
283     protected function query($q, $path = null, $page = 0) {
284         global $OUTPUT;
286         $files = array();
287         $folders = array();
288         $config = get_config('googledocs');
289         $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)";
290         $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive');
292         try {
293             // Retrieving files and folders.
294             $client = $this->get_user_oauth_client();
295             $service = new repository_googledocs\rest($client);
297             $response = $service->call('list', $params);
298         } catch (Exception $e) {
299             if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
300                 // This is raised when the service Drive API has not been enabled on Google APIs control panel.
301                 throw new repository_exception('servicenotenabled', 'repository_googledocs');
302             } else {
303                 throw $e;
304             }
305         }
307         $gfiles = isset($response->files) ? $response->files : array();
308         foreach ($gfiles as $gfile) {
309             if ($gfile->mimeType == 'application/vnd.google-apps.folder') {
310                 // This is a folder.
311                 $folders[$gfile->name . $gfile->id] = array(
312                     'title' => $gfile->name,
313                     'path' => $this->build_node_path($gfile->id, $gfile->name, $path),
314                     'date' => strtotime($gfile->modifiedTime),
315                     'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false),
316                     'thumbnail_height' => 64,
317                     'thumbnail_width' => 64,
318                     'children' => array()
319                 );
320             } else {
321                 // This is a file.
322                 $link = isset($gfile->webViewLink) ? $gfile->webViewLink : '';
323                 if (empty($link)) {
324                     $link = isset($gfile->webContentLink) ? $gfile->webContentLink : '';
325                 }
326                 if (isset($gfile->fileExtension)) {
327                     // The file has an extension, therefore we can download it.
328                     $source = json_encode([
329                         'id' => $gfile->id,
330                         'name' => $gfile->name,
331                         'exportformat' => 'download',
332                         'link' => $link
333                     ]);
334                     $title = $gfile->name;
335                 } else {
336                     // The file is probably a Google Doc file, we get the corresponding export link.
337                     // This should be improved by allowing the user to select the type of export they'd like.
338                     $type = str_replace('application/vnd.google-apps.', '', $gfile->mimeType);
339                     $title = '';
340                     $exporttype = '';
341                     $types = get_mimetypes_array();
343                     switch ($type){
344                         case 'document':
345                             $ext = $config->documentformat;
346                             $title = $gfile->name . '.gdoc';
347                             if ($ext === 'rtf') {
348                                 // Moodle user 'text/rtf' as the MIME type for RTF files.
349                                 // Google uses 'application/rtf' for the same type of file.
350                                 // See https://developers.google.com/drive/v3/web/manage-downloads.
351                                 $exporttype = 'application/rtf';
352                             } else {
353                                 $exporttype = $types[$ext]['type'];
354                             }
355                             break;
356                         case 'presentation':
357                             $ext = $config->presentationformat;
358                             $title = $gfile->name . '.gslides';
359                             $exporttype = $types[$ext]['type'];
360                             break;
361                         case 'spreadsheet':
362                             $ext = $config->spreadsheetformat;
363                             $title = $gfile->name . '.gsheet';
364                             $exporttype = $types[$ext]['type'];
365                             break;
366                         case 'drawing':
367                             $ext = $config->drawingformat;
368                             $title = $gfile->name . '.'. $ext;
369                             $exporttype = $types[$ext]['type'];
370                             break;
371                     }
372                     // Skips invalid/unknown types.
373                     if (empty($title)) {
374                         continue;
375                     }
376                     $source = json_encode([
377                         'id' => $gfile->id,
378                         'exportformat' => $exporttype,
379                         'link' => $link,
380                         'name' => $gfile->name
381                     ]);
382                 }
383                 // Adds the file to the file list. Using the itemId along with the name as key
384                 // of the array because Google Drive allows files with identical names.
385                 $thumb = '';
386                 if (isset($gfile->thumbnailLink)) {
387                     $thumb = $gfile->thumbnailLink;
388                 } else if (isset($gfile->iconLink)) {
389                     $thumb = $gfile->iconLink;
390                 }
391                 $files[$title . $gfile->id] = array(
392                     'title' => $title,
393                     'source' => $source,
394                     'date' => strtotime($gfile->modifiedTime),
395                     'size' => isset($gfile->size) ? $gfile->size : null,
396                     'thumbnail' => $thumb,
397                     'thumbnail_height' => 64,
398                     'thumbnail_width' => 64,
399                 );
400             }
401         }
403         // Filter and order the results.
404         $files = array_filter($files, array($this, 'filter'));
405         core_collator::ksort($files, core_collator::SORT_NATURAL);
406         core_collator::ksort($folders, core_collator::SORT_NATURAL);
407         return array_merge(array_values($folders), array_values($files));
408     }
410     /**
411      * Logout.
412      *
413      * @return string
414      */
415     public function logout() {
416         $client = $this->get_user_oauth_client();
417         $client->log_out();
418         return parent::logout();
419     }
421     /**
422      * Get a file.
423      *
424      * @param string $reference reference of the file.
425      * @param string $file name to save the file to.
426      * @return string JSON encoded array of information about the file.
427      */
428     public function get_file($reference, $filename = '') {
429         global $CFG;
431         if (!$this->issuer->get('enabled')) {
432             throw new repository_exception('cannotdownload', 'repository');
433         }
435         $source = json_decode($reference);
437         $client = null;
438         if (!empty($source->usesystem)) {
439             $client = \core\oauth2\api::get_system_oauth_client($this->issuer);
440         } else {
441             $client = $this->get_user_oauth_client();
442         }
444         $base = 'https://www.googleapis.com/drive/v3';
446         $newfilename = false;
447         if ($source->exportformat == 'download') {
448             $params = ['alt' => 'media'];
449             $sourceurl = new moodle_url($base . '/files/' . $source->id, $params);
450             $source = $sourceurl->out(false);
451         } else {
452             $params = ['mimeType' => $source->exportformat];
453             $sourceurl = new moodle_url($base . '/files/' . $source->id . '/export', $params);
454             $types = get_mimetypes_array();
455             $checktype = $source->exportformat;
456             if ($checktype == 'application/rtf') {
457                 $checktype = 'text/rtf';
458             }
459             foreach ($types as $extension => $info) {
460                 if ($info['type'] == $checktype) {
461                     $newfilename = $source->name . '.' . $extension;
462                     break;
463                 }
464             }
465             $source = $sourceurl->out(false);
466         }
468         // We use download_one and not the rest API because it has special timeouts etc.
469         $path = $this->prepare_file($filename);
470         $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
471         $success = $client->download_one($source, null, $options);
473         if ($success) {
474             @chmod($path, $CFG->filepermissions);
476             $result = [
477                 'path' => $path,
478                 'url' => $reference,
479             ];
480             if (!empty($newfilename)) {
481                 $result['newfilename'] = $newfilename;
482             }
483             return $result;
484         }
485         throw new repository_exception('cannotdownload', 'repository');
486     }
488     /**
489      * Prepare file reference information.
490      *
491      * We are using this method to clean up the source to make sure that it
492      * is a valid source.
493      *
494      * @param string $source of the file.
495      * @return string file reference.
496      */
497     public function get_file_reference($source) {
498         // We could do some magic upgrade code here.
499         return $source;
500     }
502     /**
503      * What kind of files will be in this repository?
504      *
505      * @return array return '*' means this repository support any files, otherwise
506      *               return mimetypes of files, it can be an array
507      */
508     public function supported_filetypes() {
509         return '*';
510     }
512     /**
513      * Tells how the file can be picked from this repository.
514      *
515      * @return int
516      */
517     public function supported_returntypes() {
518         // We can only support references if the system account is connected.
519         if (!empty($this->issuer) && $this->issuer->is_system_account_connected()) {
520             $setting = get_config('googledocs', 'supportedreturntypes');
521             if ($setting == 'internal') {
522                 return FILE_INTERNAL;
523             } else if ($setting == 'external') {
524                 return FILE_CONTROLLED_LINK;
525             } else {
526                 return FILE_CONTROLLED_LINK | FILE_INTERNAL;
527             }
528         } else {
529             return FILE_INTERNAL;
530         }
531     }
533     /**
534      * Which return type should be selected by default.
535      *
536      * @return int
537      */
538     public function default_returntype() {
539         $setting = get_config('googledocs', 'defaultreturntype');
540         $supported = get_config('googledocs', 'supportedreturntypes');
541         if (($setting == FILE_INTERNAL && $supported != 'external') || $supported == 'internal') {
542             return FILE_INTERNAL;
543         } else {
544             return FILE_CONTROLLED_LINK;
545         }
546     }
548     /**
549      * Return names of the general options.
550      * By default: no general option name.
551      *
552      * @return array
553      */
554     public static function get_type_option_names() {
555         return array('issuerid', 'pluginname',
556             'documentformat', 'drawingformat',
557             'presentationformat', 'spreadsheetformat',
558             'defaultreturntype', 'supportedreturntypes');
559     }
561     /**
562      * Store the access token.
563      */
564     public function callback() {
565         $client = $this->get_user_oauth_client();
566         // This will upgrade to an access token if we have an authorization code and save the access token in the session.
567         $client->is_logged_in();
568     }
570     /**
571      * Repository method to serve the referenced file
572      *
573      * @see send_stored_file
574      *
575      * @param stored_file $storedfile the file that contains the reference
576      * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
577      * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
578      * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
579      * @param array $options additional options affecting the file serving
580      */
581     public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
582         if (!$this->issuer->get('enabled')) {
583             throw new repository_exception('cannotdownload', 'repository');
584         }
586         $source = json_decode($storedfile->get_reference());
588         $fb = get_file_browser();
589         $context = context::instance_by_id($storedfile->get_contextid(), MUST_EXIST);
590         $info = $fb->get_file_info($context,
591                                    $storedfile->get_component(),
592                                    $storedfile->get_filearea(),
593                                    $storedfile->get_itemid(),
594                                    $storedfile->get_filepath(),
595                                    $storedfile->get_filename());
597         if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
598             // Add the current user as an OAuth writer.
599             $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
601             if ($systemauth === false) {
602                 $details = 'Cannot connect as system user';
603                 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
604             }
605             $systemservice = new repository_googledocs\rest($systemauth);
607             // Get the user oauth so we can get the account to add.
608             $url = moodle_url::make_pluginfile_url($storedfile->get_contextid(),
609                                                    $storedfile->get_component(),
610                                                    $storedfile->get_filearea(),
611                                                    $storedfile->get_itemid(),
612                                                    $storedfile->get_filepath(),
613                                                    $storedfile->get_filename(),
614                                                    $forcedownload);
615             $url->param('sesskey', sesskey());
616             $userauth = $this->get_user_oauth_client($url);
617             if (!$userauth->is_logged_in()) {
618                 redirect($userauth->get_login_url());
619             }
620             if ($userauth === false) {
621                 $details = 'Cannot connect as current user';
622                 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
623             }
624             $userinfo = $userauth->get_userinfo();
625             $useremail = $userinfo['email'];
627             $this->add_temp_writer_to_file($systemservice, $source->id, $useremail);
628         }
630         if (!empty($options['offline'])) {
631             $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename());
633             $filename = $storedfile->get_filename();
634             if (isset($downloaded['newfilename'])) {
635                 $filename = $downloaded['newfilename'];
636             }
637             send_file($downloaded['path'], $filename, $lifetime, $filter, false, $forcedownload, '', false, $options);
638         } else if ($source->link) {
639             // Do not use redirect() here because is not compatible with webservice/pluginfile.php.
640             header('Location: ' . $source->link);
641         } else {
642             $details = 'File is missing source link';
643             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
644         }
645     }
647     /**
648      * See if a folder exists within a folder
649      *
650      * @param \repository_googledocs\rest $client Authenticated client.
651      * @param string $foldername The folder we are looking for.
652      * @param string $parentid The parent folder we are looking in.
653      * @return string|boolean The file id if it exists or false.
654      */
655     protected function folder_exists_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) {
656         $q = '\'' . addslashes($parentid) . '\' in parents and trashed = false and name = \'' . addslashes($foldername). '\'';
657         $fields = 'files(id, name)';
658         $params = [ 'q' => $q, 'fields' => $fields];
659         $response = $client->call('list', $params);
660         $missing = true;
661         foreach ($response->files as $child) {
662             if ($child->name == $foldername) {
663                 return $child->id;
664             }
665         }
666         return false;
667     }
669     /**
670      * Create a folder within a folder
671      *
672      * @param \repository_googledocs\rest $client Authenticated client.
673      * @param string $foldername The folder we are creating.
674      * @param string $parentid The parent folder we are creating in.
675      *
676      * @return string The file id of the new folder.
677      */
678     protected function create_folder_in_folder(\repository_googledocs\rest $client, $foldername, $parentid) {
679         $fields = 'id';
680         $params = ['fields' => $fields];
681         $folder = ['mimeType' => 'application/vnd.google-apps.folder', 'name' => $foldername, 'parents' => [$parentid]];
682         $created = $client->call('create', $params, json_encode($folder));
683         if (empty($created->id)) {
684             $details = 'Cannot create folder:' . $foldername;
685             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
686         }
687         return $created->id;
688     }
690     /**
691      * Get simple file info for humans.
692      *
693      * @param \repository_googledocs\rest $client Authenticated client.
694      * @param string $fileid The file we are querying.
695      *
696      * @return stdClass
697      */
698     protected function get_file_summary(\repository_googledocs\rest $client, $fileid) {
699         $fields = "id,name,owners,parents";
700         $params = [
701             'fileid' => $fileid,
702             'fields' => $fields
703         ];
704         return $client->call('get', $params);
705     }
707     /**
708      * Copy a file and return the new file details. A side effect of the copy
709      * is that the owner will be the account authenticated with this oauth client.
710      *
711      * @param \repository_googledocs\rest $client Authenticated client.
712      * @param string $fileid The file we are copying.
713      * @param string $name The original filename (don't change it).
714      *
715      * @return stdClass file details.
716      */
717     protected function copy_file(\repository_googledocs\rest $client, $fileid, $name) {
718         $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink";
719         $params = [
720             'fileid' => $fileid,
721             'fields' => $fields,
722         ];
723         // Keep the original name (don't put copy at the end of it).
724         $copyinfo = [];
725         if (!empty($name)) {
726             $copyinfo = [ 'name' => $name ];
727         }
728         $fileinfo = $client->call('copy', $params, json_encode($copyinfo));
729         if (empty($fileinfo->id)) {
730             $details = 'Cannot copy file:' . $fileid;
731             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
732         }
733         return $fileinfo;
734     }
736     /**
737      * Add a writer to the permissions on the file (temporary).
738      *
739      * @param \repository_googledocs\rest $client Authenticated client.
740      * @param string $fileid The file we are updating.
741      * @param string $email The email of the writer account to add.
742      * @return boolean
743      */
744     protected function add_temp_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
745         // Expires in 7 days.
746         $expires = new DateTime();
747         $expires->add(new DateInterval("P7D"));
749         $updateeditor = [
750             'emailAddress' => $email,
751             'role' => 'writer',
752             'type' => 'user',
753             'expirationTime' => $expires->format(DateTime::RFC3339)
754         ];
755         $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false'];
756         $response = $client->call('create_permission', $params, json_encode($updateeditor));
757         if (empty($response->id)) {
758             $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid;
759             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
760         }
761         return true;
762     }
765     /**
766      * Add a writer to the permissions on the file.
767      *
768      * @param \repository_googledocs\rest $client Authenticated client.
769      * @param string $fileid The file we are updating.
770      * @param string $email The email of the writer account to add.
771      * @return boolean
772      */
773     protected function add_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
774         $updateeditor = [
775             'emailAddress' => $email,
776             'role' => 'writer',
777             'type' => 'user'
778         ];
779         $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false'];
780         $response = $client->call('create_permission', $params, json_encode($updateeditor));
781         if (empty($response->id)) {
782             $details = 'Cannot add user ' . $email . ' as a writer for document: ' . $fileid;
783             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
784         }
785         return true;
786     }
788     /**
789      * Move from root to folder
790      *
791      * @param \repository_googledocs\rest $client Authenticated client.
792      * @param string $fileid The file we are updating.
793      * @param string $folderid The id of the folder we are moving to
794      * @return boolean
795      */
796     protected function move_file_from_root_to_folder(\repository_googledocs\rest $client, $fileid, $folderid) {
797         // Set the parent.
798         $params = [
799             'fileid' => $fileid, 'addParents' => $folderid, 'removeParents' => 'root'
800         ];
801         $response = $client->call('update', $params, ' ');
802         if (empty($response->id)) {
803             $details = 'Cannot move the file to a folder: ' . $fileid;
804             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
805         }
806         return true;
807     }
809     /**
810      * Prevent writers from sharing.
811      *
812      * @param \repository_googledocs\rest $client Authenticated client.
813      * @param string $fileid The file we are updating.
814      * @return boolean
815      */
816     protected function prevent_writers_from_sharing_file(\repository_googledocs\rest $client, $fileid) {
817         // We don't want anyone but Moodle to change the sharing settings.
818         $params = [
819             'fileid' => $fileid
820         ];
821         $update = [
822             'writersCanShare' => false
823         ];
824         $response = $client->call('update', $params, json_encode($update));
825         if (empty($response->id)) {
826             $details = 'Cannot prevent writers from sharing document: ' . $fileid;
827             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
828         }
829         return true;
830     }
832     /**
833      * Allow anyone with the link to read the file.
834      *
835      * @param \repository_googledocs\rest $client Authenticated client.
836      * @param string $fileid The file we are updating.
837      * @return boolean
838      */
839     protected function set_file_sharing_anyone_with_link_can_read(\repository_googledocs\rest $client, $fileid) {
840         $updateread = [
841             'type' => 'anyone',
842             'role' => 'reader',
843             'allowFileDiscovery' => 'false'
844         ];
845         $params = ['fileid' => $fileid];
846         $response = $client->call('create_permission', $params, json_encode($updateread));
847         if (empty($response->id) || $response->id != 'anyoneWithLink') {
848             $details = 'Cannot update link sharing for the document: ' . $fileid;
849             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
850         }
851         return true;
852     }
854     /**
855      * Called when a file is selected as a "link".
856      * Invoked at MOODLE/repository/repository_ajax.php
857      *
858      * This is called at the point the reference files are being copied from the draft area to the real area
859      * (when the file has really really been selected.
860      *
861      * @param string $reference this reference is generated by
862      *                          repository::get_file_reference()
863      * @param context $context the target context for this new file.
864      * @param string $component the target component for this new file.
865      * @param string $filearea the target filearea for this new file.
866      * @param string $itemid the target itemid for this new file.
867      * @return string updated reference (final one before it's saved to db).
868      */
869     public function reference_file_selected($reference, $context, $component, $filearea, $itemid) {
870         // What we need to do here is transfer ownership to the system user (or copy)
871         // then set the permissions so anyone with the share link can view,
872         // finally update the reference to contain the share link if it was not
873         // already there (and point to new file id if we copied).
876         // Check this issuer is enabled.
877         if ($this->disabled) {
878             throw new repository_exception('cannotdownload', 'repository');
879         }
881         // Get a system oauth client and a user oauth client.
882         $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
884         if ($systemauth === false) {
885             $details = 'Cannot connect as system user';
886             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
887         }
888         // Get the system user email so we can share the file with this user.
889         $systemuserinfo = $systemauth->get_userinfo();
890         $systemuseremail = $systemuserinfo['email'];
892         $userauth = $this->get_user_oauth_client();
893         if ($userauth === false) {
894             $details = 'Cannot connect as current user';
895             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
896         }
898         // Get the details from the reference.
899         $source = json_decode($reference);
900         $userservice = new repository_googledocs\rest($userauth);
901         $systemservice = new repository_googledocs\rest($systemauth);
903         // Add Moodle as writer.
904         $this->add_writer_to_file($userservice, $source->id, $systemuseremail);
906         // Now move it to a sensible folder.
907         $contextlist = array_reverse($context->get_parent_contexts(true));
909         $cache = cache::make('repository_googledocs', 'folder');
910         $parentid = 'root';
911         $fullpath = 'root';
912         $allfolders = [];
913         foreach ($contextlist as $context) {
914             // Make sure a folder exists here.
915             $foldername = clean_param($context->get_context_name(), PARAM_PATH);
916             $allfolders[] = $foldername;
917         }
919         $allfolders[] = clean_param($component, PARAM_PATH);
920         $allfolders[] = clean_param($filearea, PARAM_PATH);
921         $allfolders[] = clean_param($itemid, PARAM_PATH);
923         // Variable $allfolders is the full path we want to put the file in - so walk it and create each folder.
925         foreach ($allfolders as $foldername) {
926             // Make sure a folder exists here.
927             $fullpath .= '/' . $foldername;
929             $folderid = $cache->get($fullpath);
930             if (empty($folderid)) {
931                 $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid);
932             }
933             if ($folderid !== false) {
934                 $cache->set($fullpath, $folderid);
935                 $parentid = $folderid;
936             } else {
937                 // Create it.
938                 $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid);
939                 $cache->set($fullpath, $parentid);
940             }
941         }
943         // Copy the file so we get a snapshot file owned by Moodle.
944         $newsource = $this->copy_file($systemservice, $source->id, $source->name);
945         // Move the copied file to the correct folder.
946         $this->move_file_from_root_to_folder($systemservice, $newsource->id, $parentid);
948         // Set the sharing options.
949         $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id);
950         $this->prevent_writers_from_sharing_file($systemservice, $newsource->id);
952         // Update the returned reference so that the stored_file in moodle points to the newly copied file.
953         $source->id = $newsource->id;
954         $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : '';
955         $source->usesystem = true;
956         if (empty($source->link)) {
957             $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : '';
958         }
959         $reference = json_encode($source);
961         return $reference;
962     }
964     /**
965      * Get human readable file info from a the reference.
966      *
967      * @param string $reference
968      * @param int $filestatus
969      */
970     public function get_reference_details($reference, $filestatus = 0) {
971         if ($this->disabled) {
972             throw new repository_exception('cannotdownload', 'repository');
973         }
974         if (empty($reference)) {
975             return get_string('unknownsource', 'repository');
976         }
977         $source = json_decode($reference);
978         if (empty($source->usesystem)) {
979             return '';
980         }
981         $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
983         if ($systemauth === false) {
984             return '';
985         }
986         $systemservice = new repository_googledocs\rest($systemauth);
987         $info = $this->get_file_summary($systemservice, $source->id);
989         $owner = '';
990         if (!empty($info->owners[0]->displayName)) {
991             $owner = $info->owners[0]->displayName;
992         }
993         if ($owner) {
994             return get_string('owner', 'repository_googledocs', $owner);
995         } else {
996             return $info->name;
997         }
998     }
1000     /**
1001      * Edit/Create Admin Settings Moodle form.
1002      *
1003      * @param moodleform $mform Moodle form (passed by reference).
1004      * @param string $classname repository class name.
1005      */
1006     public static function type_config_form($mform, $classname = 'repository') {
1007         $url = new moodle_url('/admin/tool/oauth2/issuers.php');
1008         $url = $url->out();
1010         $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url));
1012         parent::type_config_form($mform);
1013         $options = [];
1014         $issuers = \core\oauth2\api::get_all_issuers();
1016         foreach ($issuers as $issuer) {
1017             $options[$issuer->get('id')] = s($issuer->get('name'));
1018         }
1020         $strrequired = get_string('required');
1022         $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_googledocs'), $options);
1023         $mform->addHelpButton('issuerid', 'issuer', 'repository_googledocs');
1024         $mform->addRule('issuerid', $strrequired, 'required', null, 'client');
1026         $mform->addElement('static', null, '', get_string('fileoptions', 'repository_googledocs'));
1027         $choices = [
1028             'internal' => get_string('internal', 'repository_googledocs'),
1029             'external' => get_string('external', 'repository_googledocs'),
1030             'both' => get_string('both', 'repository_googledocs')
1031         ];
1032         $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_googledocs'), $choices);
1034         $choices = [
1035             FILE_INTERNAL => get_string('internal', 'repository_googledocs'),
1036             FILE_CONTROLLED_LINK => get_string('external', 'repository_googledocs'),
1037         ];
1038         $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices);
1040         $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs'));
1042         // Documents.
1043         $docsformat = array();
1044         $docsformat['html'] = 'html';
1045         $docsformat['docx'] = 'docx';
1046         $docsformat['odt'] = 'odt';
1047         $docsformat['pdf'] = 'pdf';
1048         $docsformat['rtf'] = 'rtf';
1049         $docsformat['txt'] = 'txt';
1050         core_collator::ksort($docsformat, core_collator::SORT_NATURAL);
1052         $mform->addElement('select', 'documentformat', get_string('docsformat', 'repository_googledocs'), $docsformat);
1053         $mform->setDefault('documentformat', $docsformat['rtf']);
1054         $mform->setType('documentformat', PARAM_ALPHANUM);
1056         // Drawing.
1057         $drawingformat = array();
1058         $drawingformat['jpeg'] = 'jpeg';
1059         $drawingformat['png'] = 'png';
1060         $drawingformat['svg'] = 'svg';
1061         $drawingformat['pdf'] = 'pdf';
1062         core_collator::ksort($drawingformat, core_collator::SORT_NATURAL);
1064         $mform->addElement('select', 'drawingformat', get_string('drawingformat', 'repository_googledocs'), $drawingformat);
1065         $mform->setDefault('drawingformat', $drawingformat['pdf']);
1066         $mform->setType('drawingformat', PARAM_ALPHANUM);
1068         // Presentation.
1069         $presentationformat = array();
1070         $presentationformat['pdf'] = 'pdf';
1071         $presentationformat['pptx'] = 'pptx';
1072         $presentationformat['txt'] = 'txt';
1073         core_collator::ksort($presentationformat, core_collator::SORT_NATURAL);
1075         $str = get_string('presentationformat', 'repository_googledocs');
1076         $mform->addElement('select', 'presentationformat', $str, $presentationformat);
1077         $mform->setDefault('presentationformat', $presentationformat['pptx']);
1078         $mform->setType('presentationformat', PARAM_ALPHANUM);
1080         // Spreadsheet.
1081         $spreadsheetformat = array();
1082         $spreadsheetformat['csv'] = 'csv';
1083         $spreadsheetformat['ods'] = 'ods';
1084         $spreadsheetformat['pdf'] = 'pdf';
1085         $spreadsheetformat['xlsx'] = 'xlsx';
1086         core_collator::ksort($spreadsheetformat, core_collator::SORT_NATURAL);
1088         $str = get_string('spreadsheetformat', 'repository_googledocs');
1089         $mform->addElement('select', 'spreadsheetformat', $str, $spreadsheetformat);
1090         $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']);
1091         $mform->setType('spreadsheetformat', PARAM_ALPHANUM);
1092     }
1095 /**
1096  * Callback to get the required scopes for system account.
1097  *
1098  * @param \core\oauth2\issuer $issuer
1099  * @return string
1100  */
1101 function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) {
1102     if ($issuer->get('id') == get_config('googledocs', 'issuerid')) {
1103         return 'https://www.googleapis.com/auth/drive';
1104     }
1105     return '';