PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 1.16.5
UpdraftPlus: WP Backup & Migration Plugin v1.16.5
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / includes / Dropbox2 / OAuth / Consumer / ConsumerAbstract.php

ConsumerAbstract.php in UpdraftPlus: WP Backup & Migration Plugin 1.16.5, at includes/Dropbox2/OAuth/Consumer/ConsumerAbstract.php

479 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Abstract OAuth consumer
5 * @author Ben Tadiar <ben@handcraftedbyben.co.uk>
6 * @link https://github.com/benthedesigner/dropbox
7 * @package Dropbox\OAuth
8 * @subpackage Consumer
9 */
10
11 abstract class Dropbox_ConsumerAbstract
12 {
13 // Dropbox web endpoint. v2 API has just dropped the 1/ suffix to the below.
14 const WEB_URL = 'https://www.dropbox.com/';
15
16 // OAuth flow methods
17 const AUTHORISE_METHOD = 'oauth2/authorize';
18 // Beware - the documentation in one place says oauth2/token/revoke, but that appears to be wrong
19 const DEAUTHORISE_METHOD = '2/auth/token/revoke';
20 const ACCESS_TOKEN_METHOD = 'oauth2/token';
21 // The next endpoint only exists with APIv1
22 const OAUTH_UPGRADE = 'oauth2/token_from_oauth1';
23
24 /**
25 * Signature method, either PLAINTEXT or HMAC-SHA1
26 * @var string
27 */
28 private $sigMethod = 'PLAINTEXT';
29
30 /**
31 * Output file handle
32 * @var null|resource
33 */
34 protected $outFile = null;
35
36 /**
37 * Input file handle
38 * @var null|resource
39 */
40 protected $inFile = null;
41
42 /**
43 * Authenticate using 3-legged OAuth flow, firstly
44 * checking we don't already have tokens to use
45 * @return void
46 */
47 protected function authenticate()
48 {
49 global $updraftplus;
50
51 $access_token = $this->storage->get('access_token');
52 //Check if the new token type is set if not they need to be upgraded to OAuth2
53 if (!empty($access_token) && isset($access_token->oauth_token) && !isset($access_token->token_type)) {
54 $updraftplus->log('OAuth v1 token found: upgrading to v2');
55 $this->upgradeOAuth();
56 $updraftplus->log('OAuth token upgrade successful');
57 }
58
59 if (empty($access_token) || !isset($access_token->oauth_token)) {
60 try {
61 $this->getAccessToken();
62 } catch(Exception $e) {
63 $excep_class = get_class($e);
64 // 04-Sep-2015 - Dropbox started throwing a 400, which caused a Dropbox_BadRequestException which previously wasn't being caught
65 if ('Dropbox_BadRequestException' == $excep_class || 'Dropbox_Exception' == $excep_class) {
66 global $updraftplus;
67 $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)');
68 //$this->getRequestToken();
69 $this->authorise();
70 } else {
71 throw $e;
72 }
73 }
74 }
75 }
76
77 /**
78 * Upgrade the user's OAuth1 token to a OAuth2 token
79 * @return void
80 */
81 private function upgradeOAuth()
82 {
83 // 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)
84 $url = 'https://api.dropbox.com/1/' . self::OAUTH_UPGRADE;
85 $response = $this->fetch('POST', $url, '');
86 $token = new stdClass();
87 /*
88 oauth token secret and oauth token were needed by oauth1
89 these are replaced in oauth2 with an access token
90 currently they are still there just in case a method somewhere is expecting them to both be set
91 as far as I can tell only the oauth token is used
92 after more testing token secret can be removed.
93 */
94
95 $token->oauth_token_secret = $response['body']->access_token;
96 $token->oauth_token = $response['body']->access_token;
97 $token->token_type = $response['body']->token_type;
98 $this->storage->set($token, 'access_token');
99 $this->storage->set('true','upgraded');
100 $this->storage->do_unset('request_token');
101 }
102
103 /**
104 * Obtain user authorisation
105 * The user will be redirected to Dropbox' web endpoint
106 * @link http://tools.ietf.org/html/rfc5849#section-2.2
107 * @return void
108 */
109 private function authorise()
110 {
111 // Only redirect if not using CLI
112 if (PHP_SAPI !== 'cli' && (!defined('DOING_CRON') || !DOING_CRON) && (!defined('DOING_AJAX') || !DOING_AJAX)) {
113 $url = $this->getAuthoriseUrl();
114 if (!headers_sent()) {
115 header('Location: ' . $url);
116 exit;
117 } else {
118 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'));
119 }
120 ?><?php
121 return false;
122 }
123 global $updraftplus;
124 $updraftplus->log('Dropbox reauthorisation needed; but we are running from cron, AJAX or the CLI, so this is not possible');
125 $this->storage->do_unset('access_token');
126 throw new Dropbox_Exception(sprintf(__('You need to re-authenticate with %s, as your existing credentials are not working.', 'updraftplus'), 'Dropbox'));
127 #$updraftplus->log(sprintf(__('You need to re-authenticate with %s, as your existing credentials are not working.', 'updraftplus'), 'Dropbox'), 'error');
128 return false;
129 }
130
131 /**
132 * Build the user authorisation URL
133 * @return string
134 */
135 public function getAuthoriseUrl()
136 {
137 /*
138 Generate a random key to be passed to Dropbox and stored in session to be checked to prevent CSRF
139 Uses OpenSSL or Mcrypt or defaults to pure PHP implementaion if neither are available.
140 */
141
142 global $updraftplus;
143 if (!function_exists('crypt_random_string')) $updraftplus->ensure_phpseclib('Crypt_Random', 'Crypt/Random');
144
145 $CSRF = base64_encode(crypt_random_string(16));
146 $this->storage->set($CSRF,'CSRF');
147 // Prepare request parameters
148 /*
149 For OAuth v2 Dropbox needs to use a authorisation url that matches one that is set inside the
150 Dropbox developer console. In order to check this it needs the client ID for the OAuth v2 app
151 This will use the default one unless the user is using their own Dropbox App
152
153 For users that use their own Dropbox App there is also no need to provide the callbackhome as
154 part of the CSRF as there is no need to go to auth.updraftplus.com also the redirect uri can
155 then be set to the home as default
156
157 Check if the key has dropbox: if so then remove it to stop the request from being invalid
158 */
159 $appkey = $this->storage->get('appkey');
160
161 if (!empty($appkey) && 'dropbox:' == substr($appkey, 0, 8)) {
162 $key = substr($appkey, 8);
163 } else if (!empty($appkey)) {
164 $key = $appkey;
165 }
166
167 if ('' != $this->instance_id) $this->instance_id = ':'.$this->instance_id;
168
169 $params = array(
170 'client_id' => empty($key) ? $this->oauth2_id : $key,
171 'response_type' => 'code',
172 'redirect_uri' => empty($key) ? $this->callback : $this->callbackhome,
173 'state' => empty($key) ? "POST:".$CSRF.$this->instance_id.$this->callbackhome : $CSRF.$this->instance_id,
174 );
175
176 // Build the URL and redirect the user
177 $query = '?' . http_build_query($params, '', '&');
178 $url = self::WEB_URL . self::AUTHORISE_METHOD . $query;
179 return $url;
180 }
181
182 protected function deauthenticate()
183 {
184 $url = UpdraftPlus_Dropbox_API::API_URL_V2 . self::DEAUTHORISE_METHOD;
185 $response = $this->fetch('POST', $url, '', array('api_v2' => true));
186 $this->storage->delete();
187 }
188
189 /**
190 * Acquire an access token
191 * Tokens acquired at this point should be stored to
192 * prevent having to request new tokens for each API call
193 * @link http://tools.ietf.org/html/rfc5849#section-2.3
194 */
195 public function getAccessToken()
196 {
197
198 // 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.
199 $code = $this->storage->get('code');
200 /*
201 Checks to see if the user is using their own Dropbox App if so then they need to get
202 a request token. If they are using our App then we just need to save these details
203 */
204 if (!empty($code)){
205 $appkey = $this->storage->get('appkey');
206 if (!empty($appkey)){
207 // Get the signed request URL
208 $url = UpdraftPlus_Dropbox_API::API_URL_V2 . self::ACCESS_TOKEN_METHOD;
209 $params = array(
210 'code' => $code,
211 'grant_type' => 'authorization_code',
212 'redirect_uri' => $this->callbackhome,
213 'client_id' => $this->consumerKey,
214 'client_secret' => $this->consumerSecret,
215 );
216 $response = $this->fetch('POST', $url, '' , $params);
217
218 $code = json_decode(json_encode($response['body']),true);
219
220 } else {
221 $code = base64_decode($code);
222 $code = json_decode($code, true);
223 }
224
225 /*
226 Again oauth token secret and oauth token were needed by oauth1
227 these are replaced in oauth2 with an access token
228 currently they are still there just in case a method somewhere is expecting them to both be set
229 as far as I can tell only the oauth token is used
230 after more testing token secret can be removed.
231 */
232
233 $token = new stdClass();
234 $token->oauth_token_secret = $code['access_token'];
235 $token->oauth_token = $code['access_token'];
236 $token->account_id = $code['account_id'];
237 $token->token_type = $code['token_type'];
238 $token->uid = $code['uid'];
239 $this->storage->set($token, 'access_token');
240 $this->storage->do_unset('upgraded');
241
242 //reset code
243 $this->storage->do_unset('code');
244 } else {
245 throw new Dropbox_BadRequestException("No Dropbox Code found, will try to get one now", 400);
246 }
247 }
248
249 /**
250 * Get the request/access token
251 * This will return the access/request token depending on
252 * which stage we are at in the OAuth flow, or a dummy object
253 * if we have not yet started the authentication process
254 * @return object stdClass
255 */
256 private function getToken()
257 {
258 if (!$token = $this->storage->get('access_token')) {
259 if (!$token = $this->storage->get('request_token')) {
260 $token = new stdClass();
261 $token->oauth_token = null;
262 $token->oauth_token_secret = null;
263 }
264 }
265 return $token;
266 }
267
268 /**
269 * Generate signed request URL
270 * See inline comments for description
271 * @link http://tools.ietf.org/html/rfc5849#section-3.4
272 * @param string $method HTTP request method
273 * @param string $url API endpoint to send the request to
274 * @param string $call API call to send
275 * @param array $additional Additional parameters as an associative array
276 * @return array
277 */
278 protected function getSignedRequest($method, $url, $call, array $additional = array())
279 {
280 // Get the request/access token
281 $token = $this->getToken();
282
283 // Prepare the standard request parameters differently for OAuth1 and OAuth2; we still need OAuth1 to make the request to the upgrade token endpoint
284 if (isset($token->token_type)) {
285 $params = array(
286 'access_token' => $token->oauth_token,
287 );
288
289 /*
290 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.
291 */
292
293 if (isset($additional['api_v2']) && $additional['api_v2'] == true) {
294 unset($additional['api_v2']);
295 if (isset($additional['content_download']) && $additional['content_download'] == true) {
296 unset($additional['content_download']);
297 $extra_headers = array();
298 if (isset($additional['headers'])) {
299 foreach ($additional['headers'] as $key => $header) {
300 $extra_headers[] = $header;
301 }
302 unset($additional['headers']);
303 }
304 $headers = array(
305 'Authorization: Bearer '.$params['access_token'],
306 'Content-Type:',
307 'Dropbox-API-Arg: '.json_encode($additional),
308 );
309
310 $headers = array_merge($headers, $extra_headers);
311 $additional = '';
312 } else if (isset($additional['content_upload']) && $additional['content_upload'] == true) {
313 unset($additional['content_upload']);
314 $headers = array(
315 'Authorization: Bearer '.$params['access_token'],
316 'Content-Type: application/octet-stream',
317 'Dropbox-API-Arg: '.json_encode($additional),
318 );
319 $additional = '';
320 } else {
321 $headers = array(
322 'Authorization: Bearer '.$params['access_token'],
323 'Content-Type: application/json',
324 );
325 }
326 return array(
327 'url' => $url . $call,
328 'postfields' => $additional,
329 'headers' => $headers,
330 );
331 }
332 } else {
333 // Generate a random string for the request
334 $nonce = md5(microtime(true) . uniqid('', true));
335 $params = array(
336 'oauth_consumer_key' => $this->consumerKey,
337 'oauth_token' => $token->oauth_token,
338 'oauth_signature_method' => $this->sigMethod,
339 'oauth_version' => '1.0',
340 // Generate nonce and timestamp if signature method is HMAC-SHA1
341 'oauth_timestamp' => ($this->sigMethod == 'HMAC-SHA1') ? time() : null,
342 'oauth_nonce' => ($this->sigMethod == 'HMAC-SHA1') ? $nonce : null,
343 );
344 }
345
346 // Merge with the additional request parameters
347 $params = array_merge($params, $additional);
348 ksort($params);
349
350 // URL encode each parameter to RFC3986 for use in the base string
351 $encoded = array();
352 foreach($params as $param => $value) {
353 if ($value !== null) {
354 // If the value is a file upload (prefixed with @), replace it with
355 // the destination filename, the file path will be sent in POSTFIELDS
356 if (isset($value[0]) && $value[0] === '@') $value = $params['filename'];
357 # Prevent spurious PHP warning by only doing non-arrays
358 if (!is_array($value)) $encoded[] = $this->encode($param) . '=' . $this->encode($value);
359 } else {
360 unset($params[$param]);
361 }
362 }
363
364 // Build the first part of the string
365 $base = $method . '&' . $this->encode($url . $call) . '&';
366
367 // Re-encode the encoded parameter string and append to $base
368 $base .= $this->encode(implode('&', $encoded));
369
370 // Concatenate the secrets with an ampersand
371 $key = $this->consumerSecret . '&' . $token->oauth_token_secret;
372
373 // Get the signature string based on signature method
374 $signature = $this->getSignature($base, $key);
375 $params['oauth_signature'] = $signature;
376
377 // Build the signed request URL
378 $query = '?' . http_build_query($params, '', '&');
379
380 return array(
381 'url' => $url . $call . $query,
382 'postfields' => $params,
383 );
384 }
385
386 /**
387 * Generate the oauth_signature for a request
388 * @param string $base Signature base string, used by HMAC-SHA1
389 * @param string $key Concatenated consumer and token secrets
390 */
391 private function getSignature($base, $key)
392 {
393 switch ($this->sigMethod) {
394 case 'PLAINTEXT':
395 $signature = $key;
396 break;
397 case 'HMAC-SHA1':
398 $signature = base64_encode(hash_hmac('sha1', $base, $key, true));
399 break;
400 }
401
402 return $signature;
403 }
404
405 /**
406 * Set the OAuth signature method
407 * @param string $method Either PLAINTEXT or HMAC-SHA1
408 * @return void
409 */
410 public function setSignatureMethod($method)
411 {
412 $method = strtoupper($method);
413
414 switch ($method) {
415 case 'PLAINTEXT':
416 case 'HMAC-SHA1':
417 $this->sigMethod = $method;
418 break;
419 default:
420 throw new Dropbox_Exception('Unsupported signature method ' . $method);
421 }
422 }
423
424 /**
425 * Set the output file
426 * @param resource Resource to stream response data to
427 * @return void
428 */
429 public function setOutFile($handle)
430 {
431 if (!is_resource($handle) || get_resource_type($handle) != 'stream') {
432 throw new Dropbox_Exception('Outfile must be a stream resource');
433 }
434 $this->outFile = $handle;
435 }
436
437 /**
438 * Set the input file
439 * @param resource Resource to read data from
440 * @return void
441 */
442 public function setInFile($handle) {
443 $this->inFile = $handle;
444 }
445
446 /**
447 * Parse response parameters for a token into an object
448 * Dropbox returns tokens in the response parameters, and
449 * not a JSON encoded object as per other API requests
450 * @link http://oauth.net/core/1.0/#response_parameters
451 * @param string $response
452 * @return object stdClass
453 */
454 private function parseTokenString($response)
455 {
456 $parts = explode('&', $response);
457 $token = new stdClass();
458 foreach ($parts as $part) {
459 list($k, $v) = explode('=', $part, 2);
460 $k = strtolower($k);
461 $token->$k = $v;
462 }
463 return $token;
464 }
465
466 /**
467 * Encode a value to RFC3986
468 * This is a convenience method to decode ~ symbols encoded
469 * by rawurldecode. This will encode all characters except
470 * the unreserved set, ALPHA, DIGIT, '-', '.', '_', '~'
471 * @link http://tools.ietf.org/html/rfc5849#section-3.6
472 * @param mixed $value
473 */
474 private function encode($value)
475 {
476 return str_replace('%7E', '~', rawurlencode($value));
477 }
478 }
479