PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260917
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260917
260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 120213 All 189 releases
s2member / src / includes / externals / aweber / oauth_application.php

oauth_application.php in s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions 260917, at src/includes/externals/aweber/oauth_application.php

689 lines 21.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // @codingStandardsIgnoreFile
3 if (!class_exists('CurlObject')) require_once('curl_object.php');
4 if (!class_exists('CurlResponse')) require_once('curl_response.php');
5
6 /**
7 * OAuthServiceProvider
8 *
9 * Represents the service provider in the OAuth authentication model.
10 * The class that implements the service provider will contain the
11 * specific knowledge about the API we are interfacing with, and
12 * provide useful methods for interfacing with its API.
13 *
14 * For example, an OAuthServiceProvider would know the URLs necessary
15 * to perform specific actions, the type of data that the API calls
16 * would return, and would be responsible for manipulating the results
17 * into a useful manner.
18 *
19 * It should be noted that the methods enforced by the OAuthServiceProvider
20 * interface are made so that it can interact with our OAuthApplication
21 * cleanly, rather than from a general use perspective, though some
22 * methods for those purposes do exists (such as getUserData).
23 *
24 * @package
25 * @version $id$
26 */
27 interface OAuthServiceProvider {
28
29 public function getAccessTokenUrl();
30 public function getAuthorizeUrl();
31 public function getRequestTokenUrl();
32 public function getAuthTokenFromUrl();
33 public function getBaseUri();
34 public function getUserData();
35
36 }
37
38 /**
39 * OAuthApplication
40 *
41 * Base class to represent an OAuthConsumer application. This class is
42 * intended to be extended and modified for each ServiceProvider. Each
43 * OAuthServiceProvider should have a complementary OAuthApplication
44 *
45 * The OAuthApplication class should contain any details on preparing
46 * requires that is unique or specific to that specific service provider's
47 * implementation of the OAuth model.
48 *
49 * This base class is based on OAuth 1.0, designed with AWeber's implementation
50 * as a model. An OAuthApplication built to work with a different service
51 * provider (especially an OAuth2.0 Application) may alter or bypass portions
52 * of the logic in this class to meet the needs of the service provider it
53 * is designed to interface with.
54 *
55 * @package
56 * @version $id$
57 */
58 //260816 This legacy OAuth client intentionally uses dynamic state properties; opt in explicitly on PHP 8.2+.
59 #[\AllowDynamicProperties]
60 class OAuthApplication implements AWeberOAuthAdapter {
61 public $debug = false;
62
63 public $userAgent = 'AWeber OAuth Consumer Application 1.0 - https://labs.aweber.com/';
64
65 public $format = false;
66
67 public $requiresTokenSecret = true;
68
69 public $signatureMethod = 'HMAC-SHA1';
70 public $version = '1.0';
71
72 public $curl = false;
73
74 /**
75 * @var OAuthUser User currently interacting with the service provider
76 */
77 public $user = false;
78
79 // Data binding this OAuthApplication to the consumer application it is acting
80 // as a proxy for
81 public $consumerKey = false;
82 public $consumerSecret = false;
83
84 /**
85 * __construct
86 *
87 * Create a new OAuthApplication, based on an OAuthServiceProvider
88 * @access public
89 * @return void
90 */
91 public function __construct($parentApp = false) {
92 if ($parentApp) {
93 if (!is_a($parentApp, 'OAuthServiceProvider')) {
94 throw new Exception('Parent App must be a valid OAuthServiceProvider!');
95 }
96 $this->app = $parentApp;
97 }
98 $this->user = new OAuthUser();
99 $this->curl = new CurlObject();
100 }
101
102 /**
103 * request
104 *
105 * Implemented for a standard OAuth adapter interface
106 * @param mixed $method
107 * @param mixed $uri
108 * @param array $data
109 * @param array $options
110 * @access public
111 * @return void
112 */
113 public function request($method, $uri, $data = array(), $options = array()) {
114 $uri = $this->app->removeBaseUri($uri);
115 $url = $this->app->getBaseUri() . $uri;
116
117 # WARNING: non-primative items in data must be json serialized in GET and POST.
118 if ($method == 'POST' or $method == 'GET') {
119 foreach ($data as $key => $value) {
120 if (is_array($value)) {
121 $data[$key] = json_encode($value);
122 }
123 }
124 }
125
126 $response = $this->makeRequest($method, $url, $data);
127 if (!empty($options['return'])) {
128 if ($options['return'] == 'status') {
129 return $response->headers['Status-Code'];
130 }
131 if ($options['return'] == 'headers') {
132 return $response->headers;
133 }
134 if ($options['return'] == 'integer') {
135 return intval($response->body);
136 }
137 }
138
139 $data = json_decode($response->body, true);
140
141 if (empty($options['allow_empty']) && !isset($data)) {
142 throw new AWeberResponseError($uri);
143 }
144 return $data;
145 }
146
147 /**
148 * getRequestToken
149 *
150 * Gets a new request token / secret for this user.
151 * @access public
152 * @return void
153 */
154 public function getRequestToken($callbackUrl=false) {
155 $data = ($callbackUrl)? array('oauth_callback' => $callbackUrl) : array();
156 $resp = $this->makeRequest('POST', $this->app->getRequestTokenUrl(), $data);
157 $data = $this->parseResponse($resp);
158 $this->requiredFromResponse($data, array('oauth_token', 'oauth_token_secret'));
159 $this->user->requestToken = $data['oauth_token'];
160 $this->user->tokenSecret = $data['oauth_token_secret'];
161 return $data['oauth_token'];
162 }
163
164 /**
165 * getAccessToken
166 *
167 * Makes a request for access tokens. Requires that the current user has an authorized
168 * token and token secret.
169 *
170 * @access public
171 * @return void
172 */
173 public function getAccessToken() {
174 $resp = $this->makeRequest('POST', $this->app->getAccessTokenUrl(),
175 array('oauth_verifier' => $this->user->verifier)
176 );
177 $data = $this->parseResponse($resp);
178 $this->requiredFromResponse($data, array('oauth_token', 'oauth_token_secret'));
179
180 if (empty($data['oauth_token'])) {
181 throw new AWeberOAuthDataMissing('oauth_token');
182 }
183
184 $this->user->accessToken = $data['oauth_token'];
185 $this->user->tokenSecret = $data['oauth_token_secret'];
186 return array($data['oauth_token'], $data['oauth_token_secret']);
187 }
188
189 /**
190 * parseAsError
191 *
192 * Checks if response is an error. If it is, raise an appropriately
193 * configured exception.
194 *
195 * @param mixed $response Data returned from the server, in array form
196 * @access public
197 * @throws AWeberOAuthException
198 * @return void
199 */
200 public function parseAsError($response) {
201 if (!empty($response['error'])) {
202 throw new AWeberOAuthException($response['error']['type'],
203 $response['error']['message']);
204 }
205 }
206
207 /**
208 * requiredFromResponse
209 *
210 * Enforce that all the fields in requiredFields are present and not
211 * empty in data. If a required field is empty, throw an exception.
212 *
213 * @param mixed $data Array of data
214 * @param mixed $requiredFields Array of required field names.
215 * @access protected
216 * @return void
217 */
218 protected function requiredFromResponse($data, $requiredFields) {
219 foreach ($requiredFields as $field) {
220 if (empty($data[$field])) {
221 throw new AWeberOAuthDataMissing($field);
222 }
223 }
224 }
225
226 /**
227 * get
228 *
229 * Make a get request. Used to exchange user tokens with serice provider.
230 * @param mixed $url URL to make a get request from.
231 * @param array $data Data for the request.
232 * @access protected
233 * @return void
234 */
235 protected function get($url, $data) {
236 $url = $this->_addParametersToUrl($url, $data);
237 $handle = $this->curl->init($url);
238 $resp = $this->_sendRequest($handle);
239 return $resp;
240 }
241
242 /**
243 * _addParametersToUrl
244 *
245 * Adds the parameters in associative array $data to the
246 * given URL
247 * @param String $url URL
248 * @param array $data Parameters to be added as a query string to
249 * the URL provided
250 * @access protected
251 * @return void
252 */
253 protected function _addParametersToUrl($url, $data) {
254 if (!empty($data)) {
255 if (strpos($url, '?') === false) {
256 $url .= '?'.$this->buildData($data);
257 } else {
258 $url .= '&'.$this->buildData($data);
259 }
260 }
261 return $url;
262 }
263
264 /**
265 * generateNonce
266 *
267 * Generates a 'nonce', which is a unique request id based on the
268 * timestamp. If no timestamp is provided, generate one.
269 * @param mixed $timestamp Either a timestamp (epoch seconds) or false,
270 * in which case it will generate a timestamp.
271 * @access public
272 * @return string Returns a unique nonce
273 */
274 public function generateNonce($timestamp = false) {
275 if (!$timestamp) $timestamp = $this->generateTimestamp();
276 return md5($timestamp.'-'.rand(10000,99999).'-'.uniqid());
277 }
278
279 /**
280 * generateTimestamp
281 *
282 * Generates a timestamp, in seconds
283 * @access public
284 * @return int Timestamp, in epoch seconds
285 */
286 public function generateTimestamp() {
287 return time();
288 }
289
290 /**
291 * createSignature
292 *
293 * Creates a signature on the signature base and the signature key
294 * @param mixed $sigBase Base string of data to sign
295 * @param mixed $sigKey Key to sign the data with
296 * @access public
297 * @return string The signature
298 */
299 public function createSignature($sigBase, $sigKey) {
300 switch ($this->signatureMethod) {
301 case 'HMAC-SHA1':
302 default:
303 return base64_encode(hash_hmac('sha1', $sigBase, $sigKey, true));
304 }
305 }
306
307 /**
308 * encode
309 *
310 * Short-cut for utf8_encode / rawurlencode
311 * @param mixed $data Data to encode
312 * @access protected
313 * @return void Encoded data
314 */
315 protected function encode($data) {
316 //260816 Avoid utf8_encode() deprecations on PHP 8.2+ while preserving older fallbacks where possible.
317 if (function_exists('mb_convert_encoding')) {
318 $data = mb_convert_encoding($data, 'UTF-8', 'ISO-8859-1');
319 } elseif (function_exists('iconv')) {
320 $data = iconv('ISO-8859-1', 'UTF-8', $data);
321 }
322 return rawurlencode($data);
323 }
324
325 /**
326 * createSignatureKey
327 *
328 * Creates a key that will be used to sign our signature. Signatures
329 * are signed with the consumerSecret for this consumer application and
330 * the token secret of the user that the application is acting on behalf
331 * of.
332 * @access public
333 * @return void
334 */
335 public function createSignatureKey() {
336 return $this->consumerSecret.'&'.$this->user->tokenSecret;
337 }
338
339 /**
340 * getOAuthRequestData
341 *
342 * Get all the pre-signature, OAuth specific parameters for a request.
343 * @access public
344 * @return void
345 */
346 public function getOAuthRequestData() {
347 $token = $this->user->getHighestPriorityToken();
348 $ts = $this->generateTimestamp();
349 $nonce = $this->generateNonce($ts);
350 return array(
351 'oauth_token' => $token,
352 'oauth_consumer_key' => $this->consumerKey,
353 'oauth_version' => $this->version,
354 'oauth_timestamp' => $ts,
355 'oauth_signature_method' => $this->signatureMethod,
356 'oauth_nonce' => $nonce);
357 }
358
359
360 /**
361 * mergeOAuthData
362 *
363 * @param mixed $requestData
364 * @access public
365 * @return void
366 */
367 public function mergeOAuthData($requestData) {
368 $oauthData = $this->getOAuthRequestData();
369 return array_merge($requestData, $oauthData);
370 }
371
372 /**
373 * createSignatureBase
374 *
375 * @param mixed $method String name of HTTP method, such as "GET"
376 * @param mixed $url URL where this request will go
377 * @param mixed $data Array of params for this request. This should
378 * include ALL oauth properties except for the signature.
379 * @access public
380 * @return void
381 */
382 public function createSignatureBase($method, $url, $data) {
383 $method = $this->encode(strtoupper($method));
384 $query = parse_url($url, PHP_URL_QUERY);
385 if ($query) {
386 $parts = explode('?', $url, 2);
387 $url = array_shift($parts);
388 $items = explode('&', $query);
389 foreach ($items as $item) {
390 list($key, $value) = explode('=', $item);
391 $data[rawurldecode($key)] = rawurldecode($value);
392 }
393 }
394 $url = $this->encode($url);
395 $data = $this->encode($this->collapseDataForSignature($data));
396 return $method.'&'.$url.'&'.$data;
397 }
398
399 /**
400 * collapseDataForSignature
401 *
402 * Turns an array of request data into a string, as used by the oauth
403 * signature
404 * @param mixed $data
405 * @access public
406 * @return void
407 */
408 public function collapseDataForSignature($data) {
409 ksort($data);
410 $collapse = '';
411 foreach ($data as $key => $val) {
412 if (!empty($collapse)) $collapse .= '&';
413 $collapse .= $key.'='.$this->encode($val);
414 }
415 return $collapse;
416 }
417
418 /**
419 * signRequest
420 *
421 * Signs the request.
422 *
423 * @param mixed $method HTTP method
424 * @param mixed $url URL for the request
425 * @param mixed $data The data to be signed
426 * @access public
427 * @return array The data, with the signature.
428 */
429 public function signRequest($method, $url, $data) {
430 $base = $this->createSignatureBase($method, $url, $data);
431 $key = $this->createSignatureKey();
432 $data['oauth_signature'] = $this->createSignature($base, $key);
433 ksort($data);
434 return $data;
435 }
436
437
438 /**
439 * makeRequest
440 *
441 * Public facing function to make a request
442 *
443 * @param mixed $method
444 * @param mixed $url - Reserved characters in query params MUST be escaped
445 * @param mixed $data - Reserved characters in values MUST NOT be escaped
446 * @access public
447 * @return void
448 */
449 public function makeRequest($method, $url, $data=array()) {
450
451 if ($this->debug) echo "\n** {$method}: $url\n";
452
453 switch (strtoupper($method)) {
454 case 'POST':
455 $oauth = $this->prepareRequest($method, $url, $data);
456 $resp = $this->post($url, $oauth);
457 break;
458
459 case 'GET':
460 $oauth = $this->prepareRequest($method, $url, $data);
461 $resp = $this->get($url, $oauth, $data);
462 break;
463
464 case 'DELETE':
465 $oauth = $this->prepareRequest($method, $url, $data);
466 $resp = $this->delete($url, $oauth);
467 break;
468
469 case 'PATCH':
470 $oauth = $this->prepareRequest($method, $url, array());
471 $resp = $this->patch($url, $oauth, $data);
472 break;
473 }
474
475 // enable debug output
476 if ($this->debug) {
477 echo "<pre>";
478 print_r($oauth);
479 echo " --> Status: {$resp->headers['Status-Code']}\n";
480 echo " --> Body: {$resp->body}";
481 echo "</pre>";
482 }
483
484 if (!$resp) {
485 $msg = 'Unable to connect to the AWeber API. (' . $this->error . ')';
486 $error = array('message' => $msg, 'type' => 'APIUnreachableError',
487 'documentation_url' => 'https://labs.aweber.com/docs/troubleshooting');
488 throw new AWeberAPIException($error, $url);
489 }
490
491 if($resp->headers['Status-Code'] >= 400) {
492 $data = json_decode($resp->body, true);
493 throw new AWeberAPIException($data['error'], $url);
494 }
495
496 return $resp;
497 }
498
499 /**
500 * put
501 *
502 * Prepare an OAuth put method.
503 *
504 * @param mixed $url URL where we are making the request to
505 * @param mixed $data Data that is used to make the request
506 * @access protected
507 * @return void
508 */
509 protected function patch($url, $oauth, $data) {
510 $url = $this->_addParametersToUrl($url, $oauth);
511 $handle = $this->curl->init($url);
512 $this->curl->setopt($handle, CURLOPT_CUSTOMREQUEST, 'PATCH');
513 $this->curl->setopt($handle, CURLOPT_POSTFIELDS, json_encode($data));
514 $resp = $this->_sendRequest($handle, array('Expect:', 'Content-Type: application/json'));
515 return $resp;
516 }
517
518 /**
519 * post
520 *
521 * Prepare an OAuth post method.
522 *
523 * @param mixed $url URL where we are making the request to
524 * @param mixed $data Data that is used to make the request
525 * @access protected
526 * @return void
527 */
528 protected function post($url, $oauth) {
529 $handle = $this->curl->init($url);
530 $postData = $this->buildData($oauth);
531 $this->curl->setopt($handle, CURLOPT_POST, true);
532 $this->curl->setopt($handle, CURLOPT_POSTFIELDS, $postData);
533 $resp = $this->_sendRequest($handle);
534 return $resp;
535 }
536
537 /**
538 * delete
539 *
540 * Makes a DELETE request
541 * @param mixed $url URL where we are making the request to
542 * @param mixed $data Data that is used in the request
543 * @access protected
544 * @return void
545 */
546 protected function delete($url, $data) {
547 $url = $this->_addParametersToUrl($url, $data);
548 $handle = $this->curl->init($url);
549 $this->curl->setopt($handle, CURLOPT_CUSTOMREQUEST, 'DELETE');
550 $resp = $this->_sendRequest($handle);
551 return $resp;
552 }
553
554 /**
555 * buildData
556 *
557 * Creates a string of data for either post or get requests.
558 * @param mixed $data Array of key value pairs
559 * @access public
560 * @return void
561 */
562 public function buildData($data) {
563 ksort($data);
564 $params = array();
565 foreach ($data as $key => $value) {
566 $params[] = $key.'='.$this->encode($value);
567 }
568 return implode('&', $params);
569 }
570
571 /**
572 * _sendRequest
573 *
574 * Actually makes a request.
575 * @param mixed $handle Curl handle
576 * @param array $headers Additional headers needed for request
577 * @access private
578 * @return void
579 */
580 private function _sendRequest($handle, $headers = array('Expect:')) {
581 $this->curl->setopt($handle, CURLOPT_RETURNTRANSFER, true);
582 $this->curl->setopt($handle, CURLOPT_HEADER, true);
583 $this->curl->setopt($handle, CURLOPT_HTTPHEADER, $headers);
584 $this->curl->setopt($handle, CURLOPT_USERAGENT, $this->userAgent);
585 $this->curl->setopt($handle, CURLOPT_SSL_VERIFYPEER, TRUE);
586 $this->curl->setopt($handle, CURLOPT_VERBOSE, FALSE);
587 $this->curl->setopt($handle, CURLOPT_CONNECTTIMEOUT, 10);
588 $this->curl->setopt($handle, CURLOPT_TIMEOUT, 90);
589 $resp = $this->curl->execute($handle);
590 if ($resp) {
591 return new CurlResponse($resp);
592 }
593 $this->error = $this->curl->errno($handle) . ' - ' .
594 $this->curl->error($handle);
595 return false;
596 }
597
598 /**
599 * prepareRequest
600 *
601 * @param mixed $method HTTP method
602 * @param mixed $url URL for the request
603 * @param mixed $data The data to generate oauth data and be signed
604 * @access public
605 * @return void The data, with all its OAuth variables and signature
606 */
607 public function prepareRequest($method, $url, $data) {
608 $data = $this->mergeOAuthData($data);
609 $data = $this->signRequest($method, $url, $data);
610 return $data;
611 }
612
613 /**
614 * parseResponse
615 *
616 * Parses the body of the response into an array
617 * @param mixed $string The body of a response
618 * @access public
619 * @return void
620 */
621 public function parseResponse($resp) {
622 $data = array();
623
624 if (!$resp) { return $data; }
625 if (empty($resp)) { return $data; }
626 if (empty($resp->body)) { return $data; }
627
628 switch ($this->format) {
629 case 'json':
630 $data = json_decode($resp->body);
631 break;
632 default:
633 parse_str($resp->body, $data);
634 }
635 $this->parseAsError($data);
636 return $data;
637 }
638
639 }
640
641 /**
642 * OAuthUser
643 *
644 * Simple data class representing the user in an OAuth application.
645 * @package
646 * @version $id$
647 */
648 class OAuthUser {
649
650 public $authorizedToken = false;
651 public $requestToken = false;
652 public $verifier = false;
653 public $tokenSecret = false;
654 public $accessToken = false;
655
656 /**
657 * isAuthorized
658 *
659 * Checks if this user is authorized.
660 * @access public
661 * @return void
662 */
663 public function isAuthorized() {
664 if (empty($this->authorizedToken) && empty($this->accessToken)) {
665 return false;
666 }
667 return true;
668 }
669
670
671 /**
672 * getHighestPriorityToken
673 *
674 * Returns highest priority token - used to define authorization
675 * state for a given OAuthUser
676 * @access public
677 * @return void
678 */
679 public function getHighestPriorityToken() {
680 if (!empty($this->accessToken)) return $this->accessToken;
681 if (!empty($this->authorizedToken)) return $this->authorizedToken;
682 if (!empty($this->requestToken)) return $this->requestToken;
683
684 // Return no token, new user
685 return '';
686 }
687
688 }
689