MDL-59008 mod_resource: add option to serve external files embed
[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
3425813f 134 /**
0e59638b
DW
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 */
3425813f
FM
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 }
155
156 /**
0e59638b
DW
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 */
3425813f
FM
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 }
176
177 /**
0e59638b
DW
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 */
3425813f
FM
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 }
200
3425813f
FM
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 */
6667f9e4 208 public function get_listing($path='', $page = '') {
3425813f
FM
209 if (empty($path)) {
210 $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
211 }
eca128bf
DW
212 if (!$this->issuer->get('enabled')) {
213 // Empty list of files for disabled repository.
214 return ['dynload' => false, 'list' => [], 'nologin' => true];
215 }
3425813f
FM
216
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);
221
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 }
228
229 // Query the Drive.
230 $q = "'" . str_replace("'", "\'", $id) . "' in parents";
231 $q .= ' AND trashed = false';
232 $results = $this->query($q, $path);
6667f9e4 233
234 $ret = array();
235 $ret['dynload'] = true;
989e14fe 236 $ret['defaultreturntype'] = $this->default_returntype();
3425813f
FM
237 $ret['path'] = $this->build_breadcrumb($path);
238 $ret['list'] = $results;
5823a27e
DW
239 $ret['manage'] = 'https://drive.google.com/';
240
6667f9e4 241 return $ret;
242 }
243
3425813f
FM
244 /**
245 * Search throughout the Google Drive.
246 *
0e59638b 247 * @param string $searchtext text to search for.
3425813f
FM
248 * @param int $page search page.
249 * @return array of results.
250 */
0e59638b 251 public function search($searchtext, $page = 0) {
3425813f 252 $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
0e59638b
DW
253 $str = get_string('searchfor', 'repository_googledocs', $searchtext);
254 $path = $this->build_node_path('search', $str, $path);
3425813f
FM
255
256 // Query the Drive.
0e59638b 257 $q = "fullText contains '" . str_replace("'", "\'", $searchtext) . "'";
3425813f
FM
258 $q .= ' AND trashed = false';
259 $results = $this->query($q, $path);
6667f9e4 260
261 $ret = array();
262 $ret['dynload'] = true;
3425813f
FM
263 $ret['path'] = $this->build_breadcrumb($path);
264 $ret['list'] = $results;
5823a27e 265 $ret['manage'] = 'https://drive.google.com/';
6667f9e4 266 return $ret;
267 }
268
3425813f
FM
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;
285
286 $files = array();
287 $folders = array();
15a5c4b2 288 $config = get_config('googledocs');
989e14fe 289 $fields = "files(id,name,mimeType,webContentLink,webViewLink,fileExtension,modifiedTime,size,thumbnailLink,iconLink)";
0e59638b 290 $params = array('q' => $q, 'fields' => $fields, 'spaces' => 'drive');
3425813f
FM
291
292 try {
293 // Retrieving files and folders.
0e59638b
DW
294 $client = $this->get_user_oauth_client();
295 $service = new repository_googledocs\rest($client);
296
297 $response = $service->call('list', $params);
298 } catch (Exception $e) {
3425813f
FM
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 }
306
0e59638b
DW
307 $gfiles = isset($response->files) ? $response->files : array();
308 foreach ($gfiles as $gfile) {
309 if ($gfile->mimeType == 'application/vnd.google-apps.folder') {
3425813f 310 // This is a folder.
0e59638b
DW
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),
663640f5 315 'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false),
3425813f
FM
316 'thumbnail_height' => 64,
317 'thumbnail_width' => 64,
318 'children' => array()
319 );
320 } else {
321 // This is a file.
8ece1d70
DW
322 $link = isset($gfile->webViewLink) ? $gfile->webViewLink : '';
323 if (empty($link)) {
324 $link = isset($gfile->webContentLink) ? $gfile->webContentLink : '';
325 }
0e59638b
DW
326 if (isset($gfile->fileExtension)) {
327 // The file has an extension, therefore we can download it.
6da1c55b
DW
328 $source = json_encode([
329 'id' => $gfile->id,
330 'name' => $gfile->name,
331 'exportformat' => 'download',
72643dc6 332 'link' => $link
6da1c55b 333 ]);
0e59638b 334 $title = $gfile->name;
3425813f
FM
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.
0e59638b 338 $type = str_replace('application/vnd.google-apps.', '', $gfile->mimeType);
3425813f 339 $title = '';
0e59638b 340 $exporttype = '';
15a5c4b2
SB
341 $types = get_mimetypes_array();
342
3425813f
FM
343 switch ($type){
344 case 'document':
15a5c4b2 345 $ext = $config->documentformat;
af28b228 346 $title = $gfile->name . '.gdoc';
15a5c4b2
SB
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.
0e59638b 351 $exporttype = 'application/rtf';
15a5c4b2 352 } else {
0e59638b 353 $exporttype = $types[$ext]['type'];
15a5c4b2 354 }
3425813f
FM
355 break;
356 case 'presentation':
15a5c4b2 357 $ext = $config->presentationformat;
af28b228 358 $title = $gfile->name . '.gslides';
0e59638b 359 $exporttype = $types[$ext]['type'];
3425813f
FM
360 break;
361 case 'spreadsheet':
15a5c4b2 362 $ext = $config->spreadsheetformat;
af28b228 363 $title = $gfile->name . '.gsheet';
0e59638b 364 $exporttype = $types[$ext]['type'];
15a5c4b2
SB
365 break;
366 case 'drawing':
367 $ext = $config->drawingformat;
989e14fe 368 $title = $gfile->name . '.'. $ext;
0e59638b 369 $exporttype = $types[$ext]['type'];
3425813f
FM
370 break;
371 }
372 // Skips invalid/unknown types.
0e59638b 373 if (empty($title)) {
3425813f
FM
374 continue;
375 }
6da1c55b
DW
376 $source = json_encode([
377 'id' => $gfile->id,
378 'exportformat' => $exporttype,
379 'link' => $link,
72643dc6 380 'name' => $gfile->name
6da1c55b 381 ]);
3425813f 382 }
0e59638b 383 // Adds the file to the file list. Using the itemId along with the name as key
3425813f 384 // of the array because Google Drive allows files with identical names.
0e59638b
DW
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(
3425813f
FM
392 'title' => $title,
393 'source' => $source,
0e59638b
DW
394 'date' => strtotime($gfile->modifiedTime),
395 'size' => isset($gfile->size) ? $gfile->size : null,
396 'thumbnail' => $thumb,
3425813f
FM
397 'thumbnail_height' => 64,
398 'thumbnail_width' => 64,
3425813f 399 );
3425813f
FM
400 }
401 }
402
403 // Filter and order the results.
404 $files = array_filter($files, array($this, 'filter'));
2f1e464a
PS
405 core_collator::ksort($files, core_collator::SORT_NATURAL);
406 core_collator::ksort($folders, core_collator::SORT_NATURAL);
3425813f
FM
407 return array_merge(array_values($folders), array_values($files));
408 }
409
410 /**
411 * Logout.
412 *
413 * @return string
414 */
4560fd1b 415 public function logout() {
0e59638b
DW
416 $client = $this->get_user_oauth_client();
417 $client->log_out();
6667f9e4 418 return parent::logout();
419 }
420
3425813f
FM
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 = '') {
eb459f71
PS
429 global $CFG;
430
eca128bf
DW
431 if (!$this->issuer->get('enabled')) {
432 throw new repository_exception('cannotdownload', 'repository');
433 }
434
989e14fe
DW
435 $source = json_decode($reference);
436
00879eca
DW
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 }
443
444 $base = 'https://www.googleapis.com/drive/v3';
445
af28b228 446 $newfilename = false;
989e14fe
DW
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);
af28b228
DW
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 }
989e14fe
DW
465 $source = $sourceurl->out(false);
466 }
467
468 // We use download_one and not the rest API because it has special timeouts etc.
0e59638b
DW
469 $path = $this->prepare_file($filename);
470 $options = ['filepath' => $path, 'timeout' => 15, 'followlocation' => true, 'maxredirs' => 5];
af28b228 471 $success = $client->download_one($source, null, $options);
0e59638b 472
af28b228 473 if ($success) {
0e59638b 474 @chmod($path, $CFG->filepermissions);
af28b228
DW
475
476 $result = [
0e59638b 477 'path' => $path,
af28b228
DW
478 'url' => $reference,
479 ];
480 if (!empty($newfilename)) {
481 $result['newfilename'] = $newfilename;
482 }
483 return $result;
ec7e998f 484 }
3425813f
FM
485 throw new repository_exception('cannotdownload', 'repository');
486 }
487
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) {
989e14fe
DW
498 // We could do some magic upgrade code here.
499 return $source;
41076c58 500 }
6667f9e4 501
3425813f
FM
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 */
41076c58 508 public function supported_filetypes() {
4560fd1b 509 return '*';
41076c58 510 }
3425813f
FM
511
512 /**
513 * Tells how the file can be picked from this repository.
514 *
3425813f
FM
515 * @return int
516 */
41076c58 517 public function supported_returntypes() {
989e14fe
DW
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') {
151b0f94 524 return FILE_CONTROLLED_LINK;
989e14fe 525 } else {
151b0f94 526 return FILE_CONTROLLED_LINK | FILE_INTERNAL;
989e14fe
DW
527 }
528 } else {
529 return FILE_INTERNAL;
530 }
531 }
532
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 {
151b0f94 544 return FILE_CONTROLLED_LINK;
989e14fe 545 }
41076c58 546 }
4560fd1b 547
3425813f
FM
548 /**
549 * Return names of the general options.
550 * By default: no general option name.
551 *
552 * @return array
553 */
4560fd1b 554 public static function get_type_option_names() {
0e59638b 555 return array('issuerid', 'pluginname',
15a5c4b2 556 'documentformat', 'drawingformat',
989e14fe
DW
557 'presentationformat', 'spreadsheetformat',
558 'defaultreturntype', 'supportedreturntypes');
4560fd1b
DP
559 }
560
0e59638b
DW
561 /**
562 * Store the access token.
563 */
564 public function callback() {
565 $client = $this->get_user_oauth_client();
989e14fe 566 // This will upgrade to an access token if we have an authorization code and save the access token in the session.
0e59638b
DW
567 $client->is_logged_in();
568 }
569
989e14fe
DW
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) {
eca128bf
DW
582 if (!$this->issuer->get('enabled')) {
583 throw new repository_exception('cannotdownload', 'repository');
584 }
585
989e14fe
DW
586 $source = json_decode($storedfile->get_reference());
587
151b0f94
DW
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());
596
00879eca 597 if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
151b0f94
DW
598 // Add the current user as an OAuth writer.
599 $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
600
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);
606
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'];
626
627 $this->add_temp_writer_to_file($systemservice, $source->id, $useremail);
628 }
629
d5bb9f1f
DW
630 if (!empty($options['offline'])) {
631 $downloaded = $this->get_file($storedfile->get_reference(), $storedfile->get_filename());
632
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) {
fcdd7730
JL
639 // Do not use redirect() here because is not compatible with webservice/pluginfile.php.
640 header('Location: ' . $source->link);
989e14fe
DW
641 } else {
642 $details = 'File is missing source link';
643 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
644 }
645 }
646
989e14fe
DW
647 /**
648 * See if a folder exists within a folder
649 *
72fd103a 650 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
651 * @param string $foldername The folder we are looking for.
652 * @param string $parentid The parent folder we are looking in.
989e14fe
DW
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 }
668
669 /**
670 * Create a folder within a folder
671 *
72fd103a 672 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
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 }
689
8ece1d70
DW
690 /**
691 * Get simple file info for humans.
692 *
72fd103a 693 * @param \repository_googledocs\rest $client Authenticated client.
8ece1d70
DW
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) {
151b0f94 699 $fields = "id,name,owners,parents";
8ece1d70
DW
700 $params = [
701 'fileid' => $fileid,
702 'fields' => $fields
703 ];
704 return $client->call('get', $params);
705 }
706
989e14fe
DW
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 *
72fd103a 711 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe 712 * @param string $fileid The file we are copying.
6da1c55b 713 * @param string $name The original filename (don't change it).
989e14fe
DW
714 *
715 * @return stdClass file details.
716 */
6da1c55b 717 protected function copy_file(\repository_googledocs\rest $client, $fileid, $name) {
989e14fe
DW
718 $fields = "id,name,mimeType,webContentLink,webViewLink,size,thumbnailLink,iconLink";
719 $params = [
720 'fileid' => $fileid,
6da1c55b 721 'fields' => $fields,
989e14fe 722 ];
6da1c55b
DW
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));
989e14fe
DW
729 if (empty($fileinfo->id)) {
730 $details = 'Cannot copy file:' . $fileid;
731 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
732 }
733 return $fileinfo;
734 }
735
151b0f94
DW
736 /**
737 * Add a writer to the permissions on the file (temporary).
738 *
72fd103a 739 * @param \repository_googledocs\rest $client Authenticated client.
151b0f94
DW
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 */
72fd103a 744 protected function add_temp_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
151b0f94
DW
745 // Expires in 7 days.
746 $expires = new DateTime();
747 $expires->add(new DateInterval("P7D"));
748
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 }
763
764
989e14fe
DW
765 /**
766 * Add a writer to the permissions on the file.
767 *
72fd103a 768 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
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 */
72fd103a 773 protected function add_writer_to_file(\repository_googledocs\rest $client, $fileid, $email) {
989e14fe
DW
774 $updateeditor = [
775 'emailAddress' => $email,
776 'role' => 'writer',
777 'type' => 'user'
778 ];
8ece1d70 779 $params = ['fileid' => $fileid, 'sendNotificationEmail' => 'false'];
989e14fe
DW
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 }
787
788 /**
789 * Move from root to folder
790 *
72fd103a 791 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
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 */
72fd103a 796 protected function move_file_from_root_to_folder(\repository_googledocs\rest $client, $fileid, $folderid) {
989e14fe
DW
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 }
808
809 /**
810 * Prevent writers from sharing.
811 *
72fd103a 812 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
813 * @param string $fileid The file we are updating.
814 * @return boolean
815 */
72fd103a 816 protected function prevent_writers_from_sharing_file(\repository_googledocs\rest $client, $fileid) {
989e14fe
DW
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 }
831
832 /**
833 * Allow anyone with the link to read the file.
834 *
72fd103a 835 * @param \repository_googledocs\rest $client Authenticated client.
989e14fe
DW
836 * @param string $fileid The file we are updating.
837 * @return boolean
838 */
72fd103a 839 protected function set_file_sharing_anyone_with_link_can_read(\repository_googledocs\rest $client, $fileid) {
989e14fe
DW
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 }
853
854 /**
855 * Called when a file is selected as a "link".
856 * Invoked at MOODLE/repository/repository_ajax.php
857 *
eb47ad4a
DW
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 *
989e14fe
DW
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.
72643dc6
DW
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).
989e14fe 868 */
72643dc6 869 public function reference_file_selected($reference, $context, $component, $filearea, $itemid) {
989e14fe
DW
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).
eb47ad4a 874
6a7552fe
DW
875 // Get the details from the reference.
876 $source = json_decode($reference);
877 if (!empty($source->usesystem)) {
878 // If we already copied this file to the system account - we are done.
879 return $reference;
880 }
eb47ad4a
DW
881
882 // Check this issuer is enabled.
33536fb2 883 if ($this->disabled) {
eb47ad4a
DW
884 throw new repository_exception('cannotdownload', 'repository');
885 }
886
887 // Get a system oauth client and a user oauth client.
989e14fe
DW
888 $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
889
890 if ($systemauth === false) {
891 $details = 'Cannot connect as system user';
892 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
893 }
eb47ad4a 894 // Get the system user email so we can share the file with this user.
989e14fe
DW
895 $systemuserinfo = $systemauth->get_userinfo();
896 $systemuseremail = $systemuserinfo['email'];
897
989e14fe
DW
898 $userauth = $this->get_user_oauth_client();
899 if ($userauth === false) {
900 $details = 'Cannot connect as current user';
901 throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
902 }
989e14fe
DW
903
904 $userservice = new repository_googledocs\rest($userauth);
905 $systemservice = new repository_googledocs\rest($systemauth);
906
8ece1d70
DW
907 // Add Moodle as writer.
908 $this->add_writer_to_file($userservice, $source->id, $systemuseremail);
909
989e14fe
DW
910 // Now move it to a sensible folder.
911 $contextlist = array_reverse($context->get_parent_contexts(true));
912
39f60f6c 913 $cache = cache::make('repository_googledocs', 'folder');
989e14fe 914 $parentid = 'root';
39f60f6c 915 $fullpath = 'root';
72643dc6 916 $allfolders = [];
989e14fe
DW
917 foreach ($contextlist as $context) {
918 // Make sure a folder exists here.
72643dc6
DW
919 $foldername = clean_param($context->get_context_name(), PARAM_PATH);
920 $allfolders[] = $foldername;
921 }
922
923 $allfolders[] = clean_param($component, PARAM_PATH);
924 $allfolders[] = clean_param($filearea, PARAM_PATH);
925 $allfolders[] = clean_param($itemid, PARAM_PATH);
926
eb47ad4a
DW
927 // Variable $allfolders is the full path we want to put the file in - so walk it and create each folder.
928
72643dc6
DW
929 foreach ($allfolders as $foldername) {
930 // Make sure a folder exists here.
39f60f6c 931 $fullpath .= '/' . $foldername;
8ece1d70 932
72643dc6 933 $folderid = $cache->get($fullpath);
39f60f6c
DW
934 if (empty($folderid)) {
935 $folderid = $this->folder_exists_in_folder($systemservice, $foldername, $parentid);
936 }
989e14fe 937 if ($folderid !== false) {
39f60f6c 938 $cache->set($fullpath, $folderid);
989e14fe
DW
939 $parentid = $folderid;
940 } else {
941 // Create it.
942 $parentid = $this->create_folder_in_folder($systemservice, $foldername, $parentid);
39f60f6c 943 $cache->set($fullpath, $parentid);
989e14fe
DW
944 }
945 }
946
72643dc6
DW
947 // Copy the file so we get a snapshot file owned by Moodle.
948 $newsource = $this->copy_file($systemservice, $source->id, $source->name);
949 // Move the copied file to the correct folder.
950 $this->move_file_from_root_to_folder($systemservice, $newsource->id, $parentid);
989e14fe 951
72643dc6
DW
952 // Set the sharing options.
953 $this->set_file_sharing_anyone_with_link_can_read($systemservice, $newsource->id);
954 $this->prevent_writers_from_sharing_file($systemservice, $newsource->id);
955
eb47ad4a 956 // Update the returned reference so that the stored_file in moodle points to the newly copied file.
72643dc6
DW
957 $source->id = $newsource->id;
958 $source->link = isset($newsource->webViewLink) ? $newsource->webViewLink : '';
00879eca 959 $source->usesystem = true;
72643dc6
DW
960 if (empty($source->link)) {
961 $source->link = isset($newsource->webContentLink) ? $newsource->webContentLink : '';
8ece1d70 962 }
72643dc6 963 $reference = json_encode($source);
989e14fe 964
8ece1d70
DW
965 return $reference;
966 }
989e14fe 967
8ece1d70
DW
968 /**
969 * Get human readable file info from a the reference.
970 *
971 * @param string $reference
972 * @param int $filestatus
973 */
974 public function get_reference_details($reference, $filestatus = 0) {
33536fb2 975 if ($this->disabled) {
eca128bf
DW
976 throw new repository_exception('cannotdownload', 'repository');
977 }
8ece1d70
DW
978 if (empty($reference)) {
979 return get_string('unknownsource', 'repository');
989e14fe 980 }
8ece1d70 981 $source = json_decode($reference);
00879eca
DW
982 if (empty($source->usesystem)) {
983 return '';
984 }
8ece1d70 985 $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
989e14fe 986
8ece1d70
DW
987 if ($systemauth === false) {
988 return '';
989e14fe 989 }
8ece1d70
DW
990 $systemservice = new repository_googledocs\rest($systemauth);
991 $info = $this->get_file_summary($systemservice, $source->id);
989e14fe 992
8ece1d70
DW
993 $owner = '';
994 if (!empty($info->owners[0]->displayName)) {
995 $owner = $info->owners[0]->displayName;
996 }
997 if ($owner) {
998 return get_string('owner', 'repository_googledocs', $owner);
999 } else {
1000 return $info->name;
989e14fe
DW
1001 }
1002 }
1003
3425813f
FM
1004 /**
1005 * Edit/Create Admin Settings Moodle form.
1006 *
1007 * @param moodleform $mform Moodle form (passed by reference).
1008 * @param string $classname repository class name.
1009 */
4560fd1b 1010 public static function type_config_form($mform, $classname = 'repository') {
989e14fe
DW
1011 $url = new moodle_url('/admin/tool/oauth2/issuers.php');
1012 $url = $url->out();
3425813f 1013
0e59638b 1014 $mform->addElement('static', null, '', get_string('oauth2serviceslink', 'repository_googledocs', $url));
4560fd1b
DP
1015
1016 parent::type_config_form($mform);
0e59638b
DW
1017 $options = [];
1018 $issuers = \core\oauth2\api::get_all_issuers();
1019
1020 foreach ($issuers as $issuer) {
1021 $options[$issuer->get('id')] = s($issuer->get('name'));
1022 }
989e14fe
DW
1023
1024 $strrequired = get_string('required');
1025
0e59638b
DW
1026 $mform->addElement('select', 'issuerid', get_string('issuer', 'repository_googledocs'), $options);
1027 $mform->addHelpButton('issuerid', 'issuer', 'repository_googledocs');
1028 $mform->addRule('issuerid', $strrequired, 'required', null, 'client');
4560fd1b 1029
989e14fe
DW
1030 $mform->addElement('static', null, '', get_string('fileoptions', 'repository_googledocs'));
1031 $choices = [
1032 'internal' => get_string('internal', 'repository_googledocs'),
1033 'external' => get_string('external', 'repository_googledocs'),
1034 'both' => get_string('both', 'repository_googledocs')
1035 ];
1036 $mform->addElement('select', 'supportedreturntypes', get_string('supportedreturntypes', 'repository_googledocs'), $choices);
1037
1038 $choices = [
1039 FILE_INTERNAL => get_string('internal', 'repository_googledocs'),
151b0f94 1040 FILE_CONTROLLED_LINK => get_string('external', 'repository_googledocs'),
989e14fe
DW
1041 ];
1042 $mform->addElement('select', 'defaultreturntype', get_string('defaultreturntype', 'repository_googledocs'), $choices);
15a5c4b2 1043
0e59638b 1044 $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs'));
15a5c4b2
SB
1045
1046 // Documents.
1047 $docsformat = array();
1048 $docsformat['html'] = 'html';
1049 $docsformat['docx'] = 'docx';
1050 $docsformat['odt'] = 'odt';
1051 $docsformat['pdf'] = 'pdf';
1052 $docsformat['rtf'] = 'rtf';
1053 $docsformat['txt'] = 'txt';
1054 core_collator::ksort($docsformat, core_collator::SORT_NATURAL);
1055
1056 $mform->addElement('select', 'documentformat', get_string('docsformat', 'repository_googledocs'), $docsformat);
1057 $mform->setDefault('documentformat', $docsformat['rtf']);
1058 $mform->setType('documentformat', PARAM_ALPHANUM);
1059
1060 // Drawing.
1061 $drawingformat = array();
1062 $drawingformat['jpeg'] = 'jpeg';
1063 $drawingformat['png'] = 'png';
1064 $drawingformat['svg'] = 'svg';
1065 $drawingformat['pdf'] = 'pdf';
1066 core_collator::ksort($drawingformat, core_collator::SORT_NATURAL);
1067
1068 $mform->addElement('select', 'drawingformat', get_string('drawingformat', 'repository_googledocs'), $drawingformat);
1069 $mform->setDefault('drawingformat', $drawingformat['pdf']);
1070 $mform->setType('drawingformat', PARAM_ALPHANUM);
1071
1072 // Presentation.
1073 $presentationformat = array();
1074 $presentationformat['pdf'] = 'pdf';
1075 $presentationformat['pptx'] = 'pptx';
1076 $presentationformat['txt'] = 'txt';
1077 core_collator::ksort($presentationformat, core_collator::SORT_NATURAL);
1078
989e14fe
DW
1079 $str = get_string('presentationformat', 'repository_googledocs');
1080 $mform->addElement('select', 'presentationformat', $str, $presentationformat);
15a5c4b2
SB
1081 $mform->setDefault('presentationformat', $presentationformat['pptx']);
1082 $mform->setType('presentationformat', PARAM_ALPHANUM);
1083
1084 // Spreadsheet.
1085 $spreadsheetformat = array();
1086 $spreadsheetformat['csv'] = 'csv';
1087 $spreadsheetformat['ods'] = 'ods';
1088 $spreadsheetformat['pdf'] = 'pdf';
1089 $spreadsheetformat['xlsx'] = 'xlsx';
1090 core_collator::ksort($spreadsheetformat, core_collator::SORT_NATURAL);
1091
989e14fe
DW
1092 $str = get_string('spreadsheetformat', 'repository_googledocs');
1093 $mform->addElement('select', 'spreadsheetformat', $str, $spreadsheetformat);
15a5c4b2
SB
1094 $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']);
1095 $mform->setType('spreadsheetformat', PARAM_ALPHANUM);
4560fd1b 1096 }
6667f9e4 1097}
989e14fe 1098
72fd103a
DW
1099/**
1100 * Callback to get the required scopes for system account.
1101 *
092304a3 1102 * @param \core\oauth2\issuer $issuer
72fd103a
DW
1103 * @return string
1104 */
72643dc6
DW
1105function repository_googledocs_oauth2_system_scopes(\core\oauth2\issuer $issuer) {
1106 if ($issuer->get('id') == get_config('googledocs', 'issuerid')) {
1107 return 'https://www.googleapis.com/auth/drive';
1108 }
1109 return '';
989e14fe 1110}