MDL-59700 filestorage: Rework repositories to avoid add_file_to_pool
[moodle.git] / repository / boxnet / 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 box.net repository
19  *
20  * @since Moodle 2.0
21  * @package    repository_boxnet
22  * @copyright  2010 Dongsheng Cai {@link http://dongsheng.org}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
25 require_once($CFG->dirroot . '/repository/lib.php');
26 require_once($CFG->libdir . '/boxlib.php');
28 /**
29  * repository_boxnet class implements box.net client
30  *
31  * @since Moodle 2.0
32  * @package    repository_boxnet
33  * @copyright  2010 Dongsheng Cai {@link http://dongsheng.org}
34  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
36 class repository_boxnet extends repository {
38     /** @const MANAGE_URL Manage URL. */
39     const MANAGE_URL = 'https://app.box.com/files';
41     /** @const SESSION_PREFIX Key used to store information in the session. */
42     const SESSION_PREFIX = 'repository_boxnet';
44     /** @var string Client ID */
45     protected $clientid;
47     /** @var string Client secret */
48     protected $clientsecret;
50     /** @var string Access token */
51     protected $accesstoken;
53     /** @var object Box.net object */
54     protected $boxnetclient;
56     /**
57      * Constructor
58      *
59      * @param int $repositoryid
60      * @param stdClass $context
61      * @param array $options
62      */
63     public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
64         parent::__construct($repositoryid, $context, $options);
66         $clientid = get_config('boxnet', 'clientid');
67         $clientsecret = get_config('boxnet', 'clientsecret');
68         $returnurl = new moodle_url('/repository/repository_callback.php');
69         $returnurl->param('callback', 'yes');
70         $returnurl->param('repo_id', $this->id);
71         $returnurl->param('sesskey', sesskey());
73         $this->boxnetclient = new boxnet_client($clientid, $clientsecret, $returnurl, '');
74     }
76     /**
77      * Construct a breadcrumb from a path.
78      *
79      * @param string $fullpath Path containing multiple parts separated by slashes.
80      * @return array Array expected to be generated in {@link self::get_listing()}.
81      */
82     protected function build_breadcrumb($fullpath) {
83         $breadcrumb = array(array(
84             'name' => get_string('pluginname', 'repository_boxnet'),
85             'path' => ''
86         ));
87         $breadcrumbpath = '';
88         $crumbs = explode('/', $fullpath);
89         foreach ($crumbs as $crumb) {
90             if (empty($crumb)) {
91                 // That is probably the root crumb, we've already added it.
92                 continue;
93             }
94             list($unused, $tosplit) = explode(':', $crumb, 2);
95             if (strpos($tosplit, '|') !== false) {
96                 list($id, $crumbname) = explode('|', $tosplit, 2);
97             } else {
98                 $crumbname = $tosplit;
99             }
100             $breadcrumbpath .= '/' . $crumb;
101             $breadcrumb[] = array(
102                 'name' => urldecode($crumbname),
103                 'path' => $breadcrumbpath
104             );
105         }
106         return $breadcrumb;
107     }
109     /**
110      * Build a part of the path.
111      *
112      * This is used to construct the path that the user is currently browsing.
113      * It must contain a 'type', and a 'value'. Then it can also contain a
114      * 'name' which is very useful to prevent extra queries to get the name only.
115      *
116      * See {@link self::split_part} to extra the information from a part.
117      *
118      * @param string $type Type of part, typically 'folder' or 'search'.
119      * @param string $value The value of the part, eg. a folder ID or search terms.
120      * @param string $name The name of the part.
121      * @return string type:value or type:value|name
122      */
123     protected function build_part($type, $value, $name = '') {
124         $return = $type . ':' . urlencode($value);
125         if ($name !== '') {
126             $return .= '|' . urlencode($name);
127         }
128         return $return;
129     }
131     /**
132      * Extract information from a part of path.
133      *
134      * @param string $part value generated from {@link self::build_parth()}.
135      * @return array containing type, value and name.
136      */
137     protected function split_part($part) {
138         list($type, $tosplit) = explode(':', $part);
139         $name = '';
140         if (strpos($tosplit, '|') !== false) {
141             list($value, $name) = explode('|', $tosplit, 2);
142         } else {
143             $value = $tosplit;
144         }
145         return array($type, urldecode($value), urldecode($name));
146     }
148     /**
149      * check if user logged
150      *
151      * @return boolean
152      */
153     public function check_login() {
154         return $this->boxnetclient->is_logged_in();
155     }
157     /**
158      * reset auth token
159      *
160      * @return string
161      */
162     public function logout() {
163         if ($this->check_login()) {
164             $this->boxnetclient->log_out();
165         }
166         return $this->print_login();
167     }
169     /**
170      * Search files from box.net
171      *
172      * @param string $search_text
173      * @return mixed
174      */
175     public function search($search_text, $page = 0) {
176         return $this->get_listing($this->build_part('search', $search_text));
177     }
179     /**
180      * Downloads a repository file and saves to a path.
181      *
182      * @param string $ref reference to the file
183      * @param string $filename to save file as
184      * @return array
185      */
186     public function get_file($ref, $filename = '') {
187         global $CFG;
189         $ref = unserialize(self::convert_to_valid_reference($ref));
190         $path = $this->prepare_file($filename);
191         if (!empty($ref->downloadurl)) {
192             $c = new curl();
193             $result = $c->download_one($ref->downloadurl, null, array('filepath' => $filename,
194                 'timeout' => $CFG->repositorygetfiletimeout, 'followlocation' => true));
195             $info = $c->get_info();
196             if ($result !== true || !isset($info['http_code']) || $info['http_code'] != 200) {
197                 throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
198             }
199         } else {
200             if (!$this->boxnetclient->download_file($ref->fileid, $path)) {
201                 throw new moodle_exception('cannotdownload', 'repository');
202             }
203         }
204         return array('path' => $path);
205     }
207     /**
208      * Get file listing
209      *
210      * @param string $path
211      * @param string $page
212      * @return mixed
213      */
214     public function get_listing($fullpath = '', $page = ''){
215         global $OUTPUT;
217         $ret = array();
218         $ret['list'] = array();
219         $ret['manage'] = self::MANAGE_URL;
220         $ret['dynload'] = true;
222         $crumbs = explode('/', $fullpath);
223         $path = array_pop($crumbs);
225         if (empty($path)) {
226             $type = 'folder';
227             $pathid = 0;
228             $pathname = get_string('pluginname', 'repository_boxnet');
229         } else {
230             list($type, $pathid, $pathname) = $this->split_part($path);
231         }
233         $ret['path'] = $this->build_breadcrumb($fullpath);
234         $folders = array();
235         $files = array();
237         if ($type == 'search') {
238             $result = $this->boxnetclient->search($pathname);
239         } else {
240             $result = $this->boxnetclient->get_folder_items($pathid);
241         }
242         foreach ($result->entries as $item) {
243             if ($item->type == 'folder') {
244                 $folders[$item->name . ':' . $item->id] = array(
245                     'title' => $item->name,
246                     'path' => $fullpath . '/' . $this->build_part('folder', $item->id, $item->name),
247                     'date' => strtotime($item->modified_at),
248                     'thumbnail' => $OUTPUT->image_url(file_folder_icon(64))->out(false),
249                     'thumbnail_height' => 64,
250                     'thumbnail_width' => 64,
251                     'children' => array(),
252                     'size' => $item->size,
253                 );
254             } else {
255                 $files[$item->name . ':' . $item->id] = array(
256                     'title' => $item->name,
257                     'source' => $this->build_part('file', $item->id, $item->name),
258                     'size' => $item->size,
259                     'date' => strtotime($item->modified_at),
260                     'thumbnail' => $OUTPUT->image_url(file_extension_icon($item->name, 64))->out(false),
261                     'thumbnail_height' => 64,
262                     'thumbnail_width' => 64,
263                     'author' => $item->owned_by->name,
264                 );
265             }
266         }
268         core_collator::ksort($folders, core_collator::SORT_NATURAL);
269         core_collator::ksort($files, core_collator::SORT_NATURAL);
270         $ret['list'] = array_merge($folders, $files);
271         $ret['list'] = array_filter($ret['list'], array($this, 'filter'));
273         return $ret;
274     }
276     /**
277      * Return login form
278      *
279      * @return array
280      */
281     public function print_login(){
282         $url = $this->boxnetclient->get_login_url();
283         if ($this->options['ajax']) {
284             $ret = array();
285             $popup_btn = new stdClass();
286             $popup_btn->type = 'popup';
287             $popup_btn->url = $url->out(false);
288             $ret['login'] = array($popup_btn);
289             return $ret;
290         } else {
291             echo html_writer::link($url, get_string('login', 'repository'), array('target' => '_blank'));
292         }
293     }
295     /**
296      * Names of the plugin settings
297      *
298      * @return array
299      */
300     public static function get_type_option_names() {
301         return array('clientid', 'clientsecret', 'pluginname');
302     }
304     /**
305      * Catch the request token.
306      */
307     public function callback() {
308         $this->boxnetclient->is_logged_in();
309     }
311     /**
312      * Add Plugin settings input to Moodle form
313      *
314      * @param moodleform $mform
315      * @param string $classname
316      */
317     public static function type_config_form($mform, $classname = 'repository') {
318         global $CFG;
319         parent::type_config_form($mform);
321         $clientid = get_config('boxnet', 'clientid');
322         $clientsecret = get_config('boxnet', 'clientsecret');
323         $strrequired = get_string('required');
325         $mform->addElement('text', 'clientid', get_string('clientid', 'repository_boxnet'),
326             array('value' => $clientid, 'size' => '40'));
327         $mform->addRule('clientid', $strrequired, 'required', null, 'client');
328         $mform->setType('clientid', PARAM_RAW_TRIMMED);
330         $mform->addElement('text', 'clientsecret', get_string('clientsecret', 'repository_boxnet'),
331             array('value' => $clientsecret, 'size' => '40'));
332         $mform->addRule('clientsecret', $strrequired, 'required', null, 'client');
333         $mform->setType('clientsecret', PARAM_RAW_TRIMMED);
335         $mform->addElement('static', null, '',  get_string('information', 'repository_boxnet'));
337         if (!is_https()) {
338             $mform->addElement('static', null, '',  get_string('warninghttps', 'repository_boxnet'));
339         }
340     }
342     /**
343      * Box.net supports copied and links.
344      *
345      * Theoretically this API is ready for references, though it only works for
346      * Box.net Business accounts, but it is not enabled because we are not supporting it.
347      *
348      * @return int
349      */
350     public function supported_returntypes() {
351         return FILE_INTERNAL | FILE_EXTERNAL;
352     }
354     /**
355      * Convert a reference to the new reference style.
356      *
357      * While converting Box.net to APIv2 we introduced a new format for
358      * file references, see {@link self::get_file_reference()}. This function
359      * ensures that the format is always the same regardless of the whether
360      * the reference was from APIv1 or v2.
361      *
362      * @param mixed $reference File reference.
363      * @return stdClass Valid file reference.
364      */
365     public static function convert_to_valid_reference($reference) {
366         if (strpos($reference, 'http') === 0) {
367             // It is faster to check if the reference is a URL rather than trying to unserialize it.
368             $reference = serialize((object) array('downloadurl' => $reference, 'fileid' => '', 'filename' => '', 'userid' => ''));
369         }
370         return $reference;
371     }
373     /**
374      * Prepare file reference information
375      *
376      * @param string $source
377      * @return string file referece
378      */
379     public function get_file_reference($source) {
380         global $USER;
381         list($type, $fileid, $filename) = $this->split_part($source);
382         $reference = new stdClass();
383         $reference->fileid = $fileid;
384         $reference->filename = $filename;
385         $reference->userid = $USER->id;
386         $reference->downloadurl = '';
387         if (optional_param('usefilereference', false, PARAM_BOOL)) {
388             try {
389                 $shareinfo = $this->boxnetclient->share_file($reference->fileid);
390             } catch (moodle_exception $e) {
391                 throw new repository_exception('cannotcreatereference', 'repository_boxnet');
392             }
393             $reference->downloadurl = $shareinfo->download_url;
394         }
395         return serialize($reference);
396     }
398     /**
399      * Get a link to the file.
400      *
401      * This returns the URL of the web view of the file. To generate this link the
402      * file must be shared.
403      *
404      * @param stdClass $reference Reference.
405      * @return string URL.
406      */
407     public function get_link($reference) {
408         $reference = unserialize(self::convert_to_valid_reference($reference));
409         $shareinfo = $this->boxnetclient->share_file($reference->fileid, false);
410         return $shareinfo->url;
411     }
413     /**
414      * Synchronize the references.
415      *
416      * @param stored_file $file Stored file.
417      * @return boolean
418      */
419     public function sync_reference(stored_file $file) {
420         global $CFG;
421         if ($file->get_referencelastsync() + DAYSECS > time()) {
422             // Synchronise not more often than once a day.
423             return false;
424         }
425         $c = new curl();
426         $reference = unserialize(self::convert_to_valid_reference($file->get_reference()));
427         $url = $reference->downloadurl;
428         if (file_extension_in_typegroup($file->get_filename(), 'web_image')) {
429             $path = $this->prepare_file('');
430             $result = $c->download_one($url, null, array('filepath' => $path, 'timeout' => $CFG->repositorysyncimagetimeout));
431             $info = $c->get_info();
432             if ($result === true && isset($info['http_code']) && $info['http_code'] == 200) {
433                 $file->set_synchronised_content_from_file($path);
434                 return true;
435             }
436         }
437         $c->get($url, null, array('timeout' => $CFG->repositorysyncimagetimeout, 'followlocation' => true, 'nobody' => true));
438         $info = $c->get_info();
439         if (isset($info['http_code']) && $info['http_code'] == 200 &&
440                 array_key_exists('download_content_length', $info) &&
441                 $info['download_content_length'] >= 0) {
442             $filesize = (int)$info['download_content_length'];
443             $file->set_synchronized(null, $filesize);
444             return true;
445         }
446         $file->set_missingsource();
447         return true;
448     }
450     /**
451      * Return human readable reference information
452      * {@link stored_file::get_reference()}
453      *
454      * @param string $reference
455      * @param int $filestatus status of the file, 0 - ok, 666 - source missing
456      * @return string
457      */
458     public function get_reference_details($reference, $filestatus = 0) {
459         // Indicate it's from box.net repository.
460         $reference = unserialize(self::convert_to_valid_reference($reference));
461         if (!$filestatus) {
462             return $this->get_name() . ': ' . $reference->filename;
463         } else {
464             return get_string('lostsource', 'repository', $reference->filename);
465         }
466     }
468     /**
469      * Return the source information.
470      *
471      * @param string $source Not the reference, just the source.
472      * @return string|null
473      */
474     public function get_file_source_info($source) {
475         global $USER;
476         list($type, $fileid, $filename) = $this->split_part($source);
477         return 'Box ('. fullname($USER) . '): ' . $filename;
478     }
480     /**
481      * Repository method to serve the referenced file
482      *
483      * @param stored_file $storedfile the file that contains the reference
484      * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
485      * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
486      * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
487      * @param array $options additional options affecting the file serving
488      */
489     public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
490         $ref = unserialize(self::convert_to_valid_reference($storedfile->get_reference()));
491         header('Location: ' . $ref->downloadurl);
492     }