├── twitteroauth ├── .gitignore ├── tests │ ├── bootstrap.php │ ├── kitten.jpg │ ├── video.mp4 │ ├── ConsumerTest.php │ ├── scripts │ │ └── cacert.sh │ ├── vars.php │ ├── sample_env │ ├── TokenTest.php │ ├── HmacSha1Test.php │ ├── Util │ │ └── JsonDecoderTest.php │ ├── AbstractSignatureMethodTest.php │ └── TwitterOAuthTest.php ├── src │ ├── TwitterOAuthException.php │ ├── Util │ │ └── JsonDecoder.php │ ├── Consumer.php │ ├── Token.php │ ├── HmacSha1.php │ ├── SignatureMethod.php │ ├── Response.php │ ├── Config.php │ ├── Util.php │ ├── Request.php │ └── TwitterOAuth.php ├── phpunit.xml ├── .travis.yml ├── phpmd.xml ├── autoload.php ├── composer.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md └── CODE_OF_CONDUCT.md ├── config.default.php ├── .gitignore ├── LICENSE.txt ├── README.md └── AutoFavoriter.php /twitteroauth/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | composer.lock 3 | vendor 4 | env 5 | .cache 6 | -------------------------------------------------------------------------------- /twitteroauth/tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class TwitterOAuthException extends \Exception 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /twitteroauth/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | ./tests/ 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /twitteroauth/tests/ConsumerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals("Consumer[key=$key,secret=$secret]", $consumer->__toString()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /twitteroauth/tests/scripts/cacert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CURRENT_COMMAND="$(curl -s "https://curl.haxx.se/ca/cacert.pem.sha256")" 4 | CURRENT_PARTS=($CURRENT_COMMAND) 5 | CURRENT_SHA="${CURRENT_PARTS[0]}" 6 | 7 | FILE_COMMAND="$(openssl dgst -sha256 src/cacert.pem)" 8 | FILE_PARTS=($FILE_COMMAND) 9 | FILE_SHA="${FILE_PARTS[1]}" 10 | 11 | if [ "$FILE_SHA" = "$CURRENT_SHA" ]; then 12 | echo "cacert.pem is current" 13 | exit 0 14 | fi 15 | 16 | echo "cacert.pem needs to be updated." 17 | exit 1 18 | -------------------------------------------------------------------------------- /twitteroauth/tests/vars.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class JsonDecoder 9 | { 10 | /** 11 | * Decodes a JSON string to stdObject or associative array 12 | * 13 | * @param string $string 14 | * @param bool $asArray 15 | * 16 | * @return array|object 17 | */ 18 | public static function decode($string, $asArray) 19 | { 20 | if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { 21 | return json_decode($string, $asArray, 512, JSON_BIGINT_AS_STRING); 22 | } 23 | 24 | return json_decode($string, $asArray); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /twitteroauth/phpmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | Keep TwitterOAuth source code clean. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /config.default.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $token->__toString()); 17 | } 18 | 19 | public function tokenProvider() 20 | { 21 | return [ 22 | ['oauth_token=key&oauth_token_secret=secret', 'key', 'secret'], 23 | ['oauth_token=key%2Bkey&oauth_token_secret=secret', 'key+key', 'secret'], 24 | ['oauth_token=key~key&oauth_token_secret=secret', 'key~key', 'secret'], 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Files 2 | #--------------------------- 3 | /config.php 4 | !.gitkeep 5 | # Temporary Files 6 | #--------------------------- 7 | # vim 8 | [._]*.s[a-w][a-z] 9 | [._]s[a-w][a-z] 10 | *.un~ 11 | Session.vim 12 | .netrwhist 13 | # Emacs 14 | .\#* 15 | # Backup files 16 | #--------------------------- 17 | *~ 18 | *.orig 19 | *.bak 20 | # yyyyMMdd 21 | *.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] 22 | # yyyyMMddHHmm 23 | *.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] 24 | # yyyyMMddHHmmss 25 | *.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] 26 | # OS dependent files 27 | #--------------------------- 28 | .DS_Store 29 | Thumbs.db 30 | # Bundler specific 31 | #--------------------------- 32 | /vendor/bundle/ 33 | /.bundle/ 34 | # Office specific 35 | ~$* 36 | *.tmp 37 | # Vagrant specific 38 | .vagrant/ -------------------------------------------------------------------------------- /twitteroauth/src/Consumer.php: -------------------------------------------------------------------------------- 1 | key = $key; 25 | $this->secret = $secret; 26 | $this->callbackUrl = $callbackUrl; 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function __toString() 33 | { 34 | return "Consumer[key=$this->key,secret=$this->secret]"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /twitteroauth/src/Token.php: -------------------------------------------------------------------------------- 1 | key = $key; 22 | $this->secret = $secret; 23 | } 24 | 25 | /** 26 | * Generates the basic string serialization of a token that a server 27 | * would respond to request_token and access_token calls with 28 | * 29 | * @return string 30 | */ 31 | public function __toString() 32 | { 33 | return sprintf( 34 | "oauth_token=%s&oauth_token_secret=%s", 35 | Util::urlencodeRfc3986($this->key), 36 | Util::urlencodeRfc3986($this->secret) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 tsukumi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /twitteroauth/tests/HmacSha1Test.php: -------------------------------------------------------------------------------- 1 | getRequest(), $this->getConsumer(), $this->getToken()], 20 | [ 21 | 'EBw0gHngam3BTx8kfPfNNSyKem4=', 22 | $this->getRequest(), 23 | $this->getConsumer('key', 'secret'), 24 | $this->getToken() 25 | ], 26 | [ 27 | 'kDsHFZzws2a5M6cAQjfpdNBo+v8=', 28 | $this->getRequest(), 29 | $this->getConsumer('key', 'secret'), 30 | $this->getToken('key', 'secret') 31 | ], 32 | ['EBw0gHngam3BTx8kfPfNNSyKem4=', $this->getRequest(), $this->getConsumer('key', 'secret'), null], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /twitteroauth/autoload.php: -------------------------------------------------------------------------------- 1 | TwitterOAuth [![Build Status](https://img.shields.io/travis/abraham/twitteroauth.svg)](https://travis-ci.org/abraham/twitteroauth) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/abraham/twitteroauth/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/abraham/twitteroauth/?branch=master) [![Issues Count](https://img.shields.io/github/issues/abraham/twitteroauth.svg)](https://github.com/abraham/twitteroauth/issues) [![Latest Version](https://img.shields.io/packagist/v/abraham/twitteroauth.svg)](https://packagist.org/packages/abraham/twitteroauth) [![Downloads this Month](https://img.shields.io/packagist/dm/abraham/twitteroauth.svg)](https://packagist.org/packages/abraham/twitteroauth) 2 | 3 | ------------ 4 | 5 |

The most popular PHP library for Twitter's OAuth REST API.

6 | 7 | See documentation at https://twitteroauth.com. 8 | 9 | PHP versions [listed](https://secure.php.net/supported-versions.php) as "active support" or "security fixes only" are supported. 10 | 11 | Twitter bird 12 | -------------------------------------------------------------------------------- /twitteroauth/src/HmacSha1.php: -------------------------------------------------------------------------------- 1 | getSignatureBaseString(); 31 | 32 | $parts = [$consumer->secret, null !== $token ? $token->secret : ""]; 33 | 34 | $parts = Util::urlencodeRfc3986($parts); 35 | $key = implode('&', $parts); 36 | 37 | return base64_encode(hash_hmac('sha1', $signatureBase, $key, true)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /twitteroauth/tests/Util/JsonDecoderTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, JsonDecoder::decode($input, $asArray)); 15 | } 16 | 17 | public function jsonProvider() 18 | { 19 | return [ 20 | ['[]', true, []], 21 | ['[1,2,3]', true, [1, 2, 3]], 22 | ['[{"id": 556179961825226750}]', true, [['id' => 556179961825226750]]], 23 | ['[]', false, []], 24 | ['[1,2,3]', false, [1, 2, 3]], 25 | [ 26 | '[{"id": 556179961825226750}]', 27 | false, 28 | [ 29 | $this->getClass(function ($object) { 30 | $object->id = 556179961825226750; 31 | return $object; 32 | }) 33 | ] 34 | ], 35 | 36 | ]; 37 | } 38 | 39 | /** 40 | * @param callable $callable 41 | * 42 | * @return stdClass 43 | */ 44 | private function getClass(\Closure $callable) 45 | { 46 | $object = new \stdClass(); 47 | 48 | return $callable($object); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /twitteroauth/tests/AbstractSignatureMethodTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($this->name, $this->getClass()->getName()); 21 | } 22 | 23 | /** 24 | * @dataProvider signatureDataProvider 25 | */ 26 | public function testBuildSignature($expected, $request, $consumer, $token) 27 | { 28 | $this->assertEquals($expected, $this->getClass()->buildSignature($request, $consumer, $token)); 29 | } 30 | 31 | protected function getRequest() 32 | { 33 | return $this->getMockBuilder('Abraham\TwitterOAuth\Request') 34 | ->disableOriginalConstructor() 35 | ->getMock(); 36 | } 37 | 38 | protected function getConsumer($key = null, $secret = null, $callbackUrl = null) 39 | { 40 | return $this->getMockBuilder('Abraham\TwitterOAuth\Consumer') 41 | ->setConstructorArgs([$key, $secret, $callbackUrl]) 42 | ->getMock(); 43 | } 44 | 45 | protected function getToken($key = null, $secret = null) 46 | { 47 | return $this->getMockBuilder('Abraham\TwitterOAuth\Token') 48 | ->setConstructorArgs([$key, $secret]) 49 | ->getMock(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /twitteroauth/src/SignatureMethod.php: -------------------------------------------------------------------------------- 1 | buildSignature($request, $consumer, $token); 48 | 49 | // Check for zero length, although unlikely here 50 | if (strlen($built) == 0 || strlen($signature) == 0) { 51 | return false; 52 | } 53 | 54 | if (strlen($built) != strlen($signature)) { 55 | return false; 56 | } 57 | 58 | // Avoid a timing leak with a (hopefully) time insensitive compare 59 | $result = 0; 60 | for ($i = 0; $i < strlen($signature); $i++) { 61 | $result |= ord($built[$i]) ^ ord($signature[$i]); 62 | } 63 | 64 | return $result == 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /AutoFavoriter.php: -------------------------------------------------------------------------------- 1 | get('account/verify_credentials'); 18 | 19 | if (isset($info->errors)){ 20 | 21 | // 例外 22 | echo ' Error: Connection to Twitter failed.'."\n"; 23 | echo ' Exception: '.$info->errors[0]->message."\n\n"; 24 | exit(1); 25 | 26 | } 27 | 28 | } catch(Exception $e) { 29 | 30 | // 例外 31 | echo ' Error: Connection to Twitter failed.'."\n"; 32 | echo ' Exception: '.$e."\n\n"; 33 | exit(1); 34 | 35 | } 36 | 37 | echo ' Connected to Twitter.'."\n\n"; 38 | 39 | // 検索設定 40 | if ($retweet_ignore){ 41 | 42 | // RTをいいねしないようにする 43 | $tweets_params = array('q' => $keywords.' exclude:retweets' ,'count' => $count, 'result_type' => $result_type); 44 | 45 | } else { 46 | 47 | // RTを検索結果に含める 48 | $tweets_params = array('q' => $keywords ,'count' => $count, 'result_type' => $result_type); 49 | 50 | } 51 | 52 | // ツイートを検索 53 | $tweets = $connection->get('search/tweets', $tweets_params)->statuses; 54 | 55 | // ツイートごとに実行 56 | foreach ($tweets as $key => $value) { 57 | 58 | if ($favorite) $connection->post('favorites/create', array('id' => $value->id)); // いいねを実行 59 | if ($retweet) $connection->post('statuses/retweet', array('id' => $value->id)); // RT を実行 60 | 61 | // 実行タイプを判定 62 | if ($favorite and $retweet){ 63 | $type = 'Favorite and Retweet'; 64 | } else if ($favorite){ 65 | $type = 'Favorite'; 66 | } else if ($retweet){ 67 | $type = 'Retweet'; 68 | } else { 69 | $type = 'View'; 70 | } 71 | 72 | // 表示 73 | echo ' '.$type.' '.sprintf('%02d', ($key + 1)).': Date: '.date('Y-m-d H:i:s', strtotime($value->created_at)).' ID: '.$value->id. 74 | ' Tweet: '.str_replace("\n", '', mb_substr($value->text, 0, 16)).'…'."\n"; 75 | 76 | // 高速で実行すると凍結しかねないのでランダムで1.5~5秒くらい間をおく 77 | $sleep = rand(150, 500) * 10000; 78 | usleep($sleep); 79 | 80 | } 81 | 82 | echo "\n".' Finished. Exit.'."\n\n"; 83 | -------------------------------------------------------------------------------- /twitteroauth/src/Response.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Response 11 | { 12 | /** @var string|null API path from the most recent request */ 13 | private $apiPath; 14 | /** @var int HTTP status code from the most recent request */ 15 | private $httpCode = 0; 16 | /** @var array HTTP headers from the most recent request */ 17 | private $headers = []; 18 | /** @var array|object Response body from the most recent request */ 19 | private $body = []; 20 | /** @var array HTTP headers from the most recent request that start with X */ 21 | private $xHeaders = []; 22 | 23 | /** 24 | * @param string $apiPath 25 | */ 26 | public function setApiPath($apiPath) 27 | { 28 | $this->apiPath = $apiPath; 29 | } 30 | 31 | /** 32 | * @return string|null 33 | */ 34 | public function getApiPath() 35 | { 36 | return $this->apiPath; 37 | } 38 | 39 | /** 40 | * @param array|object $body 41 | */ 42 | public function setBody($body) 43 | { 44 | $this->body = $body; 45 | } 46 | 47 | /** 48 | * @return array|object|string 49 | */ 50 | public function getBody() 51 | { 52 | return $this->body; 53 | } 54 | 55 | /** 56 | * @param int $httpCode 57 | */ 58 | public function setHttpCode($httpCode) 59 | { 60 | $this->httpCode = $httpCode; 61 | } 62 | 63 | /** 64 | * @return int 65 | */ 66 | public function getHttpCode() 67 | { 68 | return $this->httpCode; 69 | } 70 | 71 | /** 72 | * @param array $headers 73 | */ 74 | public function setHeaders(array $headers) 75 | { 76 | foreach ($headers as $key => $value) { 77 | if (substr($key, 0, 1) == 'x') { 78 | $this->xHeaders[$key] = $value; 79 | } 80 | } 81 | $this->headers = $headers; 82 | } 83 | 84 | /** 85 | * @return array 86 | */ 87 | public function getsHeaders() 88 | { 89 | return $this->headers; 90 | } 91 | 92 | /** 93 | * @param array $xHeaders 94 | */ 95 | public function setXHeaders(array $xHeaders = []) 96 | { 97 | $this->xHeaders = $xHeaders; 98 | } 99 | 100 | /** 101 | * @return array 102 | */ 103 | public function getXHeaders() 104 | { 105 | return $this->xHeaders; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /twitteroauth/src/Config.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Config 11 | { 12 | /** @var int How long to wait for a response from the API */ 13 | protected $timeout = 5; 14 | /** @var int how long to wait while connecting to the API */ 15 | protected $connectionTimeout = 5; 16 | /** @var int How many times we retry request when API is down */ 17 | protected $maxRetries = 0; 18 | /** @var int Delay in seconds before we retry the request */ 19 | protected $retriesDelay = 1; 20 | 21 | /** 22 | * Decode JSON Response as associative Array 23 | * 24 | * @see http://php.net/manual/en/function.json-decode.php 25 | * 26 | * @var bool 27 | */ 28 | protected $decodeJsonAsArray = false; 29 | /** @var string User-Agent header */ 30 | protected $userAgent = 'TwitterOAuth (+https://twitteroauth.com)'; 31 | /** @var array Store proxy connection details */ 32 | protected $proxy = []; 33 | 34 | /** @var bool Whether to encode the curl requests with gzip or not */ 35 | protected $gzipEncoding = true; 36 | 37 | /** @var integer Size for Chunked Uploads */ 38 | protected $chunkSize = 250000; // 0.25 MegaByte 39 | 40 | /** 41 | * Set the connection and response timeouts. 42 | * 43 | * @param int $connectionTimeout 44 | * @param int $timeout 45 | */ 46 | public function setTimeouts($connectionTimeout, $timeout) 47 | { 48 | $this->connectionTimeout = (int)$connectionTimeout; 49 | $this->timeout = (int)$timeout; 50 | } 51 | 52 | /** 53 | * Set the number of times to retry on error and how long between each. 54 | * 55 | * @param int $maxRetries 56 | * @param int $retriesDelay 57 | */ 58 | public function setRetries($maxRetries, $retriesDelay) 59 | { 60 | $this->maxRetries = (int)$maxRetries; 61 | $this->retriesDelay = (int)$retriesDelay; 62 | } 63 | 64 | /** 65 | * @param bool $value 66 | */ 67 | public function setDecodeJsonAsArray($value) 68 | { 69 | $this->decodeJsonAsArray = (bool)$value; 70 | } 71 | 72 | /** 73 | * @param string $userAgent 74 | */ 75 | public function setUserAgent($userAgent) 76 | { 77 | $this->userAgent = (string)$userAgent; 78 | } 79 | 80 | /** 81 | * @param array $proxy 82 | */ 83 | public function setProxy(array $proxy) 84 | { 85 | $this->proxy = $proxy; 86 | } 87 | 88 | /** 89 | * Whether to encode the curl requests with gzip or not. 90 | * 91 | * @param boolean $gzipEncoding 92 | */ 93 | public function setGzipEncoding($gzipEncoding) 94 | { 95 | $this->gzipEncoding = (bool)$gzipEncoding; 96 | } 97 | 98 | /** 99 | * Set the size of each part of file for chunked media upload. 100 | * 101 | * @param int $value 102 | */ 103 | public function setChunkSize($value) 104 | { 105 | $this->chunkSize = (int)$value; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /twitteroauth/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at abraham@abrah.am. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /twitteroauth/src/Util.php: -------------------------------------------------------------------------------- 1 | array('b','c'), 'd' => 'e') 40 | * 41 | * @param string $input 42 | * 43 | * @return array 44 | */ 45 | public static function parseParameters($input) 46 | { 47 | if (!is_string($input)) { 48 | return []; 49 | } 50 | 51 | $pairs = explode('&', $input); 52 | 53 | $parameters = []; 54 | foreach ($pairs as $pair) { 55 | $split = explode('=', $pair, 2); 56 | $parameter = Util::urldecodeRfc3986($split[0]); 57 | $value = isset($split[1]) ? Util::urldecodeRfc3986($split[1]) : ''; 58 | 59 | if (isset($parameters[$parameter])) { 60 | // We have already recieved parameter(s) with this name, so add to the list 61 | // of parameters with this name 62 | 63 | if (is_scalar($parameters[$parameter])) { 64 | // This is the first duplicate, so transform scalar (string) into an array 65 | // so we can add the duplicates 66 | $parameters[$parameter] = [$parameters[$parameter]]; 67 | } 68 | 69 | $parameters[$parameter][] = $value; 70 | } else { 71 | $parameters[$parameter] = $value; 72 | } 73 | } 74 | return $parameters; 75 | } 76 | 77 | /** 78 | * @param array $params 79 | * 80 | * @return string 81 | */ 82 | public static function buildHttpQuery(array $params) 83 | { 84 | if (empty($params)) { 85 | return ''; 86 | } 87 | 88 | // Urlencode both keys and values 89 | $keys = Util::urlencodeRfc3986(array_keys($params)); 90 | $values = Util::urlencodeRfc3986(array_values($params)); 91 | $params = array_combine($keys, $values); 92 | 93 | // Parameters are sorted by name, using lexicographical byte value ordering. 94 | // Ref: Spec: 9.1.1 (1) 95 | uksort($params, 'strcmp'); 96 | 97 | $pairs = []; 98 | foreach ($params as $parameter => $value) { 99 | if (is_array($value)) { 100 | // If two or more parameters share the same name, they are sorted by their value 101 | // Ref: Spec: 9.1.1 (1) 102 | // June 12th, 2010 - changed to sort because of issue 164 by hidetaka 103 | sort($value, SORT_STRING); 104 | foreach ($value as $duplicateValue) { 105 | $pairs[] = $parameter . '=' . $duplicateValue; 106 | } 107 | } else { 108 | $pairs[] = $parameter . '=' . $value; 109 | } 110 | } 111 | // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) 112 | // Each name-value pair is separated by an '&' character (ASCII code 38) 113 | return implode('&', $pairs); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /twitteroauth/src/Request.php: -------------------------------------------------------------------------------- 1 | parameters = $parameters; 27 | $this->httpMethod = $httpMethod; 28 | $this->httpUrl = $httpUrl; 29 | } 30 | 31 | /** 32 | * pretty much a helper function to set up the request 33 | * 34 | * @param Consumer $consumer 35 | * @param Token $token 36 | * @param string $httpMethod 37 | * @param string $httpUrl 38 | * @param array $parameters 39 | * 40 | * @return Request 41 | */ 42 | public static function fromConsumerAndToken( 43 | Consumer $consumer, 44 | Token $token = null, 45 | $httpMethod, 46 | $httpUrl, 47 | array $parameters = [], 48 | $json = false 49 | ) { 50 | $defaults = [ 51 | "oauth_version" => Request::$version, 52 | "oauth_nonce" => Request::generateNonce(), 53 | "oauth_timestamp" => time(), 54 | "oauth_consumer_key" => $consumer->key 55 | ]; 56 | if (null !== $token) { 57 | $defaults['oauth_token'] = $token->key; 58 | } 59 | 60 | // The json payload is not included in the signature on json requests, 61 | // therefore it shouldn't be included in the parameters array. 62 | if ($json) { 63 | $parameters = $defaults; 64 | } else { 65 | $parameters = array_merge($defaults, $parameters); 66 | } 67 | 68 | return new Request($httpMethod, $httpUrl, $parameters); 69 | } 70 | 71 | /** 72 | * @param string $name 73 | * @param string $value 74 | */ 75 | public function setParameter($name, $value) 76 | { 77 | $this->parameters[$name] = $value; 78 | } 79 | 80 | /** 81 | * @param string $name 82 | * 83 | * @return string|null 84 | */ 85 | public function getParameter($name) 86 | { 87 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null; 88 | } 89 | 90 | /** 91 | * @return array 92 | */ 93 | public function getParameters() 94 | { 95 | return $this->parameters; 96 | } 97 | 98 | /** 99 | * @param string $name 100 | */ 101 | public function removeParameter($name) 102 | { 103 | unset($this->parameters[$name]); 104 | } 105 | 106 | /** 107 | * The request parameters, sorted and concatenated into a normalized string. 108 | * 109 | * @return string 110 | */ 111 | public function getSignableParameters() 112 | { 113 | // Grab all parameters 114 | $params = $this->parameters; 115 | 116 | // Remove oauth_signature if present 117 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 118 | if (isset($params['oauth_signature'])) { 119 | unset($params['oauth_signature']); 120 | } 121 | 122 | return Util::buildHttpQuery($params); 123 | } 124 | 125 | /** 126 | * Returns the base string of this request 127 | * 128 | * The base string defined as the method, the url 129 | * and the parameters (normalized), each urlencoded 130 | * and the concated with &. 131 | * 132 | * @return string 133 | */ 134 | public function getSignatureBaseString() 135 | { 136 | $parts = [ 137 | $this->getNormalizedHttpMethod(), 138 | $this->getNormalizedHttpUrl(), 139 | $this->getSignableParameters() 140 | ]; 141 | 142 | $parts = Util::urlencodeRfc3986($parts); 143 | 144 | return implode('&', $parts); 145 | } 146 | 147 | /** 148 | * Returns the HTTP Method in uppercase 149 | * 150 | * @return string 151 | */ 152 | public function getNormalizedHttpMethod() 153 | { 154 | return strtoupper($this->httpMethod); 155 | } 156 | 157 | /** 158 | * parses the url and rebuilds it to be 159 | * scheme://host/path 160 | * 161 | * @return string 162 | */ 163 | public function getNormalizedHttpUrl() 164 | { 165 | $parts = parse_url($this->httpUrl); 166 | 167 | $scheme = $parts['scheme']; 168 | $host = strtolower($parts['host']); 169 | $path = $parts['path']; 170 | 171 | return "$scheme://$host$path"; 172 | } 173 | 174 | /** 175 | * Builds a url usable for a GET request 176 | * 177 | * @return string 178 | */ 179 | public function toUrl() 180 | { 181 | $postData = $this->toPostdata(); 182 | $out = $this->getNormalizedHttpUrl(); 183 | if ($postData) { 184 | $out .= '?' . $postData; 185 | } 186 | return $out; 187 | } 188 | 189 | /** 190 | * Builds the data one would send in a POST request 191 | * 192 | * @return string 193 | */ 194 | public function toPostdata() 195 | { 196 | return Util::buildHttpQuery($this->parameters); 197 | } 198 | 199 | /** 200 | * Builds the Authorization: header 201 | * 202 | * @return string 203 | * @throws TwitterOAuthException 204 | */ 205 | public function toHeader() 206 | { 207 | $first = true; 208 | $out = 'Authorization: OAuth'; 209 | foreach ($this->parameters as $k => $v) { 210 | if (substr($k, 0, 5) != "oauth") { 211 | continue; 212 | } 213 | if (is_array($v)) { 214 | throw new TwitterOAuthException('Arrays not supported in headers'); 215 | } 216 | $out .= ($first) ? ' ' : ', '; 217 | $out .= Util::urlencodeRfc3986($k) . '="' . Util::urlencodeRfc3986($v) . '"'; 218 | $first = false; 219 | } 220 | return $out; 221 | } 222 | 223 | /** 224 | * @return string 225 | */ 226 | public function __toString() 227 | { 228 | return $this->toUrl(); 229 | } 230 | 231 | /** 232 | * @param SignatureMethod $signatureMethod 233 | * @param Consumer $consumer 234 | * @param Token $token 235 | */ 236 | public function signRequest(SignatureMethod $signatureMethod, Consumer $consumer, Token $token = null) 237 | { 238 | $this->setParameter("oauth_signature_method", $signatureMethod->getName()); 239 | $signature = $this->buildSignature($signatureMethod, $consumer, $token); 240 | $this->setParameter("oauth_signature", $signature); 241 | } 242 | 243 | /** 244 | * @param SignatureMethod $signatureMethod 245 | * @param Consumer $consumer 246 | * @param Token $token 247 | * 248 | * @return string 249 | */ 250 | public function buildSignature(SignatureMethod $signatureMethod, Consumer $consumer, Token $token = null) 251 | { 252 | return $signatureMethod->buildSignature($this, $consumer, $token); 253 | } 254 | 255 | /** 256 | * @return string 257 | */ 258 | public static function generateNonce() 259 | { 260 | return md5(microtime() . mt_rand()); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /twitteroauth/tests/TwitterOAuthTest.php: -------------------------------------------------------------------------------- 1 | twitter = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET); 17 | $this->userId = explode('-', ACCESS_TOKEN)[0]; 18 | } 19 | 20 | public function testBuildClient() 21 | { 22 | $this->assertObjectHasAttribute('consumer', $this->twitter); 23 | $this->assertObjectHasAttribute('token', $this->twitter); 24 | } 25 | 26 | public function testSetOauthToken() 27 | { 28 | $twitter = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET); 29 | $twitter->setOauthToken(ACCESS_TOKEN, ACCESS_TOKEN_SECRET); 30 | $this->assertObjectHasAttribute('consumer', $twitter); 31 | $this->assertObjectHasAttribute('token', $twitter); 32 | $twitter->get('friendships/show', ['target_screen_name' => 'twitterapi']); 33 | $this->assertEquals(200, $twitter->getLastHttpCode()); 34 | } 35 | 36 | public function testOauth2Token() 37 | { 38 | $twitter = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET); 39 | $result = $twitter->oauth2('oauth2/token', ['grant_type' => 'client_credentials']); 40 | $this->assertEquals(200, $twitter->getLastHttpCode()); 41 | $this->assertObjectHasAttribute('token_type', $result); 42 | $this->assertObjectHasAttribute('access_token', $result); 43 | $this->assertEquals('bearer', $result->token_type); 44 | return $result; 45 | } 46 | 47 | /** 48 | * @depends testOauth2Token 49 | */ 50 | public function testBearerToken($accessToken) 51 | { 52 | $twitter = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET, null, $accessToken->access_token); 53 | $result = $twitter->get('statuses/user_timeline', ['screen_name' => 'twitterapi']); 54 | if ($twitter->getLastHttpCode() !== 200) { 55 | $this->assertEquals('foo', substr($accessToken->access_token, 0, 75)); 56 | $this->assertEquals('foo', print_r($result, true)); 57 | } 58 | $this->assertEquals(200, $twitter->getLastHttpCode()); 59 | return $accessToken; 60 | } 61 | 62 | // This causes issues for parallel run tests. 63 | // /** 64 | // * @depends testBearerToken 65 | // */ 66 | // public function testOauth2TokenInvalidate($accessToken) 67 | // { 68 | // $twitter = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET); 69 | // // HACK: access_token is already urlencoded but gets urlencoded again breaking the invalidate request. 70 | // $result = $twitter->oauth2( 71 | // 'oauth2/invalidate_token', 72 | // array('access_token' => urldecode($accessToken->access_token)) 73 | // ); 74 | // $this->assertEquals(200, $twitter->getLastHttpCode()); 75 | // $this->assertObjectHasAttribute('access_token', $result); 76 | // return $result; 77 | // } 78 | 79 | public function testOauthRequestToken() 80 | { 81 | $twitter = new TwitterOAuth(CONSUMER_KEY, CONSUMER_SECRET); 82 | $result = $twitter->oauth('oauth/request_token', ['oauth_callback' => OAUTH_CALLBACK]); 83 | $this->assertEquals(200, $twitter->getLastHttpCode()); 84 | $this->assertArrayHasKey('oauth_token', $result); 85 | $this->assertArrayHasKey('oauth_token_secret', $result); 86 | $this->assertArrayHasKey('oauth_callback_confirmed', $result); 87 | $this->assertEquals('true', $result['oauth_callback_confirmed']); 88 | return $result; 89 | } 90 | 91 | /** 92 | * @expectedException \Abraham\TwitterOAuth\TwitterOAuthException 93 | * @expectedExceptionMessage Could not authenticate you 94 | */ 95 | public function testOauthRequestTokenException() 96 | { 97 | $twitter = new TwitterOAuth('CONSUMER_KEY', 'CONSUMER_SECRET'); 98 | $result = $twitter->oauth('oauth/request_token', ['oauth_callback' => OAUTH_CALLBACK]); 99 | return $result; 100 | } 101 | 102 | /** 103 | * @expectedException \Abraham\TwitterOAuth\TwitterOAuthException 104 | * @expectedExceptionMessage Invalid oauth_verifier parameter 105 | * @depends testOauthRequestToken 106 | */ 107 | public function testOauthAccessTokenTokenException(array $requestToken) 108 | { 109 | // Can't test this without a browser logging into Twitter so check for the correct error instead. 110 | $twitter = new TwitterOAuth( 111 | CONSUMER_KEY, 112 | CONSUMER_SECRET, 113 | $requestToken['oauth_token'], 114 | $requestToken['oauth_token_secret'] 115 | ); 116 | $twitter->oauth("oauth/access_token", ["oauth_verifier" => "fake_oauth_verifier"]); 117 | } 118 | 119 | public function testUrl() 120 | { 121 | $url = $this->twitter->url('oauth/authorize', ['foo' => 'bar', 'baz' => 'qux']); 122 | $this->assertEquals('https://api.twitter.com/oauth/authorize?foo=bar&baz=qux', $url); 123 | } 124 | 125 | public function testGetAccountVerifyCredentials() 126 | { 127 | $user = $this->twitter->get('account/verify_credentials', [ 128 | 'include_entities' => false, 129 | 'include_email' => true 130 | ]); 131 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 132 | $this->assertObjectHasAttribute('email', $user); 133 | } 134 | 135 | // BUG: testing is too unreliable for now 136 | // public function testSetProxy() 137 | // { 138 | // $this->twitter->setProxy(array( 139 | // 'CURLOPT_PROXY' => PROXY, 140 | // 'CURLOPT_PROXYUSERPWD' => PROXYUSERPWD, 141 | // 'CURLOPT_PROXYPORT' => PROXYPORT, 142 | // )); 143 | // $this->twitter->setTimeouts(60, 60); 144 | // $result = $this->twitter->get('account/verify_credentials'); 145 | // $this->assertEquals(200, $this->twitter->getLastHttpCode()); 146 | // $this->assertObjectHasAttribute('id', $result); 147 | // } 148 | 149 | public function testGetStatusesMentionsTimeline() 150 | { 151 | $this->twitter->get('statuses/mentions_timeline'); 152 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 153 | } 154 | 155 | public function testGetSearchTweets() 156 | { 157 | $result = $this->twitter->get('search/tweets', ['q' => 'twitter']); 158 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 159 | return $result->statuses; 160 | } 161 | 162 | /** 163 | * @depends testGetSearchTweets 164 | */ 165 | public function testGetSearchTweetsWithMaxId($statuses) 166 | { 167 | $maxId = array_pop($statuses)->id_str; 168 | $this->twitter->get('search/tweets', ['q' => 'twitter', 'max_id' => $maxId]); 169 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 170 | } 171 | 172 | public function testPostFavoritesCreate() 173 | { 174 | $result = $this->twitter->post('favorites/create', ['id' => '6242973112']); 175 | if ($this->twitter->getLastHttpCode() == 403) { 176 | // Status already favorited 177 | $this->assertEquals(139, $result->errors[0]->code); 178 | } else { 179 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 180 | } 181 | } 182 | 183 | /** 184 | * @depends testPostFavoritesCreate 185 | */ 186 | public function testPostFavoritesDestroy() 187 | { 188 | $this->twitter->post('favorites/destroy', ['id' => '6242973112']); 189 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 190 | } 191 | 192 | public function testPostDirectMessagesEventsNew() 193 | { 194 | $data = [ 195 | 'event' => [ 196 | 'type' => 'message_create', 197 | 'message_create' => [ 198 | 'target' => [ 199 | 'recipient_id' => $this->userId 200 | ], 201 | 'message_data' => [ 202 | 'text' => 'Hello World!' 203 | ] 204 | ] 205 | ] 206 | ]; 207 | $result = $this->twitter->post('direct_messages/events/new', $data, true); 208 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 209 | return $result; 210 | } 211 | 212 | /** 213 | * @depends testPostDirectMessagesEventsNew 214 | */ 215 | public function testDeleteDirectMessagesEventsDestroy($message) 216 | { 217 | $this->twitter->delete('direct_messages/events/destroy', ['id' => $message->event->id]); 218 | $this->assertEquals(204, $this->twitter->getLastHttpCode()); 219 | } 220 | 221 | public function testPostStatusesUpdateWithMedia() 222 | { 223 | $this->twitter->setTimeouts(60, 30); 224 | // Image source https://www.flickr.com/photos/titrans/8548825587/ 225 | $file_path = __DIR__ . '/kitten.jpg'; 226 | $result = $this->twitter->upload('media/upload', ['media' => $file_path]); 227 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 228 | $this->assertObjectHasAttribute('media_id_string', $result); 229 | $parameters = ['status' => 'Hello World ' . time(), 'media_ids' => $result->media_id_string]; 230 | $result = $this->twitter->post('statuses/update', $parameters); 231 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 232 | if ($this->twitter->getLastHttpCode() == 200) { 233 | $result = $this->twitter->post('statuses/destroy/' . $result->id_str); 234 | } 235 | return $result; 236 | } 237 | 238 | public function testPostStatusUpdateWithInvalidMediaThrowsException() 239 | { 240 | $this->expectException(\InvalidArgumentException::class); 241 | $file_path = __DIR__ . '/12345678900987654321.jpg'; 242 | $this->assertFalse(\is_readable($file_path)); 243 | $result = $this->twitter->upload('media/upload', ['media' => $file_path]); 244 | } 245 | 246 | public function testPostStatusesUpdateWithMediaChunked() 247 | { 248 | $this->twitter->setTimeouts(60, 30); 249 | // Video source http://www.sample-videos.com/ 250 | $file_path = __DIR__ . '/video.mp4'; 251 | $result = $this->twitter->upload('media/upload', ['media' => $file_path, 'media_type' => 'video/mp4'], true); 252 | $this->assertEquals(201, $this->twitter->getLastHttpCode()); 253 | $this->assertObjectHasAttribute('media_id_string', $result); 254 | $parameters = ['status' => 'Hello World ' . time(), 'media_ids' => $result->media_id_string]; 255 | $result = $this->twitter->post('statuses/update', $parameters); 256 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 257 | if ($this->twitter->getLastHttpCode() == 200) { 258 | $result = $this->twitter->post('statuses/destroy/' . $result->id_str); 259 | } 260 | return $result; 261 | } 262 | 263 | public function testPostStatusesUpdateUtf8() 264 | { 265 | $result = $this->twitter->post('statuses/update', ['status' => 'xこんにちは世界 ' . time()]); 266 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 267 | return $result; 268 | } 269 | 270 | /** 271 | * @depends testPostStatusesUpdateUtf8 272 | */ 273 | public function testPostStatusesDestroy($status) 274 | { 275 | $this->twitter->post('statuses/destroy/' . $status->id_str); 276 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 277 | } 278 | 279 | public function testLastResult() 280 | { 281 | $this->twitter->get('search/tweets', ['q' => 'twitter']); 282 | $this->assertEquals('search/tweets', $this->twitter->getLastApiPath()); 283 | $this->assertEquals(200, $this->twitter->getLastHttpCode()); 284 | $this->assertObjectHasAttribute('statuses', $this->twitter->getLastBody()); 285 | } 286 | 287 | /** 288 | * @depends testLastResult 289 | */ 290 | public function testResetLastResponse() 291 | { 292 | $this->twitter->resetLastResponse(); 293 | $this->assertEquals('', $this->twitter->getLastApiPath()); 294 | $this->assertEquals(0, $this->twitter->getLastHttpCode()); 295 | $this->assertEquals([], $this->twitter->getLastBody()); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /twitteroauth/src/TwitterOAuth.php: -------------------------------------------------------------------------------- 1 | 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 | 22 | /** @var Response details about the result of the last request */ 23 | private $response; 24 | /** @var string|null Application bearer token */ 25 | private $bearer; 26 | /** @var Consumer Twitter application details */ 27 | private $consumer; 28 | /** @var Token|null User access token details */ 29 | private $token; 30 | /** @var HmacSha1 OAuth 1 signature type used by Twitter */ 31 | private $signatureMethod; 32 | /** @var int Number of attempts we made for the request */ 33 | private $attempts = 0; 34 | 35 | /** 36 | * Constructor 37 | * 38 | * @param string $consumerKey The Application Consumer Key 39 | * @param string $consumerSecret The Application Consumer Secret 40 | * @param string|null $oauthToken The Client Token (optional) 41 | * @param string|null $oauthTokenSecret The Client Token Secret (optional) 42 | */ 43 | public function __construct($consumerKey, $consumerSecret, $oauthToken = null, $oauthTokenSecret = null) 44 | { 45 | $this->resetLastResponse(); 46 | $this->signatureMethod = new HmacSha1(); 47 | $this->consumer = new Consumer($consumerKey, $consumerSecret); 48 | if (!empty($oauthToken) && !empty($oauthTokenSecret)) { 49 | $this->setOauthToken($oauthToken, $oauthTokenSecret); 50 | } 51 | if (empty($oauthToken) && !empty($oauthTokenSecret)) { 52 | $this->setBearer($oauthTokenSecret); 53 | } 54 | } 55 | 56 | /** 57 | * @param string $oauthToken 58 | * @param string $oauthTokenSecret 59 | */ 60 | public function setOauthToken($oauthToken, $oauthTokenSecret) 61 | { 62 | $this->token = new Token($oauthToken, $oauthTokenSecret); 63 | $this->bearer = null; 64 | } 65 | 66 | /** 67 | * @param string $oauthTokenSecret 68 | */ 69 | public function setBearer($oauthTokenSecret) 70 | { 71 | $this->bearer = $oauthTokenSecret; 72 | $this->token = null; 73 | } 74 | 75 | /** 76 | * @return string|null 77 | */ 78 | public function getLastApiPath() 79 | { 80 | return $this->response->getApiPath(); 81 | } 82 | 83 | /** 84 | * @return int 85 | */ 86 | public function getLastHttpCode() 87 | { 88 | return $this->response->getHttpCode(); 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | public function getLastXHeaders() 95 | { 96 | return $this->response->getXHeaders(); 97 | } 98 | 99 | /** 100 | * @return array|object|null 101 | */ 102 | public function getLastBody() 103 | { 104 | return $this->response->getBody(); 105 | } 106 | 107 | /** 108 | * Resets the last response cache. 109 | */ 110 | public function resetLastResponse() 111 | { 112 | $this->response = new Response(); 113 | } 114 | 115 | /** 116 | * Resets the attempts number. 117 | */ 118 | private function resetAttemptsNumber() 119 | { 120 | $this->attempts = 0; 121 | } 122 | 123 | /** 124 | * Delays the retries when they're activated. 125 | */ 126 | private function sleepIfNeeded() 127 | { 128 | if ($this->maxRetries && $this->attempts) { 129 | sleep($this->retriesDelay); 130 | } 131 | } 132 | 133 | 134 | /** 135 | * Make URLs for user browser navigation. 136 | * 137 | * @param string $path 138 | * @param array $parameters 139 | * 140 | * @return string 141 | */ 142 | public function url($path, array $parameters) 143 | { 144 | $this->resetLastResponse(); 145 | $this->response->setApiPath($path); 146 | $query = http_build_query($parameters); 147 | return sprintf('%s/%s?%s', self::API_HOST, $path, $query); 148 | } 149 | 150 | /** 151 | * Make /oauth/* requests to the API. 152 | * 153 | * @param string $path 154 | * @param array $parameters 155 | * 156 | * @return array 157 | * @throws TwitterOAuthException 158 | */ 159 | public function oauth($path, array $parameters = []) 160 | { 161 | $response = []; 162 | $this->resetLastResponse(); 163 | $this->response->setApiPath($path); 164 | $url = sprintf('%s/%s', self::API_HOST, $path); 165 | $result = $this->oAuthRequest($url, 'POST', $parameters); 166 | 167 | if ($this->getLastHttpCode() != 200) { 168 | throw new TwitterOAuthException($result); 169 | } 170 | 171 | parse_str($result, $response); 172 | $this->response->setBody($response); 173 | 174 | return $response; 175 | } 176 | 177 | /** 178 | * Make /oauth2/* requests to the API. 179 | * 180 | * @param string $path 181 | * @param array $parameters 182 | * 183 | * @return array|object 184 | */ 185 | public function oauth2($path, array $parameters = []) 186 | { 187 | $method = 'POST'; 188 | $this->resetLastResponse(); 189 | $this->response->setApiPath($path); 190 | $url = sprintf('%s/%s', self::API_HOST, $path); 191 | $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters); 192 | $authorization = 'Authorization: Basic ' . $this->encodeAppAuthorization($this->consumer); 193 | $result = $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters); 194 | $response = JsonDecoder::decode($result, $this->decodeJsonAsArray); 195 | $this->response->setBody($response); 196 | return $response; 197 | } 198 | 199 | /** 200 | * Make GET requests to the API. 201 | * 202 | * @param string $path 203 | * @param array $parameters 204 | * 205 | * @return array|object 206 | */ 207 | public function get($path, array $parameters = []) 208 | { 209 | return $this->http('GET', self::API_HOST, $path, $parameters, false); 210 | } 211 | 212 | /** 213 | * Make POST requests to the API. 214 | * 215 | * @param string $path 216 | * @param array $parameters 217 | * @param bool $json 218 | * 219 | * @return array|object 220 | */ 221 | public function post($path, array $parameters = [], $json = false) 222 | { 223 | return $this->http('POST', self::API_HOST, $path, $parameters, $json); 224 | } 225 | 226 | /** 227 | * Make DELETE requests to the API. 228 | * 229 | * @param string $path 230 | * @param array $parameters 231 | * 232 | * @return array|object 233 | */ 234 | public function delete($path, array $parameters = []) 235 | { 236 | return $this->http('DELETE', self::API_HOST, $path, $parameters, false); 237 | } 238 | 239 | /** 240 | * Make PUT requests to the API. 241 | * 242 | * @param string $path 243 | * @param array $parameters 244 | * 245 | * @return array|object 246 | */ 247 | public function put($path, array $parameters = []) 248 | { 249 | return $this->http('PUT', self::API_HOST, $path, $parameters, false); 250 | } 251 | 252 | /** 253 | * Upload media to upload.twitter.com. 254 | * 255 | * @param string $path 256 | * @param array $parameters 257 | * @param boolean $chunked 258 | * 259 | * @return array|object 260 | */ 261 | public function upload($path, array $parameters = [], $chunked = false) 262 | { 263 | if ($chunked) { 264 | return $this->uploadMediaChunked($path, $parameters); 265 | } else { 266 | return $this->uploadMediaNotChunked($path, $parameters); 267 | } 268 | } 269 | 270 | /** 271 | * Progression of media upload 272 | * 273 | * @param string $media_id 274 | * 275 | * @return array|object 276 | */ 277 | public function mediaStatus($media_id) 278 | { 279 | return $this->http('GET', self::UPLOAD_HOST, 'media/upload', [ 280 | 'command' => 'STATUS', 281 | 'media_id' => $media_id 282 | ], false); 283 | } 284 | 285 | /** 286 | * Private method to upload media (not chunked) to upload.twitter.com. 287 | * 288 | * @param string $path 289 | * @param array $parameters 290 | * 291 | * @return array|object 292 | */ 293 | private function uploadMediaNotChunked($path, array $parameters) 294 | { 295 | if (! is_readable($parameters['media']) || 296 | ($file = file_get_contents($parameters['media'])) === false) { 297 | throw new \InvalidArgumentException('You must supply a readable file'); 298 | } 299 | $parameters['media'] = base64_encode($file); 300 | return $this->http('POST', self::UPLOAD_HOST, $path, $parameters, false); 301 | } 302 | 303 | /** 304 | * Private method to upload media (chunked) to upload.twitter.com. 305 | * 306 | * @param string $path 307 | * @param array $parameters 308 | * 309 | * @return array|object 310 | */ 311 | private function uploadMediaChunked($path, array $parameters) 312 | { 313 | $init = $this->http('POST', self::UPLOAD_HOST, $path, $this->mediaInitParameters($parameters), false); 314 | // Append 315 | $segmentIndex = 0; 316 | $media = fopen($parameters['media'], 'rb'); 317 | while (!feof($media)) { 318 | $this->http('POST', self::UPLOAD_HOST, 'media/upload', [ 319 | 'command' => 'APPEND', 320 | 'media_id' => $init->media_id_string, 321 | 'segment_index' => $segmentIndex++, 322 | 'media_data' => base64_encode(fread($media, $this->chunkSize)) 323 | ], false); 324 | } 325 | fclose($media); 326 | // Finalize 327 | $finalize = $this->http('POST', self::UPLOAD_HOST, 'media/upload', [ 328 | 'command' => 'FINALIZE', 329 | 'media_id' => $init->media_id_string 330 | ], false); 331 | return $finalize; 332 | } 333 | 334 | /** 335 | * Private method to get params for upload media chunked init. 336 | * Twitter docs: https://dev.twitter.com/rest/reference/post/media/upload-init.html 337 | * 338 | * @param array $parameters 339 | * 340 | * @return array 341 | */ 342 | private function mediaInitParameters(array $parameters) 343 | { 344 | $allowed_keys = ['media_type', 'additional_owners', 'media_category', 'shared']; 345 | $base = [ 346 | 'command' => 'INIT', 347 | 'total_bytes' => filesize($parameters['media']) 348 | ]; 349 | $allowed_parameters = array_intersect_key($parameters, array_flip($allowed_keys)); 350 | return array_merge($base, $allowed_parameters); 351 | } 352 | 353 | /** 354 | * Cleanup any parameters that are known not to work. 355 | * 356 | * @param array $parameters 357 | * 358 | * @return array 359 | */ 360 | private function cleanUpParameters(array $parameters) 361 | { 362 | foreach ($parameters as $key => $value) { 363 | // PHP coerces `true` to `"1"` which some Twitter APIs don't like. 364 | if (is_bool($value)) { 365 | $parameters[$key] = var_export($value, true); 366 | } 367 | } 368 | return $parameters; 369 | } 370 | 371 | /** 372 | * @param string $method 373 | * @param string $host 374 | * @param string $path 375 | * @param array $parameters 376 | * @param bool $json 377 | * 378 | * @return array|object 379 | */ 380 | private function http($method, $host, $path, array $parameters, $json) 381 | { 382 | $this->resetLastResponse(); 383 | $this->resetAttemptsNumber(); 384 | $url = sprintf('%s/%s/%s.json', $host, self::API_VERSION, $path); 385 | $this->response->setApiPath($path); 386 | if (!$json) { 387 | $parameters = $this->cleanUpParameters($parameters); 388 | } 389 | return $this->makeRequests($url, $method, $parameters, $json); 390 | } 391 | 392 | /** 393 | * 394 | * Make requests and retry them (if enabled) in case of Twitter's problems. 395 | * 396 | * @param string $method 397 | * @param string $url 398 | * @param string $method 399 | * @param array $parameters 400 | * @param bool $json 401 | * 402 | * @return array|object 403 | */ 404 | private function makeRequests($url, $method, array $parameters, $json) 405 | { 406 | do { 407 | $this->sleepIfNeeded(); 408 | $result = $this->oAuthRequest($url, $method, $parameters, $json); 409 | $response = JsonDecoder::decode($result, $this->decodeJsonAsArray); 410 | $this->response->setBody($response); 411 | $this->attempts++; 412 | // Retry up to our $maxRetries number if we get errors greater than 500 (over capacity etc) 413 | } while ($this->requestsAvailable()); 414 | 415 | return $response; 416 | } 417 | 418 | /** 419 | * Checks if we have to retry request if API is down. 420 | * 421 | * @return bool 422 | */ 423 | private function requestsAvailable() 424 | { 425 | return ($this->maxRetries && ($this->attempts <= $this->maxRetries) && $this->getLastHttpCode() >= 500); 426 | } 427 | 428 | /** 429 | * Format and sign an OAuth / API request 430 | * 431 | * @param string $url 432 | * @param string $method 433 | * @param array $parameters 434 | * @param bool $json 435 | * 436 | * @return string 437 | * @throws TwitterOAuthException 438 | */ 439 | private function oAuthRequest($url, $method, array $parameters, $json = false) 440 | { 441 | $request = Request::fromConsumerAndToken($this->consumer, $this->token, $method, $url, $parameters, $json); 442 | if (array_key_exists('oauth_callback', $parameters)) { 443 | // Twitter doesn't like oauth_callback as a parameter. 444 | unset($parameters['oauth_callback']); 445 | } 446 | if ($this->bearer === null) { 447 | $request->signRequest($this->signatureMethod, $this->consumer, $this->token); 448 | $authorization = $request->toHeader(); 449 | if (array_key_exists('oauth_verifier', $parameters)) { 450 | // Twitter doesn't always work with oauth in the body and in the header 451 | // and it's already included in the $authorization header 452 | unset($parameters['oauth_verifier']); 453 | } 454 | } else { 455 | $authorization = 'Authorization: Bearer ' . $this->bearer; 456 | } 457 | return $this->request($request->getNormalizedHttpUrl(), $method, $authorization, $parameters, $json); 458 | } 459 | 460 | /** 461 | * Set Curl options. 462 | * 463 | * @return array 464 | */ 465 | private function curlOptions() 466 | { 467 | $options = [ 468 | // CURLOPT_VERBOSE => true, 469 | CURLOPT_CONNECTTIMEOUT => $this->connectionTimeout, 470 | CURLOPT_HEADER => true, 471 | CURLOPT_RETURNTRANSFER => true, 472 | CURLOPT_SSL_VERIFYHOST => 2, 473 | CURLOPT_SSL_VERIFYPEER => true, 474 | CURLOPT_TIMEOUT => $this->timeout, 475 | CURLOPT_USERAGENT => $this->userAgent, 476 | ]; 477 | 478 | if ($this->useCAFile()) { 479 | $options[CURLOPT_CAINFO] = __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem'; 480 | } 481 | 482 | if ($this->gzipEncoding) { 483 | $options[CURLOPT_ENCODING] = 'gzip'; 484 | } 485 | 486 | if (!empty($this->proxy)) { 487 | $options[CURLOPT_PROXY] = $this->proxy['CURLOPT_PROXY']; 488 | $options[CURLOPT_PROXYUSERPWD] = $this->proxy['CURLOPT_PROXYUSERPWD']; 489 | $options[CURLOPT_PROXYPORT] = $this->proxy['CURLOPT_PROXYPORT']; 490 | $options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC; 491 | $options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP; 492 | } 493 | 494 | return $options; 495 | } 496 | 497 | /** 498 | * Make an HTTP request 499 | * 500 | * @param string $url 501 | * @param string $method 502 | * @param string $authorization 503 | * @param array $postfields 504 | * @param bool $json 505 | * 506 | * @return string 507 | * @throws TwitterOAuthException 508 | */ 509 | private function request($url, $method, $authorization, array $postfields, $json = false) 510 | { 511 | $options = $this->curlOptions(); 512 | $options[CURLOPT_URL] = $url; 513 | $options[CURLOPT_HTTPHEADER] = ['Accept: application/json', $authorization, 'Expect:']; 514 | 515 | switch ($method) { 516 | case 'GET': 517 | break; 518 | case 'POST': 519 | $options[CURLOPT_POST] = true; 520 | if ($json) { 521 | $options[CURLOPT_HTTPHEADER][] = 'Content-type: application/json'; 522 | $options[CURLOPT_POSTFIELDS] = json_encode($postfields); 523 | } else { 524 | $options[CURLOPT_POSTFIELDS] = Util::buildHttpQuery($postfields); 525 | } 526 | break; 527 | case 'DELETE': 528 | $options[CURLOPT_CUSTOMREQUEST] = 'DELETE'; 529 | break; 530 | case 'PUT': 531 | $options[CURLOPT_CUSTOMREQUEST] = 'PUT'; 532 | break; 533 | } 534 | 535 | if (in_array($method, ['GET', 'PUT', 'DELETE']) && !empty($postfields)) { 536 | $options[CURLOPT_URL] .= '?' . Util::buildHttpQuery($postfields); 537 | } 538 | 539 | 540 | $curlHandle = curl_init(); 541 | curl_setopt_array($curlHandle, $options); 542 | $response = curl_exec($curlHandle); 543 | 544 | // Throw exceptions on cURL errors. 545 | if (curl_errno($curlHandle) > 0) { 546 | $error = curl_error($curlHandle); 547 | $errorNo = curl_errno($curlHandle); 548 | curl_close($curlHandle); 549 | throw new TwitterOAuthException($error, $errorNo); 550 | } 551 | 552 | $this->response->setHttpCode(curl_getinfo($curlHandle, CURLINFO_HTTP_CODE)); 553 | $parts = explode("\r\n\r\n", $response); 554 | $responseBody = array_pop($parts); 555 | $responseHeader = array_pop($parts); 556 | $this->response->setHeaders($this->parseHeaders($responseHeader)); 557 | 558 | curl_close($curlHandle); 559 | 560 | return $responseBody; 561 | } 562 | 563 | /** 564 | * Get the header info to store. 565 | * 566 | * @param string $header 567 | * 568 | * @return array 569 | */ 570 | private function parseHeaders($header) 571 | { 572 | $headers = []; 573 | foreach (explode("\r\n", $header) as $line) { 574 | if (strpos($line, ':') !== false) { 575 | list ($key, $value) = explode(': ', $line); 576 | $key = str_replace('-', '_', strtolower($key)); 577 | $headers[$key] = trim($value); 578 | } 579 | } 580 | return $headers; 581 | } 582 | 583 | /** 584 | * Encode application authorization header with base64. 585 | * 586 | * @param Consumer $consumer 587 | * 588 | * @return string 589 | */ 590 | private function encodeAppAuthorization(Consumer $consumer) 591 | { 592 | $key = rawurlencode($consumer->key); 593 | $secret = rawurlencode($consumer->secret); 594 | return base64_encode($key . ':' . $secret); 595 | } 596 | 597 | /** 598 | * Is the code running from a Phar module. 599 | * 600 | * @return boolean 601 | */ 602 | private function pharRunning() 603 | { 604 | return class_exists('Phar') && \Phar::running(false) !== ''; 605 | } 606 | 607 | /** 608 | * Use included CA file instead of OS provided list. 609 | * 610 | * @return boolean 611 | */ 612 | private function useCAFile() 613 | { 614 | /* Use CACert file when not in a PHAR file. */ 615 | return !$this->pharRunning(); 616 | } 617 | } 618 | --------------------------------------------------------------------------------