DrupalOAuthClient.inc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <?php
  2. class DrupalOAuthClient {
  3. public $version = OAUTH_COMMON_VERSION_1_RFC;
  4. protected $consumer;
  5. protected $requestToken;
  6. protected $accessToken;
  7. protected $signatureMethod;
  8. /**
  9. * Creates an instance of the DrupalOAuthClient.
  10. *
  11. * @param DrupalOAuthConsumer $consumer
  12. * The consumer.
  13. * @param OAuthToken $request_token
  14. * Optional. A request token to use.
  15. * @param OAuthSignatureMethod $signature_method
  16. * Optional. The signature method to use.
  17. * @param integer $version
  18. * Optional. The version to use - either OAUTH_COMMON_VERSION_1_RFC or OAUTH_COMMON_VERSION_1.
  19. */
  20. public function __construct($consumer, $request_token = NULL, $signature_method = NULL, $version = NULL) {
  21. $this->consumer = $consumer;
  22. $this->requestToken = $request_token;
  23. $this->signatureMethod = $signature_method;
  24. if ($version) {
  25. $this->version = $version;
  26. }
  27. // Set to the default signature method if no method was specified
  28. if (!$this->signatureMethod) {
  29. if (!empty($this->consumer->configuration['signature_method'])) {
  30. $signature_method = substr(strtolower($this->consumer->configuration['signature_method']), 5);
  31. }
  32. else {
  33. $signature_method = 'SHA1';
  34. }
  35. $this->signatureMethod = self::signatureMethod($signature_method);
  36. }
  37. }
  38. /**
  39. * Convenience function to get signing method implementations.
  40. *
  41. * @param string $method
  42. * Optional. The hmac hashing algorithm to use. Defaults to 'sha512' which
  43. * has superseded sha1 as the recommended alternative.
  44. * @param bool $fallback_to_sha1
  45. * Optional. Whether sha1 should be used as a fallback if the selected
  46. * hashing algorithm is unavailable.
  47. * @return OAuthSignatureMethod
  48. * The signature method object.
  49. */
  50. public static function signatureMethod($method = 'SHA1', $fallback_to_sha1 = TRUE) {
  51. $sign = NULL;
  52. if (in_array(drupal_strtolower($method), hash_algos())) {
  53. $sign = new OAuthSignatureMethod_HMAC($method);
  54. }
  55. else if ($fallback_to_sha1) {
  56. $sign = new OAuthSignatureMethod_HMAC('SHA1');
  57. }
  58. return $sign;
  59. }
  60. /**
  61. * Gets a request token from the provider.
  62. *
  63. * @param string $endpoint
  64. * Optional. The endpoint path for the provider.
  65. * - If you provide the full URL (e.g. "http://example.com/oauth/request_token"),
  66. * then it will be used.
  67. * - If you provide only the path (e.g. "oauth/request_token"), it will
  68. * be converted into a full URL by prepending the provider_url.
  69. * - If you provide nothing it will default to '/oauth/request_token'.
  70. * @param array $options
  71. * An associative array of additional optional options, with the following keys:
  72. * - 'params'
  73. * An associative array of parameters that should be included in the
  74. * request.
  75. * - 'realm'
  76. * A string to be used as the http authentication realm in the request.
  77. * - 'get' (default FALSE)
  78. * Whether to use GET as the HTTP-method instead of POST.
  79. * - 'callback'
  80. * A full URL of where the user should be sent after the request token
  81. * has been authorized.
  82. * Only used by versions higher than OAUTH_COMMON_VERSION_1.
  83. * @return DrupalOAuthToken
  84. * The returned request token.
  85. */
  86. public function getRequestToken($endpoint = NULL, $options = array()) {
  87. if ($this->requestToken) {
  88. return clone $this->requestToken;
  89. }
  90. $options += array(
  91. 'params' => array(),
  92. 'realm' => NULL,
  93. 'get' => FALSE,
  94. 'callback' => NULL,
  95. );
  96. if (empty($endpoint)) {
  97. if (!empty($this->consumer->configuration['request_endpoint'])) {
  98. $endpoint = $this->consumer->configuration['request_endpoint'];
  99. }
  100. else {
  101. $endpoint = '/oauth/request_token';
  102. }
  103. }
  104. if ($this->version > OAUTH_COMMON_VERSION_1) {
  105. $options['params']['oauth_callback'] = $options['callback'] ? $options['callback'] : 'oob';
  106. }
  107. $response = $this->get($endpoint, array(
  108. 'params' => $options['params'],
  109. 'realm' => $options['realm'],
  110. 'get' => $options['get'],
  111. ));
  112. $params = array();
  113. parse_str($response, $params);
  114. if (empty($params['oauth_token']) || empty($params['oauth_token_secret'])) {
  115. throw new Exception('No valid request token was returned');
  116. }
  117. if ($this->version > OAUTH_COMMON_VERSION_1 && empty($params['oauth_callback_confirmed'])) {
  118. $this->version = OAUTH_COMMON_VERSION_1;
  119. }
  120. $this->requestToken = new DrupalOAuthToken($params['oauth_token'], $params['oauth_token_secret'], $this->consumer, array(
  121. 'type' => OAUTH_COMMON_TOKEN_TYPE_REQUEST,
  122. 'version' => $this->version,
  123. ));
  124. return clone $this->requestToken;
  125. }
  126. /**
  127. * Constructs the url that the user should be sent to to authorize the
  128. * request token.
  129. *
  130. * @param string $endpoint
  131. * Optional. The endpoint path for the provider.
  132. * - If you provide the full URL (e.g. "http://example.com/oauth/authorize"),
  133. * then it will be used.
  134. * - If you provide only the path (e.g. "oauth/authorize"), it will
  135. * be converted into a full URL by prepending the provider_url.
  136. * - If you provide nothing it will default to '/oauth/authorize'.
  137. * @param array $options
  138. * An associative array of additional optional options, with the following keys:
  139. * - 'params'
  140. * An associative array of parameters that should be included in the
  141. * request.
  142. * - 'callback'
  143. * A full URL of where the user should be sent after the request token
  144. * has been authorized.
  145. * Only used by version OAUTH_COMMON_VERSION_1.
  146. * @return string
  147. * The url.
  148. */
  149. public function getAuthorizationUrl($endpoint = NULL, $options = array()) {
  150. $options += array(
  151. 'params' => array(),
  152. 'callback' => NULL,
  153. );
  154. if (empty($endpoint)) {
  155. if (!empty($this->consumer->configuration['authorization_endpoint'])) {
  156. $endpoint = $this->consumer->configuration['authorization_endpoint'];
  157. }
  158. else {
  159. $endpoint = '/oauth/authorize';
  160. }
  161. }
  162. if ($this->version == OAUTH_COMMON_VERSION_1 && $options['callback']) {
  163. $options['params']['oauth_callback'] = $options['callback'];
  164. }
  165. $options['params']['oauth_token'] = $this->requestToken->key;
  166. $endpoint = $this->getAbsolutePath($endpoint);
  167. $append_query = strpos($endpoint, '?') === FALSE ? '?' : '&';
  168. return $endpoint . $append_query . http_build_query($options['params'], NULL, '&');
  169. }
  170. /**
  171. * Fetches the access token using the request token.
  172. *
  173. * @param string $endpoint
  174. * Optional. The endpoint path for the provider.
  175. * - If you provide the full URL (e.g. "http://example.com/oauth/access_token"),
  176. * then it will be used.
  177. * - If you provide only the path (e.g. "oauth/access_token"), it will
  178. * be converted into a full URL by prepending the provider_url.
  179. * - If you provide nothing it will default to '/oauth/access_token'.
  180. * @param array $options
  181. * An associative array of additional optional options, with the following keys:
  182. * - 'params'
  183. * An associative array of parameters that should be included in the
  184. * request.
  185. * - 'realm'
  186. * A string to be used as the http authentication realm in the request.
  187. * - 'get' (default FALSE)
  188. * Whether to use GET as the HTTP-method instead of POST.
  189. * - 'verifier'
  190. * A string containing a verifier for he user from the provider.
  191. * Only used by versions higher than OAUTH_COMMON_VERSION_1.
  192. * @return DrupalOAuthToken
  193. * The access token.
  194. */
  195. public function getAccessToken($endpoint = NULL, $options = array()) {
  196. if ($this->accessToken) {
  197. return clone $this->accessToken;
  198. }
  199. $options += array(
  200. 'params' => array(),
  201. 'realm' => NULL,
  202. 'get' => FALSE,
  203. 'verifier' => NULL,
  204. );
  205. if (empty($endpoint)) {
  206. if (!empty($this->consumer->configuration['access_endpoint'])) {
  207. $endpoint = $this->consumer->configuration['access_endpoint'];
  208. }
  209. else {
  210. $endpoint = '/oauth/access_token';
  211. }
  212. }
  213. if ($this->version > OAUTH_COMMON_VERSION_1 && $options['verifier'] !== NULL) {
  214. $options['params']['oauth_verifier'] = $options['verifier'];
  215. }
  216. $response = $this->get($endpoint, array(
  217. 'token' => TRUE,
  218. 'params' => $options['params'],
  219. 'realm' => $options['realm'],
  220. 'get' => $options['get'],
  221. ));
  222. $params = array();
  223. parse_str($response, $params);
  224. if (empty($params['oauth_token']) || empty($params['oauth_token_secret'])) {
  225. throw new Exception('No valid access token was returned');
  226. }
  227. // Check if we've has recieved this token previously and if so use the old one
  228. //TODO: Is this safe!? What if eg. multiple users are getting the same access token from the provider?
  229. $this->accessToken = DrupalOAuthToken::loadByKey($params['oauth_token'], $this->consumer);
  230. //TODO: Can a secret change even though the token doesn't? If so it needs to be changed.
  231. if (!$this->accessToken) {
  232. $this->accessToken = new DrupalOAuthToken($params['oauth_token'], $params['oauth_token_secret'], $this->consumer, array(
  233. 'type' => OAUTH_COMMON_TOKEN_TYPE_ACCESS,
  234. ));
  235. }
  236. return clone $this->accessToken;
  237. }
  238. /**
  239. * Make an OAuth request.
  240. *
  241. * @param string $path
  242. * The path being requested.
  243. * - If you provide the full URL (e.g. "http://example.com/oauth/request_token"),
  244. * then it will be used.
  245. * - If you provide only the path (e.g. "oauth/request_token"), it will
  246. * be converted into a full URL by prepending the provider_url.
  247. * @param array $options
  248. * An associative array of additional options, with the following keys:
  249. * - 'token' (default FALSE)
  250. * Whether a token should be used or not.
  251. * - 'params'
  252. * An associative array of parameters that should be included in the
  253. * request.
  254. * - 'realm'
  255. * A string to be used as the http authentication realm in the request.
  256. * - 'get' (default FALSE)
  257. * Whether to use GET as the HTTP-method instead of POST.
  258. * @return string
  259. * a string containing the response body.
  260. */
  261. protected function get($path, $options = array()) {
  262. $options += array(
  263. 'token' => FALSE,
  264. 'params' => array(),
  265. 'realm' => NULL,
  266. 'get' => FALSE,
  267. );
  268. if (empty($options['realm']) && !empty($this->consumer->configuration['authentication_realm'])) {
  269. $options['realm'] = $this->consumer->configuration['authentication_realm'];
  270. }
  271. $token = $options['token'] ? $this->requestToken : NULL;
  272. $path = $this->getAbsolutePath($path);
  273. $req = OAuthRequest::from_consumer_and_token($this->consumer, $token,
  274. $options['get'] ? 'GET' : 'POST', $path, $options['params']);
  275. $req->sign_request($this->signatureMethod, $this->consumer, $token);
  276. $url = $req->get_normalized_http_url();
  277. $params = array();
  278. foreach ($req->get_parameters() as $param_key => $param_value) {
  279. if (substr($param_key, 0, 5) != 'oauth') {
  280. $params[$param_key] = $param_value;
  281. }
  282. }
  283. if (!empty($params)) {
  284. $url .= '?' . http_build_query($params);
  285. }
  286. $headers = array(
  287. 'Accept: application/x-www-form-urlencoded',
  288. $req->to_header($options['realm']),
  289. );
  290. $ch = curl_init();
  291. curl_setopt($ch, CURLOPT_URL, $url);
  292. if (!$options['get']) {
  293. curl_setopt($ch, CURLOPT_POST, 1);
  294. curl_setopt($ch, CURLOPT_POSTFIELDS, '');
  295. }
  296. $oauth_version = _oauth_common_version();
  297. curl_setopt($ch, CURLOPT_USERAGENT, 'Drupal/' . VERSION . ' OAuth/' . $oauth_version);
  298. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  299. curl_setopt($ch, CURLOPT_HEADER, 1);
  300. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  301. $response = curl_exec($ch);
  302. $error = curl_error($ch);
  303. curl_close($ch);
  304. if ($error) {
  305. throw new Exception($error);
  306. }
  307. $result = $this->interpretResponse($response);
  308. if ($result->responseCode != 200) {
  309. throw new Exception('Failed to fetch data from url "' . $path . '" (HTTP response code ' . $result->responseCode . ' ' . $result->responseMessage . '): ' . $result->body, $result->responseCode);
  310. }
  311. return $result->body;
  312. }
  313. /**
  314. * Makes sure a path is an absolute path
  315. *
  316. * Prepends provider url if the path isn't absolute.
  317. *
  318. * @param string $path
  319. * The path to make absolute.
  320. * @return string
  321. * The absolute path.
  322. */
  323. protected function getAbsolutePath($path) {
  324. $protocols = array(
  325. 'http',
  326. 'https'
  327. );
  328. $protocol = strpos($path, '://');
  329. $protocol = $protocol ? substr($path, 0, $protocol) : '';
  330. if (!in_array($protocol, $protocols)) {
  331. $path = $this->consumer->configuration['provider_url'] . $path;
  332. }
  333. return $path;
  334. }
  335. protected function interpretResponse($res) {
  336. list($headers, $body) = preg_split('/\r\n\r\n/', $res, 2);
  337. $obj = (object) array(
  338. 'headers' => $headers,
  339. 'body' => $body,
  340. );
  341. $matches = array();
  342. if (preg_match('/HTTP\/1.\d (\d{3}) (.*)/', $headers, $matches)) {
  343. $obj->responseCode = trim($matches[1]);
  344. $obj->responseMessage = trim($matches[2]);
  345. // Handle HTTP/1.1 100 Continue
  346. if ($obj->responseCode == 100) {
  347. return $this->interpretResponse($body);
  348. }
  349. }
  350. return $obj;
  351. }
  352. }