<?php
/**
 * Dropbox Uploader
 *
 * Copyright (c) 2009 Jaka Jancar
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @author Jaka Jancar [jaka@kubje.org] [http://jaka.kubje.org/]
 * @version 1.1.12
 */
class DropboxUploader {
    /**
     * Certificate Authority Certificate source types
     */
    const CACERT_SOURCE_SYSTEM = 0;
    const CACERT_SOURCE_FILE   = 1;
    const CACERT_SOURCE_DIR    = 2;
    /**
     * Dropbox configuration
     */
    const DROPBOX_UPLOAD_LIMIT_IN_BYTES = 314572800;
    const HTTPS_DROPBOX_COM_HOME        = 'https://www.dropbox.com/home';
    const HTTPS_DROPBOX_COM_LOGIN       = 'https://www.dropbox.com/login';
    const HTTPS_DROPBOX_COM_UPLOAD      = 'https://dl-web.dropbox.com/upload';
    /**
     * DropboxUploader Error Flags and Codes
     */
    const FLAG_DROPBOX_GENERIC        = 0x10000000;
    const FLAG_LOCAL_FILE_IO          = 0x10010000;
    const CODE_FILE_READ_ERROR        = 0x10010101;
    const CODE_TEMP_FILE_CREATE_ERROR = 0x10010102;
    const CODE_TEMP_FILE_WRITE_ERROR  = 0x10010103;
    const FLAG_PARAMETER_INVALID      = 0x10020000;
    const CODE_PARAMETER_TYPE_ERROR   = 0x10020101;
    const CODE_FILESIZE_TOO_LARGE     = 0x10020201;
    const FLAG_REMOTE                 = 0x10040000;
    const CODE_CURL_ERROR             = 0x10040101;
    const CODE_LOGIN_ERROR            = 0x10040201;
    const CODE_UPLOAD_ERROR           = 0x10040401;
    const CODE_SCRAPING_FORM          = 0x10040801;
    const CODE_SCRAPING_LOGIN         = 0x10040802;
    const CODE_CURL_EXTENSION_MISSING = 0x10080101;
    protected $email;
    protected $password;
    protected $caCertSourceType = self::CACERT_SOURCE_SYSTEM;
    protected $caCertSource;
    protected $loggedIn = FALSE;
    protected $cookies = array();

    /**
     * Constructor
     *
     * @param string $email
     * @param string $password
     * @throws Exception
     */
    public function __construct($email, $password) {
        // Check requirements
        if (!extension_loaded('curl'))
            throw new Exception('DropboxUploader requires the cURL extension.', self::CODE_CURL_EXTENSION_MISSING);

        if (empty($email) || empty($password)) {
            throw new Exception((empty($email) ? 'Email' : 'Password') . ' must not be empty.', self::CODE_PARAMETER_TYPE_ERROR);
        }

        $this->email    = $email;
        $this->password = $password;
    }

    public function setCaCertificateDir($dir) {
        $this->caCertSourceType = self::CACERT_SOURCE_DIR;
        $this->caCertSource     = $dir;
    }

    public function setCaCertificateFile($file) {
        $this->caCertSourceType = self::CACERT_SOURCE_FILE;
        $this->caCertSource     = $file;
    }

    public function upload($source, $remoteDir = '/', $remoteName = NULL) {
        if (!is_file($source) or !is_readable($source))
            throw new Exception("File '$source' does not exist or is not readable.", self::CODE_FILE_READ_ERROR);

        $filesize = filesize($source);
        if ($filesize < 0 or $filesize > self::DROPBOX_UPLOAD_LIMIT_IN_BYTES) {
            throw new Exception("File '$source' too large ($filesize bytes).", self::CODE_FILESIZE_TOO_LARGE);
        }

        if (!is_string($remoteDir))
            throw new Exception("Remote directory must be a string, is " . gettype($remoteDir) . " instead.", self::CODE_PARAMETER_TYPE_ERROR);

        if (is_null($remoteName)) {
            # intentionally left blank
        } else if (!is_string($remoteName)) {
            throw new Exception("Remote filename must be a string, is " . gettype($remoteDir) . " instead.", self::CODE_PARAMETER_TYPE_ERROR);
        } else {
            $source .= ';filename=' . $remoteName;
        }

        if (!$this->loggedIn)
            $this->login();

        $data  = $this->request(self::HTTPS_DROPBOX_COM_HOME);
        $token = $this->extractToken($data, self::HTTPS_DROPBOX_COM_UPLOAD);

        $postData = array(
            'plain' => 'yes',
            'file'  => '@' . $source,
            'dest'  => $remoteDir,
            't'     => $token
        );
        $data     = $this->request(self::HTTPS_DROPBOX_COM_UPLOAD, $postData);
        if (strpos($data, 'HTTP/1.1 302 FOUND') === FALSE)
            throw new Exception('Upload failed!', self::CODE_UPLOAD_ERROR);
    }

