*/ class TwitterOAuth extends Config { const API_VERSION = '1.1'; const API_HOST = 'https://api.twitter.com'; const UPLOAD_HOST = 'https://upload.twitter.com'; /** @var Response details about the result of the last request */ private $response; /** @var string|null Application bearer token */ private $bearer; /** @var Consumer Twitter application details */ private $consumer; /** @var Token|null User access token details */ private $token; /** @var HmacSha1 OAuth 1 signature type used by Twitter */ private $signatureMethod; /** * Constructor * * @param string $consumerKey The Application Consumer Key * @param string $consumerSecret The Application Consumer Secret * @param string|null $oauthToken The Client Token (optional) * @param string|null $oauthTokenSecret The Client Token Secret (optional) */ public function __construct($consumerKey, $consumerSecret, $oauthToken = null, $oauthTokenSecret = null) { $this->resetLastResponse(); $this->signatureMethod = new HmacSha1(); $this->consumer = new Consumer($consumerKey, $consumerSecret); if (!empty($oauthToken) && !empty($oauthTokenSecret)) { $this->token = new Token($oauthToken, $oauthTokenSecret); } if (empty($oauthToken) && !empty($oauthTokenSecret)) { $this->bearer = $oauthTokenSecret; } } /** * @param string $oauthToken * @param string $oauthTokenSecret */ public function setOauthToken($oauthToken, $oauthTokenSecret) { $this->token = new Token($oauthToken, $oauthTokenSecret); } /** * @return string|null */ public function getLastApiPath() { return $this->response->getApiPath(); } /** * @return int */ public function getLastHttpCode() { return $this->response->getHttpCode(); } /** * @return array */ public function getLastXHeaders() { return $this->response->getXHeaders(); } /** * @return array|object|null */ public function getLastBody() { return $this->response->getBody(); } /** * Resets the last response cache. */ public function resetLastResponse() { $this->response = new Response(); } /** * Make URLs for user browser navigation. * * @param string $path * @param array $parameters * * @return string */ public function url($path, array $parameters) { $this->resetLastResponse(); $this->response->setApiPath($path); $query = http_build_query($parameters); return sprintf('%s/%s?%s', self::API_HOST, $path, $query); } /** * Make /oauth/* requests to the API. * * @param string $path * @param array $parameters * * @return array * @throws TwitterOAuthException */ public function oauth($path, array $parameters = []) { $response = []; $this->resetLastResponse(); $this->response->setApiPath($path); $url = sprintf('%s/%s', self::API_HOST, $path); $result = $this->oAuthRequest($url, 'POST', $parameters); if ($this->getLastHttpCode() != 200) { throw new TwitterOAuthException($result); } parse_str($result, $response); $this->response->setBody($response); return $response; } /** * Make /oauth2/* requests to the API. * * @param string $path * @param array $parameters * * @return array|object */ public function oauth2($path, array $parameters = []) { $method = 'POST'; $this->resetLastResponse(); $this->response->setApiPath($path); $url = sprintf('%s/%s', self::API_HOST, $path); $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters); $authorization = 'Authorization: Basic ' . $this->encodeAppAuthorization($this->consumer); $result = $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters); $response = JsonDecoder::decode($result, $this->decodeJsonAsArray); $this->response->setBody($response); return $response; } /** * Make GET requests to the API. * * @param string $path * @param array $parameters * * @return array|object */ public function get($path, array $parameters = []) { return $this->http('GET', self::API_HOST, $path, $parameters); } /** * Make POST requests to the API. * * @param string $path * @param array $parameters * * @return array|object */ public function post($path, array $parameters = []) { return $this->http('POST', self::API_HOST, $path, $parameters); } /** * Make DELETE requests to the API. * * @param string $path * @param array $parameters * * @return array|object */ public function delete($path, array $parameters = []) { return $this->http('DELETE', self::API_HOST, $path, $parameters); } /** * Make PUT requests to the API. * * @param string $path * @param array $parameters * * @return array|object */ public function put($path, array $parameters = []) { return $this->http('PUT', self::API_HOST, $path, $parameters); } /** * Upload media to upload.twitter.com. * * @param string $path * @param array $parameters * @param boolean $chunked * * @return array|object */ public function upload($path, array $parameters = [], $chunked = false) { if ($chunked) { return $this->uploadMediaChunked($path, $parameters); } else { return $this->uploadMediaNotChunked($path, $parameters); } } /** * Private method to upload media (not chunked) to upload.twitter.com. * * @param string $path * @param array $parameters * * @return array|object */ private function uploadMediaNotChunked($path, array $parameters) { $file = file_get_contents($parameters['media']); $base = base64_encode($file); $parameters['media'] = $base; return $this->http('POST', self::UPLOAD_HOST, $path, $parameters); } /** * Private method to upload media (chunked) to upload.twitter.com. * * @param string $path * @param array $parameters * * @return array|object */ private function uploadMediaChunked($path, array $parameters) { $init = $this->http('POST', self::UPLOAD_HOST, $path, $this->mediaInitParameters($parameters)); // Append $segmentIndex = 0; $media = fopen($parameters['media'], 'rb'); while (!feof($media)) { $this->http('POST', self::UPLOAD_HOST, 'media/upload', [ 'command' => 'APPEND', 'media_id' => $init->media_id_string, 'segment_index' => $segmentIndex++, 'media_data' => base64_encode(fread($media, $this->chunkSize)) ]); } fclose($media); // Finalize $finalize = $this->http('POST', self::UPLOAD_HOST, 'media/upload', [ 'command' => 'FINALIZE', 'media_id' => $init->media_id_string ]); return $finalize; } /** * Private method to get params for upload media chunked init. * Twitter docs: https://dev.twitter.com/rest/reference/post/media/upload-init.html * * @param array $parameters * * @return array */ private function mediaInitParameters(array $parameters) { $return = [ 'command' => 'INIT', 'media_type' => $parameters['media_type'], 'total_bytes' => filesize($parameters['media']) ]; if (isset($parameters['additional_owners'])) { $return['additional_owners'] = $parameters['additional_owners']; } if (isset($parameters['media_category'])) { $return['media_category'] = $parameters['media_category']; } return $return; } /** * @param string $method * @param string $host * @param string $path * @param array $parameters * * @return array|object */ private function http($method, $host, $path, array $parameters) { $this->resetLastResponse(); $url = sprintf('%s/%s/%s.json', $host, self::API_VERSION, $path); $this->response->setApiPath($path); $result = $this->oAuthRequest($url, $method, $parameters); $response = JsonDecoder::decode($result, $this->decodeJsonAsArray); $this->response->setBody($response); return $response; } /** * Format and sign an OAuth / API request * * @param string $url * @param string $method * @param array $parameters * * @return string * @throws TwitterOAuthException */ private function oAuthRequest($url, $method, array $parameters) { $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters); if (array_key_exists('oauth_callback', $parameters)) { // Twitter doesn't like oauth_callback as a parameter. unset($parameters['oauth_callback']); } if ($this->bearer === null) { $request->signRequest($this->signatureMethod, $this->consumer, $this->token); $authorization = $request->toHeader(); if (array_key_exists('oauth_verifier', $parameters)) { // Twitter doesn't always work with oauth in the body and in the header // and it's already included in the $authorization header unset($parameters['oauth_verifier']); } } else { $authorization = 'Authorization: Bearer ' . $this->bearer; } return $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters); } /** * Set Curl options. * * @return array */ private function curlOptions() { $options = [ // CURLOPT_VERBOSE => true, CURLOPT_CONNECTTIMEOUT => $this->connectionTimeout, CURLOPT_HEADER => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_USERAGENT => $this->userAgent, ]; if ($this->useCAFile()) { $options[CURLOPT_CAINFO] = __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem'; } if($this->gzipEncoding) { $options[CURLOPT_ENCODING] = 'gzip'; } if (!empty($this->proxy)) { $options[CURLOPT_PROXY] = $this->proxy['CURLOPT_PROXY']; $options[CURLOPT_PROXYUSERPWD] = $this->proxy['CURLOPT_PROXYUSERPWD']; $options[CURLOPT_PROXYPORT] = $this->proxy['CURLOPT_PROXYPORT']; $options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC; $options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP; } return $options; } /** * Make an HTTP request * * @param string $url * @param string $method * @param string $authorization * @param array $postfields * * @return string * @throws TwitterOAuthException */ private function request($url, $method, $authorization, array $postfields) { $options = $this->curlOptions($url, $authorization); $options[CURLOPT_URL] = $url; $options[CURLOPT_HTTPHEADER] = ['Accept: application/json', $authorization, 'Expect:']; switch ($method) { case 'GET': break; case 'POST': $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = Util::buildHttpQuery($postfields); break; case 'DELETE': $options[CURLOPT_CUSTOMREQUEST] = 'DELETE'; break; case 'PUT': $options[CURLOPT_CUSTOMREQUEST] = 'PUT'; break; } if (in_array($method, ['GET', 'PUT', 'DELETE']) && !empty($postfields)) { $options[CURLOPT_URL] .= '?' . Util::buildHttpQuery($postfields); } $curlHandle = curl_init(); curl_setopt_array($curlHandle, $options); $response = curl_exec($curlHandle); // Throw exceptions on cURL errors. if (curl_errno($curlHandle) > 0) { throw new TwitterOAuthException(curl_error($curlHandle), curl_errno($curlHandle)); } $this->response->setHttpCode(curl_getinfo($curlHandle, CURLINFO_HTTP_CODE)); $parts = explode("\r\n\r\n", $response); $responseBody = array_pop($parts); $responseHeader = array_pop($parts); $this->response->setHeaders($this->parseHeaders($responseHeader)); curl_close($curlHandle); return $responseBody; } /** * Get the header info to store. * * @param string $header * * @return array */ private function parseHeaders($header) { $headers = []; foreach (explode("\r\n", $header) as $line) { if (strpos($line, ':') !== false) { list ($key, $value) = explode(': ', $line); $key = str_replace('-', '_', strtolower($key)); $headers[$key] = trim($value); } } return $headers; } /** * Encode application authorization header with base64. * * @param Consumer $consumer * * @return string */ private function encodeAppAuthorization(Consumer $consumer) { $key = rawurlencode($consumer->key); $secret = rawurlencode($consumer->secret); return base64_encode($key . ':' . $secret); } /** * Is the code running from a Phar module. * * @return boolean */ private function pharRunning() { return class_exists('Phar') && \Phar::running(false) !== ''; } /** * Use included CA file instead of OS provided list. * * @return boolean */ private function useCAFile() { /* Use CACert file when not in a PHAR file. */ return !$this->pharRunning(); } }