MDL-59505 oauth2: Fix storage of access controlled links
[moodle.git] / repository / googledocs / lib.php
CommitLineData
6667f9e4 1<?php
10d53fd3
DC
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
6667f9e4 17/**
3425813f 18 * This plugin is used to access Google Drive.
6667f9e4 19 *
5bcfd504 20 * @since Moodle 2.0
67233725 21 * @package repository_googledocs
61506a0a 22 * @copyright 2009 Dan Poltawski <talktodan@gmail.com>
d078f6d3 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6667f9e4 24 */
3425813f
FM
25
26defined('MOODLE_INTERNAL') || die();
27
67233725 28require_once($CFG->dirroot . '/repository/lib.php');
151b0f94 29require_once($CFG->libdir . '/filebrowser/file_browser.php');
6667f9e4 30
67233725
DC
31/**
32 * Google Docs Plugin
33 *
5bcfd504 34 * @since Moodle 2.0
67233725
DC
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 */
6667f9e4 39class repository_googledocs extends repository {
6667f9e4 40
3425813f 41 /**
0e59638b
DW
42 * OAuth 2 client
43 * @var \core\oauth2\client
3425813f
FM
44 */
45 private $client = null;
6667f9e4 46
3425813f 47 /**
0e59638b
DW
48 * OAuth 2 Issuer
49 * @var \core\oauth2\issuer
3425813f 50 */
0e59638b 51 private $issuer = null;
3425813f
FM
52
53 /**
0e59638b 54 * Additional scopes required for drive.
3425813f 55 */
0e59638b 56 const SCOPES = 'https://www.googleapis.com/auth/drive';
3425813f
FM
57
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);
69
5afb4f0e
DP
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 }
33536fb2
DW
75
76 if ($this->issuer && !$this->issuer->get('enabled')) {
77 $this->disabled = true;
78 }
6667f9e4 79 }
80
3425813f 81 /**
0e59638b 82 * Get a cached user authenticated oauth client.
3425813f 83 *
72fd103a 84 * @param moodle_url $overrideurl - Use this url instead of the repo callback.
0e59638b 85 * @return \core\oauth2\client
3425813f 86 */
151b0f94 87 protected function get_user_oauth_client($overrideurl = false) {
0e59638b
DW
88 if ($this->client) {
89 return $this->client;
3425813f 90 }
151b0f94
DW
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 }
3425813f 99
0e59638b 100 $this->client = \core\oauth2\api::get_user_oauth_client($this->issuer, $returnurl, self::SCOPES);
3425813f 101
0e59638b 102 return $this->client;
3425813f
FM
103 }
104
105 /**
106 * Checks whether the user is authenticate or not.
107 *
108 * @return bool true when logged in.
109 */
6667f9e4 110 public function check_login() {
0e59638b
DW
111 $client = $this->get_user_oauth_client();
112 return $client->is_logged_in();
6667f9e4 113 }
114
3425813f
FM
115 /**
116 * Print or return the login form.
117 *
118 * @return void|array for ajax.
119 */
4560fd1b 120 public function print_login() {
0e59638b
DW
121 $client = $this->get_user_oauth_client();
122 $url = $client->get_login_url();
4560fd1b
DP
123
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>';
6667f9e4 131 }
132 }
133
fd8790d5
SL
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;
141
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);
146
147 $PAGE->set_pagelayout('embedded');
148 echo $OUTPUT->header();
149
150 $repositoryname = get_string('pluginname', 'repository_googledocs');
151
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);
157
158 echo $OUTPUT->footer();
159 }
160
3425813f 161 /**
0e59638b
DW
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 */
3425813f
FM
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 }
182
183 /**
0e59638b
DW
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 */
3425813f
FM
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 }
203
204 /**
0e59638b
DW
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 */
3425813f
FM
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 }
227
3425813f
FM
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 */
6667f9e4 235 public function get_listing($path='', $page = '') {
3425813f
FM
236 if (empty($path)) {
237 $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
238 }
eca128bf
DW
239 if (!$this->issuer->get('enabled')) {
240 // Empty list of files for disabled repository.
241 return ['dynload' => false, 'list' => [], 'nologin' => true];
242 }
3425813f
FM
243
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);
248
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 }
255
256 // Query the Drive.
257 $q = "'" . str_replace("'", "\'", $id) . "' in parents";
258 $q .= ' AND trashed = false';
259 $results = $this->query($q, $path);
6667f9e4 260
261 $ret = array();
262 $ret['dynload'] = true;
989e14fe 263 $ret['defaultreturntype'] = $this->default_returntype();
3425813f
FM
264 $ret['path'] = $this->build_breadcrumb($path);
265 $ret['list'] = $results;
5823a27e
DW
266 $ret['manage'] = 'https://drive.google.com/';
267
6667f9e4 268 return $ret;
269 }
270
3425813f
FM
271 /**
272 * Search throughout the Google Drive.
273 *
0e59638b 274 * @param string $searchtext text to search for.
3425813f
FM
275 * @param int $page search page.
276 * @return array of results.
277 */
0e59638b 278 public function search($searchtext, $page = 0) {
3425813f 279 $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
0e59638b
DW
280 $str = get_string('searchfor', 'repository_googledocs', $searchtext);
281 $path = $this->build_node_path('search', $str, $path);
3425813f
FM
282
283 // Query the Drive.
0e59638b 284 $q = "fullText contains '" . str_replace("'", "\'", $searchtext) . "'";
3425813f
FM
285 $q .= ' AND trashed = false';
286 $results = $this->query($q, $path);
6667f9e4 287
288 $ret = array();
289 $ret['dynload'] = true;
3425813f
FM
290 $ret['path'] = $this->build_breadcrumb($path);
291 $ret['list'] = $results;
5823a27e 292 $ret['manage'] = 'https://drive.google.com/';
6667f9e4 293 return $ret;
294 }
295
3425813f
FM
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;
312
313 $files = array();
314 $folders = array();
15a5c4b2 315 $config = get_config('googledocs');
989e14fe 316 $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)";
0e59638b 317 $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive');
3425813f
FM
318
319 try {
320 // Retrieving files and folders.
0e59638b
DW
321 $client = $this->get_user_oauth_client();
322 $service = new repository_googledocs\rest($client);
323
324 $response = $service->call('list', $params);
325 } catch (Exception $e) {
3425813f
FM
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 }
333
0e59638b
DW
334 $gfiles = isset($response->files) ? $response->files : array();
335 foreach ($gfiles as $gfile) {
336 if ($gfile->mimeType == 'application/vnd.google-apps.folder') {
3425813f 337 // This is a folder.
0e59638b
DW
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),
663640f5 342 'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false),
3425813f
FM
343 'thumbnail_height' => 64,
344 'thumbnail_width' => 64,
345 'children' => array()
346 );
347 } else {
348 // This is a file.
8ece1d70
DW
349 $link = isset($gfile->webViewLink) ? $gfile->webViewLink : '';
350 if (empty($link)) {
351 $link = isset($gfile->webContentLink) ? $gfile->webContentLink : '';
352 }
0e59638b
DW
353 if (isset($gfile->fileExtension)) {
354 // The file has an extension, therefore we can download it.
6da1c55b
DW
355 $source = json_encode([
356 'id' => $gfile->id,
357 'name' => $gfile->name,
358 'exportformat' => 'download',
72643dc6 359 'link' => $link
6da1c55b 360 ]);
0e59638b 361 $title = $gfile->name;
3425813f
FM
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.
0e59638b 365 $type = str_replace('application/vnd.google-apps.', '', $gfile->mimeType);
3425813f 366 $title = '';
0e59638b 367 $exporttype = '';
15a5c4b2
SB
368 $types = get_mimetypes_array();
369
3425813f
FM
370 switch ($type){
371 case 'document':
15a5c4b2 372 $ext = $config->documentformat;
af28b228 373 $title = $gfile->name . '.gdoc';
15a5c4b2
SB
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.
0e59638b 378 $exporttype = 'application/rtf';
15a5c4b2 379 } else {
0e59638b 380 $exporttype = $types[$ext]['type'];
15a5c4b2 381 }
3425813f
FM
382 break;
383 case 'presentation':
15a5c4b2 384 $ext = $config->presentationformat;
af28b228 385 $title = $gfile->name . '.gslides';
0e59638b 386 $exporttype = $types[$ext]['type'];
3425813f
FM
387 break;
388 case 'spreadsheet':
15a5c4b2 389 $ext = $config->spreadsheetformat;
af28b228 390 $title = $gfile->name . '.gsheet';
0e59638b 391 $exporttype = $types[$ext]['type'];
15a5c4b2
SB
392 break;
393 case 'drawing':
394 $ext = $config->drawingformat;
989e14fe 395 $title = $gfile->name . '.'. $ext;
0e59638b 396 $exporttype = $types[$ext]['type'];
3425813f
FM
397 break;
398 }
399 // Skips invalid/unknown types.
0e59638b 400 if (empty($title)) {
3425813f
FM
401 continue;
402 }
6da1c55b
DW
403 $source = json_encode([
404 'id' => $gfile->id,
405 'exportformat' => $exporttype,
406 'link' => $link,
72643dc6 407 'name' => $gfile->name
6da1c55b 408 ]);
3425813f 409 }
0e59638b 410 // Adds the file to the file list. Using the itemId along with the name as key
3425813f 411 // of the array because Google Drive allows files with identical names.
0e59638b
DW
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(
3425813f
FM
419 'title' => $title,
420 'source' => $source,
0e59638b
DW
421 'date' => strtotime($gfile->modifiedTime),
422 'size' => isset($gfile->size) ? $gfile->size : null,
423 'thumbnail' => $thumb,
3425813f
FM
424 'thumbnail_height' => 64,
425 'thumbnail_width' => 64,
3425813f 426 );
3425813f
FM
427 }
428 }
429
430 // Filter and order the results.
431 $files = array_filter($files, array($this, 'filter'));
2f1e464a
PS
432 core_collator::ksort($files, core_collator::SORT_NATURAL);
433 core_collator::ksort($folders, core_collator::SORT_NATURAL);
3425813f
FM
434 return array_merge(array_values($folders), array_values($files));
435 }
436
437 /**
438 * Logout.
439 *
440 * @return string
441 */
4560fd1b 442 public function logout() {
0e59638b
DW
443 $client = $this->get_user_oauth_client();
444 $client->log_out();
6667f9e4 445 return parent::logout();
446 }
447
3425813f
FM
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 = '') {
eb459f71
PS
456 global $CFG;
457
eca128bf
DW
458 if (!$this->issuer->get('enabled')) {
459 throw new repository_exception('cannotdownload', 'repository');
460 }
461
989e14fe
DW
462 $source = json_decode($reference);
463
00879eca
DW
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 }
470
471 $base = 'https://www.googleapis.com/drive/v3';
472
af28b228 473 $newfilename = false;
989e14fe
DW
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);
af28b228
DW
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 }
989e14fe
DW
492 $source = $sourceurl->out(false);
493 }
494
495 // We use download_one and not the rest API because it has special timeouts etc.
0e59638b
DW
496 $path = $this->prepare_file($filename);
497 $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
af28b228 498 $success = $client->download_one($source, null, $options);
0e59638b 499
af28b228 500 if ($success) {
0e59638b 501 @chmod($path, $CFG->filepermissions);
af28b228
DW
502
503 $result = [
0e59638b 504 'path' => $path,
af28b228
DW
505 'url' => $reference,
506 ];
507 if (!empty($newfilename)) {
508 $result['newfilename'] = $newfilename;
509 }
510 return $result;
ec7e998f 511 }
3425813f
FM
512 throw new repository_exception('cannotdownload', 'repository');
513 }
514
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) {
989e14fe
DW
525 // We could do some magic upgrade code here.
526 return $source;
41076c58 527 }
6667f9e4 528
3425813f
FM
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 */
41076c58 535 public function supported_filetypes() {
4560fd1b 536 return '*';
41076c58 537 }
3425813f
FM
538
539 /**
540 * Tells how the file can be picked from this repository.
541 *
3425813f
FM
542 * @return int
543 */
41076c58 544 public function supported_returntypes() {
989e14fe
DW
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') {
151b0f94 551 return FILE_CONTROLLED_LINK;
989e14fe 552 } else {
151b0f94 553 return FILE_CONTROLLED_LINK | FILE_INTERNAL;
989e14fe
DW
554 }
555 } else {
556 return FILE_INTERNAL;
557 }
558 }
559
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 {
151b0f94 571 return FILE_CONTROLLED_LINK;
989e14fe 572 }
41076c58 573 }
4560fd1b 574
3425813f
FM
575 /**
576 * Return names of the general options.
577 * By default: no general option name.
578 *
579 * @return array
580 */
4560fd1b 581 public static function get_type_option_names() {
0e59638b 582 return array('issuerid', 'pluginname',
15a5c4b2 583 'documentformat', 'drawingformat',
989e14fe
DW
584 'presentationformat', 'spreadsheetformat',
585 'defaultreturntype', 'supportedreturntypes');
4560fd1b
DP
586 }
587
0e59638b
DW
588 /**
589 * Store the access token.
590 */
591 public function callback() {
592 $client = $this->get_user_oauth_client();
989e14fe 593 // This will upgrade to an access token if we have an authorization code and save the access token in the session.
0e59638b
DW
594 $client->is_logged_in();
595 }
596
989e14fe
DW
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) {
eca128bf
DW
609 if (!$this->issuer->get('enabled')) {
610 throw new repository_exception('cannotdownload', 'repository');
611 }
612
989e14fe
DW
613 $source = json_decode($storedfile->get_reference());
614
151b0f94
DW
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());
623
00879eca 624 if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
151b0f94
DW
625 // Add the current user as an OAuth writer.
626 $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
627
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);
633
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());
fd8790d5
SL
643 $param = ($options['embed'] == true) ? false : $url;
644 $userauth = $this->get_user_oauth_client($param);
151b0f94 645 if (!$userauth->is_logged_in()) {
fd8790d5
SL
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 }
151b0f94
DW
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'];
660
661 $this->add_temp_writer_to_file($systemservice, $source->id, $useremail);
662 }
663
d5bb9f1f
DW
664 if (!empty($options['offline'])) {
665 $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename());
666
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) {
fcdd7730
JL
673 // Do not use redirect() here because is not compatible with webservice/pluginfile.php.
674 header('Location: ' . $source->link);
989e14fe
DW
675 } else {
676 $details = 'File is missing source link';
677 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
678 }
679 }
680
989e14fe
DW
681 /**
682 * See if a folder exists within a folder
683 *
72fd103a 684 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
685 * @param string $foldername The folder we are looking for.
686 * @param string $parentid The parent folder we are looking in.
989e14fe
DW
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 }
702
703 /**
704 * Create a folder within a folder
705 *
72fd103a 706 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
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 }
723
8ece1d70
DW
724 /**
725 * Get simple file info for humans.
726 *
72fd103a 727 * @param \repository_googledocs\rest $client Authenticated client.
8ece1d70
DW
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) {
151b0f94 733 $fields = "id,name,owners,parents";
8ece1d70
DW
734 $params = [
735 'fileid' => $fileid,
736 'fields' => $fields
737 ];
738 return $client->call('get', $params);
739 }
740
989e14fe
DW
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 *
72fd103a 745 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe 746 * @param string $fileid The file we are copying.
6da1c55b 747 * @param string $name The original filename (don't change it).
989e14fe
DW
748 *
749 * @return stdClass file details.
750 */
6da1c55b 751 protected function copy_file(\repository_googledocs\rest $client, $fileid, $name) {
989e14fe
DW
752 $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink";
753 $params = [
754 'fileid' => $fileid,
6da1c55b 755 'fields' => $fields,
989e14fe 756 ];
6da1c55b
DW
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));
989e14fe
DW
763 if (empty($fileinfo->id)) {
764 $details = 'Cannot copy file:' . $fileid;
765 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
766 }
767 return $fileinfo;
768 }
769
151b0f94
DW
770 /**
771 * Add a writer to the permissions on the file (temporary).
772 *
72fd103a 773 * @param \repository_googledocs\rest $client Authenticated client.
151b0f94
DW
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 */
72fd103a 778 protected function add_temp_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
151b0f94
DW
779 // Expires in 7 days.
780 $expires = new DateTime();
781 $expires->add(new DateInterval("P7D"));
782
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 }
797
798
989e14fe
DW
799 /**
800 * Add a writer to the permissions on the file.
801 *
72fd103a 802 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
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 */
72fd103a 807 protected function add_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
989e14fe
DW
808 $updateeditor = [
809 'emailAddress' => $email,
810 'role' => 'writer',
811 'type' => 'user'
812 ];
8ece1d70 813 $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false'];
989e14fe
DW
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 }
821
822 /**
823 * Move from root to folder
824 *
72fd103a 825 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
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 */
72fd103a 830 protected function move_file_from_root_to_folder(\repository_googledocs\rest $client, $fileid, $folderid) {
989e14fe
DW
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 }
842
843 /**
844 * Prevent writers from sharing.
845 *
72fd103a 846 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
847 * @param string $fileid The file we are updating.
848 * @return boolean
849 */
72fd103a 850 protected function prevent_writers_from_sharing_file(\repository_googledocs\rest $client, $fileid) {
989e14fe
DW
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 }
865
866 /**
867 * Allow anyone with the link to read the file.
868 *
72fd103a 869 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
870 * @param string $fileid The file we are updating.
871 * @return boolean
872 */
72fd103a 873 protected function set_file_sharing_anyone_with_link_can_read(\repository_googledocs\rest $client, $fileid) {
989e14fe
DW
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 }
887
888 /**
889 * Called when a file is selected as a "link".
890 * Invoked at MOODLE/repository/repository_ajax.php
891 *
eb47ad4a
DW
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 *
989e14fe
DW
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.
72643dc6
DW
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).
989e14fe 902 */
72643dc6 903 public function reference_file_selected($reference, $context, $component, $filearea, $itemid) {
e0a90324
DM
904 global $CFG, $SITE;
905
989e14fe
DW
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).
eb47ad4a 910
6a7552fe
DW
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 }
eb47ad4a
DW
917
918 // Check this issuer is enabled.
33536fb2 919 if ($this->disabled) {
eb47ad4a
DW
920 throw new repository_exception('cannotdownload', 'repository');
921 }
922
923 // Get a system oauth client and a user oauth client.
989e14fe
DW
924 $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
925
926 if ($systemauth === false) {
927 $details = 'Cannot connect as system user';
928 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
929 }
eb47ad4a 930 // Get the system user email so we can share the file with this user.
989e14fe
DW
931 $systemuserinfo = $systemauth->get_userinfo();
932 $systemuseremail = $systemuserinfo['email'];
933
989e14fe
DW
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 }
989e14fe
DW
939
940 $userservice = new repository_googledocs\rest($userauth);
941 $systemservice = new repository_googledocs\rest($systemauth);
942
8ece1d70
DW
943 // Add Moodle as writer.
944 $this->add_writer_to_file($userservice, $source->id, $systemuseremail);
945
989e14fe
DW
946 // Now move it to a sensible folder.
947 $contextlist = array_reverse($context->get_parent_contexts(true));
948
39f60f6c 949 $cache = cache::make('repository_googledocs', 'folder');
989e14fe 950 $parentid = 'root';
39f60f6c 951 $fullpath = 'root';
72643dc6 952 $allfolders = [];
989e14fe 953 foreach ($contextlist as $context) {
e0a90324
DM
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);
958
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 }
969
970 $foldername = clean_param($foldername, PARAM_PATH);
72643dc6
DW
971 $allfolders[] = $foldername;
972 }
973
974 $allfolders[] = clean_param($component, PARAM_PATH);
975 $allfolders[] = clean_param($filearea, PARAM_PATH);
976 $allfolders[] = clean_param($itemid, PARAM_PATH);
977
eb47ad4a
DW
978 // Variable $allfolders is the full path we want to put the file in - so walk it and create each folder.
979
72643dc6
DW
980 foreach ($allfolders as $foldername) {
981 // Make sure a folder exists here.
39f60f6c 982 $fullpath .= '/' . $foldername;
8ece1d70 983
72643dc6 984 $folderid = $cache->get($fullpath);
39f60f6c
DW
985 if (empty($folderid)) {
986 $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid);
987 }
989e14fe 988 if ($folderid !== false) {
39f60f6c 989 $cache->set($fullpath, $folderid);
989e14fe
DW
990 $parentid = $folderid;
991 } else {
992 // Create it.
993 $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid);
39f60f6c 994 $cache->set($fullpath, $parentid);
989e14fe
DW
995 }
996 }
997
72643dc6
DW
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);
989e14fe 1002
72643dc6
DW
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);
1006
eb47ad4a 1007 // Update the returned reference so that the stored_file in moodle points to the newly copied file.
72643dc6
DW
1008 $source->id = $newsource->id;
1009 $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : '';
00879eca 1010 $source->usesystem = true;
72643dc6
DW
1011 if (empty($source->link)) {
1012 $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : '';
8ece1d70 1013 }
72643dc6 1014 $reference = json_encode($source);
989e14fe 1015
8ece1d70
DW
1016 return $reference;
1017 }
989e14fe 1018
8ece1d70
DW
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) {
33536fb2 1026 if ($this->disabled) {
eca128bf
DW
1027 throw new repository_exception('cannotdownload', 'repository');
1028 }
8ece1d70
DW
1029 if (empty($reference)) {
1030 return get_string('unknownsource', 'repository');
989e14fe 1031 }
8ece1d70 1032 $source = json_decode($reference);
00879eca
DW
1033 if (empty($source->usesystem)) {
1034 return '';
1035 }
8ece1d70 1036 $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
989e14fe 1037
8ece1d70
DW
1038 if ($systemauth === false) {
1039 return '';
989e14fe 1040 }
8ece1d70
DW
1041 $systemservice = new repository_googledocs\rest($systemauth);
1042 $info = $this->get_file_summary($systemservice, $source->id);
989e14fe 1043
8ece1d70
DW
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;
989e14fe
DW
1052 }
1053 }
1054
3425813f
FM
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 */
4560fd1b 1061 public static function type_config_form($mform, $classname = 'repository') {
989e14fe
DW
1062 $url = new moodle_url('/admin/tool/oauth2/issuers.php');
1063 $url = $url->out();
3425813f 1064
0e59638b 1065 $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url));
4560fd1b
DP
1066
1067 parent::type_config_form($mform);
0e59638b
DW
1068 $options = [];
1069 $issuers = \core\oauth2\api::get_all_issuers();
1070
1071 foreach ($issuers as $issuer) {
1072 $options[$issuer->get('id')] = s($issuer->get('name'));
1073 }
989e14fe
DW
1074
1075 $strrequired = get_string('required');
1076
0e59638b
DW
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');
4560fd1b 1080
989e14fe
DW
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);
1088
1089 $choices = [
1090 FILE_INTERNAL => get_string('internal', 'repository_googledocs'),
151b0f94 1091 FILE_CONTROLLED_LINK => get_string('external', 'repository_googledocs'),
989e14fe
DW
1092 ];
1093 $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices);
15a5c4b2 1094
0e59638b 1095 $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs'));
15a5c4b2
SB
1096
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);
1106
1107 $mform->addElement('select', 'documentformat', get_string('docsformat', 'repository_googledocs'), $docsformat);
1108 $mform->setDefault('documentformat', $docsformat['rtf']);
1109 $mform->setType('documentformat', PARAM_ALPHANUM);
1110
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);
1118
1119 $mform->addElement('select', 'drawingformat', get_string('drawingformat', 'repository_googledocs'), $drawingformat);
1120 $mform->setDefault('drawingformat', $drawingformat['pdf']);
1121 $mform->setType('drawingformat', PARAM_ALPHANUM);
1122
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);
1129
989e14fe
DW
1130 $str = get_string('presentationformat', 'repository_googledocs');
1131 $mform->addElement('select', 'presentationformat', $str, $presentationformat);
15a5c4b2
SB
1132 $mform->setDefault('presentationformat', $presentationformat['pptx']);
1133 $mform->setType('presentationformat', PARAM_ALPHANUM);
1134
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);
1142
989e14fe
DW
1143 $str = get_string('spreadsheetformat', 'repository_googledocs');
1144 $mform->addElement('select', 'spreadsheetformat', $str, $spreadsheetformat);
15a5c4b2
SB
1145 $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']);
1146 $mform->setType('spreadsheetformat', PARAM_ALPHANUM);
4560fd1b 1147 }
6667f9e4 1148}
989e14fe 1149
72fd103a
DW
1150/**
1151 * Callback to get the required scopes for system account.
1152 *
092304a3 1153 * @param \core\oauth2\issuer $issuer
72fd103a
DW
1154 * @return string
1155 */
72643dc6
DW
1156function 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 '';
989e14fe 1161}