<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * This plugin is used to access user's dropbox files
 *
 * @since 2.0
 * @package    repository_dropbox
 * @copyright  2012 Marina Glancy
 * @copyright  2010 Dongsheng Cai {@link http://dongsheng.org}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
require_once($CFG->dirroot . '/repository/lib.php');
require_once(dirname(__FILE__).'/locallib.php');

/**
 * Repository to access Dropbox files
 *
 * @package    repository_dropbox
 * @copyright  2010 Dongsheng Cai
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class repository_dropbox extends repository {
    /** @var dropbox the instance of dropbox client */
    private $dropbox;
    /** @var array files */
    public $files;
    /** @var bool flag of login status */
    public $logged=false;
    /** @var int maximum size of file to cache in moodle filepool */
    public $cachelimit=null;

    /** @var int cached file ttl */
    private $cachedfilettl = null;

    /**
     * Constructor of dropbox plugin
     *
     * @param int $repositoryid
     * @param stdClass $context
     * @param array $options
     */
    public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
        global $CFG;
        $options['page']    = optional_param('p', 1, PARAM_INT);
        parent::__construct($repositoryid, $context, $options);

        $this->setting = 'dropbox_';

        $this->dropbox_key = $this->get_option('dropbox_key');
        $this->dropbox_secret  = $this->get_option('dropbox_secret');

        // one day
        $this->cachedfilettl = 60 * 60 * 24;

        if (isset($options['access_key'])) {
            $this->access_key = $options['access_key'];
        } else {
            $this->access_key = get_user_preferences($this->setting.'_access_key', '');
        }
        if (isset($options['access_secret'])) {
            $this->access_secret = $options['access_secret'];
        } else {
            $this->access_secret = get_user_preferences($this->setting.'_access_secret', '');
        }

        if (!empty($this->access_key) && !empty($this->access_secret)) {
            $this->logged = true;
        }

        $callbackurl = new moodle_url($CFG->wwwroot.'/repository/repository_callback.php', array(
            'callback'=>'yes',
            'repo_id'=>$repositoryid
            ));

        $args = array(
            'oauth_consumer_key'=>$this->dropbox_key,
            'oauth_consumer_secret'=>$this->dropbox_secret,
            'oauth_callback' => $callbackurl->out(false),
            'api_root' => 'https://api.dropbox.com/1/oauth',
        );

        $this->dropbox = new dropbox($args);
    }

    /**
     * Set access key
     *
     * @param string $access_key
     */
    public function set_access_key($access_key) {
        $this->access_key = $access_key;
    }

    /**
     * Set access secret
     *
     * @param string $access_secret
     */
    public function set_access_secret($access_secret) {
        $this->access_secret = $access_secret;
    }


    /**
     * Check if moodle has got access token and secret
     *
     * @return bool
     */
    public function check_login() {
        return !empty($this->logged);
    }

    /**
     * Generate dropbox login url
     *
     * @return array
     */
    public function print_login() {
        $result = $this->dropbox->request_token();
        set_user_preference($this->setting.'_request_secret', $result['oauth_token_secret']);
        $url = $result['authorize_url'];
        if ($this->options['ajax']) {
            $ret = array();
            $popup_btn = new stdClass();
            $popup_btn->type = 'popup';
            $popup_btn->url = $url;
            $ret['login'] = array($popup_btn);
            return $ret;
        } else {
            echo '<a target="_blank" href="'.$url.'">'.get_string('login', 'repository').'</a>';
        }
    }

    /**
     * Request access token
     *
     * @return array
     */
    public function callback() {
        $token  = optional_param('oauth_token', '', PARAM_TEXT);
        $secret = get_user_preferences($this->setting.'_request_secret', '');
        $access_token = $this->dropbox->get_access_token($token, $secret);
        set_user_preference($this->setting.'_access_key', $access_token['oauth_token']);
        set_user_preference($this->setting.'_access_secret', $access_token['oauth_token_secret']);
    }

    /**
     * Get dropbox files
     *
     * @param string $path
     * @param int $page
     * @return array
     */
    public function get_listing($path = '', $page = '1') {
        global $OUTPUT;
        if (empty($path) || $path=='/') {
            $path = '/';
        } else {
            $path = file_correct_filepath($path);
        }
        $encoded_path = str_replace("%2F", "/", rawurlencode($path));

        $list = array();
        $list['list'] = array();
        $list['manage'] = 'https://www.dropbox.com/home';
        $list['dynload'] = true;
        $list['nosearch'] = true;
        $list['logouturl'] = 'https://www.dropbox.com/logout';
        $list['message'] = get_string('logoutdesc', 'repository_dropbox');
        // process breadcrumb trail
        $list['path'] = array(
            array('name'=>get_string('dropbox', 'repository_dropbox'), 'path'=>'/')
        );

        $result = $this->dropbox->get_listing($encoded_path, $this->access_key, $this->access_secret);

        if (!is_object($result) || empty($result)) {
            return $list;
        }
        if (empty($result->path)) {
            $current_path = '/';
        } else {
            $current_path = file_correct_filepath($result->path);
        }

        $trail = '';
        if (!empty($path)) {
            $parts = explode('/', $path);
            if (count($parts) > 1) {
                foreach ($parts as $part) {
                    if (!empty($part)) {
                        $trail .= ('/'.$part);
                        $list['path'][] = array('name'=>$part, 'path'=>$trail);
                    }
                }
            } else {
                $list['path'][] = array('name'=>$path, 'path'=>$path);
            }
        }

        if (!empty($result->error)) {
            // reset access key
            set_user_preference($this->setting.'_access_key', '');
            set_user_preference($this->setting.'_access_secret', '');
            throw new repository_exception('repositoryerror', 'repository', '', $result->error);
        }
        if (empty($result->contents) or !is_array($result->contents)) {
            return $list;
        }
        $files = $result->contents;
        $dirslist = array();
        $fileslist = array();
        foreach ($files as $file) {
            if ($file->is_dir) {
                $dirslist[] = array(
                    'title' => substr($file->path, strpos($file->path, $current_path)+strlen($current_path)),
                    'path' => file_correct_filepath($file->path),
                    'date' => strtotime($file->modified),
                    'thumbnail' => $OUTPUT->pix_url(file_folder_icon(64))->out(false),
                    'thumbnail_height' => 64,
                    'thumbnail_width' => 64,
                    'children' => array(),
                );
            } else {
                $thumbnail = null;
                if ($file->thumb_exists) {
                    $thumburl = new moodle_url('/repository/dropbox/thumbnail.php',
                            array('repo_id' => $this->id,
                                'ctx_id' => $this->context->id,
                                'source' => $file->path,
                                'rev' => $file->rev // include revision to avoid cache problems
                            ));
                    $thumbnail = $thumburl->out(false);
                }
                $fileslist[] = array(
                    'title' => substr($file->path, strpos($file->path, $current_path)+strlen($current_path)),
                    'source' => $file->path,
                    'size' => $file->bytes,
                    'date' => strtotime($file->modified),
                    'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file->path, 64))->out(false),
                    'realthumbnail' => $thumbnail,
                    'thumbnail_height' => 64,
                    'thumbnail_width' => 64,
                );
            }
        }
        $fileslist = array_filter($fileslist, array($this, 'filter'));
        $list['list'] = array_merge($dirslist, array_values($fileslist));
        return $list;
    }

    /**
     * Displays a thumbnail for current user's dropbox file
     *
     * @param string $string
     */
    public function send_thumbnail($source) {
        $saveas = $this->prepare_file('');
        try {
            $access_key = get_user_preferences($this->setting.'_access_key', '');
            $access_secret = get_user_preferences($this->setting.'_access_secret', '');
            $this->dropbox->set_access_token($access_key, $access_secret);
            $this->dropbox->get_thumbnail($source, $saveas, self::SYNCIMAGE_TIMEOUT);
            $content = file_get_contents($saveas);
            unlink($saveas);
            // set 30 days lifetime for the image. If the image is changed in dropbox it will have
            // different revision number and URL will be different. It is completely safe
            // to cache thumbnail in the browser for a long time
            send_file($content, basename($source), 30*24*60*60, 0, true);
        } catch (Exception $e) {}
    }

    /**
     * Logout from dropbox
     * @return array
     */
    public function logout() {
        set_user_preference($this->setting.'_access_key', '');
        set_user_preference($this->setting.'_access_secret', '');
        $this->access_key    = '';
        $this->access_secret = '';
        return $this->print_login();
    }

    /**
     * Set dropbox option
     * @param array $options
     * @return mixed
     */
    public function set_option($options = array()) {
        if (!empty($options['dropbox_key'])) {
            set_config('dropbox_key', trim($options['dropbox_key']), 'dropbox');
        }
        if (!empty($options['dropbox_secret'])) {
            set_config('dropbox_secret', trim($options['dropbox_secret']), 'dropbox');
        }
        if (!empty($options['dropbox_cachelimit'])) {
            $this->cachelimit = (int)trim($options['dropbox_cachelimit']);
            set_config('dropbox_cachelimit', $this->cachelimit, 'dropbox');
        }
        unset($options['dropbox_key']);
        unset($options['dropbox_secret']);
        unset($options['dropbox_cachelimit']);
        $ret = parent::set_option($options);
        return $ret;
    }

    /**
     * Get dropbox options
     * @param string $config
     * @return mixed
     */
    public function get_option($config = '') {
        if ($config==='dropbox_key') {
            return trim(get_config('dropbox', 'dropbox_key'));
        } elseif ($config==='dropbox_secret') {
            return trim(get_config('dropbox', 'dropbox_secret'));
        } elseif ($config==='dropbox_cachelimit') {
            return $this->max_cache_bytes();
        } else {
            $options = parent::get_option();
            $options['dropbox_key'] = trim(get_config('dropbox', 'dropbox_key'));
            $options['dropbox_secret'] = trim(get_config('dropbox', 'dropbox_secret'));
            $options['dropbox_cachelimit'] = $this->max_cache_bytes();
        }
        return $options;
    }

    /**
     * Fixes references in DB that contains user credentials
     *
     * @param string $reference contents of DB field files_reference.reference
     */
    public function fix_old_style_reference($reference) {
        $ref = unserialize($reference);
        if (!isset($ref->url)) {
            $this->dropbox->set_access_token($ref->access_key, $ref->access_secret);
            $ref->url = $this->dropbox->get_file_share_link($ref->path, self::GETFILE_TIMEOUT);
            if (!$ref->url) {
                // some error occurred, do not fix reference for now
                return $reference;
            }
        }
        unset($ref->access_key);
        unset($ref->access_secret);
        $newreference = serialize($ref);
        if ($newreference !== $reference) {
            // we need to update references in the database
            global $DB;
            $params = array(
                'newreference' => $newreference,
                'newhash' => sha1($newreference),
                'reference' => $reference,
                'hash' => sha1($reference),
                'repoid' => $this->id
            );
            $refid = $DB->get_field_sql('SELECT id FROM {files_reference}
                WHERE reference = :reference AND referencehash = :hash
                AND repositoryid = :repoid', $params);
            if (!$refid) {
                return $newreference;
            }
            $existingrefid = $DB->get_field_sql('SELECT id FROM {files_reference}
                    WHERE reference = :newreference AND referencehash = :newhash
                    AND repositoryid = :repoid', $params);
            if ($existingrefid) {
                // the same reference already exists, we unlink all files from it,
                // link them to the current reference and remove the old one
                $DB->execute('UPDATE {files} SET referencefileid = :refid
                    WHERE referencefileid = :existingrefid',
                    array('refid' => $refid, 'existingrefid' => $existingrefid));
                $DB->delete_records('files_reference', array('id' => $existingrefid));
            }
            // update the reference
            $params['refid'] = $refid;
            $DB->execute('UPDATE {files_reference}
                SET reference = :newreference, referencehash = :newhash
                WHERE id = :refid', $params);
        }
        return $newreference;
    }

    /**
     * Converts a URL received from dropbox API function 'shares' into URL that
     * can be used to download/access file directly
     *
     * @param string $sharedurl
     * @return string
     */
    private function get_file_download_link($sharedurl) {
        return preg_replace('|^(\w*://)www(.dropbox.com)|','\1dl\2',$sharedurl);
    }

    /**
     * Downloads a file from external repository and saves it in temp dir
     *
     * @throws moodle_exception when file could not be downloaded
     *
     * @param string $reference the content of files.reference field or result of
     * function {@link repository_dropbox::get_file_reference()}
     * @param string $saveas filename (without path) to save the downloaded file in the
     * temporary directory, if omitted or file already exists the new filename will be generated
     * @return array with elements:
     *   path: internal location of the file
     *   url: URL to the source (from parameters)
     */
    public function get_file($reference, $saveas = '') {
        $ref = unserialize($reference);
        $saveas = $this->prepare_file($saveas);
        if (isset($ref->access_key) && isset($ref->access_secret) && isset($ref->path)) {
            $this->dropbox->set_access_token($ref->access_key, $ref->access_secret);
            return $this->dropbox->get_file($ref->path, $saveas, self::GETFILE_TIMEOUT);
        } else if (isset($ref->url)) {
            $c = new curl;
            $url = $this->get_file_download_link($ref->url);
            $result = $c->download_one($url, null, array('filepath' => $saveas, 'timeout' => self::GETFILE_TIMEOUT, 'followlocation' => true));
            $info = $c->get_info();
            if ($result !== true || !isset($info['http_code']) || $info['http_code'] != 200) {
                throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
            }
            return array('path'=>$saveas, 'url'=>$url);
        }
        throw new moodle_exception('cannotdownload', 'repository');
    }
    /**
     * Add Plugin settings input to Moodle form
     *
     * @param moodleform $mform Moodle form (passed by reference)
     * @param string $classname repository class name
     */
    public static function type_config_form($mform, $classname = 'repository') {
        global $CFG;
        parent::type_config_form($mform);
        $key    = get_config('dropbox', 'dropbox_key');
        $secret = get_config('dropbox', 'dropbox_secret');

        if (empty($key)) {
            $key = '';
        }
        if (empty($secret)) {
            $secret = '';
        }

        $strrequired = get_string('required');

        $mform->addElement('text', 'dropbox_key', get_string('apikey', 'repository_dropbox'), array('value'=>$key,'size' => '40'));
        $mform->addElement('text', 'dropbox_secret', get_string('secret', 'repository_dropbox'), array('value'=>$secret,'size' => '40'));

        $mform->addRule('dropbox_key', $strrequired, 'required', null, 'client');
        $mform->addRule('dropbox_secret', $strrequired, 'required', null, 'client');
        $str_getkey = get_string('instruction', 'repository_dropbox');
        $mform->addElement('static', null, '',  $str_getkey);

        $mform->addElement('text', 'dropbox_cachelimit', get_string('cachelimit', 'repository_dropbox'), array('size' => '40'));
        $mform->addRule('dropbox_cachelimit', null, 'numeric', null, 'client');
        $mform->setType('dropbox_cachelimit', PARAM_INT);
        $mform->addElement('static', 'dropbox_cachelimit_info', '',  get_string('cachelimit_info', 'repository_dropbox'));
    }

    /**
     * Option names of dropbox plugin
     *
     * @return array
     */
    public static function get_type_option_names() {
        return array('dropbox_key', 'dropbox_secret', 'pluginname', 'dropbox_cachelimit');
    }

    /**
     * Dropbox plugin supports all kinds of files
     *
     * @return array
     */
    public function supported_filetypes() {
        return '*';
    }

    /**
     * User cannot use the external link to dropbox
     *
     * @return int
     */
    public function supported_returntypes() {
        return FILE_INTERNAL | FILE_REFERENCE | FILE_EXTERNAL;
    }

    /**
     * Return file URL for external link
     *
     * @param string $reference the result of get_file_reference()
     * @return string
     */
    public function get_link($reference) {
        $ref = unserialize($reference);
        if (!isset($ref->url)) {
            $this->dropbox->set_access_token($ref->access_key, $ref->access_secret);
            $ref->url = $this->dropbox->get_file_share_link($ref->path, self::GETFILE_TIMEOUT);
        }
        return $this->get_file_download_link($ref->url);
    }

    /**
     * Prepare file reference information
     *
     * @param string $source
     * @return string file referece
     */
    public function get_file_reference($source) {
        global $USER;
        $reference = new stdClass;
        $reference->path = $source;
        $reference->userid = $USER->id;
        $reference->username = fullname($USER);
        $reference->access_key = get_user_preferences($this->setting.'_access_key', '');
        $reference->access_secret = get_user_preferences($this->setting.'_access_secret', '');

        // by API we don't know if we need this reference to just download a file from dropbox
        // into moodle filepool or create a reference. Since we need to create a shared link
        // only in case of reference we analyze the script parameter
        $usefilereference = optional_param('usefilereference', false, PARAM_BOOL);
        if ($usefilereference) {
            $this->dropbox->set_access_token($reference->access_key, $reference->access_secret);
            $url = $this->dropbox->get_file_share_link($source, self::GETFILE_TIMEOUT);
            if ($url) {
                unset($reference->access_key);
                unset($reference->access_secret);
                $reference->url = $url;
            }
        }
        return serialize($reference);
    }

    /**
     * Returns information about file in this repository by reference
     * {@link repository::get_file_reference()}
     * {@link repository::get_file()}
     *
     * Returns null if file not found or is not readable
     *
     * @param stdClass $reference file reference db record
     * @return null|stdClass that has 'filepath' property
     */
    public function get_file_by_reference($reference) {
        global $USER;
        $ref = unserialize($reference->reference);
        if (!isset($ref->url)) {
            // this is an old-style reference in DB. We need to fix it
            $ref = unserialize($this->fix_old_style_reference($reference->reference));
        }
        if (!isset($ref->url)) {
            return null;
        }
        $c = new curl;
        $url = $this->get_file_download_link($ref->url);
        if (file_extension_in_typegroup($ref->path, 'web_image')) {
            $saveas = $this->prepare_file('');
            try {
                $result = $c->download_one($url, array(), array('filepath' => $saveas, 'timeout' => self::SYNCIMAGE_TIMEOUT, 'followlocation' => true));
                $info = $c->get_info();
                if ($result === true && isset($info['http_code']) && $info['http_code'] == 200) {
                    return (object)array('filepath' => $saveas);
                }
            } catch (Exception $e) {}
        }
        $c->get($url, null, array('timeout' => self::SYNCIMAGE_TIMEOUT, 'followlocation' => true, 'nobody' => true));
        $info = $c->get_info();
        if (isset($info['http_code']) && $info['http_code'] == 200 &&
                array_key_exists('download_content_length', $info) &&
                $info['download_content_length'] >= 0) {
            return (object)array('filesize' => (int)$info['download_content_length']);
        }
        return null;
    }

    /**
     * Cache file from external repository by reference
     *
     * Dropbox repository regularly caches all external files that are smaller than
     * {@link repository_dropbox::max_cache_bytes()}
     *
     * @param string $reference this reference is generated by
     *                          repository::get_file_reference()
     * @param stored_file $storedfile created file reference
     */
    public function cache_file_by_reference($reference, $storedfile) {
        try {
            $this->import_external_file_contents($storedfile, $this->max_cache_bytes());
        } catch (Exception $e) {}
    }

    /**
     * Return human readable reference information
     * {@link stored_file::get_reference()}
     *
     * @param string $reference
     * @param int $filestatus status of the file, 0 - ok, 666 - source missing
     * @return string
     */
    public function get_reference_details($reference, $filestatus = 0) {
        global $USER;
        $ref  = unserialize($reference);
        $detailsprefix = $this->get_name();
        if (isset($ref->userid) && $ref->userid != $USER->id && isset($ref->username)) {
            $detailsprefix .= ' ('.$ref->username.')';
        }
        $details = $detailsprefix;
        if (isset($ref->path)) {
            $details .= ': '. $ref->path;
        }
        if (isset($ref->path) && !$filestatus) {
            // Indicate this is from dropbox with path
            return $details;
        } else {
            if (isset($ref->url)) {
                $details = $detailsprefix. ': '. $ref->url;
            }
            return get_string('lostsource', 'repository', $details);
        }
    }

    /**
     * Return the source information
     *
     * @param string $source
     * @return string
     */
    public function get_file_source_info($source) {
        global $USER;
        return 'Dropbox ('.fullname($USER).'): ' . $source;
    }

    /**
     * Returns the maximum size of the Dropbox files to cache in moodle
     *
     * Note that {@link repository_dropbox::get_file_by_reference()} called by
     * {@link repository::sync_external_file()} will try to cache images even
     * when they are bigger in order to generate thumbnails. However there is
     * a small timeout for downloading images for synchronisation and it will
     * probably fail if the image is too big.
     *
     * @return int
     */
    public function max_cache_bytes() {
        if ($this->cachelimit === null) {
            $this->cachelimit = (int)get_config('dropbox', 'dropbox_cachelimit');
        }
        return $this->cachelimit;
    }

    /**
     * Repository method to serve the referenced file
     *
     * This method is ivoked from {@link send_stored_file()}.
     * Dropbox repository first caches the file by reading it into temporary folder and then
     * serves from there.
     *
     * @param stored_file $storedfile the file that contains the reference
     * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours)
     * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
     * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
     * @param array $options additional options affecting the file serving
     */
    public function send_file($storedfile, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) {
        $ref = unserialize($storedfile->get_reference());
        if ($storedfile->get_filesize() > $this->max_cache_bytes()) {
            header('Location: '.$this->get_file_download_link($ref->url));
            die;
        }
        try {
            $this->import_external_file_contents($storedfile, $this->max_cache_bytes());
            if (!is_array($options)) {
                $options = array();
            }
            $options['sendcachedexternalfile'] = true;
            send_stored_file($storedfile, $lifetime, $filter, $forcedownload, $options);
        } catch (moodle_exception $e) {
            // redirect to Dropbox, it will show the error.
            // We redirect to Dropbox shared link, not to download link here!
            header('Location: '.$ref->url);
            die;
        }
    }

    /**
     * Caches all references to Dropbox files in moodle filepool
     *
     * Invoked by {@link repository_dropbox_cron()}. Only files smaller than
     * {@link repository_dropbox::max_cache_bytes()} and only files which
     * synchronisation timeout have not expired are cached.
     */
    public function cron() {
        $fs = get_file_storage();
        $files = $fs->get_external_files($this->id);
        foreach ($files as $file) {
            try {
                // This call will cache all files that are smaller than max_cache_bytes()
                // and synchronise file size of all others
                $this->import_external_file_contents($file, $this->max_cache_bytes());
            } catch (moodle_exception $e) {}
        }
    }
}

/**
 * Dropbox plugin cron task
 */
function repository_dropbox_cron() {
    $instances = repository::get_instances(array('type'=>'dropbox'));
    foreach ($instances as $instance) {
        $instance->cron();
    }
}
