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