Add upstream
This commit is contained in:
392
wp-content/plugins/updraftplus/includes/Dropbox2/API.php
Normal file
392
wp-content/plugins/updraftplus/includes/Dropbox2/API.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Dropbox API base class
|
||||
* @author Ben Tadiar <ben@handcraftedbyben.co.uk>
|
||||
* @link https://github.com/benthedesigner/dropbox
|
||||
* @link https://www.dropbox.com/developers
|
||||
* @link https://status.dropbox.com Dropbox status
|
||||
* @package Dropbox
|
||||
*/
|
||||
class UpdraftPlus_Dropbox_API {
|
||||
// API Endpoints
|
||||
const API_URL_V2 = 'https://api.dropboxapi.com/';
|
||||
const CONTENT_URL_V2 = 'https://content.dropboxapi.com/2/';
|
||||
|
||||
/**
|
||||
* OAuth consumer object
|
||||
* @var null|OAuth\Consumer
|
||||
*/
|
||||
private $OAuth;
|
||||
|
||||
/**
|
||||
* The root level for file paths
|
||||
* Either `dropbox` or `sandbox` (preferred)
|
||||
* @var null|string
|
||||
*/
|
||||
private $root;
|
||||
|
||||
/**
|
||||
* Format of the API response
|
||||
* @var string
|
||||
*/
|
||||
private $responseFormat = 'php';
|
||||
|
||||
/**
|
||||
* JSONP callback
|
||||
* @var string
|
||||
*/
|
||||
private $callback = 'dropboxCallback';
|
||||
|
||||
/**
|
||||
* Chunk size used for chunked uploads
|
||||
* @see \Dropbox\API::chunkedUpload()
|
||||
*/
|
||||
private $chunkSize = 4194304;
|
||||
|
||||
/**
|
||||
* Set the OAuth consumer object
|
||||
* See 'General Notes' at the link below for information on access type
|
||||
* @link https://www.dropbox.com/developers/reference/api
|
||||
* @param OAuth\Consumer\ConsumerAbstract $OAuth
|
||||
* @param string $root Dropbox app access type
|
||||
*/
|
||||
public function __construct(Dropbox_ConsumerAbstract $OAuth, $root = 'sandbox') {
|
||||
$this->OAuth = $OAuth;
|
||||
$this->setRoot($root);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about the user's account
|
||||
* @return object stdClass
|
||||
*/
|
||||
public function accountInfo() {
|
||||
$call = '2/users/get_current_account';
|
||||
$params = array('api_v2' => true);
|
||||
$response = $this->fetch('POST', self::API_URL_V2, $call, $params);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about the user's quota
|
||||
* @param array $options - valid keys are 'timeout'
|
||||
* @return object stdClass
|
||||
*/
|
||||
public function quotaInfo($options = array()) {
|
||||
$call = '2/users/get_space_usage';
|
||||
// Cases have been seen (Apr 2019) where a response came back (HTTP/2.0 response header - suspected outgoing web hosting proxy, as everyone else seems to get HTTP/1.0 and I'm not aware that current Curl versions would do HTTP/2.0 without specifically being told to) after 180 seconds; a valid response, but took a long time.
|
||||
$params = array(
|
||||
'api_v2' => true,
|
||||
'timeout' => isset($options['timeout']) ? $options['timeout'] : 20
|
||||
);
|
||||
$response = $this->fetch('POST', self::API_URL_V2, $call, $params);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads large files to Dropbox in mulitple chunks
|
||||
* @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)
|
||||
* @param integer $offset position to seek to when opening the file
|
||||
* @param string $uploadID existing upload_id to resume an upload
|
||||
* @param string|array function to call back to upon each chunk
|
||||
* @return stdClass
|
||||
*/
|
||||
public function chunkedUpload($file, $filename = false, $path = '', $overwrite = true, $offset = 0, $uploadID = null, $callback = null) {
|
||||
|
||||
if (file_exists($file)) {
|
||||
if ($handle = @fopen($file, 'r')) {
|
||||
// Set initial upload ID and offset
|
||||
if ($offset > 0) {
|
||||
fseek($handle, $offset);
|
||||
}
|
||||
|
||||
/*
|
||||
Set firstCommit to true so that the upload session start endpoint is called.
|
||||
*/
|
||||
$firstCommit = (0 == $offset);
|
||||
|
||||
// Read from the file handle until EOF, uploading each chunk
|
||||
while ($data = fread($handle, $this->chunkSize)) {
|
||||
|
||||
// Set the file, request parameters and send the request
|
||||
$this->OAuth->setInFile($data);
|
||||
|
||||
if ($firstCommit) {
|
||||
$params = array(
|
||||
'close' => false,
|
||||
'api_v2' => true,
|
||||
'content_upload' => true
|
||||
);
|
||||
$response = $this->fetch('POST', self::CONTENT_URL_V2, 'files/upload_session/start', $params);
|
||||
$firstCommit = false;
|
||||
} else {
|
||||
$params = array(
|
||||
'cursor' => array(
|
||||
'session_id' => $uploadID,
|
||||
// If you send it as a string, Dropbox will be unhappy
|
||||
'offset' => (int)$offset
|
||||
),
|
||||
'api_v2' => true,
|
||||
'content_upload' => true
|
||||
);
|
||||
$response = $this->append_upload($params, false);
|
||||
}
|
||||
|
||||
// On subsequent chunks, use the upload ID returned by the previous request
|
||||
if (isset($response['body']->session_id)) {
|
||||
$uploadID = $response['body']->session_id;
|
||||
}
|
||||
|
||||
/*
|
||||
API v2 no longer returns the offset, we need to manually work this out. So check that there are no errors and update the offset as well as calling the callback method.
|
||||
*/
|
||||
if (!isset($response['body']->error)) {
|
||||
$offset = ftell($handle);
|
||||
if ($callback) {
|
||||
call_user_func($callback, $offset, $uploadID, $file);
|
||||
}
|
||||
$this->OAuth->setInFile(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the chunked upload
|
||||
$filename = (is_string($filename)) ? $filename : basename($file);
|
||||
$params = array(
|
||||
'cursor' => array(
|
||||
'session_id' => $uploadID,
|
||||
'offset' => $offset
|
||||
),
|
||||
'commit' => array(
|
||||
'path' => '/' . $this->encodePath($path . $filename),
|
||||
'mode' => 'add'
|
||||
),
|
||||
'api_v2' => true,
|
||||
'content_upload' => true
|
||||
);
|
||||
$response = $this->append_upload($params, true);
|
||||
return $response;
|
||||
} else {
|
||||
throw new Exception('Could not open ' . $file . ' for reading');
|
||||
}
|
||||
}
|
||||
|
||||
// Throw an Exception if the file does not exist
|
||||
throw new Exception('Local file ' . $file . ' does not exist');
|
||||
}
|
||||
|
||||
private function append_upload($params, $last_call) {
|
||||
try {
|
||||
if ($last_call){
|
||||
$response = $this->fetch('POST', self::CONTENT_URL_V2, 'files/upload_session/finish', $params);
|
||||
} else {
|
||||
$response = $this->fetch('POST', self::CONTENT_URL_V2, 'files/upload_session/append_v2', $params);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$responseCheck = json_decode($e->getMessage());
|
||||
if (isset($responseCheck) && strpos($responseCheck[0] , 'incorrect_offset') !== false) {
|
||||
$expected_offset = $responseCheck[1];
|
||||
throw new Exception('Submitted input out of alignment: got ['.$params['cursor']['offset'].'] expected ['.$expected_offset.']');
|
||||
|
||||
// $params['cursor']['offset'] = $responseCheck[1];
|
||||
// $response = $this->append_upload($params, $last_call);
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunked downloads a file from Dropbox, it will return false if a file handle is not passed and will return true if the call was successful.
|
||||
*
|
||||
* @param string $file Path - to file, relative to root, including path
|
||||
* @param resource $outFile - the local file handle
|
||||
* @param array $options - any extra options to be passed e.g headers
|
||||
* @return boolean - a boolean to indicate success or failure
|
||||
*/
|
||||
public function download($file, $outFile = null, $options = array()) {
|
||||
// Only allow php response format for this call
|
||||
if ($this->responseFormat !== 'php') {
|
||||
throw new Exception('This method only supports the `php` response format');
|
||||
}
|
||||
|
||||
if ($outFile) {
|
||||
$this->OAuth->setOutFile($outFile);
|
||||
|
||||
$params = array('path' => '/' . $file, 'api_v2' => true, 'content_download' => true);
|
||||
|
||||
if (isset($options['headers'])) {
|
||||
foreach ($options['headers'] as $key => $header) {
|
||||
$headers[] = $key . ': ' . $header;
|
||||
}
|
||||
$params['headers'] = $headers;
|
||||
}
|
||||
|
||||
$file = $this->encodePath($file);
|
||||
$call = 'files/download';
|
||||
|
||||
$response = $this->fetch('GET', self::CONTENT_URL_V2, $call, $params);
|
||||
|
||||
fclose($outFile);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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=1000] Maximum number of results to return (1-1000)
|
||||
* @param integer [$start=0] Result number to start from
|
||||
* @return array
|
||||
*/
|
||||
public function search($query, $path = '', $limit = 1000, $start = 0) {
|
||||
$call = '2/files/search';
|
||||
$path = $this->encodePath($path);
|
||||
// APIv2 requires that the path match this regex: String(pattern="(/(.|[\r\n])*)?|(ns:[0-9]+(/.*)?)")
|
||||
if ($path && '/' != substr($path, 0, 1)) $path = "/$path";
|
||||
$params = array(
|
||||
'path' => $path,
|
||||
'query' => $query,
|
||||
'start' => $start,
|
||||
'max_results' => ($limit < 1) ? 1 : (($limit > 1000) ? 1000 : (int) $limit),
|
||||
'api_v2' => true,
|
||||
);
|
||||
$response = $this->fetch('POST', self::API_URL_V2, $call, $params);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file or folder
|
||||
* @param string $path The path to the file or folder to be deleted
|
||||
* @return object stdClass
|
||||
*/
|
||||
public function delete($path) {
|
||||
$call = '2/files/delete';
|
||||
$params = array('path' => '/' . $this->normalisePath($path), 'api_v2' => true);
|
||||
$response = $this->fetch('POST', self::API_URL_V2, $call, $params);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intermediate fetch function
|
||||
* @param string $method The HTTP method
|
||||
* @param string $url The API endpoint
|
||||
* @param string $call The API method to call
|
||||
* @param array $params Additional parameters
|
||||
* @return mixed
|
||||
*/
|
||||
private function fetch($method, $url, $call, array $params = array()) {
|
||||
// Make the API call via the consumer
|
||||
$response = $this->OAuth->fetch($method, $url, $call, $params);
|
||||
|
||||
// Format the response and return
|
||||
switch ($this->responseFormat) {
|
||||
case 'json':
|
||||
return json_encode($response);
|
||||
case 'jsonp':
|
||||
$response = json_encode($response);
|
||||
return $this->callback . '(' . $response . ')';
|
||||
default:
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the API response format
|
||||
* @param string $format One of php, json or jsonp
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the chunk size for chunked uploads
|
||||
* If $chunkSize is empty, set to 4194304 bytes (4 MB)
|
||||
* @see \Dropbox\API\chunkedUpload()
|
||||
*/
|
||||
public function setChunkSize($chunkSize = 4194304) {
|
||||
if (!is_int($chunkSize)) {
|
||||
throw new Exception('Expecting chunk size to be an integer, got ' . gettype($chunkSize));
|
||||
} elseif ($chunkSize > 157286400) {
|
||||
throw new Exception('Chunk size must not exceed 157286400 bytes, got ' . $chunkSize);
|
||||
} else {
|
||||
$this->chunkSize = $chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the JSONP callback function
|
||||
* @param string $function
|
||||
* @return void
|
||||
*/
|
||||
public function setCallback($function) {
|
||||
$this->callback = $function;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 string $isFilename Is $data a filename?
|
||||
* @return boolean|string Mime type and encoding of the file
|
||||
*/
|
||||
private 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
|
||||
*/
|
||||
private 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
|
||||
*/
|
||||
private function encodePath($path) {
|
||||
// in APIv1, encoding was needed because parameters were passed as part of the URL; this is no longer done in our APIv2 SDK; hence, all that we now do here is normalise.
|
||||
return $this->normalisePath($path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Dropbox Exception class
|
||||
* @author Ben Tadiar <ben@handcraftedbyben.co.uk>
|
||||
* @link https://github.com/benthedesigner/dropbox
|
||||
* @package Dropbox
|
||||
*/
|
||||
class Dropbox_Exception extends Exception {
|
||||
}
|
||||
|
||||
class Dropbox_BadRequestException extends Exception {
|
||||
}
|
||||
|
||||
class Dropbox_CurlException extends Exception {
|
||||
}
|
||||
|
||||
class Dropbox_NotAcceptableException extends Exception {
|
||||
}
|
||||
|
||||
class Dropbox_NotFoundException extends Exception {
|
||||
}
|
||||
|
||||
class Dropbox_NotModifiedException extends Exception {
|
||||
}
|
||||
|
||||
class Dropbox_UnsupportedMediaTypeException extends Exception {
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Abstract OAuth consumer
|
||||
* @author Ben Tadiar <ben@handcraftedbyben.co.uk>
|
||||
* @link https://github.com/benthedesigner/dropbox
|
||||
* @package Dropbox\OAuth
|
||||
* @subpackage Consumer
|
||||
*/
|
||||
|
||||
abstract class Dropbox_ConsumerAbstract
|
||||
{
|
||||
// Dropbox web endpoint. v2 API has just dropped the 1/ suffix to the below.
|
||||
const WEB_URL = 'https://www.dropbox.com/';
|
||||
|
||||
// OAuth flow methods
|
||||
const AUTHORISE_METHOD = 'oauth2/authorize';
|
||||
// Beware - the documentation in one place says oauth2/token/revoke, but that appears to be wrong
|
||||
const DEAUTHORISE_METHOD = '2/auth/token/revoke';
|
||||
const ACCESS_TOKEN_METHOD = 'oauth2/token';
|
||||
// The next endpoint only exists with APIv1
|
||||
const OAUTH_UPGRADE = 'oauth2/token_from_oauth1';
|
||||
|
||||
/**
|
||||
* Signature method, either PLAINTEXT or HMAC-SHA1
|
||||
* @var string
|
||||
*/
|
||||
private $sigMethod = 'PLAINTEXT';
|
||||
|
||||
/**
|
||||
* Output file handle
|
||||
* @var null|resource
|
||||
*/
|
||||
protected $outFile = null;
|
||||
|
||||
/**
|
||||
* Input file handle
|
||||
* @var null|resource
|
||||
*/
|
||||
protected $inFile = null;
|
||||
|
||||
/**
|
||||
* Authenticate using 3-legged OAuth flow, firstly
|
||||
* checking we don't already have tokens to use
|
||||
* @return void
|
||||
*/
|
||||
protected function authenticate()
|
||||
{
|
||||
global $updraftplus;
|
||||
|
||||
$access_token = $this->storage->get('access_token');
|
||||
//Check if the new token type is set if not they need to be upgraded to OAuth2
|
||||
if (!empty($access_token) && isset($access_token->oauth_token) && !isset($access_token->token_type)) {
|
||||
$updraftplus->log('OAuth v1 token found: upgrading to v2');
|
||||
$this->upgradeOAuth();
|
||||
$updraftplus->log('OAuth token upgrade successful');
|
||||
}
|
||||
|
||||
if (empty($access_token) || !isset($access_token->oauth_token)) {
|
||||
try {
|
||||
$this->getAccessToken();
|
||||
} catch(Exception $e) {
|
||||
$excep_class = get_class($e);
|
||||
// 04-Sep-2015 - Dropbox started throwing a 400, which caused a Dropbox_BadRequestException which previously wasn't being caught
|
||||
if ('Dropbox_BadRequestException' == $excep_class || 'Dropbox_Exception' == $excep_class) {
|
||||
global $updraftplus;
|
||||
$updraftplus->log($e->getMessage().' - need to reauthenticate this site with Dropbox (if this fails, then you can also try wiping your settings from the Expert Settings section)');
|
||||
//$this->getRequestToken();
|
||||
$this->authorise();
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the user's OAuth1 token to a OAuth2 token
|
||||
* @return void
|
||||
*/
|
||||
private function upgradeOAuth()
|
||||
{
|
||||
// N.B. This call only exists under API v1 - i.e. there is no APIv2 equivalent. Hence the APIv1 endpoint (API_URL) is used, and not the v2 (API_URL_V2)
|
||||
$url = 'https://api.dropbox.com/1/' . self::OAUTH_UPGRADE;
|
||||
$response = $this->fetch('POST', $url, '');
|
||||
$token = new stdClass();
|
||||
/*
|
||||
oauth token secret and oauth token were needed by oauth1
|
||||
these are replaced in oauth2 with an access token
|
||||
currently they are still there just in case a method somewhere is expecting them to both be set
|
||||
as far as I can tell only the oauth token is used
|
||||
after more testing token secret can be removed.
|
||||
*/
|
||||
|
||||
$token->oauth_token_secret = $response['body']->access_token;
|
||||
$token->oauth_token = $response['body']->access_token;
|
||||
$token->token_type = $response['body']->token_type;
|
||||
$this->storage->set($token, 'access_token');
|
||||
$this->storage->set('true','upgraded');
|
||||
$this->storage->do_unset('request_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain user authorisation
|
||||
* The user will be redirected to Dropbox' web endpoint
|
||||
* @link http://tools.ietf.org/html/rfc5849#section-2.2
|
||||
* @return void
|
||||
*/
|
||||
private function authorise()
|
||||
{
|
||||
// Only redirect if not using CLI
|
||||
if (PHP_SAPI !== 'cli' && (!defined('DOING_CRON') || !DOING_CRON) && (!defined('DOING_AJAX') || !DOING_AJAX)) {
|
||||
$url = $this->getAuthoriseUrl();
|
||||
if (!headers_sent()) {
|
||||
header('Location: ' . $url);
|
||||
exit;
|
||||
} else {
|
||||
throw new Dropbox_Exception(sprintf(__('The %s authentication could not go ahead, because something else on your site is breaking it. Try disabling your other plugins and switching to a default theme. (Specifically, you are looking for the component that sends output (most likely PHP warnings/errors) before the page begins. Turning off any debugging settings may also help).', 'updraftplus'), 'Dropbox'));
|
||||
}
|
||||
?><?php
|
||||
return false;
|
||||
}
|
||||
global $updraftplus;
|
||||
$updraftplus->log('Dropbox reauthorisation needed; but we are running from cron, AJAX or the CLI, so this is not possible');
|
||||
$this->storage->do_unset('access_token');
|
||||
throw new Dropbox_Exception(sprintf(__('You need to re-authenticate with %s, as your existing credentials are not working.', 'updraftplus'), 'Dropbox'));
|
||||
#$updraftplus->log(sprintf(__('You need to re-authenticate with %s, as your existing credentials are not working.', 'updraftplus'), 'Dropbox'), 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the user authorisation URL
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthoriseUrl()
|
||||
{
|
||||
/*
|
||||
Generate a random key to be passed to Dropbox and stored in session to be checked to prevent CSRF
|
||||
Uses OpenSSL or Mcrypt or defaults to pure PHP implementaion if neither are available.
|
||||
*/
|
||||
|
||||
global $updraftplus;
|
||||
if (!function_exists('crypt_random_string')) $updraftplus->ensure_phpseclib('Crypt_Random');
|
||||
|
||||
$CSRF = base64_encode(crypt_random_string(16));
|
||||
$this->storage->set($CSRF,'CSRF');
|
||||
// Prepare request parameters
|
||||
/*
|
||||
For OAuth v2 Dropbox needs to use a authorisation url that matches one that is set inside the
|
||||
Dropbox developer console. In order to check this it needs the client ID for the OAuth v2 app
|
||||
This will use the default one unless the user is using their own Dropbox App
|
||||
|
||||
For users that use their own Dropbox App there is also no need to provide the callbackhome as
|
||||
part of the CSRF as there is no need to go to auth.updraftplus.com also the redirect uri can
|
||||
then be set to the home as default
|
||||
|
||||
Check if the key has dropbox: if so then remove it to stop the request from being invalid
|
||||
*/
|
||||
$appkey = $this->storage->get('appkey');
|
||||
|
||||
if (!empty($appkey) && 'dropbox:' == substr($appkey, 0, 8)) {
|
||||
$key = substr($appkey, 8);
|
||||
} else if (!empty($appkey)) {
|
||||
$key = $appkey;
|
||||
}
|
||||
|
||||
if ('' != $this->instance_id) $this->instance_id = ':'.$this->instance_id;
|
||||
|
||||
$params = array(
|
||||
'client_id' => empty($key) ? $this->oauth2_id : $key,
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => empty($key) ? $this->callback : $this->callbackhome,
|
||||
'state' => empty($key) ? "POST:".$CSRF.$this->instance_id.$this->callbackhome : $CSRF.$this->instance_id,
|
||||
);
|
||||
|
||||
// Build the URL and redirect the user
|
||||
$query = '?' . http_build_query($params, '', '&');
|
||||
$url = self::WEB_URL . self::AUTHORISE_METHOD . $query;
|
||||
return $url;
|
||||
}
|
||||
|
||||
protected function deauthenticate()
|
||||
{
|
||||
$url = UpdraftPlus_Dropbox_API::API_URL_V2 . self::DEAUTHORISE_METHOD;
|
||||
$response = $this->fetch('POST', $url, '', array('api_v2' => true));
|
||||
$this->storage->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire an access token
|
||||
* Tokens acquired at this point should be stored to
|
||||
* prevent having to request new tokens for each API call
|
||||
* @link http://tools.ietf.org/html/rfc5849#section-2.3
|
||||
*/
|
||||
public function getAccessToken()
|
||||
{
|
||||
|
||||
// If this is non-empty, then we just received a code. It is stored in 'code' - our next job is to put it into the proper place.
|
||||
$code = $this->storage->get('code');
|
||||
/*
|
||||
Checks to see if the user is using their own Dropbox App if so then they need to get
|
||||
a request token. If they are using our App then we just need to save these details
|
||||
*/
|
||||
if (!empty($code)){
|
||||
$appkey = $this->storage->get('appkey');
|
||||
if (!empty($appkey)){
|
||||
// Get the signed request URL
|
||||
$url = UpdraftPlus_Dropbox_API::API_URL_V2 . self::ACCESS_TOKEN_METHOD;
|
||||
$params = array(
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => $this->callbackhome,
|
||||
'client_id' => $this->consumerKey,
|
||||
'client_secret' => $this->consumerSecret,
|
||||
);
|
||||
$response = $this->fetch('POST', $url, '' , $params);
|
||||
|
||||
$code = json_decode(json_encode($response['body']),true);
|
||||
|
||||
} else {
|
||||
$code = base64_decode($code);
|
||||
$code = json_decode($code, true);
|
||||
}
|
||||
|
||||
/*
|
||||
Again oauth token secret and oauth token were needed by oauth1
|
||||
these are replaced in oauth2 with an access token
|
||||
currently they are still there just in case a method somewhere is expecting them to both be set
|
||||
as far as I can tell only the oauth token is used
|
||||
after more testing token secret can be removed.
|
||||
*/
|
||||
|
||||
$token = new stdClass();
|
||||
$token->oauth_token_secret = $code['access_token'];
|
||||
$token->oauth_token = $code['access_token'];
|
||||
$token->account_id = $code['account_id'];
|
||||
$token->token_type = $code['token_type'];
|
||||
$token->uid = $code['uid'];
|
||||
$this->storage->set($token, 'access_token');
|
||||
$this->storage->do_unset('upgraded');
|
||||
|
||||
//reset code
|
||||
$this->storage->do_unset('code');
|
||||
} else {
|
||||
throw new Dropbox_BadRequestException("No Dropbox Code found, will try to get one now", 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request/access token
|
||||
* This will return the access/request token depending on
|
||||
* which stage we are at in the OAuth flow, or a dummy object
|
||||
* if we have not yet started the authentication process
|
||||
* @return object stdClass
|
||||
*/
|
||||
private function getToken()
|
||||
{
|
||||
if (!$token = $this->storage->get('access_token')) {
|
||||
if (!$token = $this->storage->get('request_token')) {
|
||||
$token = new stdClass();
|
||||
$token->oauth_token = null;
|
||||
$token->oauth_token_secret = null;
|
||||
}
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate signed request URL
|
||||
* See inline comments for description
|
||||
* @link http://tools.ietf.org/html/rfc5849#section-3.4
|
||||
* @param string $method HTTP request method
|
||||
* @param string $url API endpoint to send the request to
|
||||
* @param string $call API call to send
|
||||
* @param array $additional Additional parameters as an associative array
|
||||
* @return array
|
||||
*/
|
||||
protected function getSignedRequest($method, $url, $call, array $additional = array())
|
||||
{
|
||||
// Get the request/access token
|
||||
$token = $this->getToken();
|
||||
|
||||
// Prepare the standard request parameters differently for OAuth1 and OAuth2; we still need OAuth1 to make the request to the upgrade token endpoint
|
||||
if (isset($token->token_type)) {
|
||||
$params = array(
|
||||
'access_token' => $token->oauth_token,
|
||||
);
|
||||
|
||||
/*
|
||||
To keep this API backwards compatible with the API v1 endpoints all v2 endpoints will also send to this method a api_v2 parameter this will then return just the access token as the signed request is not needed for any calls.
|
||||
*/
|
||||
|
||||
if (isset($additional['api_v2']) && $additional['api_v2'] == true) {
|
||||
unset($additional['api_v2']);
|
||||
if (isset($additional['timeout'])) unset($additional['timeout']);
|
||||
if (isset($additional['content_download']) && $additional['content_download'] == true) {
|
||||
unset($additional['content_download']);
|
||||
$extra_headers = array();
|
||||
if (isset($additional['headers'])) {
|
||||
foreach ($additional['headers'] as $key => $header) {
|
||||
$extra_headers[] = $header;
|
||||
}
|
||||
unset($additional['headers']);
|
||||
}
|
||||
$headers = array(
|
||||
'Authorization: Bearer '.$params['access_token'],
|
||||
'Content-Type:',
|
||||
'Dropbox-API-Arg: '.json_encode($additional),
|
||||
);
|
||||
|
||||
$headers = array_merge($headers, $extra_headers);
|
||||
$additional = '';
|
||||
} else if (isset($additional['content_upload']) && $additional['content_upload'] == true) {
|
||||
unset($additional['content_upload']);
|
||||
$headers = array(
|
||||
'Authorization: Bearer '.$params['access_token'],
|
||||
'Content-Type: application/octet-stream',
|
||||
'Dropbox-API-Arg: '.json_encode($additional),
|
||||
);
|
||||
$additional = '';
|
||||
} else {
|
||||
$headers = array(
|
||||
'Authorization: Bearer '.$params['access_token'],
|
||||
'Content-Type: application/json',
|
||||
);
|
||||
}
|
||||
return array(
|
||||
'url' => $url . $call,
|
||||
'postfields' => $additional,
|
||||
'headers' => $headers,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Generate a random string for the request
|
||||
$nonce = md5(microtime(true) . uniqid('', true));
|
||||
$params = array(
|
||||
'oauth_consumer_key' => $this->consumerKey,
|
||||
'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 (isset($value[0]) && $value[0] === '@') $value = $params['filename'];
|
||||
# Prevent spurious PHP warning by only doing non-arrays
|
||||
if (!is_array($value)) $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->consumerSecret . '&' . $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
|
||||
*/
|
||||
private function getSignature($base, $key)
|
||||
{
|
||||
switch ($this->sigMethod) {
|
||||
case 'PLAINTEXT':
|
||||
$signature = $key;
|
||||
break;
|
||||
case 'HMAC-SHA1':
|
||||
$signature = base64_encode(hash_hmac('sha1', $base, $key, true));
|
||||
break;
|
||||
}
|
||||
|
||||
return $signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the OAuth signature method
|
||||
* @param string $method Either PLAINTEXT or HMAC-SHA1
|
||||
* @return void
|
||||
*/
|
||||
public function setSignatureMethod($method)
|
||||
{
|
||||
$method = strtoupper($method);
|
||||
|
||||
switch ($method) {
|
||||
case 'PLAINTEXT':
|
||||
case 'HMAC-SHA1':
|
||||
$this->sigMethod = $method;
|
||||
break;
|
||||
default:
|
||||
throw new Dropbox_Exception('Unsupported signature method ' . $method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the output file
|
||||
* @param resource Resource to stream response data to
|
||||
* @return void
|
||||
*/
|
||||
public function setOutFile($handle)
|
||||
{
|
||||
if (!is_resource($handle) || get_resource_type($handle) != 'stream') {
|
||||
throw new Dropbox_Exception('Outfile must be a stream resource');
|
||||
}
|
||||
$this->outFile = $handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the input file
|
||||
* @param resource Resource to read data from
|
||||
* @return void
|
||||
*/
|
||||
public function setInFile($handle) {
|
||||
$this->inFile = $handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 object stdClass
|
||||
*/
|
||||
private 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
|
||||
*/
|
||||
private function encode($value)
|
||||
{
|
||||
return str_replace('%7E', '~', rawurlencode($value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* OAuth consumer using PHP cURL
|
||||
* @author Ben Tadiar <ben@handcraftedbyben.co.uk>
|
||||
* @link https://github.com/benthedesigner/dropbox
|
||||
* @package Dropbox\OAuth
|
||||
* @subpackage Consumer
|
||||
*/
|
||||
|
||||
class Dropbox_Curl extends Dropbox_ConsumerAbstract
|
||||
{
|
||||
/**
|
||||
* Default cURL options
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultOptions = array(
|
||||
CURLOPT_VERBOSE => true,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLINFO_HEADER_OUT => false,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
);
|
||||
|
||||
/**
|
||||
* Store the last response form the API
|
||||
* @var mixed
|
||||
*/
|
||||
protected $lastResponse = null;
|
||||
|
||||
/**
|
||||
* Set properties and begin authentication
|
||||
* @param string $key
|
||||
* @param string $secret
|
||||
* @param \Dropbox\OAuth\Consumer\StorageInterface $storage
|
||||
* @param string $callback
|
||||
*/
|
||||
public function __construct($key, $oauth2_id, $secret, Dropbox_StorageInterface $storage, $callback = null, $callbackhome = null, $deauthenticate = false, $instance_id = '')
|
||||
{
|
||||
// Check the cURL extension is loaded
|
||||
if (!extension_loaded('curl')) {
|
||||
throw new Dropbox_Exception('The cURL OAuth consumer requires the cURL extension. Please speak to your web hosting provider so that this missing PHP component can be installed.');
|
||||
}
|
||||
|
||||
$this->consumerKey = $key;
|
||||
$this->oauth2_id = $oauth2_id;
|
||||
$this->consumerSecret = $secret;
|
||||
$this->storage = $storage;
|
||||
$this->callback = $callback;
|
||||
$this->callbackhome = $callbackhome;
|
||||
$this->instance_id = $instance_id;
|
||||
|
||||
if ($deauthenticate) {
|
||||
$this->deauthenticate();
|
||||
} else {
|
||||
$this->authenticate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an API call
|
||||
* @todo Improve error handling
|
||||
* @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
|
||||
* @return string|object stdClass
|
||||
*/
|
||||
public function fetch($method, $url, $call, array $additional = array())
|
||||
{
|
||||
// Get the signed request URL
|
||||
$request = $this->getSignedRequest($method, $url, $call, $additional);
|
||||
|
||||
// Initialise and execute a cURL request
|
||||
$handle = curl_init($request['url']);
|
||||
|
||||
// Get the default options array
|
||||
$options = $this->defaultOptions;
|
||||
if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) {
|
||||
$options[CURLOPT_CAINFO] = UPDRAFTPLUS_DIR.'/includes/cacert.pem';
|
||||
}
|
||||
if (UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')) {
|
||||
$options[CURLOPT_SSL_VERIFYPEER] = false;
|
||||
} else {
|
||||
$options[CURLOPT_SSL_VERIFYPEER] = true;
|
||||
}
|
||||
|
||||
if (!class_exists('WP_HTTP_Proxy')) require_once(ABSPATH.WPINC.'/class-http.php');
|
||||
$proxy = new WP_HTTP_Proxy();
|
||||
|
||||
if ($proxy->is_enabled()) {
|
||||
# WP_HTTP_Proxy returns empty strings if nothing is set
|
||||
$user = $proxy->username();
|
||||
$pass = $proxy->password();
|
||||
$host = $proxy->host();
|
||||
$port = (int)$proxy->port();
|
||||
if (empty($port)) $port = 8080;
|
||||
if (!empty($host) && $proxy->send_through_proxy($request['url'])) {
|
||||
$options[CURLOPT_PROXY] = $host;
|
||||
$options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP;
|
||||
$options[CURLOPT_PROXYPORT] = $port;
|
||||
if (!empty($user) && !empty($pass)) {
|
||||
$options[CURLOPT_PROXYAUTH] = CURLAUTH_ANY;
|
||||
$options[CURLOPT_PROXYUSERPWD] = sprintf('%s:%s', $user, $pass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Add check to see if it's an API v2 call if so then json encode the contents. This is so that it is backwards compatible with API v1 endpoints.
|
||||
*/
|
||||
if (isset($additional['api_v2']) && !empty($request['postfields'])) {
|
||||
$request['postfields'] = json_encode($request['postfields']);
|
||||
} elseif (empty($request['postfields'])) {
|
||||
// if the postfields are empty then we don't want to send the application/json header if it's set as Dropbox will return an error
|
||||
$key = array_search('Content-Type: application/json', $request['headers']);
|
||||
if (false !== $key) unset($request['headers'][$key]);
|
||||
}
|
||||
|
||||
if (isset($request['headers']) && !empty($request['headers'])) $options[CURLOPT_HTTPHEADER] = $request['headers'];
|
||||
|
||||
if ($method == 'GET' && $this->outFile) { // GET
|
||||
$options[CURLOPT_RETURNTRANSFER] = false;
|
||||
$options[CURLOPT_HEADER] = false;
|
||||
$options[CURLOPT_FILE] = $this->outFile;
|
||||
$options[CURLOPT_BINARYTRANSFER] = true;
|
||||
$options[CURLOPT_FAILONERROR] = true;
|
||||
$this->outFile = null;
|
||||
} elseif ($method == 'POST' && $this->outFile) { // POST
|
||||
$options[CURLOPT_POST] = true;
|
||||
$options[CURLOPT_RETURNTRANSFER] = false;
|
||||
$options[CURLOPT_HEADER] = false;
|
||||
$options[CURLOPT_FILE] = $this->outFile;
|
||||
$options[CURLOPT_BINARYTRANSFER] = true;
|
||||
$options[CURLOPT_FAILONERROR] = true;
|
||||
$this->outFile = null;
|
||||
} elseif ($method == 'POST' && $this->inFile) { // POST
|
||||
$options[CURLOPT_POST] = true;
|
||||
$options[CURLOPT_POSTFIELDS] = $this->inFile;
|
||||
} elseif ($method == 'POST') { // POST
|
||||
$options[CURLOPT_POST] = true;
|
||||
$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;
|
||||
}
|
||||
|
||||
if (isset($additional['timeout'])) {
|
||||
$options[CURLOPT_TIMEOUT] = $additional['timeout'];
|
||||
}
|
||||
|
||||
// Set the cURL options at once
|
||||
curl_setopt_array($handle, $options);
|
||||
// Execute, get any error and close
|
||||
$response = curl_exec($handle);
|
||||
$error = curl_error($handle);
|
||||
$getinfo = curl_getinfo($handle);
|
||||
|
||||
curl_close($handle);
|
||||
|
||||
//Check if a cURL error has occured
|
||||
if ($response === false) {
|
||||
throw new Dropbox_CurlException($error);
|
||||
} else {
|
||||
// Parse the response if it is a string
|
||||
if (is_string($response)) {
|
||||
$response = $this->parse($response);
|
||||
}
|
||||
|
||||
// Set the last response
|
||||
$this->lastResponse = $response;
|
||||
|
||||
$code = (!empty($response['code'])) ? $response['code'] : $getinfo['http_code'];
|
||||
|
||||
// The API doesn't return an error message for the 304 status code...
|
||||
// 304's are only returned when the path supplied during metadata calls has not been modified
|
||||
if ($code == 304) {
|
||||
$response['body'] = new stdClass;
|
||||
$response['body']->error = 'The folder contents have not changed';
|
||||
}
|
||||
|
||||
// Check if an error occurred and throw an Exception
|
||||
if (!empty($response['body']->error) || $code >= 400) {
|
||||
// Dropbox returns error messages inconsistently...
|
||||
if (!empty($response['body']->error) && $response['body']->error instanceof stdClass) {
|
||||
$array = array_values((array) $response['body']->error);
|
||||
//Dropbox API v2 only throws 409 errors if this error is a incorrect_offset then we need the entire error array not just the message. PHP Exception messages have to be a string so JSON encode the array.
|
||||
if (strpos($array[0] , 'incorrect_offset') !== false) {
|
||||
$message = json_encode($array);
|
||||
} elseif (strpos($array[0] , 'lookup_failed') !== false ) {
|
||||
//re-structure the array so it is correctly formatted for API
|
||||
//Note: Dropbox v2 returns different errors at different stages hence this fix
|
||||
$correctOffset = array(
|
||||
'0' => $array[1]->{'.tag'},
|
||||
'1' => $array[1]->correct_offset
|
||||
);
|
||||
|
||||
$message = json_encode($correctOffset);
|
||||
} else {
|
||||
$message = $array[0];
|
||||
}
|
||||
} elseif (!empty($response['body']->error)) {
|
||||
$message = $response['body']->error;
|
||||
} elseif (is_string($response['body'])) {
|
||||
// 31 Mar 2017 - This case has been found to exist; though the docs imply that there's always an 'error' property and that what is returned in JSON, we found a case of this being returned just as a simple string, but detectable via an HTTP 400: Error in call to API function "files/upload_session/append_v2": HTTP header "Dropbox-API-Arg": cursor.offset: expected integer, got string
|
||||
$message = $response['body'];
|
||||
} else {
|
||||
$message = "HTTP bad response code: $code";
|
||||
}
|
||||
|
||||
// Throw an Exception with the appropriate with the appropriate message and code
|
||||
switch ($code) {
|
||||
case 304:
|
||||
throw new Dropbox_NotModifiedException($message, 304);
|
||||
case 400:
|
||||
throw new Dropbox_BadRequestException($message, 400);
|
||||
case 404:
|
||||
throw new Dropbox_NotFoundException($message, 404);
|
||||
case 406:
|
||||
throw new Dropbox_NotAcceptableException($message, 406);
|
||||
case 415:
|
||||
throw new Dropbox_UnsupportedMediaTypeException($message, 415);
|
||||
case 401:
|
||||
//401 means oauth token is expired continue to manually handle the exception depending on the situation
|
||||
break;
|
||||
case 409:
|
||||
//409 in API V2 every error will return with a 409 to find out what the error is the error description should be checked.
|
||||
throw new Dropbox_Exception($message, $code);
|
||||
default:
|
||||
throw new Dropbox_Exception($message, $code);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a cURL response
|
||||
* @param string $response
|
||||
* @return array
|
||||
*/
|
||||
private 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/[\.\d]+ 100#i', $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/[\.\d]+ ([0-9]{3})#i';
|
||||
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;
|
||||
}
|
||||
|
||||
if (is_string($body)) {
|
||||
$body_lines = explode("\r\n", $body);
|
||||
if (preg_match('#^HTTP/[\.\d]+ 100#i', $body_lines[0]) && preg_match('#^HTTP/\d#i', $body_lines[2])) {
|
||||
return $this->parse($body);
|
||||
}
|
||||
}
|
||||
|
||||
return array('code' => $code, 'body' => $body, 'headers' => $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the response for the last API request
|
||||
* @return mixed
|
||||
*/
|
||||
public function getlastResponse()
|
||||
{
|
||||
return $this->lastResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* OAuth consumer using the WordPress API
|
||||
* @author David Anderson <david@updraftplus.com>
|
||||
* @link https://github.com/DavidAnderson684/Dropbox
|
||||
* @package Dropbox\OAuth
|
||||
* @subpackage Consumer
|
||||
*/
|
||||
|
||||
class Dropbox_ConsumerWordPress extends Dropbox_ConsumerAbstract
|
||||
{
|
||||
|
||||
/**
|
||||
* Set properties and begin authentication
|
||||
* @param string $key
|
||||
* @param string $secret
|
||||
* @param \Dropbox\OAuth\Consumer\StorageInterface $storage
|
||||
* @param string $callback
|
||||
*/
|
||||
public function __construct($key, $secret, Dropbox_StorageInterface $storage, $callback = null)
|
||||
{
|
||||
// Check we are in a WordPress environment
|
||||
if (!defined('ABSPATH')) {
|
||||
throw new Dropbox_Exception('The WordPress OAuth consumer requires a WordPress environment');
|
||||
}
|
||||
|
||||
$this->consumerKey = $key;
|
||||
$this->consumerSecret = $secret;
|
||||
$this->storage = $storage;
|
||||
$this->callback = $callback;
|
||||
$this->authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return array
|
||||
*/
|
||||
public function fetch($method, $url, $call, array $additional = array())
|
||||
{
|
||||
// Get the signed request URL
|
||||
$request = $this->getSignedRequest($method, $url, $call, $additional);
|
||||
if ($method == 'GET') {
|
||||
$args = array ( );
|
||||
$response = wp_remote_get($request['url'], $args);
|
||||
$this->outFile = null;
|
||||
} elseif ($method == 'POST') {
|
||||
$args = array( 'body' => $request['postfields'] );
|
||||
$response = wp_remote_post($request['url'], $args );
|
||||
} elseif ($method == 'PUT' && $this->inFile) {
|
||||
return new WP_Error('unsupported', "WordPress does not have a native HTTP PUT function");
|
||||
}
|
||||
|
||||
// If the response body is not a JSON encoded string
|
||||
// we'll return the entire response body
|
||||
// Important to do this first, as the next section relies on the decoding having taken place
|
||||
if (!$body = json_decode(wp_remote_retrieve_body($response))) {
|
||||
$body = wp_remote_retrieve_body($response);
|
||||
}
|
||||
|
||||
// Check if an error occurred and throw an Exception. This is part of the authentication process - don't modify.
|
||||
if (!empty($body->error)) {
|
||||
$message = $body->error . ' (Status Code: ' . wp_remote_retrieve_response_code($response) . ')';
|
||||
throw new Dropbox_Exception($message);
|
||||
}
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
$message = $response->get_error_message();
|
||||
throw new Dropbox_Exception($message);
|
||||
}
|
||||
|
||||
$results = array ( 'body' => $body, 'code' => wp_remote_retrieve_response_code($response), 'headers' => $response['headers'] );
|
||||
return $results;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This class provides the functionality to encrypt
|
||||
* and decrypt access tokens stored by the application
|
||||
* @author Ben Tadiar <ben@handcraftedbyben.co.uk>
|
||||
* @link https://github.com/benthedesigner/dropbox
|
||||
* @package Dropbox\Oauth
|
||||
* @subpackage Storage
|
||||
*/
|
||||
|
||||
/* UpdraftPlus notes
|
||||
Using this was fairly pointless (it encrypts storage credentials at rest). But, it's implemented now, so needs supporting.
|
||||
Investigation shows that mcrypt and phpseclib native encryption using different padding schemes.
|
||||
As a result, that which is encrypted by phpseclib native can be decrypted by mcrypt, but not vice-versa. Each can (as you'd expect) decrypt the results of their own encryption.
|
||||
As a consequence, it makes sense to always encrypt with phpseclib native, and prefer decrypting with with mcrypt if it is available and otherwise fall back to phpseclib.
|
||||
We could deliberately re-encrypt all loaded information with phpseclib native, but there seems little need for that yet. There can only be a problem if mcrypt is disabled - which pre-July-2015 meant that Dropbox wouldn't work at all. Now, it will force a re-authorisation.
|
||||
*/
|
||||
|
||||
class Dropbox_Encrypter
|
||||
{
|
||||
// Encryption settings - default settings yield encryption to AES (256-bit) standard
|
||||
// @todo Provide PHPDOC for each class constant
|
||||
const KEY_SIZE = 32;
|
||||
const IV_SIZE = 16;
|
||||
|
||||
/**
|
||||
* Encryption key
|
||||
* @var null|string
|
||||
*/
|
||||
private $key = null;
|
||||
|
||||
/**
|
||||
* Check Mcrypt is loaded and set the encryption key
|
||||
* @param string $key
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($key)
|
||||
{
|
||||
if (preg_match('/^[A-Za-z0-9]+$/', $key) && $length = strlen($key) === self::KEY_SIZE) {
|
||||
# Short-cut so that the mbstring extension is not required
|
||||
$this->key = $key;
|
||||
} elseif (($length = mb_strlen($key, '8bit')) !== self::KEY_SIZE) {
|
||||
throw new Dropbox_Exception('Expecting a ' . self::KEY_SIZE . ' byte key, got ' . $length);
|
||||
} else {
|
||||
// Set the encryption key
|
||||
$this->key = $key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the OAuth token
|
||||
* @param \stdClass $token Serialized token object
|
||||
* @return string
|
||||
*/
|
||||
public function encrypt($token)
|
||||
{
|
||||
|
||||
// Encryption: we always use phpseclib for this
|
||||
global $updraftplus;
|
||||
$ensure_phpseclib = $updraftplus->ensure_phpseclib('Crypt_AES');
|
||||
|
||||
if (is_wp_error($ensure_phpseclib)) {
|
||||
$updraftplus->log("Failed to load phpseclib classes (".$ensure_phpseclib->get_error_code()."): ".$ensure_phpseclib->get_error_message());
|
||||
$updraftplus->log("Failed to load phpseclib classes (".$ensure_phpseclib->get_error_code()."): ".$ensure_phpseclib->get_error_message(), 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
$updraftplus->ensure_phpseclib('Crypt_Rijndael');
|
||||
|
||||
if (!function_exists('crypt_random_string')) require_once(UPDRAFTPLUS_DIR.'/vendor/phpseclib/phpseclib/phpseclib/Crypt/Random.php');
|
||||
|
||||
$iv = crypt_random_string(self::IV_SIZE);
|
||||
|
||||
// Defaults to CBC mode
|
||||
$rijndael = new Crypt_Rijndael();
|
||||
|
||||
$rijndael->setKey($this->key);
|
||||
|
||||
$rijndael->setIV($iv);
|
||||
|
||||
$cipherText = $rijndael->encrypt($token);
|
||||
|
||||
return base64_encode($iv . $cipherText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the ciphertext
|
||||
* @param string $cipherText
|
||||
* @return object \stdClass Unserialized token
|
||||
*/
|
||||
public function decrypt($cipherText)
|
||||
{
|
||||
|
||||
// Decryption: prefer mcrypt, if available (since it can decrypt data encrypted by either mcrypt or phpseclib)
|
||||
|
||||
$cipherText = base64_decode($cipherText);
|
||||
$iv = substr($cipherText, 0, self::IV_SIZE);
|
||||
$cipherText = substr($cipherText, self::IV_SIZE);
|
||||
|
||||
if (function_exists('mcrypt_decrypt')) {
|
||||
// @codingStandardsIgnoreLine
|
||||
$token = @mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $this->key, $cipherText, MCRYPT_MODE_CBC, $iv);
|
||||
} else {
|
||||
global $updraftplus;
|
||||
$updraftplus->ensure_phpseclib('Crypt_Rijndael');
|
||||
|
||||
$rijndael = new Crypt_Rijndael();
|
||||
$rijndael->setKey($this->key);
|
||||
$rijndael->setIV($iv);
|
||||
$token = $rijndael->decrypt($cipherText);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* OAuth storage handler interface
|
||||
* @author Ben Tadiar <ben@handcraftedbyben.co.uk>
|
||||
* @link https://github.com/benthedesigner/dropbox
|
||||
* @package Dropbox\OAuth
|
||||
* @subpackage Storage
|
||||
*/
|
||||
|
||||
interface Dropbox_StorageInterface
|
||||
{
|
||||
/**
|
||||
* Get a token by type
|
||||
* @param string $type Token type to retrieve
|
||||
*/
|
||||
public function get($type);
|
||||
|
||||
/**
|
||||
* Set a token by type
|
||||
* @param \stdClass $token Token object to set
|
||||
* @param string $type Token type
|
||||
*/
|
||||
public function set($token, $type);
|
||||
|
||||
/**
|
||||
* Delete tokens for the current session/user
|
||||
*/
|
||||
public function delete();
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* OAuth storage handler using WordPress options
|
||||
* This can only be used if you have a WordPress environment loaded, such that the (get|update|delete)_option functions are available
|
||||
* See an example usage in http://wordpress.org/extend/plugins/updraftplus
|
||||
* @author David Anderson <david@updraftplus.com>
|
||||
* @link https://updraftplus.com
|
||||
* @package Dropbox\Oauth
|
||||
* @subpackage Storage
|
||||
*/
|
||||
|
||||
class Dropbox_WordPress implements Dropbox_StorageInterface
|
||||
{
|
||||
/**
|
||||
* Option name
|
||||
* @var string
|
||||
*/
|
||||
protected $option_name_prefix = 'dropbox_token';
|
||||
|
||||
/**
|
||||
* Option name (array storage)
|
||||
* @var string
|
||||
*/
|
||||
protected $option_array = '';
|
||||
|
||||
/**
|
||||
* Encyption object
|
||||
* @var Encrypter|null
|
||||
*/
|
||||
protected $encrypter = null;
|
||||
|
||||
/**
|
||||
* Backup module object
|
||||
* @var Backup_module_object|null
|
||||
*/
|
||||
protected $backup_module_object = null;
|
||||
|
||||
/**
|
||||
* Check if an instance of the encrypter is passed, set the encryption object
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Dropbox_Encrypter $encrypter = null, $option_name_prefix = 'dropbox_token', $option_array = 'dropbox', $backup_module_object)
|
||||
{
|
||||
if ($encrypter instanceof Dropbox_Encrypter) {
|
||||
$this->encrypter = $encrypter;
|
||||
}
|
||||
|
||||
if ($backup_module_object instanceof UpdraftPlus_BackupModule) {
|
||||
$this->backup_module_object = $backup_module_object;
|
||||
}
|
||||
|
||||
$this->option_name_prefix = $option_name_prefix;
|
||||
$this->option_array = $option_array;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entry from the Dropbox options in the database
|
||||
* If the encryption object is set then decrypt the token before returning
|
||||
* @param string $type is the key to retrieve
|
||||
* @return array|bool
|
||||
*/
|
||||
public function get($type)
|
||||
{
|
||||
if ($type != 'request_token' && $type != 'access_token' && $type != 'appkey' && $type != 'CSRF' && $type != 'code') {
|
||||
throw new Dropbox_Exception("Expected a type of either 'request_token', 'access_token', 'CSRF' or 'code', got '$type'");
|
||||
} else {
|
||||
if (false !== ($opts = $this->backup_module_object->get_options())) {
|
||||
if ($type == 'request_token' || $type == 'access_token'){
|
||||
if (!empty($opts[$this->option_name_prefix.$type])) {
|
||||
$gettoken = $opts[$this->option_name_prefix.$type];
|
||||
$token = $this->decrypt($gettoken);
|
||||
return $token;
|
||||
}
|
||||
} else {
|
||||
if (!empty($opts[$type])) {
|
||||
return $opts[$type];
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the database by type
|
||||
* If the value is a token and the encryption object is set then encrypt the token before storing
|
||||
* @param \stdClass Token object to set
|
||||
* @param string $type Token type
|
||||
* @return void
|
||||
*/
|
||||
public function set($token, $type)
|
||||
{
|
||||
if ($type != 'request_token' && $type != 'access_token' && $type != 'upgraded' && $type != 'CSRF' && $type != 'code') {
|
||||
throw new Dropbox_Exception("Expected a type of either 'request_token', 'access_token', 'CSRF', 'upgraded' or 'code', got '$type'");
|
||||
} else {
|
||||
|
||||
$opts = $this->backup_module_object->get_options();
|
||||
|
||||
if ($type == 'access_token'){
|
||||
$token = $this->encrypt($token);
|
||||
$opts[$this->option_name_prefix.$type] = $token;
|
||||
} else if ($type == 'request_token' ) {
|
||||
$opts[$this->option_name_prefix.$type] = $token;
|
||||
} else {
|
||||
$opts[$type] = $token;
|
||||
}
|
||||
|
||||
$this->backup_module_object->set_options($opts, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a value in the database by type rather than setting to null / empty
|
||||
* set the value to null here so that when it gets to the options filter it will
|
||||
* unset the value there, this avoids a bug where if the value is not set then
|
||||
* the option filter will take the value from the database and save that version back.
|
||||
*
|
||||
* N.B. Before PHP 7.0, you can't call a method name unset()
|
||||
*
|
||||
* @param string $type Token type
|
||||
* @return void
|
||||
*/
|
||||
public function do_unset($type)
|
||||
{
|
||||
if ($type != 'request_token' && $type != 'access_token' && $type != 'upgraded' && $type != 'CSRF' && $type != 'code') {
|
||||
throw new Dropbox_Exception("Expected a type of either 'request_token', 'access_token', 'CSRF', 'upgraded' or 'code', got '$type'");
|
||||
} else {
|
||||
|
||||
$opts = $this->backup_module_object->get_options();
|
||||
|
||||
if ($type == 'access_token' || $type == 'request_token'){
|
||||
$opts[$this->option_name_prefix.$type] = null;
|
||||
} else {
|
||||
$opts[$type] = null;
|
||||
}
|
||||
$this->backup_module_object->set_options($opts, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the request and access tokens currently stored in the database
|
||||
* @return bool
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
$opts = $this->backup_module_object->get_options();
|
||||
$opts[$this->option_name_prefix.'request_token'] = null;
|
||||
$opts[$this->option_name_prefix.'access_token'] = null;
|
||||
unset($opts['ownername']);
|
||||
unset($opts['upgraded']);
|
||||
$this->backup_module_object->set_options($opts, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the Encrypter to encrypt a token and return it
|
||||
* If there is not encrypter object, return just the
|
||||
* serialized token object for storage
|
||||
* @param stdClass $token OAuth token to encrypt
|
||||
* @return stdClass|string
|
||||
*/
|
||||
protected function encrypt($token)
|
||||
{
|
||||
// Serialize the token object
|
||||
$token = serialize($token);
|
||||
|
||||
// Encrypt the token if there is an Encrypter instance
|
||||
if ($this->encrypter instanceof Dropbox_Encrypter) {
|
||||
$token = $this->encrypter->encrypt($token);
|
||||
}
|
||||
|
||||
// Return the token
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a token using the Encrypter object and return it
|
||||
* If there is no Encrypter object, assume the token was stored
|
||||
* serialized and return the unserialized token object
|
||||
* @param stdClass $token OAuth token to encrypt
|
||||
* @return stdClass|string
|
||||
*/
|
||||
protected function decrypt($token)
|
||||
{
|
||||
// Decrypt the token if there is an Encrypter instance
|
||||
if ($this->encrypter instanceof Dropbox_Encrypter) {
|
||||
$token = $this->encrypter->decrypt($token);
|
||||
}
|
||||
|
||||
// Return the unserialized token
|
||||
return @unserialize($token);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user