2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
18 * This plugin is used to access user's dropbox files
21 * @package repository_dropbox
22 * @copyright 2012 Marina Glancy
23 * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 require_once($CFG->dirroot
. '/repository/lib.php');
27 require_once(dirname(__FILE__
).'/locallib.php');
30 * Repository to access Dropbox files
32 * @package repository_dropbox
33 * @copyright 2010 Dongsheng Cai
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 class repository_dropbox
extends repository
{
37 /** @var dropbox the instance of dropbox client */
39 /** @var array files */
41 /** @var bool flag of login status */
43 /** @var int maximum size of file to cache in moodle filepool */
44 public $cachelimit=null;
46 /** @var int cached file ttl */
47 private $cachedfilettl = null;
50 * Constructor of dropbox plugin
52 * @param int $repositoryid
53 * @param stdClass $context
54 * @param array $options
56 public function __construct($repositoryid, $context = SYSCONTEXTID
, $options = array()) {
58 $options['page'] = optional_param('p', 1, PARAM_INT
);
59 parent
::__construct($repositoryid, $context, $options);
61 $this->setting
= 'dropbox_';
63 $this->dropbox_key
= $this->get_option('dropbox_key');
64 $this->dropbox_secret
= $this->get_option('dropbox_secret');
67 $this->cachedfilettl
= 60 * 60 * 24;
69 if (isset($options['access_key'])) {
70 $this->access_key
= $options['access_key'];
72 $this->access_key
= get_user_preferences($this->setting
.'_access_key', '');
74 if (isset($options['access_secret'])) {
75 $this->access_secret
= $options['access_secret'];
77 $this->access_secret
= get_user_preferences($this->setting
.'_access_secret', '');
80 if (!empty($this->access_key
) && !empty($this->access_secret
)) {
84 $callbackurl = new moodle_url($CFG->wwwroot
.'/repository/repository_callback.php', array(
86 'repo_id'=>$repositoryid
90 'oauth_consumer_key'=>$this->dropbox_key
,
91 'oauth_consumer_secret'=>$this->dropbox_secret
,
92 'oauth_callback' => $callbackurl->out(false),
93 'api_root' => 'https://api.dropbox.com/1/oauth',
96 $this->dropbox
= new dropbox($args);
102 * @param string $access_key
104 public function set_access_key($access_key) {
105 $this->access_key
= $access_key;
111 * @param string $access_secret
113 public function set_access_secret($access_secret) {
114 $this->access_secret
= $access_secret;
119 * Check if moodle has got access token and secret
123 public function check_login() {
124 return !empty($this->logged
);
128 * Generate dropbox login url
132 public function print_login() {
133 $result = $this->dropbox
->request_token();
134 set_user_preference($this->setting
.'_request_secret', $result['oauth_token_secret']);
135 $url = $result['authorize_url'];
136 if ($this->options
['ajax']) {
138 $popup_btn = new stdClass();
139 $popup_btn->type
= 'popup';
140 $popup_btn->url
= $url;
141 $ret['login'] = array($popup_btn);
144 echo '<a target="_blank" href="'.$url.'">'.get_string('login', 'repository').'</a>';
149 * Request access token
153 public function callback() {
154 $token = optional_param('oauth_token', '', PARAM_TEXT
);
155 $secret = get_user_preferences($this->setting
.'_request_secret', '');
156 $access_token = $this->dropbox
->get_access_token($token, $secret);
157 set_user_preference($this->setting
.'_access_key', $access_token['oauth_token']);
158 set_user_preference($this->setting
.'_access_secret', $access_token['oauth_token_secret']);
164 * @param string $path
168 public function get_listing($path = '', $page = '1') {
170 if (empty($path) ||
$path=='/') {
173 $path = file_correct_filepath($path);
175 $encoded_path = str_replace("%2F", "/", rawurlencode($path));
178 $list['list'] = array();
179 $list['manage'] = 'https://www.dropbox.com/home';
180 $list['dynload'] = true;
181 $list['nosearch'] = true;
182 $list['logouturl'] = 'https://www.dropbox.com/logout';
183 $list['message'] = get_string('logoutdesc', 'repository_dropbox');
184 // process breadcrumb trail
185 $list['path'] = array(
186 array('name'=>get_string('dropbox', 'repository_dropbox'), 'path'=>'/')
189 $result = $this->dropbox
->get_listing($encoded_path, $this->access_key
, $this->access_secret
);
191 if (!is_object($result) ||
empty($result)) {
194 if (empty($result->path
)) {
197 $current_path = file_correct_filepath($result->path
);
202 $parts = explode('/', $path);
203 if (count($parts) > 1) {
204 foreach ($parts as $part) {
206 $trail .= ('/'.$part);
207 $list['path'][] = array('name'=>$part, 'path'=>$trail);
211 $list['path'][] = array('name'=>$path, 'path'=>$path);
215 if (!empty($result->error
)) {
217 set_user_preference($this->setting
.'_access_key', '');
218 set_user_preference($this->setting
.'_access_secret', '');
219 throw new repository_exception('repositoryerror', 'repository', '', $result->error
);
221 if (empty($result->contents
) or !is_array($result->contents
)) {
224 $files = $result->contents
;
226 $fileslist = array();
227 foreach ($files as $file) {
230 'title' => substr($file->path
, strpos($file->path
, $current_path)+
strlen($current_path)),
231 'path' => file_correct_filepath($file->path
),
232 'date' => strtotime($file->modified
),
233 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(64))->out(false),
234 'thumbnail_height' => 64,
235 'thumbnail_width' => 64,
236 'children' => array(),
240 if ($file->thumb_exists
) {
241 $thumburl = new moodle_url('/repository/dropbox/thumbnail.php',
242 array('repo_id' => $this->id
,
243 'ctx_id' => $this->context
->id
,
244 'source' => $file->path
,
245 'rev' => $file->rev
// include revision to avoid cache problems
247 $thumbnail = $thumburl->out(false);
249 $fileslist[] = array(
250 'title' => substr($file->path
, strpos($file->path
, $current_path)+
strlen($current_path)),
251 'source' => $file->path
,
252 'size' => $file->bytes
,
253 'date' => strtotime($file->modified
),
254 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file->path
, 64))->out(false),
255 'realthumbnail' => $thumbnail,
256 'thumbnail_height' => 64,
257 'thumbnail_width' => 64,
261 $fileslist = array_filter($fileslist, array($this, 'filter'));
262 $list['list'] = array_merge($dirslist, array_values($fileslist));
267 * Displays a thumbnail for current user's dropbox file
269 * @param string $string
271 public function send_thumbnail($source) {
272 $saveas = $this->prepare_file('');
274 $access_key = get_user_preferences($this->setting
.'_access_key', '');
275 $access_secret = get_user_preferences($this->setting
.'_access_secret', '');
276 $this->dropbox
->set_access_token($access_key, $access_secret);
277 $this->dropbox
->get_thumbnail($source, $saveas, self
::SYNCIMAGE_TIMEOUT
);
278 $content = file_get_contents($saveas);
280 // set 30 days lifetime for the image. If the image is changed in dropbox it will have
281 // different revision number and URL will be different. It is completely safe
282 // to cache thumbnail in the browser for a long time
283 send_file($content, basename($source), 30*24*60*60, 0, true);
284 } catch (Exception
$e) {}
288 * Logout from dropbox
291 public function logout() {
292 set_user_preference($this->setting
.'_access_key', '');
293 set_user_preference($this->setting
.'_access_secret', '');
294 $this->access_key
= '';
295 $this->access_secret
= '';
296 return $this->print_login();
301 * @param array $options
304 public function set_option($options = array()) {
305 if (!empty($options['dropbox_key'])) {
306 set_config('dropbox_key', trim($options['dropbox_key']), 'dropbox');
308 if (!empty($options['dropbox_secret'])) {
309 set_config('dropbox_secret', trim($options['dropbox_secret']), 'dropbox');
311 if (!empty($options['dropbox_cachelimit'])) {
312 $this->cachelimit
= (int)trim($options['dropbox_cachelimit']);
313 set_config('dropbox_cachelimit', $this->cachelimit
, 'dropbox');
315 unset($options['dropbox_key']);
316 unset($options['dropbox_secret']);
317 unset($options['dropbox_cachelimit']);
318 $ret = parent
::set_option($options);
323 * Get dropbox options
324 * @param string $config
327 public function get_option($config = '') {
328 if ($config==='dropbox_key') {
329 return trim(get_config('dropbox', 'dropbox_key'));
330 } elseif ($config==='dropbox_secret') {
331 return trim(get_config('dropbox', 'dropbox_secret'));
332 } elseif ($config==='dropbox_cachelimit') {
333 return $this->max_cache_bytes();
335 $options = parent
::get_option();
336 $options['dropbox_key'] = trim(get_config('dropbox', 'dropbox_key'));
337 $options['dropbox_secret'] = trim(get_config('dropbox', 'dropbox_secret'));
338 $options['dropbox_cachelimit'] = $this->max_cache_bytes();
344 * Fixes references in DB that contains user credentials
346 * @param string $reference contents of DB field files_reference.reference
348 public function fix_old_style_reference($reference) {
349 $ref = unserialize($reference);
350 if (!isset($ref->url
)) {
351 $this->dropbox
->set_access_token($ref->access_key
, $ref->access_secret
);
352 $ref->url
= $this->dropbox
->get_file_share_link($ref->path
, self
::GETFILE_TIMEOUT
);
354 // some error occurred, do not fix reference for now
358 unset($ref->access_key
);
359 unset($ref->access_secret
);
360 $newreference = serialize($ref);
361 if ($newreference !== $reference) {
362 // we need to update references in the database
365 'newreference' => $newreference,
366 'newhash' => sha1($newreference),
367 'reference' => $reference,
368 'hash' => sha1($reference),
369 'repoid' => $this->id
371 $refid = $DB->get_field_sql('SELECT id FROM {files_reference}
372 WHERE reference = :reference AND referencehash = :hash
373 AND repositoryid = :repoid', $params);
375 return $newreference;
377 $existingrefid = $DB->get_field_sql('SELECT id FROM {files_reference}
378 WHERE reference = :newreference AND referencehash = :newhash
379 AND repositoryid = :repoid', $params);
380 if ($existingrefid) {
381 // the same reference already exists, we unlink all files from it,
382 // link them to the current reference and remove the old one
383 $DB->execute('UPDATE {files} SET referencefileid = :refid
384 WHERE referencefileid = :existingrefid',
385 array('refid' => $refid, 'existingrefid' => $existingrefid));
386 $DB->delete_records('files_reference', array('id' => $existingrefid));
388 // update the reference
389 $params['refid'] = $refid;
390 $DB->execute('UPDATE {files_reference}
391 SET reference = :newreference, referencehash = :newhash
392 WHERE id = :refid', $params);
394 return $newreference;
398 * Converts a URL received from dropbox API function 'shares' into URL that
399 * can be used to download/access file directly
401 * @param string $sharedurl
404 private function get_file_download_link($sharedurl) {
405 return preg_replace('|^(\w*://)www(.dropbox.com)|','\1dl\2',$sharedurl);
409 * Downloads a file from external repository and saves it in temp dir
411 * @throws moodle_exception when file could not be downloaded
413 * @param string $reference the content of files.reference field or result of
414 * function {@link repository_dropbox::get_file_reference()}
415 * @param string $saveas filename (without path) to save the downloaded file in the
416 * temporary directory, if omitted or file already exists the new filename will be generated
417 * @return array with elements:
418 * path: internal location of the file
419 * url: URL to the source (from parameters)
421 public function get_file($reference, $saveas = '') {
422 $ref = unserialize($reference);
423 $saveas = $this->prepare_file($saveas);
424 if (isset($ref->access_key
) && isset($ref->access_secret
) && isset($ref->path
)) {
425 $this->dropbox
->set_access_token($ref->access_key
, $ref->access_secret
);
426 return $this->dropbox
->get_file($ref->path
, $saveas, self
::GETFILE_TIMEOUT
);
427 } else if (isset($ref->url
)) {
429 $url = $this->get_file_download_link($ref->url
);
430 $result = $c->download_one($url, null, array('filepath' => $saveas, 'timeout' => self
::GETFILE_TIMEOUT
, 'followlocation' => true));
431 $info = $c->get_info();
432 if ($result !== true ||
!isset($info['http_code']) ||
$info['http_code'] != 200) {
433 throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
435 return array('path'=>$saveas, 'url'=>$url);
437 throw new moodle_exception('cannotdownload', 'repository');
440 * Add Plugin settings input to Moodle form
442 * @param moodleform $mform Moodle form (passed by reference)
443 * @param string $classname repository class name
445 public static function type_config_form($mform, $classname = 'repository') {
447 parent
::type_config_form($mform);
448 $key = get_config('dropbox', 'dropbox_key');
449 $secret = get_config('dropbox', 'dropbox_secret');
454 if (empty($secret)) {
458 $strrequired = get_string('required');
460 $mform->addElement('text', 'dropbox_key', get_string('apikey', 'repository_dropbox'), array('value'=>$key,'size' => '40'));
461 $mform->addElement('text', 'dropbox_secret', get_string('secret', 'repository_dropbox'), array('value'=>$secret,'size' => '40'));
463 $mform->addRule('dropbox_key', $strrequired, 'required', null, 'client');
464 $mform->addRule('dropbox_secret', $strrequired, 'required', null, 'client');
465 $str_getkey = get_string('instruction', 'repository_dropbox');
466 $mform->addElement('static', null, '', $str_getkey);
468 $mform->addElement('text', 'dropbox_cachelimit', get_string('cachelimit', 'repository_dropbox'), array('size' => '40'));
469 $mform->addRule('dropbox_cachelimit', null, 'numeric', null, 'client');
470 $mform->setType('dropbox_cachelimit', PARAM_INT
);
471 $mform->addElement('static', 'dropbox_cachelimit_info', '', get_string('cachelimit_info', 'repository_dropbox'));
475 * Option names of dropbox plugin
479 public static function get_type_option_names() {
480 return array('dropbox_key', 'dropbox_secret', 'pluginname', 'dropbox_cachelimit');
484 * Dropbox plugin supports all kinds of files
488 public function supported_filetypes() {
493 * User cannot use the external link to dropbox
497 public function supported_returntypes() {
498 return FILE_INTERNAL | FILE_REFERENCE | FILE_EXTERNAL
;
502 * Return file URL for external link
504 * @param string $reference the result of get_file_reference()
507 public function get_link($reference) {
508 $ref = unserialize($reference);
509 if (!isset($ref->url
)) {
510 $this->dropbox
->set_access_token($ref->access_key
, $ref->access_secret
);
511 $ref->url
= $this->dropbox
->get_file_share_link($ref->path
, self
::GETFILE_TIMEOUT
);
513 return $this->get_file_download_link($ref->url
);
517 * Prepare file reference information
519 * @param string $source
520 * @return string file referece
522 public function get_file_reference($source) {
524 $reference = new stdClass
;
525 $reference->path
= $source;
526 $reference->userid
= $USER->id
;
527 $reference->username
= fullname($USER);
528 $reference->access_key
= get_user_preferences($this->setting
.'_access_key', '');
529 $reference->access_secret
= get_user_preferences($this->setting
.'_access_secret', '');
531 // by API we don't know if we need this reference to just download a file from dropbox
532 // into moodle filepool or create a reference. Since we need to create a shared link
533 // only in case of reference we analyze the script parameter
534 $usefilereference = optional_param('usefilereference', false, PARAM_BOOL
);
535 if ($usefilereference) {
536 $this->dropbox
->set_access_token($reference->access_key
, $reference->access_secret
);
537 $url = $this->dropbox
->get_file_share_link($source, self
::GETFILE_TIMEOUT
);
539 unset($reference->access_key
);
540 unset($reference->access_secret
);
541 $reference->url
= $url;
544 return serialize($reference);
548 * Returns information about file in this repository by reference
549 * {@link repository::get_file_reference()}
550 * {@link repository::get_file()}
552 * Returns null if file not found or is not readable
554 * @param stdClass $reference file reference db record
555 * @return null|stdClass that has 'filepath' property
557 public function get_file_by_reference($reference) {
559 $ref = unserialize($reference->reference
);
560 if (!isset($ref->url
)) {
561 // this is an old-style reference in DB. We need to fix it
562 $ref = unserialize($this->fix_old_style_reference($reference->reference
));
564 if (!isset($ref->url
)) {
568 $url = $this->get_file_download_link($ref->url
);
569 if (file_extension_in_typegroup($ref->path
, 'web_image')) {
570 $saveas = $this->prepare_file('');
572 $result = $c->download_one($url, array(), array('filepath' => $saveas, 'timeout' => self
::SYNCIMAGE_TIMEOUT
, 'followlocation' => true));
573 $info = $c->get_info();
574 if ($result === true && isset($info['http_code']) && $info['http_code'] == 200) {
575 return (object)array('filepath' => $saveas);
577 } catch (Exception
$e) {}
579 $c->get($url, null, array('timeout' => self
::SYNCIMAGE_TIMEOUT
, 'followlocation' => true, 'nobody' => true));
580 $info = $c->get_info();
581 if (isset($info['http_code']) && $info['http_code'] == 200 &&
582 array_key_exists('download_content_length', $info) &&
583 $info['download_content_length'] >= 0) {
584 return (object)array('filesize' => (int)$info['download_content_length']);
590 * Cache file from external repository by reference
592 * Dropbox repository regularly caches all external files that are smaller than
593 * {@link repository_dropbox::max_cache_bytes()}
595 * @param string $reference this reference is generated by
596 * repository::get_file_reference()
597 * @param stored_file $storedfile created file reference
599 public function cache_file_by_reference($reference, $storedfile) {
601 $this->import_external_file_contents($storedfile, $this->max_cache_bytes());
602 } catch (Exception
$e) {}
606 * Return human readable reference information
607 * {@link stored_file::get_reference()}
609 * @param string $reference
610 * @param int $filestatus status of the file, 0 - ok, 666 - source missing
613 public function get_reference_details($reference, $filestatus = 0) {
615 $ref = unserialize($reference);
616 $detailsprefix = $this->get_name();
617 if (isset($ref->userid
) && $ref->userid
!= $USER->id
&& isset($ref->username
)) {
618 $detailsprefix .= ' ('.$ref->username
.')';
620 $details = $detailsprefix;
621 if (isset($ref->path
)) {
622 $details .= ': '. $ref->path
;
624 if (isset($ref->path
) && !$filestatus) {
625 // Indicate this is from dropbox with path
628 if (isset($ref->url
)) {
629 $details = $detailsprefix. ': '. $ref->url
;
631 return get_string('lostsource', 'repository', $details);
636 * Return the source information
638 * @param string $source
641 public function get_file_source_info($source) {
643 return 'Dropbox ('.fullname($USER).'): ' . $source;
647 * Returns the maximum size of the Dropbox files to cache in moodle
649 * Note that {@link repository_dropbox::get_file_by_reference()} called by
650 * {@link repository::sync_external_file()} will try to cache images even
651 * when they are bigger in order to generate thumbnails. However there is
652 * a small timeout for downloading images for synchronisation and it will
653 * probably fail if the image is too big.
657 public function max_cache_bytes() {
658 if ($this->cachelimit
=== null) {
659 $this->cachelimit
= (int)get_config('dropbox', 'dropbox_cachelimit');
661 return $this->cachelimit
;
665 * Repository method to serve the referenced file
667 * This method is ivoked from {@link send_stored_file()}.
668 * Dropbox repository first caches the file by reading it into temporary folder and then
671 * @param stored_file $storedfile the file that contains the reference
672 * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours)
673 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
674 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
675 * @param array $options additional options affecting the file serving
677 public function send_file($storedfile, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) {
678 $ref = unserialize($storedfile->get_reference());
679 if ($storedfile->get_filesize() > $this->max_cache_bytes()) {
680 header('Location: '.$this->get_file_download_link($ref->url
));
684 $this->import_external_file_contents($storedfile, $this->max_cache_bytes());
685 if (!is_array($options)) {
688 $options['sendcachedexternalfile'] = true;
689 send_stored_file($storedfile, $lifetime, $filter, $forcedownload, $options);
690 } catch (moodle_exception
$e) {
691 // redirect to Dropbox, it will show the error.
692 // We redirect to Dropbox shared link, not to download link here!
693 header('Location: '.$ref->url
);
699 * Caches all references to Dropbox files in moodle filepool
701 * Invoked by {@link repository_dropbox_cron()}. Only files smaller than
702 * {@link repository_dropbox::max_cache_bytes()} and only files which
703 * synchronisation timeout have not expired are cached.
705 public function cron() {
706 $fs = get_file_storage();
707 $files = $fs->get_external_files($this->id
);
708 foreach ($files as $file) {
710 // This call will cache all files that are smaller than max_cache_bytes()
711 // and synchronise file size of all others
712 $this->import_external_file_contents($file, $this->max_cache_bytes());
713 } catch (moodle_exception
$e) {}
719 * Dropbox plugin cron task
721 function repository_dropbox_cron() {
722 $instances = repository
::get_instances(array('type'=>'dropbox'));
723 foreach ($instances as $instance) {