PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 261001
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v261001
261001 260927 260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 All 191 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 261001, at src/includes/externals/aweber/oauth_application.php

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