    public function uploadString($string, $remoteName, $remoteDir = '/') {
        $exception = NULL;

        $file = tempnam(sys_get_temp_dir(), 'DBUploadString');
        if (!is_file($file))
            throw new Exception("Can not create temporary file.", self::CODE_TEMP_FILE_CREATE_ERROR);

        $bytes = file_put_contents($file, $string);
        if ($bytes === FALSE) {
            unlink($file);
            throw new Exception("Can not write to temporary file '$file'.", self::CODE_TEMP_FILE_WRITE_ERROR);
        }

        try {
            $this->upload($file, $remoteDir, $remoteName);
        } catch (Exception $exception) {
            # intentionally left blank
        }

        unlink($file);

        if ($exception)
            throw $exception;
    }

    protected function login() {
        $data  = $this->request(self::HTTPS_DROPBOX_COM_LOGIN);
        $token = $this->extractTokenFromLoginForm($data);

        $postData = array(
            'login_email'    => (string) $this->email,
            'login_password' => (string) $this->password,
            't'              => $token
        );
        $data     = $this->request(self::HTTPS_DROPBOX_COM_LOGIN, http_build_query($postData));

        if (stripos($data, 'location: /home') === FALSE)
            throw new Exception('Login unsuccessful.', self::CODE_LOGIN_ERROR);

        $this->loggedIn = TRUE;
    }

    protected function request($url, $postData = NULL) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, (string) $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);
        switch ($this->caCertSourceType) {
            case self::CACERT_SOURCE_FILE:
                curl_setopt($ch, CURLOPT_CAINFO, (string) $this->caCertSource);
                break;
            case self::CACERT_SOURCE_DIR:
                curl_setopt($ch, CURLOPT_CAPATH, (string) $this->caCertSource);
                break;
        }
        curl_setopt($ch, CURLOPT_HEADER, TRUE);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        if (NULL !== $postData) {
            curl_setopt($ch, CURLOPT_POST, TRUE);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        }

        // Send cookies
        $rawCookies = array();
        foreach ($this->cookies as $k => $v)
            $rawCookies[] = "$k=$v";
        $rawCookies = implode(';', $rawCookies);
        curl_setopt($ch, CURLOPT_COOKIE, $rawCookies);

        $data  = curl_exec($ch);
        $error = sprintf('Curl error: (#%d) %s', curl_errno($ch), curl_error($ch));
        curl_close($ch);

        if ($data === FALSE) {
            throw new Exception($error, self::CODE_CURL_ERROR);
        }

        // Store received cookies
        preg_match_all('/Set-Cookie: ([^=]+)=(.*?);/i', $data, $matches, PREG_SET_ORDER);
        foreach ($matches as $match)
            $this->cookies[$match[1]] = $match[2];

        return $data;
    }

    protected function extractToken($html, $formAction) {
        $quot    = preg_quote($formAction, '/');
        $pattern = '/<form [^>]*' . $quot . '[^>]*>.*?(?:<input [^>]*name="t" [^>]*value="(.*?)"[^>]*>).*?<\/form>/is';
        if (!preg_match($pattern, $html, $matches))
            throw new Exception("Cannot extract token! (form action is '$formAction')", self::CODE_SCRAPING_FORM);
        return $matches[1];
    }

    protected function extractTokenFromLoginForm($html) {
        // <input type="hidden" name="t" value="UJygzfv9DLLCS-is7cLwgG7z" />
        if (!preg_match('#<input type="hidden" name="t" value="([A-Za-z0-9_-]+)" />#', $html, $matches))
            throw new Exception('Cannot extract login CSRF token.', self::CODE_SCRAPING_LOGIN);
        return $matches[1];
    }

}
