0
|
1 <?php
|
|
2 /**
|
|
3 * The most popular PHP library for use with the Twitter OAuth REST API.
|
|
4 *
|
|
5 * @license MIT
|
|
6 */
|
|
7 namespace Abraham\TwitterOAuth;
|
|
8
|
|
9 use Abraham\TwitterOAuth\Util\JsonDecoder;
|
|
10
|
|
11 /**
|
|
12 * TwitterOAuth class for interacting with the Twitter API.
|
|
13 *
|
|
14 * @author Abraham Williams <[email protected]>
|
|
15 */
|
|
16 class TwitterOAuth extends Config
|
|
17 {
|
|
18 const API_VERSION = '1.1';
|
|
19 const API_HOST = 'https://api.twitter.com';
|
|
20 const UPLOAD_HOST = 'https://upload.twitter.com';
|
|
21 const UPLOAD_CHUNK = 40960; // 1024 * 40
|
|
22
|
|
23 /** @var Response details about the result of the last request */
|
|
24 private $response;
|
|
25 /** @var string|null Application bearer token */
|
|
26 private $bearer;
|
|
27 /** @var Consumer Twitter application details */
|
|
28 private $consumer;
|
|
29 /** @var Token|null User access token details */
|
|
30 private $token;
|
|
31 /** @var HmacSha1 OAuth 1 signature type used by Twitter */
|
|
32 private $signatureMethod;
|
|
33
|
|
34 /**
|
|
35 * Constructor
|
|
36 *
|
|
37 * @param string $consumerKey The Application Consumer Key
|
|
38 * @param string $consumerSecret The Application Consumer Secret
|
|
39 * @param string|null $oauthToken The Client Token (optional)
|
|
40 * @param string|null $oauthTokenSecret The Client Token Secret (optional)
|
|
41 */
|
|
42 public function __construct($consumerKey, $consumerSecret, $oauthToken = null, $oauthTokenSecret = null)
|
|
43 {
|
|
44 $this->resetLastResponse();
|
|
45 $this->signatureMethod = new HmacSha1();
|
|
46 $this->consumer = new Consumer($consumerKey, $consumerSecret);
|
|
47 if (!empty($oauthToken) && !empty($oauthTokenSecret)) {
|
|
48 $this->token = new Token($oauthToken, $oauthTokenSecret);
|
|
49 }
|
|
50 if (empty($oauthToken) && !empty($oauthTokenSecret)) {
|
|
51 $this->bearer = $oauthTokenSecret;
|
|
52 }
|
|
53 }
|
|
54
|
|
55 /**
|
|
56 * @param string $oauthToken
|
|
57 * @param string $oauthTokenSecret
|
|
58 */
|
|
59 public function setOauthToken($oauthToken, $oauthTokenSecret)
|
|
60 {
|
|
61 $this->token = new Token($oauthToken, $oauthTokenSecret);
|
|
62 }
|
|
63
|
|
64 /**
|
|
65 * @return string|null
|
|
66 */
|
|
67 public function getLastApiPath()
|
|
68 {
|
|
69 return $this->response->getApiPath();
|
|
70 }
|
|
71
|
|
72 /**
|
|
73 * @return int
|
|
74 */
|
|
75 public function getLastHttpCode()
|
|
76 {
|
|
77 return $this->response->getHttpCode();
|
|
78 }
|
|
79
|
|
80 /**
|
|
81 * @return array
|
|
82 */
|
|
83 public function getLastXHeaders()
|
|
84 {
|
|
85 return $this->response->getXHeaders();
|
|
86 }
|
|
87
|
|
88 /**
|
|
89 * @return array|object|null
|
|
90 */
|
|
91 public function getLastBody()
|
|
92 {
|
|
93 return $this->response->getBody();
|
|
94 }
|
|
95
|
|
96 /**
|
|
97 * Resets the last response cache.
|
|
98 */
|
|
99 public function resetLastResponse()
|
|
100 {
|
|
101 $this->response = new Response();
|
|
102 }
|
|
103
|
|
104 /**
|
|
105 * Make URLs for user browser navigation.
|
|
106 *
|
|
107 * @param string $path
|
|
108 * @param array $parameters
|
|
109 *
|
|
110 * @return string
|
|
111 */
|
|
112 public function url($path, array $parameters)
|
|
113 {
|
|
114 $this->resetLastResponse();
|
|
115 $this->response->setApiPath($path);
|
|
116 $query = http_build_query($parameters);
|
|
117 return sprintf('%s/%s?%s', self::API_HOST, $path, $query);
|
|
118 }
|
|
119
|
|
120 /**
|
|
121 * Make /oauth/* requests to the API.
|
|
122 *
|
|
123 * @param string $path
|
|
124 * @param array $parameters
|
|
125 *
|
|
126 * @return array
|
|
127 * @throws TwitterOAuthException
|
|
128 */
|
|
129 public function oauth($path, array $parameters = [])
|
|
130 {
|
|
131 $response = [];
|
|
132 $this->resetLastResponse();
|
|
133 $this->response->setApiPath($path);
|
|
134 $url = sprintf('%s/%s', self::API_HOST, $path);
|
|
135 $result = $this->oAuthRequest($url, 'POST', $parameters);
|
|
136
|
|
137 if ($this->getLastHttpCode() != 200) {
|
|
138 throw new TwitterOAuthException($result);
|
|
139 }
|
|
140
|
|
141 parse_str($result, $response);
|
|
142 $this->response->setBody($response);
|
|
143
|
|
144 return $response;
|
|
145 }
|
|
146
|
|
147 /**
|
|
148 * Make /oauth2/* requests to the API.
|
|
149 *
|
|
150 * @param string $path
|
|
151 * @param array $parameters
|
|
152 *
|
|
153 * @return array|object
|
|
154 */
|
|
155 public function oauth2($path, array $parameters = [])
|
|
156 {
|
|
157 $method = 'POST';
|
|
158 $this->resetLastResponse();
|
|
159 $this->response->setApiPath($path);
|
|
160 $url = sprintf('%s/%s', self::API_HOST, $path);
|
|
161 $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters);
|
|
162 $authorization = 'Authorization: Basic ' . $this->encodeAppAuthorization($this->consumer);
|
|
163 $result = $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters);
|
|
164 $response = JsonDecoder::decode($result, $this->decodeJsonAsArray);
|
|
165 $this->response->setBody($response);
|
|
166 return $response;
|
|
167 }
|
|
168
|
|
169 /**
|
|
170 * Make GET requests to the API.
|
|
171 *
|
|
172 * @param string $path
|
|
173 * @param array $parameters
|
|
174 *
|
|
175 * @return array|object
|
|
176 */
|
|
177 public function get($path, array $parameters = [])
|
|
178 {
|
|
179 return $this->http('GET', self::API_HOST, $path, $parameters);
|
|
180 }
|
|
181
|
|
182 /**
|
|
183 * Make POST requests to the API.
|
|
184 *
|
|
185 * @param string $path
|
|
186 * @param array $parameters
|
|
187 *
|
|
188 * @return array|object
|
|
189 */
|
|
190 public function post($path, array $parameters = [])
|
|
191 {
|
|
192 return $this->http('POST', self::API_HOST, $path, $parameters);
|
|
193 }
|
|
194
|
|
195 /**
|
|
196 * Make DELETE requests to the API.
|
|
197 *
|
|
198 * @param string $path
|
|
199 * @param array $parameters
|
|
200 *
|
|
201 * @return array|object
|
|
202 */
|
|
203 public function delete($path, array $parameters = [])
|
|
204 {
|
|
205 return $this->http('DELETE', self::API_HOST, $path, $parameters);
|
|
206 }
|
|
207
|
|
208 /**
|
|
209 * Make PUT requests to the API.
|
|
210 *
|
|
211 * @param string $path
|
|
212 * @param array $parameters
|
|
213 *
|
|
214 * @return array|object
|
|
215 */
|
|
216 public function put($path, array $parameters = [])
|
|
217 {
|
|
218 return $this->http('PUT', self::API_HOST, $path, $parameters);
|
|
219 }
|
|
220
|
|
221 /**
|
|
222 * Upload media to upload.twitter.com.
|
|
223 *
|
|
224 * @param string $path
|
|
225 * @param array $parameters
|
|
226 * @param boolean $chunked
|
|
227 *
|
|
228 * @return array|object
|
|
229 */
|
|
230 public function upload($path, array $parameters = [], $chunked = false)
|
|
231 {
|
|
232 if ($chunked) {
|
|
233 return $this->uploadMediaChunked($path, $parameters);
|
|
234 } else {
|
|
235 return $this->uploadMediaNotChunked($path, $parameters);
|
|
236 }
|
|
237 }
|
|
238
|
|
239 /**
|
|
240 * Private method to upload media (not chunked) to upload.twitter.com.
|
|
241 *
|
|
242 * @param string $path
|
|
243 * @param array $parameters
|
|
244 *
|
|
245 * @return array|object
|
|
246 */
|
|
247 private function uploadMediaNotChunked($path, array $parameters)
|
|
248 {
|
|
249 $file = file_get_contents($parameters['media']);
|
|
250 $base = base64_encode($file);
|
|
251 $parameters['media'] = $base;
|
|
252 return $this->http('POST', self::UPLOAD_HOST, $path, $parameters);
|
|
253 }
|
|
254
|
|
255 /**
|
|
256 * Private method to upload media (chunked) to upload.twitter.com.
|
|
257 *
|
|
258 * @param string $path
|
|
259 * @param array $parameters
|
|
260 *
|
|
261 * @return array|object
|
|
262 */
|
|
263 private function uploadMediaChunked($path, array $parameters)
|
|
264 {
|
|
265 // Init
|
|
266 $init = $this->http('POST', self::UPLOAD_HOST, $path, [
|
|
267 'command' => 'INIT',
|
|
268 'media_type' => $parameters['media_type'],
|
|
269 'total_bytes' => filesize($parameters['media'])
|
|
270 ]);
|
|
271 // Append
|
|
272 $segment_index = 0;
|
|
273 $media = fopen($parameters['media'], 'rb');
|
|
274 while (!feof($media))
|
|
275 {
|
|
276 $this->http('POST', self::UPLOAD_HOST, 'media/upload', [
|
|
277 'command' => 'APPEND',
|
|
278 'media_id' => $init->media_id_string,
|
|
279 'segment_index' => $segment_index++,
|
|
280 'media_data' => base64_encode(fread($media, self::UPLOAD_CHUNK))
|
|
281 ]);
|
|
282 }
|
|
283 fclose($media);
|
|
284 // Finalize
|
|
285 $finalize = $this->http('POST', self::UPLOAD_HOST, 'media/upload', [
|
|
286 'command' => 'FINALIZE',
|
|
287 'media_id' => $init->media_id_string
|
|
288 ]);
|
|
289 return $finalize;
|
|
290 }
|
|
291
|
|
292 /**
|
|
293 * @param string $method
|
|
294 * @param string $host
|
|
295 * @param string $path
|
|
296 * @param array $parameters
|
|
297 *
|
|
298 * @return array|object
|
|
299 */
|
|
300 private function http($method, $host, $path, array $parameters)
|
|
301 {
|
|
302 $this->resetLastResponse();
|
|
303 $url = sprintf('%s/%s/%s.json', $host, self::API_VERSION, $path);
|
|
304 $this->response->setApiPath($path);
|
|
305 $result = $this->oAuthRequest($url, $method, $parameters);
|
|
306 $response = JsonDecoder::decode($result, $this->decodeJsonAsArray);
|
|
307 $this->response->setBody($response);
|
|
308 return $response;
|
|
309 }
|
|
310
|
|
311 /**
|
|
312 * Format and sign an OAuth / API request
|
|
313 *
|
|
314 * @param string $url
|
|
315 * @param string $method
|
|
316 * @param array $parameters
|
|
317 *
|
|
318 * @return string
|
|
319 * @throws TwitterOAuthException
|
|
320 */
|
|
321 private function oAuthRequest($url, $method, array $parameters)
|
|
322 {
|
|
323 $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters);
|
|
324 if (array_key_exists('oauth_callback', $parameters)) {
|
|
325 // Twitter doesn't like oauth_callback as a parameter.
|
|
326 unset($parameters['oauth_callback']);
|
|
327 }
|
|
328 if ($this->bearer === null) {
|
|
329 $request->signRequest($this->signatureMethod, $this->consumer, $this->token);
|
|
330 $authorization = $request->toHeader();
|
|
331 } else {
|
|
332 $authorization = 'Authorization: Bearer ' . $this->bearer;
|
|
333 }
|
|
334 return $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters);
|
|
335 }
|
|
336
|
|
337 /**
|
|
338 * Make an HTTP request
|
|
339 *
|
|
340 * @param string $url
|
|
341 * @param string $method
|
|
342 * @param string $authorization
|
|
343 * @param array $postfields
|
|
344 *
|
|
345 * @return string
|
|
346 * @throws TwitterOAuthException
|
|
347 */
|
|
348 private function request($url, $method, $authorization, array $postfields)
|
|
349 {
|
|
350 /* Curl settings */
|
|
351 $options = [
|
|
352 // CURLOPT_VERBOSE => true,
|
|
353 CURLOPT_CAINFO => __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem',
|
|
354 CURLOPT_CONNECTTIMEOUT => $this->connectionTimeout,
|
|
355 CURLOPT_HEADER => true,
|
|
356 CURLOPT_HTTPHEADER => ['Accept: application/json', $authorization, 'Expect:'],
|
|
357 CURLOPT_RETURNTRANSFER => true,
|
|
358 CURLOPT_SSL_VERIFYHOST => 2,
|
|
359 CURLOPT_SSL_VERIFYPEER => true,
|
|
360 CURLOPT_TIMEOUT => $this->timeout,
|
|
361 CURLOPT_URL => $url,
|
|
362 CURLOPT_USERAGENT => $this->userAgent,
|
|
363 ];
|
|
364
|
|
365 /* Remove CACert file when in a PHAR file. */
|
|
366 if ($this->pharRunning()) {
|
|
367 unset($options[CURLOPT_CAINFO]);
|
|
368 }
|
|
369
|
|
370 if($this->gzipEncoding) {
|
|
371 $options[CURLOPT_ENCODING] = 'gzip';
|
|
372 }
|
|
373
|
|
374 if (!empty($this->proxy)) {
|
|
375 $options[CURLOPT_PROXY] = $this->proxy['CURLOPT_PROXY'];
|
|
376 $options[CURLOPT_PROXYUSERPWD] = $this->proxy['CURLOPT_PROXYUSERPWD'];
|
|
377 $options[CURLOPT_PROXYPORT] = $this->proxy['CURLOPT_PROXYPORT'];
|
|
378 $options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC;
|
|
379 $options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP;
|
|
380 }
|
|
381
|
|
382 switch ($method) {
|
|
383 case 'GET':
|
|
384 break;
|
|
385 case 'POST':
|
|
386 $options[CURLOPT_POST] = true;
|
|
387 $options[CURLOPT_POSTFIELDS] = Util::buildHttpQuery($postfields);
|
|
388 break;
|
|
389 case 'DELETE':
|
|
390 $options[CURLOPT_CUSTOMREQUEST] = 'DELETE';
|
|
391 break;
|
|
392 case 'PUT':
|
|
393 $options[CURLOPT_CUSTOMREQUEST] = 'PUT';
|
|
394 break;
|
|
395 }
|
|
396
|
|
397 if (in_array($method, ['GET', 'PUT', 'DELETE']) && !empty($postfields)) {
|
|
398 $options[CURLOPT_URL] .= '?' . Util::buildHttpQuery($postfields);
|
|
399 }
|
|
400
|
|
401
|
|
402 $curlHandle = curl_init();
|
|
403 curl_setopt_array($curlHandle, $options);
|
|
404 $response = curl_exec($curlHandle);
|
|
405
|
|
406 // Throw exceptions on cURL errors.
|
|
407 if (curl_errno($curlHandle) > 0) {
|
|
408 throw new TwitterOAuthException(curl_error($curlHandle), curl_errno($curlHandle));
|
|
409 }
|
|
410
|
|
411 $this->response->setHttpCode(curl_getinfo($curlHandle, CURLINFO_HTTP_CODE));
|
|
412 $parts = explode("\r\n\r\n", $response);
|
|
413 $responseBody = array_pop($parts);
|
|
414 $responseHeader = array_pop($parts);
|
|
415 $this->response->setHeaders($this->parseHeaders($responseHeader));
|
|
416
|
|
417 curl_close($curlHandle);
|
|
418
|
|
419 return $responseBody;
|
|
420 }
|
|
421
|
|
422 /**
|
|
423 * Get the header info to store.
|
|
424 *
|
|
425 * @param string $header
|
|
426 *
|
|
427 * @return array
|
|
428 */
|
|
429 private function parseHeaders($header)
|
|
430 {
|
|
431 $headers = [];
|
|
432 foreach (explode("\r\n", $header) as $line) {
|
|
433 if (strpos($line, ':') !== false) {
|
|
434 list ($key, $value) = explode(': ', $line);
|
|
435 $key = str_replace('-', '_', strtolower($key));
|
|
436 $headers[$key] = trim($value);
|
|
437 }
|
|
438 }
|
|
439 return $headers;
|
|
440 }
|
|
441
|
|
442 /**
|
|
443 * Encode application authorization header with base64.
|
|
444 *
|
|
445 * @param Consumer $consumer
|
|
446 *
|
|
447 * @return string
|
|
448 */
|
|
449 private function encodeAppAuthorization(Consumer $consumer)
|
|
450 {
|
|
451 $key = rawurlencode($consumer->key);
|
|
452 $secret = rawurlencode($consumer->secret);
|
|
453 return base64_encode($key . ':' . $secret);
|
|
454 }
|
|
455
|
|
456 /**
|
|
457 * Is the code running from a Phar module.
|
|
458 *
|
|
459 * @return boolean
|
|
460 */
|
|
461 private function pharRunning()
|
|
462 {
|
|
463 return class_exists('Phar') && \Phar::running(false) !== '';
|
|
464 }
|
|
465 }
|