* @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; } // 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 continue; 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/1.1 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/1.1 ([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/1.1 100#i', $body_lines[0]) && preg_match('#^HTTP/1.#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; } }