MDL-47297 repository_googledocs: Use the newer Google API
[moodle.git] / repository / googledocs / lib.php
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.
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/>.
17 /**
18  * This plugin is used to access Google Drive.
19  *
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
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->dirroot . '/repository/lib.php');
29 require_once($CFG->libdir . '/google/lib.php');
30 require_once($CFG->libdir . '/google/Google/Service/Drive.php');
32 /**
33  * Google Docs Plugin
34  *
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
39  */
40 class repository_googledocs extends repository {
42     /**
43      * Google Client.
44      * @var Google_Client
45      */
46     private $client = null;
48     /**
49      * Google Drive Service.
50      * @var Google_Drive_Service
51      */
52     private $service = null;
54     /**
55      * Session key to store the accesstoken.
56      * @var string
57      */
58     const SESSIONKEY = 'googledrive_accesstoken';
60     /**
61      * URI to the callback file for OAuth.
62      * @var string
63      */
64     const CALLBACKURL = '/admin/oauth2callback.php';
66     /**
67      * Constructor.
68      *
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
74      */
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 = get_google_client();
81         $this->client->setClientId(get_config('googledocs', 'clientid'));
82         $this->client->setClientSecret(get_config('googledocs', 'secret'));
83         $this->client->setScopes(array(Google_Service_Drive::DRIVE_READONLY));
84         $this->client->setRedirectUri($callbackurl->out(false));
85         $this->service = new Google_Service_Drive($this->client);
87         $this->check_login();
88     }
90     /**
91      * Returns the access token if any.
92      *
93      * @return string|null access token.
94      */
95     protected function get_access_token() {
96         global $SESSION;
97         if (isset($SESSION->{self::SESSIONKEY})) {
98             return $SESSION->{self::SESSIONKEY};
99         }
100         return null;
101     }
103     /**
104      * Store the access token in the session.
105      *
106      * @param string $token token to store.
107      * @return void
108      */
109     protected function store_access_token($token) {
110         global $SESSION;
111         $SESSION->{self::SESSIONKEY} = $token;
112     }
114     /**
115      * Callback method during authentication.
116      *
117      * @return void
118      */
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());
123         }
124     }
126     /**
127      * Checks whether the user is authenticate or not.
128      *
129      * @return bool true when logged in.
130      */
131     public function check_login() {
132         if ($token = $this->get_access_token()) {
133             $this->client->setAccessToken($token);
134             return true;
135         }
136         return false;
137     }
139     /**
140      * Print or return the login form.
141      *
142      * @return void|array for ajax.
143      */
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>';
159         }
160     }
162     /**
163     * Build the breadcrumb from a path.
164     *
165     * @param string $path to create a breadcrumb from.
166     * @return array containing name and path of each crumb.
167     */
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)
177             );
178             $tmp = end($breadcrumb);
179             $crumbtrail = $tmp['path'];
180         }
181         return $breadcrumb;
182     }
184     /**
185     * Generates a safe path to a node.
186     *
187     * Typically, a node will be id|Name of the node.
188     *
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.
193     */
194     protected function build_node_path($id, $name = '', $root = '') {
195         $path = $id;
196         if (!empty($name)) {
197             $path .= '|' . urlencode($name);
198         }
199         if (!empty($root)) {
200             $path = trim($root, '/') . '/' . $path;
201         }
202         return $path;
203     }
205     /**
206     * Returns information about a node in a path.
207     *
208     * @see self::build_node_path()
209     * @param string $node to extrat information from.
210     * @return array about the node.
211     */
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 = '';
219         }
220         $id = urldecode($id);
221         return array(
222             0 => $id,
223             1 => $name,
224             'id' => $id,
225             'name' => $name
226         );
227     }
230     /**
231      * List the files and folders.
232      *
233      * @param  string $path path to browse.
234      * @param  string $page page to browse.
235      * @return array of result.
236      */
237     public function get_listing($path='', $page = '') {
238         if (empty($path)) {
239             $path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
240         }
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);
252         }
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;
264     }
266     /**
267      * Search throughout the Google Drive.
268      *
269      * @param string $search_text text to search for.
270      * @param int $page search page.
271      * @return array of results.
272      */
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;
287     }
289     /**
290      * Query Google Drive for files and folders using a search query.
291      *
292      * Documentation about the query format can be found here:
293      *   https://developers.google.com/drive/search-parameters
294      *
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().
297      *
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.
302      */
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_Service_Exception $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;
320             }
321         }
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()
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 = '';
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;
361                     }
362                     // Skips invalid/unknown types.
363                     if (empty($title) || !isset($item['exportLinks'][$exportType])) {
364                         continue;
365                     }
366                     $source = $item['exportLinks'][$exportType];
367                 }
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.
380                 );
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'];
388                 }
389             }
390         }
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));
397     }
399     /**
400      * Logout.
401      *
402      * @return string
403      */
404     public function logout() {
405         $this->store_access_token(null);
406         return parent::logout();
407     }
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 = '') {
417         global $CFG;
419         $auth = $this->client->getAuth();
420         $request = $auth->authenticatedRequest(new Google_Http_Request($reference));
421         if ($request->getResponseHttpCode() == 200) {
422             $path = $this->prepare_file($filename);
423             $content = $request->getResponseBody();
424             if (file_put_contents($path, $content) !== false) {
425                 @chmod($path, $CFG->filepermissions);
426                 return array(
427                     'path' => $path,
428                     'url' => $reference
429                 );
430             }
431         }
432         throw new repository_exception('cannotdownload', 'repository');
433     }
435     /**
436      * Prepare file reference information.
437      *
438      * We are using this method to clean up the source to make sure that it
439      * is a valid source.
440      *
441      * @param string $source of the file.
442      * @return string file reference.
443      */
444     public function get_file_reference($source) {
445         return clean_param($source, PARAM_URL);
446     }
448     /**
449      * What kind of files will be in this repository?
450      *
451      * @return array return '*' means this repository support any files, otherwise
452      *               return mimetypes of files, it can be an array
453      */
454     public function supported_filetypes() {
455         return '*';
456     }
458     /**
459      * Tells how the file can be picked from this repository.
460      *
461      * Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE.
462      *
463      * @return int
464      */
465     public function supported_returntypes() {
466         return FILE_INTERNAL;
467     }
469     /**
470      * Return names of the general options.
471      * By default: no general option name.
472      *
473      * @return array
474      */
475     public static function get_type_option_names() {
476         return array('clientid', 'secret', 'pluginname');
477     }
479     /**
480      * Edit/Create Admin Settings Moodle form.
481      *
482      * @param moodleform $mform Moodle form (passed by reference).
483      * @param string $classname repository class name.
484      */
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');
504     }
506 // Icon from: http://www.iconspedia.com/icon/google-2706.html.