PluginProbe
InfiniteWP Client / trunk
InfiniteWP Client vtrunk
1.13.10 1.13.7 trunk 0.1.4 0.1.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.11.0 1.11.1 1.12.1 1.12.3 All 92 releases
iwp-client / lib / Dropbox2 / OAuth / Consumer / ConsumerAbstract.php

ConsumerAbstract.php in InfiniteWP Client trunk, at lib/Dropbox2/OAuth/Consumer/ConsumerAbstract.php

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