MDL-50576 mod_forum: Correct use of movedicussions cap when posting This is a partial...
[moodle.git] / repository / googledocs / lib.php
blobd6c77d88e95598794a039b46c7f9e70f83f59a49
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * This plugin is used to access Google Drive.
20 * @since Moodle 2.0
21 * @package repository_googledocs
22 * @copyright 2009 Dan Poltawski <talktodan@gmail.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->dirroot . '/repository/lib.php');
29 require_once($CFG->libdir . '/google/Google_Client.php');
30 require_once($CFG->libdir . '/google/contrib/Google_DriveService.php');
32 /**
33 * Google Docs Plugin
35 * @since Moodle 2.0
36 * @package repository_googledocs
37 * @copyright 2009 Dan Poltawski <talktodan@gmail.com>
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class repository_googledocs extends repository {
42 /**
43 * Google Client.
44 * @var Google_Client
46 private $client = null;
48 /**
49 * Google Drive Service.
50 * @var Google_DriveService
52 private $service = null;
54 /**
55 * Session key to store the accesstoken.
56 * @var string
58 const SESSIONKEY = 'googledrive_accesstoken';
60 /**
61 * URI to the callback file for OAuth.
62 * @var string
64 const CALLBACKURL = '/admin/oauth2callback.php';
66 /**
67 * Constructor.
69 * @param int $repositoryid repository instance id.
70 * @param int|stdClass $context a context id or context object.
71 * @param array $options repository options.
72 * @param int $readonly indicate this repo is readonly or not.
73 * @return void
75 public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
76 parent::__construct($repositoryid, $context, $options, $readonly = 0);
78 $callbackurl = new moodle_url(self::CALLBACKURL);
80 $this->client = new Google_Client();
81 $this->client->setClientId(get_config('googledocs', 'clientid'));
82 $this->client->setClientSecret(get_config('googledocs', 'secret'));
83 $this->client->setScopes(array('https://www.googleapis.com/auth/drive.readonly'));
84 $this->client->setRedirectUri($callbackurl->out(false));
85 $this->service = new Google_DriveService($this->client);
87 $this->check_login();
90 /**
91 * Returns the access token if any.
93 * @return string|null access token.
95 protected function get_access_token() {
96 global $SESSION;
97 if (isset($SESSION->{self::SESSIONKEY})) {
98 return $SESSION->{self::SESSIONKEY};
100 return null;
104 * Store the access token in the session.
106 * @param string $token token to store.
107 * @return void
109 protected function store_access_token($token) {
110 global $SESSION;
111 $SESSION->{self::SESSIONKEY} = $token;
115 * Callback method during authentication.
117 * @return void
119 public function callback() {
120 if ($code = optional_param('oauth2code', null, PARAM_RAW)) {
121 $this->client->authenticate($code);
122 $this->store_access_token($this->client->getAccessToken());
127 * Checks whether the user is authenticate or not.
129 * @return bool true when logged in.
131 public function check_login() {
132 if ($token = $this->get_access_token()) {
133 $this->client->setAccessToken($token);
134 return true;
136 return false;
140 * Print or return the login form.
142 * @return void|array for ajax.
144 public function print_login() {
145 $returnurl = new moodle_url('/repository/repository_callback.php');
146 $returnurl->param('callback', 'yes');
147 $returnurl->param('repo_id', $this->id);
148 $returnurl->param('sesskey', sesskey());
150 $url = new moodle_url($this->client->createAuthUrl());
151 $url->param('state', $returnurl->out_as_local_url(false));
152 if ($this->options['ajax']) {
153 $popup = new stdClass();
154 $popup->type = 'popup';
155 $popup->url = $url->out(false);
156 return array('login' => array($popup));
157 } else {
158 echo '<a target="_blank" href="'.$url->out(false).'">'.get_string('login', 'repository').'</a>';
163 * Build the breadcrumb from a path.
165 * @param string $path to create a breadcrumb from.
166 * @return array containing name and path of each crumb.
168 protected function build_breadcrumb($path) {
169 $bread = explode('/', $path);
170 $crumbtrail = '';
171 foreach ($bread as $crumb) {
172 list($id, $name) = $this->explode_node_path($crumb);
173 $name = empty($name) ? $id : $name;
174 $breadcrumb[] = array(
175 'name' => $name,
176 'path' => $this->build_node_path($id, $name, $crumbtrail)
178 $tmp = end($breadcrumb);
179 $crumbtrail = $tmp['path'];
181 return $breadcrumb;
185 * Generates a safe path to a node.
187 * Typically, a node will be id|Name of the node.
189 * @param string $id of the node.
190 * @param string $name of the node, will be URL encoded.
191 * @param string $root to append the node on, must be a result of this function.
192 * @return string path to the node.
194 protected function build_node_path($id, $name = '', $root = '') {
195 $path = $id;
196 if (!empty($name)) {
197 $path .= '|' . urlencode($name);
199 if (!empty($root)) {
200 $path = trim($root, '/') . '/' . $path;
202 return $path;
206 * Returns information about a node in a path.
208 * @see self::build_node_path()
209 * @param string $node to extrat information from.
210 * @return array about the node.
212 protected function explode_node_path($node) {
213 if (strpos($node, '|') !== false) {
214 list($id, $name) = explode('|', $node, 2);
215 $name = urldecode($name);
216 } else {
217 $id = $node;
218 $name = '';
220 $id = urldecode($id);
221 return array(
222 0 => $id,
223 1 => $name,
224 'id' => $id,
225 'name' => $name
231 * List the files and folders.
233 * @param string $path path to browse.
234 * @param string $page page to browse.
235 * @return array of result.
237 public function get_listing($path='', $page = '') {
238 if (empty($path)) {
239 $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
242 // We analyse the path to extract what to browse.
243 $trail = explode('/', $path);
244 $uri = array_pop($trail);
245 list($id, $name) = $this->explode_node_path($uri);
247 // Handle the special keyword 'search', which we defined in self::search() so that
248 // we could set up a breadcrumb in the search results. In any other case ID would be
249 // 'root' which is a special keyword set up by Google, or a parent (folder) ID.
250 if ($id === 'search') {
251 return $this->search($name);
254 // Query the Drive.
255 $q = "'" . str_replace("'", "\'", $id) . "' in parents";
256 $q .= ' AND trashed = false';
257 $results = $this->query($q, $path);
259 $ret = array();
260 $ret['dynload'] = true;
261 $ret['path'] = $this->build_breadcrumb($path);
262 $ret['list'] = $results;
263 return $ret;
267 * Search throughout the Google Drive.
269 * @param string $search_text text to search for.
270 * @param int $page search page.
271 * @return array of results.
273 public function search($search_text, $page = 0) {
274 $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
275 $path = $this->build_node_path('search', $search_text, $path);
277 // Query the Drive.
278 $q = "fullText contains '" . str_replace("'", "\'", $search_text) . "'";
279 $q .= ' AND trashed = false';
280 $results = $this->query($q, $path);
282 $ret = array();
283 $ret['dynload'] = true;
284 $ret['path'] = $this->build_breadcrumb($path);
285 $ret['list'] = $results;
286 return $ret;
290 * Query Google Drive for files and folders using a search query.
292 * Documentation about the query format can be found here:
293 * https://developers.google.com/drive/search-parameters
295 * This returns a list of files and folders with their details as they should be
296 * formatted and returned by functions such as get_listing() or search().
298 * @param string $q search query as expected by the Google API.
299 * @param string $path parent path of the current files, will not be used for the query.
300 * @param int $page page.
301 * @return array of files and folders.
303 protected function query($q, $path = null, $page = 0) {
304 global $OUTPUT;
306 $files = array();
307 $folders = array();
308 $fields = "items(id,title,mimeType,downloadUrl,fileExtension,exportLinks,modifiedDate,fileSize,thumbnailLink)";
309 $params = array('q' => $q, 'fields' => $fields);
311 try {
312 // Retrieving files and folders.
313 $response = $this->service->files->listFiles($params);
314 } catch (Google_ServiceException $e) {
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;
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']),
331 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(64))->out(false),
332 'thumbnail_height' => 64,
333 'thumbnail_width' => 64,
334 'children' => array()
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 = '';
348 switch ($type){
349 case 'document':
350 $title = $item['title'] . '.rtf';
351 $exportType = 'application/rtf';
352 break;
353 case 'presentation':
354 $title = $item['title'] . '.pptx';
355 $exportType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
356 break;
357 case 'spreadsheet':
358 $title = $item['title'] . '.xlsx';
359 $exportType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
360 break;
362 // Skips invalid/unknown types.
363 if (empty($title) || !isset($item['exportLinks'][$exportType])) {
364 continue;
366 $source = $item['exportLinks'][$exportType];
368 // Adds the file to the file list. Using the itemId along with the title as key
369 // of the array because Google Drive allows files with identical names.
370 $files[$title . $item['id']] = array(
371 'title' => $title,
372 'source' => $source,
373 'date' => strtotime($item['modifiedDate']),
374 'size' => isset($item['fileSize']) ? $item['fileSize'] : null,
375 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($title, 64))->out(false),
376 'thumbnail_height' => 64,
377 'thumbnail_width' => 64,
378 // Do not use real thumbnails as they wouldn't work if the user disabled 3rd party
379 // plugins in his browser, or if they're not logged in their Google account.
382 // Sometimes the real thumbnails can't be displayed, for example if 3rd party cookies are disabled
383 // or if the user is not logged in Google anymore. But this restriction does not seem to be applied
384 // to a small subset of files.
385 $extension = strtolower(pathinfo($title, PATHINFO_EXTENSION));
386 if (isset($item['thumbnailLink']) && in_array($extension, array('jpg', 'png', 'txt', 'pdf'))) {
387 $files[$title . $item['id']]['realthumbnail'] = $item['thumbnailLink'];
392 // Filter and order the results.
393 $files = array_filter($files, array($this, 'filter'));
394 core_collator::ksort($files, core_collator::SORT_NATURAL);
395 core_collator::ksort($folders, core_collator::SORT_NATURAL);
396 return array_merge(array_values($folders), array_values($files));
400 * Logout.
402 * @return string
404 public function logout() {
405 $this->store_access_token(null);
406 return parent::logout();
410 * Get a file.
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.
416 public function get_file($reference, $filename = '') {
417 global $CFG;
419 $request = new Google_HttpRequest($reference);
420 $httpRequest = Google_Client::$io->authenticatedRequest($request);
421 if ($httpRequest->getResponseHttpCode() == 200) {
422 $path = $this->prepare_file($filename);
423 $content = $httpRequest->getResponseBody();
424 if (file_put_contents($path, $content) !== false) {
425 @chmod($path, $CFG->filepermissions);
426 return array(
427 'path' => $path,
428 'url' => $reference
432 throw new repository_exception('cannotdownload', 'repository');
436 * Prepare file reference information.
438 * We are using this method to clean up the source to make sure that it
439 * is a valid source.
441 * @param string $source of the file.
442 * @return string file reference.
444 public function get_file_reference($source) {
445 return clean_param($source, PARAM_URL);
449 * What kind of files will be in this repository?
451 * @return array return '*' means this repository support any files, otherwise
452 * return mimetypes of files, it can be an array
454 public function supported_filetypes() {
455 return '*';
459 * Tells how the file can be picked from this repository.
461 * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE.
463 * @return int
465 public function supported_returntypes() {
466 return FILE_INTERNAL;
470 * Return names of the general options.
471 * By default: no general option name.
473 * @return array
475 public static function get_type_option_names() {
476 return array('clientid', 'secret', 'pluginname');
480 * Edit/Create Admin Settings Moodle form.
482 * @param moodleform $mform Moodle form (passed by reference).
483 * @param string $classname repository class name.
485 public static function type_config_form($mform, $classname = 'repository') {
487 $callbackurl = new moodle_url(self::CALLBACKURL);
489 $a = new stdClass;
490 $a->docsurl = get_docs_url('Google_OAuth_2.0_setup');
491 $a->callbackurl = $callbackurl->out(false);
493 $mform->addElement('static', null, '', get_string('oauthinfo', 'repository_googledocs', $a));
495 parent::type_config_form($mform);
496 $mform->addElement('text', 'clientid', get_string('clientid', 'repository_googledocs'));
497 $mform->setType('clientid', PARAM_RAW_TRIMMED);
498 $mform->addElement('text', 'secret', get_string('secret', 'repository_googledocs'));
499 $mform->setType('secret', PARAM_RAW_TRIMMED);
501 $strrequired = get_string('required');
502 $mform->addRule('clientid', $strrequired, 'required', null, 'client');
503 $mform->addRule('secret', $strrequired, 'required', null, 'client');
506 // Icon from: http://www.iconspedia.com/icon/google-2706.html.