MDL-58090 oauth2: Coding style
[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');
28cdc043 29require_once($CFG->libdir . '/google/lib.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
FM
41 /**
42 * Google Client.
43 * @var Google_Client
44 */
45 private $client = null;
6667f9e4 46
3425813f
FM
47 /**
48 * Google Drive Service.
28cdc043 49 * @var Google_Drive_Service
3425813f
FM
50 */
51 private $service = null;
52
53 /**
54 * Session key to store the accesstoken.
55 * @var string
56 */
57 const SESSIONKEY = 'googledrive_accesstoken';
4560fd1b 58
3425813f
FM
59 /**
60 * URI to the callback file for OAuth.
61 * @var string
62 */
63 const CALLBACKURL = '/admin/oauth2callback.php';
64
65 /**
66 * Constructor.
67 *
68 * @param int $repositoryid repository instance id.
69 * @param int|stdClass $context a context id or context object.
70 * @param array $options repository options.
71 * @param int $readonly indicate this repo is readonly or not.
72 * @return void
73 */
74 public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
75 parent::__construct($repositoryid, $context, $options, $readonly = 0);
76
77 $callbackurl = new moodle_url(self::CALLBACKURL);
78
28cdc043 79 $this->client = get_google_client();
3425813f
FM
80 $this->client->setClientId(get_config('googledocs', 'clientid'));
81 $this->client->setClientSecret(get_config('googledocs', 'secret'));
28cdc043 82 $this->client->setScopes(array(Google_Service_Drive::DRIVE_READONLY));
3425813f 83 $this->client->setRedirectUri($callbackurl->out(false));
28cdc043 84 $this->service = new Google_Service_Drive($this->client);
4560fd1b 85
94742bca 86 $this->check_login();
6667f9e4 87 }
88
3425813f
FM
89 /**
90 * Returns the access token if any.
91 *
92 * @return string|null access token.
93 */
94 protected function get_access_token() {
95 global $SESSION;
96 if (isset($SESSION->{self::SESSIONKEY})) {
97 return $SESSION->{self::SESSIONKEY};
98 }
99 return null;
100 }
101
102 /**
103 * Store the access token in the session.
104 *
105 * @param string $token token to store.
106 * @return void
107 */
108 protected function store_access_token($token) {
109 global $SESSION;
110 $SESSION->{self::SESSIONKEY} = $token;
111 }
112
113 /**
114 * Callback method during authentication.
115 *
116 * @return void
117 */
118 public function callback() {
119 if ($code = optional_param('oauth2code', null, PARAM_RAW)) {
120 $this->client->authenticate($code);
121 $this->store_access_token($this->client->getAccessToken());
122 }
123 }
124
125 /**
126 * Checks whether the user is authenticate or not.
127 *
128 * @return bool true when logged in.
129 */
6667f9e4 130 public function check_login() {
3425813f
FM
131 if ($token = $this->get_access_token()) {
132 $this->client->setAccessToken($token);
133 return true;
134 }
135 return false;
6667f9e4 136 }
137
3425813f
FM
138 /**
139 * Print or return the login form.
140 *
141 * @return void|array for ajax.
142 */
4560fd1b 143 public function print_login() {
3425813f
FM
144 $returnurl = new moodle_url('/repository/repository_callback.php');
145 $returnurl->param('callback', 'yes');
146 $returnurl->param('repo_id', $this->id);
147 $returnurl->param('sesskey', sesskey());
4560fd1b 148
3425813f
FM
149 $url = new moodle_url($this->client->createAuthUrl());
150 $url->param('state', $returnurl->out_as_local_url(false));
4560fd1b
DP
151 if ($this->options['ajax']) {
152 $popup = new stdClass();
153 $popup->type = 'popup';
154 $popup->url = $url->out(false);
155 return array('login' => array($popup));
156 } else {
157 echo '<a target="_blank" href="'.$url->out(false).'">'.get_string('login', 'repository').'</a>';
6667f9e4 158 }
159 }
160
3425813f
FM
161 /**
162 * Build the breadcrumb from a path.
163 *
164 * @param string $path to create a breadcrumb from.
165 * @return array containing name and path of each crumb.
166 */
167 protected function build_breadcrumb($path) {
168 $bread = explode('/', $path);
169 $crumbtrail = '';
170 foreach ($bread as $crumb) {
171 list($id, $name) = $this->explode_node_path($crumb);
172 $name = empty($name) ? $id : $name;
173 $breadcrumb[] = array(
174 'name' => $name,
175 'path' => $this->build_node_path($id, $name, $crumbtrail)
176 );
177 $tmp = end($breadcrumb);
178 $crumbtrail = $tmp['path'];
179 }
180 return $breadcrumb;
181 }
182
183 /**
184 * Generates a safe path to a node.
185 *
186 * Typically, a node will be id|Name of the node.
187 *
188 * @param string $id of the node.
189 * @param string $name of the node, will be URL encoded.
190 * @param string $root to append the node on, must be a result of this function.
191 * @return string path to the node.
192 */
193 protected function build_node_path($id, $name = '', $root = '') {
194 $path = $id;
195 if (!empty($name)) {
196 $path .= '|' . urlencode($name);
197 }
198 if (!empty($root)) {
199 $path = trim($root, '/') . '/' . $path;
200 }
201 return $path;
202 }
203
204 /**
205 * Returns information about a node in a path.
206 *
207 * @see self::build_node_path()
208 * @param string $node to extrat information from.
209 * @return array about the node.
210 */
211 protected function explode_node_path($node) {
212 if (strpos($node, '|') !== false) {
213 list($id, $name) = explode('|', $node, 2);
214 $name = urldecode($name);
215 } else {
216 $id = $node;
217 $name = '';
218 }
219 $id = urldecode($id);
220 return array(
221 0 => $id,
222 1 => $name,
223 'id' => $id,
224 'name' => $name
225 );
226 }
227
228
229 /**
230 * List the files and folders.
231 *
232 * @param string $path path to browse.
233 * @param string $page page to browse.
234 * @return array of result.
235 */
6667f9e4 236 public function get_listing($path='', $page = '') {
3425813f
FM
237 if (empty($path)) {
238 $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
239 }
240
241 // We analyse the path to extract what to browse.
242 $trail = explode('/', $path);
243 $uri = array_pop($trail);
244 list($id, $name) = $this->explode_node_path($uri);
245
246 // Handle the special keyword 'search', which we defined in self::search() so that
247 // we could set up a breadcrumb in the search results. In any other case ID would be
248 // 'root' which is a special keyword set up by Google, or a parent (folder) ID.
249 if ($id === 'search') {
250 return $this->search($name);
251 }
252
253 // Query the Drive.
254 $q = "'" . str_replace("'", "\'", $id) . "' in parents";
255 $q .= ' AND trashed = false';
256 $results = $this->query($q, $path);
6667f9e4 257
258 $ret = array();
259 $ret['dynload'] = true;
3425813f
FM
260 $ret['path'] = $this->build_breadcrumb($path);
261 $ret['list'] = $results;
6667f9e4 262 return $ret;
263 }
264
3425813f
FM
265 /**
266 * Search throughout the Google Drive.
267 *
268 * @param string $search_text text to search for.
269 * @param int $page search page.
270 * @return array of results.
271 */
68a7c9a6 272 public function search($search_text, $page = 0) {
3425813f
FM
273 $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
274 $path = $this->build_node_path('search', $search_text, $path);
275
276 // Query the Drive.
277 $q = "fullText contains '" . str_replace("'", "\'", $search_text) . "'";
278 $q .= ' AND trashed = false';
279 $results = $this->query($q, $path);
6667f9e4 280
281 $ret = array();
282 $ret['dynload'] = true;
3425813f
FM
283 $ret['path'] = $this->build_breadcrumb($path);
284 $ret['list'] = $results;
6667f9e4 285 return $ret;
286 }
287
3425813f
FM
288 /**
289 * Query Google Drive for files and folders using a search query.
290 *
291 * Documentation about the query format can be found here:
292 * https://developers.google.com/drive/search-parameters
293 *
294 * This returns a list of files and folders with their details as they should be
295 * formatted and returned by functions such as get_listing() or search().
296 *
297 * @param string $q search query as expected by the Google API.
298 * @param string $path parent path of the current files, will not be used for the query.
299 * @param int $page page.
300 * @return array of files and folders.
301 */
302 protected function query($q, $path = null, $page = 0) {
303 global $OUTPUT;
304
305 $files = array();
306 $folders = array();
307 $fields = "items(id,title,mimeType,downloadUrl,fileExtension,exportLinks,modifiedDate,fileSize,thumbnailLink)";
308 $params = array('q' => $q, 'fields' => $fields);
15a5c4b2 309 $config = get_config('googledocs');
3425813f
FM
310
311 try {
312 // Retrieving files and folders.
313 $response = $this->service->files->listFiles($params);
28cdc043 314 } catch (Google_Service_Exception $e) {
3425813f
FM
315 if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
316 // This is raised when the service Drive API has not been enabled on Google APIs control panel.
317 throw new repository_exception('servicenotenabled', 'repository_googledocs');
318 } else {
319 throw $e;
320 }
321 }
322
323 $items = isset($response['items']) ? $response['items'] : array();
324 foreach ($items as $item) {
325 if ($item['mimeType'] == 'application/vnd.google-apps.folder') {
326 // This is a folder.
327 $folders[$item['title'] . $item['id']] = array(
328 'title' => $item['title'],
329 'path' => $this->build_node_path($item['id'], $item['title'], $path),
330 'date' => strtotime($item['modifiedDate']),
663640f5 331 'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false),
3425813f
FM
332 'thumbnail_height' => 64,
333 'thumbnail_width' => 64,
334 'children' => array()
335 );
336 } else {
337 // This is a file.
338 if (isset($item['fileExtension'])) {
339 // The file has an extension, therefore there is a download link.
340 $title = $item['title'];
341 $source = $item['downloadUrl'];
342 } else {
343 // The file is probably a Google Doc file, we get the corresponding export link.
344 // This should be improved by allowing the user to select the type of export they'd like.
345 $type = str_replace('application/vnd.google-apps.', '', $item['mimeType']);
346 $title = '';
347 $exportType = '';
15a5c4b2
SB
348 $types = get_mimetypes_array();
349
3425813f
FM
350 switch ($type){
351 case 'document':
15a5c4b2
SB
352 $ext = $config->documentformat;
353 $title = $item['title'] . '.'. $ext;
354 if ($ext === 'rtf') {
355 // Moodle user 'text/rtf' as the MIME type for RTF files.
356 // Google uses 'application/rtf' for the same type of file.
357 // See https://developers.google.com/drive/v3/web/manage-downloads.
358 $exportType = 'application/rtf';
359 } else {
360 $exportType = $types[$ext]['type'];
361 }
3425813f
FM
362 break;
363 case 'presentation':
15a5c4b2
SB
364 $ext = $config->presentationformat;
365 $title = $item['title'] . '.'. $ext;
366 $exportType = $types[$ext]['type'];
3425813f
FM
367 break;
368 case 'spreadsheet':
15a5c4b2
SB
369 $ext = $config->spreadsheetformat;
370 $title = $item['title'] . '.'. $ext;
371 $exportType = $types[$ext]['type'];
372 break;
373 case 'drawing':
374 $ext = $config->drawingformat;
375 $title = $item['title'] . '.'. $ext;
376 $exportType = $types[$ext]['type'];
3425813f
FM
377 break;
378 }
379 // Skips invalid/unknown types.
380 if (empty($title) || !isset($item['exportLinks'][$exportType])) {
381 continue;
382 }
383 $source = $item['exportLinks'][$exportType];
384 }
385 // Adds the file to the file list. Using the itemId along with the title as key
386 // of the array because Google Drive allows files with identical names.
387 $files[$title . $item['id']] = array(
388 'title' => $title,
389 'source' => $source,
390 'date' => strtotime($item['modifiedDate']),
391 'size' => isset($item['fileSize']) ? $item['fileSize'] : null,
663640f5 392 'thumbnail' => $OUTPUT->image_url(file_extension_icon($title, 64))->out(false),
3425813f
FM
393 'thumbnail_height' => 64,
394 'thumbnail_width' => 64,
395 // Do not use real thumbnails as they wouldn't work if the user disabled 3rd party
396 // plugins in his browser, or if they're not logged in their Google account.
397 );
398
399 // Sometimes the real thumbnails can't be displayed, for example if 3rd party cookies are disabled
400 // or if the user is not logged in Google anymore. But this restriction does not seem to be applied
401 // to a small subset of files.
402 $extension = strtolower(pathinfo($title, PATHINFO_EXTENSION));
403 if (isset($item['thumbnailLink']) && in_array($extension, array('jpg', 'png', 'txt', 'pdf'))) {
404 $files[$title . $item['id']]['realthumbnail'] = $item['thumbnailLink'];
405 }
406 }
407 }
408
409 // Filter and order the results.
410 $files = array_filter($files, array($this, 'filter'));
2f1e464a
PS
411 core_collator::ksort($files, core_collator::SORT_NATURAL);
412 core_collator::ksort($folders, core_collator::SORT_NATURAL);
3425813f
FM
413 return array_merge(array_values($folders), array_values($files));
414 }
415
416 /**
417 * Logout.
418 *
419 * @return string
420 */
4560fd1b 421 public function logout() {
3425813f 422 $this->store_access_token(null);
6667f9e4 423 return parent::logout();
424 }
425
3425813f
FM
426 /**
427 * Get a file.
428 *
429 * @param string $reference reference of the file.
430 * @param string $file name to save the file to.
431 * @return string JSON encoded array of information about the file.
432 */
433 public function get_file($reference, $filename = '') {
eb459f71
PS
434 global $CFG;
435
28cdc043
FM
436 $auth = $this->client->getAuth();
437 $request = $auth->authenticatedRequest(new Google_Http_Request($reference));
438 if ($request->getResponseHttpCode() == 200) {
3425813f 439 $path = $this->prepare_file($filename);
28cdc043 440 $content = $request->getResponseBody();
3425813f 441 if (file_put_contents($path, $content) !== false) {
eb459f71 442 @chmod($path, $CFG->filepermissions);
3425813f
FM
443 return array(
444 'path' => $path,
445 'url' => $reference
446 );
447 }
ec7e998f 448 }
3425813f
FM
449 throw new repository_exception('cannotdownload', 'repository');
450 }
451
452 /**
453 * Prepare file reference information.
454 *
455 * We are using this method to clean up the source to make sure that it
456 * is a valid source.
457 *
458 * @param string $source of the file.
459 * @return string file reference.
460 */
461 public function get_file_reference($source) {
462 return clean_param($source, PARAM_URL);
41076c58 463 }
6667f9e4 464
3425813f
FM
465 /**
466 * What kind of files will be in this repository?
467 *
468 * @return array return '*' means this repository support any files, otherwise
469 * return mimetypes of files, it can be an array
470 */
41076c58 471 public function supported_filetypes() {
4560fd1b 472 return '*';
41076c58 473 }
3425813f
FM
474
475 /**
476 * Tells how the file can be picked from this repository.
477 *
478 * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE.
479 *
480 * @return int
481 */
41076c58
DC
482 public function supported_returntypes() {
483 return FILE_INTERNAL;
484 }
4560fd1b 485
3425813f
FM
486 /**
487 * Return names of the general options.
488 * By default: no general option name.
489 *
490 * @return array
491 */
4560fd1b 492 public static function get_type_option_names() {
15a5c4b2
SB
493 return array('clientid', 'secret', 'pluginname',
494 'documentformat', 'drawingformat',
495 'presentationformat', 'spreadsheetformat');
4560fd1b
DP
496 }
497
3425813f
FM
498 /**
499 * Edit/Create Admin Settings Moodle form.
500 *
501 * @param moodleform $mform Moodle form (passed by reference).
502 * @param string $classname repository class name.
503 */
4560fd1b 504 public static function type_config_form($mform, $classname = 'repository') {
3425813f
FM
505 $callbackurl = new moodle_url(self::CALLBACKURL);
506
4560fd1b 507 $a = new stdClass;
8b503936 508 $a->docsurl = get_docs_url('Google_OAuth_2.0_setup');
3425813f 509 $a->callbackurl = $callbackurl->out(false);
4560fd1b
DP
510
511 $mform->addElement('static', null, '', get_string('oauthinfo', 'repository_googledocs', $a));
512
513 parent::type_config_form($mform);
514 $mform->addElement('text', 'clientid', get_string('clientid', 'repository_googledocs'));
999427e9 515 $mform->setType('clientid', PARAM_RAW_TRIMMED);
4560fd1b 516 $mform->addElement('text', 'secret', get_string('secret', 'repository_googledocs'));
999427e9 517 $mform->setType('secret', PARAM_RAW_TRIMMED);
4560fd1b
DP
518
519 $strrequired = get_string('required');
520 $mform->addRule('clientid', $strrequired, 'required', null, 'client');
521 $mform->addRule('secret', $strrequired, 'required', null, 'client');
15a5c4b2
SB
522
523 $mform->addElement('static', null, '', get_string('importformat', 'repository_googledocs', $a));
524
525 // Documents.
526 $docsformat = array();
527 $docsformat['html'] = 'html';
528 $docsformat['docx'] = 'docx';
529 $docsformat['odt'] = 'odt';
530 $docsformat['pdf'] = 'pdf';
531 $docsformat['rtf'] = 'rtf';
532 $docsformat['txt'] = 'txt';
533 core_collator::ksort($docsformat, core_collator::SORT_NATURAL);
534
535 $mform->addElement('select', 'documentformat', get_string('docsformat', 'repository_googledocs'), $docsformat);
536 $mform->setDefault('documentformat', $docsformat['rtf']);
537 $mform->setType('documentformat', PARAM_ALPHANUM);
538
539 // Drawing.
540 $drawingformat = array();
541 $drawingformat['jpeg'] = 'jpeg';
542 $drawingformat['png'] = 'png';
543 $drawingformat['svg'] = 'svg';
544 $drawingformat['pdf'] = 'pdf';
545 core_collator::ksort($drawingformat, core_collator::SORT_NATURAL);
546
547 $mform->addElement('select', 'drawingformat', get_string('drawingformat', 'repository_googledocs'), $drawingformat);
548 $mform->setDefault('drawingformat', $drawingformat['pdf']);
549 $mform->setType('drawingformat', PARAM_ALPHANUM);
550
551 // Presentation.
552 $presentationformat = array();
553 $presentationformat['pdf'] = 'pdf';
554 $presentationformat['pptx'] = 'pptx';
555 $presentationformat['txt'] = 'txt';
556 core_collator::ksort($presentationformat, core_collator::SORT_NATURAL);
557
558 $mform->addElement('select', 'presentationformat', get_string('presentationformat', 'repository_googledocs'), $presentationformat);
559 $mform->setDefault('presentationformat', $presentationformat['pptx']);
560 $mform->setType('presentationformat', PARAM_ALPHANUM);
561
562 // Spreadsheet.
563 $spreadsheetformat = array();
564 $spreadsheetformat['csv'] = 'csv';
565 $spreadsheetformat['ods'] = 'ods';
566 $spreadsheetformat['pdf'] = 'pdf';
567 $spreadsheetformat['xlsx'] = 'xlsx';
568 core_collator::ksort($spreadsheetformat, core_collator::SORT_NATURAL);
569
570 $mform->addElement('select', 'spreadsheetformat', get_string('spreadsheetformat', 'repository_googledocs'), $spreadsheetformat);
571 $mform->setDefault('spreadsheetformat', $spreadsheetformat['xlsx']);
572 $mform->setType('spreadsheetformat', PARAM_ALPHANUM);
4560fd1b 573 }
6667f9e4 574}
4560fd1b 575// Icon from: http://www.iconspedia.com/icon/google-2706.html.