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/>.
16
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 */
25require_once($CFG->dirroot . '/repository/lib.php');
26require_once($CFG->libdir . '/boxlib.php');
27
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 */
36class repository_boxnet extends repository {
37
38    /** @const MANAGE_URL Manage URL. */
39    const MANAGE_URL = 'https://app.box.com/files';
40
41    /** @const SESSION_PREFIX Key used to store information in the session. */
42    const SESSION_PREFIX = 'repository_boxnet';
43
44    /** @var string Client ID */
45    protected $clientid;
46
47    /** @var string Client secret */
48    protected $clientsecret;
49
50    /** @var string Access token */
51    protected $accesstoken;
52
53    /** @var object Box.net object */
54    protected $boxnetclient;
55
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);
65
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());
72
73        $this->boxnetclient = new boxnet_client($clientid, $clientsecret, $returnurl, '');
74    }
75
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    }
108
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    }
130
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    }
147
148    /**
149     * check if user logged
150     *
151     * @return boolean
152     */
153    public function check_login() {
154        return $this->boxnetclient->is_logged_in();
155    }
156
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    }
168
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    }
178
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;
188
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    }
206
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;
216
217        $ret = array();
218        $ret['list'] = array();
219        $ret['manage'] = self::MANAGE_URL;
220        $ret['dynload'] = true;
221
222        $crumbs = explode('/', $fullpath);
223        $path = array_pop($crumbs);
224
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        }
232
233        $ret['path'] = $this->build_breadcrumb($fullpath);
234        $folders = array();
235        $files = array();
236
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        }
267
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'));
272
273        return $ret;
274    }
275
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    }
294
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    }
303
304    /**
305     * Catch the request token.
306     */
307    public function callback() {
308        $this->boxnetclient->is_logged_in();
309    }
310
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);
320
321        $clientid = get_config('boxnet', 'clientid');
322        $clientsecret = get_config('boxnet', 'clientsecret');
323        $strrequired = get_string('required');
324
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);
329
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);
334
335        $mform->addElement('static', null, '',  get_string('information', 'repository_boxnet'));
336
337        if (!is_https()) {
338            $mform->addElement('static', null, '',  get_string('warninghttps', 'repository_boxnet'));
339        }
340    }
341
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    }
353
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    }
372
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    }
397
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    }
412
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    }
449
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    }
467
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    }
479
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    }
493}
494