File Manager

Current Directory: /home/astoriaah/www/old15/administrator/components/com_akeeba/engine/Postproc/Connector
Viewing File: /home/astoriaah/www/old15/administrator/components/com_akeeba/engine/Postproc/Connector/Dropbox.php
<?php /** * Akeeba Engine * The modular PHP5 site backup engine * * @copyright Copyright (c)2006-2015 Nicholas K. Dionysopoulos * @license GNU GPL version 3 or, at your option, any later version * @package akeebaengine * * * Dropbox API implementation in PHP * * Modified by Nicholas K. Dionysopoulos <nikosdion@gmail.com> * * Based on the work of Ben Tadiar <ben@handcraftedbyben.co.uk>, found at * https://github.com/benthedesigner/dropbox and licensed under the MIT license. * * The following license notice is present in the original code: * * Copyright (c) 2012 Ben Tadiar * * 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. */ namespace Akeeba\Engine\Postproc\Connector; // Protection against direct access defined('AKEEBAENGINE') or die(); use Psr\Log\LogLevel; use Akeeba\Engine\Factory; use Akeeba\Engine\Postproc\Connector\Dropbox\Exception\Base as DropboxException; class Dropbox { // Dropbox web endpoint const WEB_URL = 'https://www.dropbox.com/1/'; const API_URL = 'https://api.dropbox.com/1/'; const CONTENT_URL = 'https://api-content.dropbox.com/1/'; // OAuth flow methods const REQUEST_TOKEN_METHOD = 'oauth/request_token'; const AUTHORISE_METHOD = 'oauth/authorize'; const ACCESS_TOKEN_METHOD = 'oauth/access_token'; /** @var array Default cURL options */ private $defaultOptions = array( CURLOPT_SSL_VERIFYPEER => false, CURLOPT_VERBOSE => true, CURLOPT_HEADER => true, CURLINFO_HEADER_OUT => false, CURLOPT_RETURNTRANSFER => true, CURLOPT_CAINFO => AKEEBA_CACERT_PEM, //CURLOPT_FOLLOWLOCATION => true, ); /** @var string Signature method, either PLAINTEXT or HMAC-SHA1 */ private $sigMethod = 'HMAC-SHA1'; /** @var null|resource Output file handle */ protected $outFile = null; /** @var null|resource Input file handle */ protected $inFile = null; /** @var string Application public key */ private $appKey = ''; /** @var string Application secret key */ private $appSecret = ''; /** @var object Account token */ private $token = ''; /** @var object Request token, sent to DropBox when requesting authorisation */ private $requestToken = ''; /** * The root level for file paths * Either `dropbox` or `sandbox` (preferred) * * @var null|string */ private $root = 'dropbox'; /** @var string The response format, e.g. php, json, ... */ private $responseFormat = 'json'; /** * JSONP callback * * @var string */ private $callback = 'dropboxCallback'; /** @var string Unique ID for chunked uploads */ private $upload_id = null; /** @var bool Flag for chunked uploads */ private $chunked = false; /* * ========================================================================= * GETTERS / SETTERS * ========================================================================= */ /** * Sets the API keys * * @param object $keys */ public function setAppKeys($keys) { $this->appKey = $keys->app; $this->appSecret = $keys->secret; } /** * Returns the API keys * * @return object */ public function getAppKeys() { $keys = (object)array( 'app' => $this->appKey, 'secret' => $this->appSecret ); return $keys; } /** * Sets the OAuth token * * @param object $token */ public function setToken($token) { $this->token = $token; } /** * Returns the OAuth token * * @return object */ public function getToken() { if ( !is_object($this->token)) { $this->token = (object)array( 'oauth_token' => null, 'oauth_token_secret' => null ); } return $this->token; } /** * Sets the OAuth authorisation request token * * @param object $token */ public function setReqToken($token) { $this->requestToken = $token; } /** * Returns the OAuth authorisation request token * * @return object */ public function getReqToken() { if ( !is_object($this->requestToken)) { $this->requestToken = (object)array( 'oauth_token' => null, 'oauth_token_secret' => null ); } return $this->requestToken; } /** * Set the OAuth signature method * * @param string $method Either PLAINTEXT or HMAC-SHA1 * * @throws DropboxException * @return void */ public function setSignatureMethod($method) { $method = strtoupper($method); switch ($method) { case 'PLAINTEXT': case 'HMAC-SHA1': $this->sigMethod = $method; break; default: throw new DropboxException('Unsupported signature method ' . $method); } } public function getSignatureMethod() { return $this->sigMethod; } /** * Set the API response format * * @param string $format One of php, json or jsonp * * @throws \Exception * @return void */ public function setResponseFormat($format) { $format = strtolower($format); if ( !in_array($format, array('php', 'json', 'jsonp'))) { throw new \Exception("Expected a format of php, json or jsonp, got '$format'"); } else { $this->responseFormat = $format; } } /** * Return the API response format * * @return string */ public function getResponseFormat() { return $this->responseFormat; } /** * Set the JSONP callback function * * @param string $function * * @return void */ public function setCallback($function) { $this->callback = $function; } /** * Set the root level * * @param mixed $root * * @throws \Exception * @return void */ public function setRoot($root) { if ($root !== 'sandbox' && $root !== 'dropbox') { throw new \Exception("Expected a root of either 'dropbox' or 'sandbox', got '$root'"); } else { $this->root = $root; } } /** * Returns the root level (dropbox or sandbox) * * @return string Root level ("dropbox" or "sandbox") */ public function getRoot() { return $this->root; } /** * Set the output file * * @param $handle * * @throws DropboxException * @return void */ public function setOutFile($handle) { if ( !is_resource($handle) || get_resource_type($handle) != 'stream') { throw new DropboxException('Outfile must be a stream resource'); } $this->outFile = $handle; } /** * Set the input file * * @param $handle * * @throws DropboxException * @return void */ public function setInFile($handle) { if ( !is_resource($handle) || get_resource_type($handle) != 'stream') { throw new DropboxException('Infile must be a stream resource'); } fseek($handle, 0); $this->inFile = $handle; } /* * ========================================================================= * PUBLIC API * ========================================================================= */ /** * Returns an OAuth authorisation request URL. Remember to store the request * token, fetching it with getReqToken(); it is required by getAccessToken() * This is the first step of the three-step OAuth authentication flow * * @return string The URL to redirect the browser to */ public function getAuthoriseUrl() { $this->getRequestToken(); // Prepare request parameters $params = array( 'oauth_token' => $this->requestToken->oauth_token, 'oauth_token_secret' => $this->requestToken->oauth_token_secret ); // Build the URL and redirect the user $query = '?' . http_build_query($params, '', '&'); $url = self::WEB_URL . self::AUTHORISE_METHOD . $query; return $url; } /** * Fetches the access token (step 3 of OAuth authentication) */ public function getAccessToken() { $token = $this->getReqToken(); $this->setToken($token); $response = $this->fetch('POST', self::API_URL, self::ACCESS_TOKEN_METHOD); $token = $this->parseTokenString($response['body']); $this->setToken($token); } /** * Retrieves information about the user's account * * @return \stdClass */ public function accountInfo() { $response = $this->fetch('POST', self::API_URL, 'account/info'); return $response; } /** * Uploads a physical file from disk * Dropbox impose a 150MB limit to files uploaded via the API. If the file * exceeds this limit or does not exist, an \Exception will be thrown * * @param string $file Absolute path to the file to be uploaded * @param string|bool $filename The destination filename of the uploaded file * @param string $path Path to upload the file to, relative to root * @param boolean $overwrite Should the file be overwritten? (Default: true) * * @throws \Exception * @return \stdClass */ public function putFile($file, $filename = false, $path = '', $overwrite = true) { if (file_exists($file)) { if (filesize($file) <= 157286400) { $call = 'files/' . $this->root . '/' . $this->encodePath($path); // If no filename is provided we'll use the original filename $filename = (is_string($filename)) ? $filename : basename($file); $params = array( 'filename' => $filename, 'file' => '@' . str_replace('\\', '/', $file) . ';filename=' . $filename, 'overwrite' => (int)$overwrite, ); $response = $this->fetch('POST', self::CONTENT_URL, $call, $params); return $response; } throw new \Exception('File exceeds 150MB upload limit'); } // Throw an \Exception if the file does not exist throw new \Exception('Local file ' . $file . ' does not exist'); } /** * Performs a step of a chunked upload * * @param string $file Local path to the file that will be uploaded * @param string $path Path on DropBox * @param int $size Size of the chunks (Mb) * @param string $filename Name of the file created on DropBox * @param bool $overwrite Should I overwrite the file? * @param int $offset The offset to continue uploading from * @param string $upload_id The upload ID of the file being processed * * @throws DropboxException * @return object|string */ public function putChunkedFile($file, $path, $size = 1, $filename = '', $overwrite = true, $offset = null, $upload_id = null) { $chunkSize = 1024 * 1024 * $size; $this->chunked = true; $handle = @fopen($file, 'rb+'); if ($handle === false) { Factory::getLog() ->log(LogLevel::ERROR, __CLASS__ . '::' . __METHOD__ . " Could not open $file for chunked transfer to Dropbox."); return false; } $retry = 0; if (is_null($offset) && is_null($upload_id)) { Factory::getLog() ->log(LogLevel::DEBUG, __CLASS__ . '::' . __METHOD__ . " - Starting chunked transfer with chunk size of $size Mb ($chunkSize bytes)"); $offset = 0; $upload_id = null; } else { Factory::getLog() ->log(LogLevel::DEBUG, __CLASS__ . '::' . __METHOD__ . " - Continuing chunked transfer with chunk size of $size Mb ($chunkSize bytes) from offset $offset"); @fseek($handle, $offset); } $uploaded = false; while ( !feof($handle) && !$uploaded) { // Retry only 3 times, after that abort the upload if ($retry > 3) { throw new DropboxException('Could not upload chunk to DropBox. Tried 3 times, aborting'); } Factory::getLog() ->log(LogLevel::DEBUG, __CLASS__ . '::' . __METHOD__ . " - Reading up to $chunkSize bytes"); $chunk = $this->readFully($handle, $chunkSize); $reallyRead = strlen($chunk); Factory::getLog()->log(LogLevel::DEBUG, __CLASS__ . '::' . __METHOD__ . " - Read $reallyRead bytes"); $params = array( 'upload_id' => $upload_id, 'offset' => $offset ); if (empty($upload_id)) { unset($params['upload_id']); } // Prevent exception throwing, so I can handle the offset error $response = $this->fetch('PUT', self::CONTENT_URL, 'chunked_upload', $params, $chunk, false); // Wrong offset, let's rollback to the correct one if ($response['code'] == 400) { $offset = $response['body']->offset; Factory::getLog() ->log(LogLevel::DEBUG, __CLASS__ . '::' . __METHOD__ . " - Got HTTP 400. Rewinding to offset $offset. Retry count: $retry"); @fseek($handle, $offset); $retry += 1; usleep(200); continue; } // If I'm here, let's reset the counter $retry = 0; if (empty($upload_id)) { $upload_id = $response['body']->upload_id; Factory::getLog() ->log(LogLevel::DEBUG, __CLASS__ . '::' . __METHOD__ . " - Got new upload_id: " . $response['body']->upload_id); } $uploaded = true; } // Not done yet? Return the response body to the caller. if ( !feof($handle)) { return $response['body']; } Factory::getLog() ->log(LogLevel::DEBUG, __CLASS__ . '::' . __METHOD__ . " *** Done uploading {$size}M chunks to Dropbox"); // This will have to change, now it's just to have a filename if ( !$filename) { $filename = basename($file); } Factory::getLog() ->log(LogLevel::DEBUG, __CLASS__ . '::' . __METHOD__ . " - Committing chunked upload. Overwrite is set to " . (int)$overwrite); $call = 'commit_chunked_upload/' . $this->root . '/' . $this->encodePath($path . '/' . $filename); $params = array( 'overwrite' => (int)$overwrite, 'upload_id' => $upload_id ); $response = $this->fetch('POST', self::CONTENT_URL, $call, $params); return $response['body']; } /** * Uploads file data from a stream * Note: This function is experimental and requires further testing * * @param resource $stream A readable stream created using fopen() * @param string $filename The destination filename, including path * @param boolean $overwrite Should the file be overwritten? (Default: true) * * @return array */ public function putStream($stream, $filename, $overwrite = true) { $this->setInFile($stream); $path = $this->encodePath($filename); $call = 'files_put/' . $this->root . '/' . $path; $params = array('overwrite' => (int)$overwrite); $response = $this->fetch('PUT', self::CONTENT_URL, $call, $params); return $response; } /** * Downloads a file * Returns the base filename, raw file data and mime type returned by Fileinfo * * @param string $file Path to file, relative to root, including path * @param bool|string $outFile Filename to write the downloaded file to * @param string $revision The revision of the file to retrieve * * @throws \Exception * @return array */ public function getFile($file, $outFile = false, $revision = null) { // Only allow php response format for this call if ($this->responseFormat !== 'php') { throw new \Exception('This method only supports the `php` response format'); } $handle = null; if ($outFile !== false) { // Create a file handle if $outFile is specified if ( !$handle = fopen($outFile, 'w')) { throw new \Exception("Unable to open file handle for $outFile"); } else { $this->setOutFile($handle); } } $file = $this->encodePath($file); $call = 'files/' . $this->root . '/' . $file; $params = array('rev' => $revision); $response = $this->fetch('GET', self::CONTENT_URL, $call, $params); // Close the file handle if one was opened if ($handle) { fclose($handle); } return array( 'name' => ($outFile) ? $outFile : basename($file), 'mime' => $this->getMimeType(($outFile) ? '' : $response['body'], $outFile), 'meta' => json_decode($response['headers']['x-dropbox-metadata']), 'data' => $response['body'], ); } /** * Retrieves file and folder metadata * * @param string $path The path to the file/folder, relative to root * @param string $rev Return metadata for a specific revision (Default: latest rev) * @param int $limit Maximum number of listings to return * @param bool|string $hash Metadata hash to compare against * @param bool $list Return contents field with response * @param bool $deleted Include files/folders that have been deleted * * @return \stdClass */ public function metaData($path = null, $rev = null, $limit = 10000, $hash = false, $list = true, $deleted = false) { $call = 'metadata/' . $this->root . '/' . $this->encodePath($path); $params = array( 'file_limit' => ($limit < 1) ? 1 : (($limit > 10000) ? 10000 : (int)$limit), 'hash' => (is_string($hash)) ? $hash : 0, 'list' => (int)$list, 'include_deleted' => (int)$deleted, 'rev' => (is_string($rev)) ? $rev : null, ); $response = $this->fetch('POST', self::API_URL, $call, $params); return $response; } /** * Obtains metadata for the previous revisions of a file * * @param string $file Path to the file, relative to root * @param integer $limit Number of revisions to return (1-1000) * * @return array */ public function revisions($file, $limit = 10) { $call = 'revisions/' . $this->root . '/' . $this->encodePath($file); $params = array( 'rev_limit' => ($limit < 1) ? 1 : (($limit > 1000) ? 1000 : (int)$limit), ); $response = $this->fetch('GET', self::API_URL, $call, $params); return $response; } /** * Restores a file path to a previous revision * * @param string $file Path to the file, relative to root * @param string $revision The revision of the file to restore * * @return \stdClass */ public function restore($file, $revision) { $call = 'restore/' . $this->root . '/' . $this->encodePath($file); $params = array('rev' => $revision); $response = $this->fetch('POST', self::API_URL, $call, $params); return $response; } /** * Returns metadata for all files and folders that match the search query * * @param mixed $query The search string. Must be at least 3 characters long * @param string $path The path to the folder you want to search in * @param integer $limit Maximum number of results to return (1-1000) * @param boolean $deleted Include deleted files/folders in the search * * @return array */ public function search($query, $path = '', $limit = 1000, $deleted = false) { $call = 'search/' . $this->root . '/' . $this->encodePath($path); $params = array( 'query' => $query, 'file_limit' => ($limit < 1) ? 1 : (($limit > 1000) ? 1000 : (int)$limit), 'include_deleted' => (int)$deleted, ); $response = $this->fetch('GET', self::API_URL, $call, $params); return $response; } /** * Creates and returns a shareable link to files or folders * * @param string $path The path to the file/folder you want a sharable link to * * @return \stdClass */ public function shares($path) { $call = 'shares/' . $this->root . '/' . $this->encodePath($path); $response = $this->fetch('POST', self::API_URL, $call); return $response; } /** * Returns a link directly to a file * * @param string $path The path to the media file you want a direct link to * * @return \stdClass */ public function media($path) { $call = 'media/' . $this->root . '/' . $this->encodePath($path); $response = $this->fetch('POST', self::API_URL, $call); return $response; } /** * Gets a thumbnail for an image * * @param string $file The path to the image you wish to thumbnail * @param string $format The thumbnail format, either JPEG or PNG * @param string $size The size of the thumbnail * * @throws \Exception * @return array */ public function thumbnails($file, $format = 'JPEG', $size = 'small') { // Only allow php response format for this call if ($this->responseFormat !== 'php') { throw new \Exception('This method only supports the `php` response format'); } $format = strtoupper($format); // If $format is not 'PNG', default to 'JPEG' if ($format != 'PNG') { $format = 'JPEG'; } $size = strtolower($size); $sizes = array('s', 'm', 'l', 'xl', 'small', 'medium', 'large'); // If $size is not valid, default to 'small' if ( !in_array($size, $sizes)) { $size = 'small'; } $call = 'thumbnails/' . $this->root . '/' . $this->encodePath($file); $params = array('format' => $format, 'size' => $size); $response = $this->fetch('GET', self::CONTENT_URL, $call, $params); return array( 'name' => basename($file), 'mime' => $this->getMimeType($response['body']), 'meta' => json_decode($response['headers']['x-dropbox-metadata']), 'data' => $response['body'], ); } /** * Copies a file or folder to a new location * * @param string $from File or folder to be copied, relative to root * @param string $to Destination path, relative to root * * @return \stdClass */ public function copy($from, $to) { $call = 'fileops/copy'; $params = array( 'root' => $this->root, 'from_path' => $this->normalisePath($from), 'to_path' => $this->normalisePath($to), ); $response = $this->fetch('POST', self::API_URL, $call, $params); return $response; } /** * Creates a folder * * @param string $path New folder to create relative to root * * @return \stdClass */ public function create($path) { $call = 'fileops/create_folder'; $params = array('root' => $this->root, 'path' => $this->normalisePath($path)); $response = $this->fetch('POST', self::API_URL, $call, $params); return $response; } /** * Deletes a file or folder * * @param string $path The path to the file or folder to be deleted * * @return \stdClass */ public function delete($path) { $call = 'fileops/delete'; $params = array('root' => $this->root, 'path' => $this->normalisePath($path)); $response = $this->fetch('POST', self::API_URL, $call, $params); return $response; } /** * Moves a file or folder to a new location * * @param string $from File or folder to be moved, relative to root * @param string $to Destination path, relative to root * * @return \stdClass */ public function move($from, $to) { $call = 'fileops/move'; $params = array( 'root' => $this->root, 'from_path' => $this->normalisePath($from), 'to_path' => $this->normalisePath($to), ); $response = $this->fetch('POST', self::API_URL, $call, $params); return $response; } /* * ========================================================================= * PRIVATE API * ========================================================================= */ /** * Acquire an unauthorised request token * * @link http://tools.ietf.org/html/rfc5849#section-2.1 * @return void */ protected function getRequestToken() { $url = self::API_URL . self::REQUEST_TOKEN_METHOD; $response = $this->fetch('POST', $url, ''); $token = $this->parseTokenString($response['body']); $this->requestToken = $token; } /** * Execute an API call * * @param string $method The HTTP method * @param string $url The API endpoint * @param string $call The API method to call * @param array $additional Additional parameters * @param array $explicitPost Force fields that will be in the post request * @param bool $throwup Should I throw a new \Exception or return the error? * * @throws DropboxException * @return string|\stdClass */ protected function fetch($method, $url, $call, array $additional = array(), $explicitPost = array(), $throwup = true) { // Get the signed request URL $request = $this->getSignedRequest($method, $url, $call, $additional); // Initialise and execute a cURL request $ch = curl_init($request['url']); // Get the default options array $options = $this->defaultOptions; if ($method == 'GET' && $this->outFile) // GET { $options[CURLOPT_RETURNTRANSFER] = false; $options[CURLOPT_HEADER] = false; $options[CURLOPT_FILE] = $this->outFile; $options[CURLOPT_BINARYTRANSFER] = true; $this->outFile = null; } elseif ($method == 'POST') // POST { $options[CURLOPT_POST] = true; if ($explicitPost) { $options[CURLOPT_POSTFIELDS] = $explicitPost; } else { $options[CURLOPT_POSTFIELDS] = $request['postfields']; } } elseif ($method == 'PUT' && $this->inFile) // PUT { $options[CURLOPT_PUT] = true; $options[CURLOPT_INFILE] = $this->inFile; // @todo Update so the data is not loaded into memory to get its size $options[CURLOPT_INFILESIZE] = strlen(stream_get_contents($this->inFile)); fseek($this->inFile, 0); $this->inFile = null; } // I'm uploading using chunks elseif ($method == 'PUT' && $this->chunked) { $options[CURLOPT_CUSTOMREQUEST] = "PUT"; $options[CURLOPT_POSTFIELDS] = $explicitPost; $options[CURLOPT_HTTPHEADER] = array("Content-Type: application/octet-stream"); } // Set the cURL options at once curl_setopt_array($ch, $options); @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // Execute and parse the response $response = curl_exec($ch); curl_close($ch); // Parse the response if it is a string if (is_string($response)) { $response = $this->parse($response); } // Check if an error occurred and throw an \Exception if ( !empty($response['body']->error) && $throwup) { $message = $response['body']->error . ' (Status Code: ' . $response['code'] . ')'; throw new DropboxException($message, $response['code']); } return $response; } /** * Parse a cURL response * * @param string $response * * @return array */ protected function parse($response) { // Explode the response into headers and body parts (separated by double EOL) list($headers, $response) = explode("\r\n\r\n", $response, 2); // Explode response headers $lines = explode("\r\n", $headers); // If the status code is 100, the API server must send a final response // We need to explode the response again to get the actual response if (preg_match('#^HTTP/1.1 100#', $lines[0])) { list($headers, $response) = explode("\r\n\r\n", $response, 2); $lines = explode("\r\n", $headers); } // Get the HTTP response code from the first line $first = array_shift($lines); $pattern = '#^HTTP/1.1 ([0-9]{3})#'; preg_match($pattern, $first, $matches); $code = $matches[1]; // Parse the remaining headers into an associative array $headers = array(); foreach ($lines as $line) { list($k, $v) = explode(': ', $line, 2); $headers[strtolower($k)] = $v; } // If the response body is not a JSON encoded string // we'll return the entire response body if ( !$body = json_decode($response)) { $body = $response; } return array('code' => $code, 'body' => $body, 'headers' => $headers); } /** * Generate signed request URL */ protected function getSignedRequest($method, $url, $call, array $additional = array()) { $token = $this->getToken(); // Generate a random string for the request $nonce = md5(microtime(true) . uniqid('', true)); // Prepare the standard request parameters $params = array( 'oauth_consumer_key' => $this->appKey, 'oauth_token' => $token->oauth_token, 'oauth_signature_method' => $this->sigMethod, 'oauth_version' => '1.0', // Generate nonce and timestamp if signature method is HMAC-SHA1 'oauth_timestamp' => ($this->sigMethod == 'HMAC-SHA1') ? time() : null, 'oauth_nonce' => ($this->sigMethod == 'HMAC-SHA1') ? $nonce : null, ); // Merge with the additional request parameters $params = array_merge($params, $additional); ksort($params); // URL encode each parameter to RFC3986 for use in the base string $encoded = array(); foreach ($params as $param => $value) { if ($value !== null) { // If the value is a file upload (prefixed with @), replace it with // the destination filename, the file path will be sent in POSTFIELDS if ($value[0] === '@') { $value = $params['filename']; } $encoded[] = $this->encode($param) . '=' . $this->encode($value); } else { unset($params[$param]); } } // Build the first part of the string $base = $method . '&' . $this->encode($url . $call) . '&'; // Re-encode the encoded parameter string and append to $base $base .= $this->encode(implode('&', $encoded)); // Concatenate the secrets with an ampersand $key = $this->appSecret . '&' . $token->oauth_token_secret; // Get the signature string based on signature method $signature = $this->getSignature($base, $key); $params['oauth_signature'] = $signature; // Build the signed request URL $query = '?' . http_build_query($params, '', '&'); return array( 'url' => $url . $call . $query, 'postfields' => $params, ); } /** * Generate the oauth_signature for a request * * @param string $base Signature base string, used by HMAC-SHA1 * @param string $key Concatenated consumer and token secrets * * @return string */ protected function getSignature($base, $key) { switch ($this->sigMethod) { case 'PLAINTEXT': $signature = $key; break; case 'HMAC-SHA1': //$hash = hash_hmac('sha1', $base, $key, true); $hash = pack('H*', sha1( (str_pad($key, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . pack('H*', sha1((str_pad($key, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $base)))); $signature = base64_encode($hash); break; } return $signature; } /** * Parse response parameters for a token into an object * Dropbox returns tokens in the response parameters, and * not a JSON encoded object as per other API requests * * @link http://oauth.net/core/1.0/#response_parameters * * @param string $response * * @return \stdClass */ protected function parseTokenString($response) { $parts = explode('&', $response); $token = new \stdClass(); foreach ($parts as $part) { list($k, $v) = explode('=', $part, 2); $k = strtolower($k); $token->$k = $v; } return $token; } /** * Encode a value to RFC3986 * This is a convenience method to decode ~ symbols encoded * by rawurldecode. This will encode all characters except * the unreserved set, ALPHA, DIGIT, '-', '.', '_', '~' * * @link http://tools.ietf.org/html/rfc5849#section-3.6 * * @param mixed $value * * @return mixed */ protected function encode($value) { return str_replace('%7E', '~', rawurlencode($value)); } /** * Get the mime type of downloaded file * If the Fileinfo extension is not loaded, return false * * @param string $data File contents as a string or filename * @param bool|string $isFilename Is $data a filename? * * @return boolean|string Mime type and encoding of the file */ protected function getMimeType($data, $isFilename = false) { if (extension_loaded('fileinfo')) { $finfo = new \finfo(FILEINFO_MIME); if ($isFilename !== false) { return $finfo->file($data); } return $finfo->buffer($data); } return false; } /** * Trim the path of forward slashes and replace * consecutive forward slashes with a single slash * * @param string $path The path to normalise * * @return string */ protected function normalisePath($path) { $path = preg_replace('#/+#', '/', trim($path, '/')); return $path; } /** * Encode the path, then replace encoded slashes * with literal forward slash characters * * @param string $path The path to encode * * @return string */ protected function encodePath($path) { $path = $this->normalisePath($path); $path = str_replace('%2F', '/', rawurlencode($path)); return $path; } /** * More accurate way to read a part of a file. * Sometimes you could get incorrect values and since DropBox wants the size correct to the byte * let's double check it. * * @param resource $inStream File handle * @param int $numBytes Number of bytes to read * * @return string * @throws \Exception */ private static function readFully($inStream, $numBytes) { $full = ''; $bytesRemaining = $numBytes; while ( !feof($inStream) && $bytesRemaining > 0) { $part = fread($inStream, $bytesRemaining); if ($part === false) { throw new \Exception("Error reading from \$inStream."); } $full .= $part; $bytesRemaining -= strlen($part); } return $full; } }