oauth_application.php 21 KB

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