16
15
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18
* This plugin is used to access Google Drive.
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 . '/textlib.class.php');
30
require_once($CFG->libdir . '/google/Google_Client.php');
31
require_once($CFG->libdir . '/google/contrib/Google_DriveService.php');
19
34
* Google Docs Plugin
23
* @subpackage googledocs
37
* @package repository_googledocs
24
38
* @copyright 2009 Dan Poltawski <talktodan@gmail.com>
25
39
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28
require_once($CFG->libdir.'/googleapi.php');
30
41
class repository_googledocs extends repository {
31
private $subauthtoken = '';
33
public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
35
parent::__construct($repositoryid, $context, $options);
37
// TODO: I wish there was somewhere we could explicitly put this outside of constructor..
38
$googletoken = optional_param('token', false, PARAM_RAW);
40
$gauth = new google_authsub(false, $googletoken); // will throw exception if fails
41
google_docs::set_sesskey($gauth->get_sessiontoken(), $USER->id);
47
private $client = null;
50
* Google Drive Service.
51
* @var Google_DriveService
53
private $service = null;
56
* Session key to store the accesstoken.
59
const SESSIONKEY = 'googledrive_accesstoken';
62
* URI to the callback file for OAuth.
65
const CALLBACKURL = '/admin/oauth2callback.php';
70
* @param int $repositoryid repository instance id.
71
* @param int|stdClass $context a context id or context object.
72
* @param array $options repository options.
73
* @param int $readonly indicate this repo is readonly or not.
76
public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array(), $readonly = 0) {
77
parent::__construct($repositoryid, $context, $options, $readonly = 0);
79
$callbackurl = new moodle_url(self::CALLBACKURL);
81
$this->client = new Google_Client();
82
$this->client->setClientId(get_config('googledocs', 'clientid'));
83
$this->client->setClientSecret(get_config('googledocs', 'secret'));
84
$this->client->setScopes(array('https://www.googleapis.com/auth/drive.readonly'));
85
$this->client->setRedirectUri($callbackurl->out(false));
86
$this->service = new Google_DriveService($this->client);
43
88
$this->check_login();
92
* Returns the access token if any.
94
* @return string|null access token.
96
protected function get_access_token() {
98
if (isset($SESSION->{self::SESSIONKEY})) {
99
return $SESSION->{self::SESSIONKEY};
105
* Store the access token in the session.
107
* @param string $token token to store.
110
protected function store_access_token($token) {
112
$SESSION->{self::SESSIONKEY} = $token;
116
* Callback method during authentication.
120
public function callback() {
121
if ($code = optional_param('oauth2code', null, PARAM_RAW)) {
122
$this->client->authenticate($code);
123
$this->store_access_token($this->client->getAccessToken());
128
* Checks whether the user is authenticate or not.
130
* @return bool true when logged in.
46
132
public function check_login() {
49
$sesskey = google_docs::get_sesskey($USER->id);
53
$gauth = new google_authsub($sesskey);
54
$this->subauthtoken = $sesskey;
57
// sesskey is not valid, delete store and re-auth
58
google_docs::delete_sesskey($USER->id);
133
if ($token = $this->get_access_token()) {
134
$this->client->setAccessToken($token);
65
public function print_login($ajax = true){
69
$popup_btn = new stdClass();
70
$popup_btn->type = 'popup';
71
$returnurl = $CFG->wwwroot.'/repository/repository_callback.php?callback=yes&repo_id='.$this->id;
72
$popup_btn->url = google_authsub::login_url($returnurl, google_docs::REALM);
73
$ret['login'] = array($popup_btn);
141
* Print or return the login form.
143
* @return void|array for ajax.
145
public function print_login() {
146
$returnurl = new moodle_url('/repository/repository_callback.php');
147
$returnurl->param('callback', 'yes');
148
$returnurl->param('repo_id', $this->id);
149
$returnurl->param('sesskey', sesskey());
151
$url = new moodle_url($this->client->createAuthUrl());
152
$url->param('state', $returnurl->out_as_local_url(false));
153
if ($this->options['ajax']) {
154
$popup = new stdClass();
155
$popup->type = 'popup';
156
$popup->url = $url->out(false);
157
return array('login' => array($popup));
159
echo '<a target="_blank" href="'.$url->out(false).'">'.get_string('login', 'repository').'</a>';
164
* Build the breadcrumb from a path.
166
* @param string $path to create a breadcrumb from.
167
* @return array containing name and path of each crumb.
169
protected function build_breadcrumb($path) {
170
$bread = explode('/', $path);
172
foreach ($bread as $crumb) {
173
list($id, $name) = $this->explode_node_path($crumb);
174
$name = empty($name) ? $id : $name;
175
$breadcrumb[] = array(
177
'path' => $this->build_node_path($id, $name, $crumbtrail)
179
$tmp = end($breadcrumb);
180
$crumbtrail = $tmp['path'];
186
* Generates a safe path to a node.
188
* Typically, a node will be id|Name of the node.
190
* @param string $id of the node.
191
* @param string $name of the node, will be URL encoded.
192
* @param string $root to append the node on, must be a result of this function.
193
* @return string path to the node.
195
protected function build_node_path($id, $name = '', $root = '') {
198
$path .= '|' . urlencode($name);
201
$path = trim($root, '/') . '/' . $path;
207
* Returns information about a node in a path.
209
* @see self::build_node_path()
210
* @param string $node to extrat information from.
211
* @return array about the node.
213
protected function explode_node_path($node) {
214
if (strpos($node, '|') !== false) {
215
list($id, $name) = explode('|', $node, 2);
216
$name = urldecode($name);
221
$id = urldecode($id);
232
* List the files and folders.
234
* @param string $path path to browse.
235
* @param string $page page to browse.
236
* @return array of result.
78
238
public function get_listing($path='', $page = '') {
79
$gdocs = new google_docs(new google_authsub($this->subauthtoken));
82
$ret['dynload'] = true;
83
$ret['list'] = $gdocs->get_file_list();
87
public function search($query){
88
$gdocs = new google_docs(new google_authsub($this->subauthtoken));
91
$ret['dynload'] = true;
92
$ret['list'] = $gdocs->get_file_list($query);
96
public function logout(){
99
$token = google_docs::get_sesskey($USER->id);
101
$gauth = new google_authsub($token);
102
// revoke token from google
103
$gauth->revoke_session_token();
105
google_docs::delete_sesskey($USER->id);
106
$this->subauthtoken = '';
240
$path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
243
// We analyse the path to extract what to browse.
244
$trail = explode('/', $path);
245
$uri = array_pop($trail);
246
list($id, $name) = $this->explode_node_path($uri);
248
// Handle the special keyword 'search', which we defined in self::search() so that
249
// we could set up a breadcrumb in the search results. In any other case ID would be
250
// 'root' which is a special keyword set up by Google, or a parent (folder) ID.
251
if ($id === 'search') {
252
return $this->search($name);
256
$q = "'" . str_replace("'", "\'", $id) . "' in parents";
257
$q .= ' AND trashed = false';
258
$results = $this->query($q, $path);
261
$ret['dynload'] = true;
262
$ret['path'] = $this->build_breadcrumb($path);
263
$ret['list'] = $results;
268
* Search throughout the Google Drive.
270
* @param string $search_text text to search for.
271
* @param int $page search page.
272
* @return array of results.
274
public function search($search_text, $page = 0) {
275
$path = $this->build_node_path('root', get_string('pluginname', 'repository_googledocs'));
276
$path = $this->build_node_path('search', $search_text, $path);
279
$q = "fullText contains '" . str_replace("'", "\'", $search_text) . "'";
280
$q .= ' AND trashed = false';
281
$results = $this->query($q, $path);
284
$ret['dynload'] = true;
285
$ret['path'] = $this->build_breadcrumb($path);
286
$ret['list'] = $results;
291
* Query Google Drive for files and folders using a search query.
293
* Documentation about the query format can be found here:
294
* https://developers.google.com/drive/search-parameters
296
* This returns a list of files and folders with their details as they should be
297
* formatted and returned by functions such as get_listing() or search().
299
* @param string $q search query as expected by the Google API.
300
* @param string $path parent path of the current files, will not be used for the query.
301
* @param int $page page.
302
* @return array of files and folders.
304
protected function query($q, $path = null, $page = 0) {
309
$fields = "items(id,title,mimeType,downloadUrl,fileExtension,exportLinks,modifiedDate,fileSize,thumbnailLink)";
310
$params = array('q' => $q, 'fields' => $fields);
313
// Retrieving files and folders.
314
$response = $this->service->files->listFiles($params);
315
} catch (Google_ServiceException $e) {
316
if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
317
// This is raised when the service Drive API has not been enabled on Google APIs control panel.
318
throw new repository_exception('servicenotenabled', 'repository_googledocs');
324
$items = isset($response['items']) ? $response['items'] : array();
325
foreach ($items as $item) {
326
if ($item['mimeType'] == 'application/vnd.google-apps.folder') {
328
$folders[$item['title'] . $item['id']] = array(
329
'title' => $item['title'],
330
'path' => $this->build_node_path($item['id'], $item['title'], $path),
331
'date' => strtotime($item['modifiedDate']),
332
'thumbnail' => $OUTPUT->pix_url(file_folder_icon(64))->out(false),
333
'thumbnail_height' => 64,
334
'thumbnail_width' => 64,
335
'children' => array()
339
if (isset($item['fileExtension'])) {
340
// The file has an extension, therefore there is a download link.
341
$title = $item['title'];
342
$source = $item['downloadUrl'];
344
// The file is probably a Google Doc file, we get the corresponding export link.
345
// This should be improved by allowing the user to select the type of export they'd like.
346
$type = str_replace('application/vnd.google-apps.', '', $item['mimeType']);
351
$title = $item['title'] . '.rtf';
352
$exportType = 'application/rtf';
355
$title = $item['title'] . '.pptx';
356
$exportType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
359
$title = $item['title'] . '.xlsx';
360
$exportType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
363
// Skips invalid/unknown types.
364
if (empty($title) || !isset($item['exportLinks'][$exportType])) {
367
$source = $item['exportLinks'][$exportType];
369
// Adds the file to the file list. Using the itemId along with the title as key
370
// of the array because Google Drive allows files with identical names.
371
$files[$title . $item['id']] = array(
374
'date' => strtotime($item['modifiedDate']),
375
'size' => isset($item['fileSize']) ? $item['fileSize'] : null,
376
'thumbnail' => $OUTPUT->pix_url(file_extension_icon($title, 64))->out(false),
377
'thumbnail_height' => 64,
378
'thumbnail_width' => 64,
379
// Do not use real thumbnails as they wouldn't work if the user disabled 3rd party
380
// plugins in his browser, or if they're not logged in their Google account.
383
// Sometimes the real thumbnails can't be displayed, for example if 3rd party cookies are disabled
384
// or if the user is not logged in Google anymore. But this restriction does not seem to be applied
385
// to a small subset of files.
386
$extension = strtolower(pathinfo($title, PATHINFO_EXTENSION));
387
if (isset($item['thumbnailLink']) && in_array($extension, array('jpg', 'png', 'txt', 'pdf'))) {
388
$files[$title . $item['id']]['realthumbnail'] = $item['thumbnailLink'];
393
// Filter and order the results.
394
$files = array_filter($files, array($this, 'filter'));
395
collatorlib::ksort($files, collatorlib::SORT_NATURAL);
396
collatorlib::ksort($folders, collatorlib::SORT_NATURAL);
397
return array_merge(array_values($folders), array_values($files));
405
public function logout() {
406
$this->store_access_token(null);
108
407
return parent::logout();
111
public function get_file($url, $file) {
113
throw new repository_exception('cannotdownload', 'repository');
413
* @param string $reference reference of the file.
414
* @param string $file name to save the file to.
415
* @return string JSON encoded array of information about the file.
417
public function get_file($reference, $filename = '') {
418
$request = new Google_HttpRequest($reference);
419
$httpRequest = Google_Client::$io->authenticatedRequest($request);
420
if ($httpRequest->getResponseHttpCode() == 200) {
421
$path = $this->prepare_file($filename);
422
$content = $httpRequest->getResponseBody();
423
if (file_put_contents($path, $content) !== false) {
115
$path = $this->prepare_file($file);
117
$fp = fopen($path, 'w');
118
$gdocs = new google_docs(new google_authsub($this->subauthtoken));
119
$gdocs->download_file($url, $fp);
121
return array('path'=>$path, 'url'=>$url);
430
throw new repository_exception('cannotdownload', 'repository');
434
* Prepare file reference information.
436
* We are using this method to clean up the source to make sure that it
439
* @param string $source of the file.
440
* @return string file reference.
442
public function get_file_reference($source) {
443
return clean_param($source, PARAM_URL);
447
* What kind of files will be in this repository?
449
* @return array return '*' means this repository support any files, otherwise
450
* return mimetypes of files, it can be an array
124
452
public function supported_filetypes() {
125
return array('document');
457
* Tells how the file can be picked from this repository.
459
* Maximum value is FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE.
127
463
public function supported_returntypes() {
128
464
return FILE_INTERNAL;
468
* Return names of the general options.
469
* By default: no general option name.
473
public static function get_type_option_names() {
474
return array('clientid', 'secret', 'pluginname');
478
* Edit/Create Admin Settings Moodle form.
480
* @param moodleform $mform Moodle form (passed by reference).
481
* @param string $classname repository class name.
483
public static function type_config_form($mform, $classname = 'repository') {
485
$callbackurl = new moodle_url(self::CALLBACKURL);
488
$a->docsurl = get_docs_url('Google_OAuth_2.0_setup');
489
$a->callbackurl = $callbackurl->out(false);
491
$mform->addElement('static', null, '', get_string('oauthinfo', 'repository_googledocs', $a));
493
parent::type_config_form($mform);
494
$mform->addElement('text', 'clientid', get_string('clientid', 'repository_googledocs'));
495
$mform->setType('clientid', PARAM_RAW_TRIMMED);
496
$mform->addElement('text', 'secret', get_string('secret', 'repository_googledocs'));
497
$mform->setType('secret', PARAM_RAW_TRIMMED);
499
$strrequired = get_string('required');
500
$mform->addRule('clientid', $strrequired, 'required', null, 'client');
501
$mform->addRule('secret', $strrequired, 'required', null, 'client');
131
//Icon from: http://www.iconspedia.com/icon/google-2706.html
504
// Icon from: http://www.iconspedia.com/icon/google-2706.html.