├── .gitignore ├── README.md ├── vendor ├── dg │ └── twitter-php │ │ ├── .gitignore │ │ ├── .github │ │ └── funding.yml │ │ ├── src │ │ ├── twitter.class.php │ │ ├── Twitter.php │ │ └── OAuth.php │ │ ├── examples │ │ ├── send.php │ │ ├── custom-request.php │ │ ├── search.php │ │ └── load.php │ │ ├── composer.json │ │ ├── license.md │ │ └── readme.md ├── composer │ ├── autoload_psr4.php │ ├── autoload_namespaces.php │ ├── LICENSE │ ├── autoload_classmap.php │ ├── autoload_static.php │ ├── installed.json │ ├── autoload_real.php │ └── ClassLoader.php └── autoload.php ├── composer.json ├── lib └── zwitscher.php ├── boot.php ├── package.yml ├── LICENSE └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zwitscher 2 | 🐣 twittern mit REDAXO 3 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "dg/twitter-php": "^4.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/zwitscher.php: -------------------------------------------------------------------------------- 1 | getPath('vendor/'.'autoload.php'); 4 | -------------------------------------------------------------------------------- /vendor/composer/autoload_psr4.php: -------------------------------------------------------------------------------- 1 | send('I am fine'); // you can add $imagePath or array of image paths as second argument 12 | 13 | } catch (DG\Twitter\TwitterException $e) { 14 | echo 'Error: ' . $e->getMessage(); 15 | } 16 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dg/twitter-php", 3 | "description": "Small and easy Twitter library for PHP", 4 | "keywords": ["twitter", "oauth"], 5 | "homepage": "https://github.com/dg/twitter-php", 6 | "license": ["BSD-3-Clause"], 7 | "authors": [ 8 | { 9 | "name": "David Grudl", 10 | "homepage": "https://davidgrudl.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.1", 15 | "ext-curl": "*", 16 | "ext-json": "*" 17 | }, 18 | "autoload": { 19 | "classmap": ["src/"] 20 | }, 21 | "extra": { 22 | "branch-alias": { 23 | "dev-master": "4.0-dev" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/examples/custom-request.php: -------------------------------------------------------------------------------- 1 | request('statuses/retweets_of_me', 'GET'); 12 | 13 | ?> 14 | 15 | 16 | Twitter retweets of me 17 | 18 | 27 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/examples/search.php: -------------------------------------------------------------------------------- 1 | search('#nette'); 11 | // or use hashmap: $results = $twitter->search(['q' => '#nette', 'geocode' => '50.088224,15.975611,20km']); 12 | 13 | ?> 14 | 15 | 16 | Twitter search demo 17 | 18 | 27 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/examples/load.php: -------------------------------------------------------------------------------- 1 | load(Twitter::ME_AND_FRIENDS); 15 | 16 | ?> 17 | 18 | 19 | Twitter timeline demo 20 | 21 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Friends Of REDAXO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/composer/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) Nils Adermann, Jordi Boggiano 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /vendor/composer/autoload_classmap.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/dg/twitter-php/src/Twitter.php', 10 | 'DG\\Twitter\\OAuth\\Consumer' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 11 | 'DG\\Twitter\\OAuth\\Exception' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 12 | 'DG\\Twitter\\OAuth\\Request' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 13 | 'DG\\Twitter\\OAuth\\SignatureMethod' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 14 | 'DG\\Twitter\\OAuth\\SignatureMethod_HMAC_SHA1' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 15 | 'DG\\Twitter\\OAuth\\SignatureMethod_PLAINTEXT' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 16 | 'DG\\Twitter\\OAuth\\SignatureMethod_RSA_SHA1' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 17 | 'DG\\Twitter\\OAuth\\Token' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 18 | 'DG\\Twitter\\OAuth\\Util' => $vendorDir . '/dg/twitter-php/src/OAuth.php', 19 | 'DG\\Twitter\\Twitter' => $vendorDir . '/dg/twitter-php/src/Twitter.php', 20 | ); 21 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Copyright (c) 2008 David Grudl 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of David Grudl nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/..' . '/dg/twitter-php/src/Twitter.php', 11 | 'DG\\Twitter\\OAuth\\Consumer' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 12 | 'DG\\Twitter\\OAuth\\Exception' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 13 | 'DG\\Twitter\\OAuth\\Request' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 14 | 'DG\\Twitter\\OAuth\\SignatureMethod' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 15 | 'DG\\Twitter\\OAuth\\SignatureMethod_HMAC_SHA1' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 16 | 'DG\\Twitter\\OAuth\\SignatureMethod_PLAINTEXT' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 17 | 'DG\\Twitter\\OAuth\\SignatureMethod_RSA_SHA1' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 18 | 'DG\\Twitter\\OAuth\\Token' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 19 | 'DG\\Twitter\\OAuth\\Util' => __DIR__ . '/..' . '/dg/twitter-php/src/OAuth.php', 20 | 'DG\\Twitter\\Twitter' => __DIR__ . '/..' . '/dg/twitter-php/src/Twitter.php', 21 | ); 22 | 23 | public static function getInitializer(ClassLoader $loader) 24 | { 25 | return \Closure::bind(function () use ($loader) { 26 | $loader->classMap = ComposerStaticInit3dc24853ab255ce2977620ec7964f167::$classMap; 27 | 28 | }, null, ClassLoader::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "dg/twitter-php", 4 | "version": "v4.1.2", 5 | "version_normalized": "4.1.2.0", 6 | "source": { 7 | "type": "git", 8 | "url": "https://github.com/dg/twitter-php.git", 9 | "reference": "450baa2fd7b5e815c5f050b019c4486d1b1b01da" 10 | }, 11 | "dist": { 12 | "type": "zip", 13 | "url": "https://api.github.com/repos/dg/twitter-php/zipball/450baa2fd7b5e815c5f050b019c4486d1b1b01da", 14 | "reference": "450baa2fd7b5e815c5f050b019c4486d1b1b01da", 15 | "shasum": "" 16 | }, 17 | "require": { 18 | "ext-curl": "*", 19 | "ext-json": "*", 20 | "php": ">=7.1" 21 | }, 22 | "time": "2020-05-11T22:48:42+00:00", 23 | "type": "library", 24 | "extra": { 25 | "branch-alias": { 26 | "dev-master": "4.0-dev" 27 | } 28 | }, 29 | "installation-source": "dist", 30 | "autoload": { 31 | "classmap": [ 32 | "src/" 33 | ] 34 | }, 35 | "notification-url": "https://packagist.org/downloads/", 36 | "license": [ 37 | "BSD-3-Clause" 38 | ], 39 | "authors": [ 40 | { 41 | "name": "David Grudl", 42 | "homepage": "https://davidgrudl.com" 43 | } 44 | ], 45 | "description": "Small and easy Twitter library for PHP", 46 | "homepage": "https://github.com/dg/twitter-php", 47 | "keywords": [ 48 | "oauth", 49 | "twitter" 50 | ] 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); 27 | if ($useStaticLoader) { 28 | require_once __DIR__ . '/autoload_static.php'; 29 | 30 | call_user_func(\Composer\Autoload\ComposerStaticInit3dc24853ab255ce2977620ec7964f167::getInitializer($loader)); 31 | } else { 32 | $map = require __DIR__ . '/autoload_namespaces.php'; 33 | foreach ($map as $namespace => $path) { 34 | $loader->set($namespace, $path); 35 | } 36 | 37 | $map = require __DIR__ . '/autoload_psr4.php'; 38 | foreach ($map as $namespace => $path) { 39 | $loader->setPsr4($namespace, $path); 40 | } 41 | 42 | $classMap = require __DIR__ . '/autoload_classmap.php'; 43 | if ($classMap) { 44 | $loader->addClassMap($classMap); 45 | } 46 | } 47 | 48 | $loader->register(true); 49 | 50 | return $loader; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "5127c09c1a6129cb30a86e759da370f5", 8 | "packages": [ 9 | { 10 | "name": "dg/twitter-php", 11 | "version": "v4.1.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/dg/twitter-php.git", 15 | "reference": "450baa2fd7b5e815c5f050b019c4486d1b1b01da" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/dg/twitter-php/zipball/450baa2fd7b5e815c5f050b019c4486d1b1b01da", 20 | "reference": "450baa2fd7b5e815c5f050b019c4486d1b1b01da", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-curl": "*", 25 | "ext-json": "*", 26 | "php": ">=7.1" 27 | }, 28 | "type": "library", 29 | "extra": { 30 | "branch-alias": { 31 | "dev-master": "4.0-dev" 32 | } 33 | }, 34 | "autoload": { 35 | "classmap": [ 36 | "src/" 37 | ] 38 | }, 39 | "notification-url": "https://packagist.org/downloads/", 40 | "license": [ 41 | "BSD-3-Clause" 42 | ], 43 | "authors": [ 44 | { 45 | "name": "David Grudl", 46 | "homepage": "https://davidgrudl.com" 47 | } 48 | ], 49 | "description": "Small and easy Twitter library for PHP", 50 | "homepage": "https://github.com/dg/twitter-php", 51 | "keywords": [ 52 | "oauth", 53 | "twitter" 54 | ], 55 | "time": "2020-05-11T22:48:42+00:00" 56 | } 57 | ], 58 | "packages-dev": [], 59 | "aliases": [], 60 | "minimum-stability": "stable", 61 | "stability-flags": [], 62 | "prefer-stable": false, 63 | "prefer-lowest": false, 64 | "platform": [], 65 | "platform-dev": [] 66 | } 67 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/readme.md: -------------------------------------------------------------------------------- 1 | [Twitter for PHP](https://phpfashion.com/twitter-for-php) [![Buy me a coffee](https://files.nette.org/images/coffee1s.png)](https://nette.org/make-donation?to=twitter-php) 2 | ================================ 3 | 4 | [![Downloads this Month](https://img.shields.io/packagist/dm/dg/twitter-php.svg)](https://packagist.org/packages/dg/twitter-php) 5 | 6 | Twitter for PHP is a very small and easy-to-use library for sending 7 | messages to Twitter and receiving status updates. 8 | 9 | If you like this, **[please make a donation now](https://nette.org/make-donation?to=twitter-php)**. Thank you! 10 | 11 | It requires PHP 5.4 or newer with CURL extension and is licensed under the New BSD License. 12 | You can obtain the latest version from our [GitHub repository](https://github.com/dg/twitter-php) 13 | or install it via Composer: 14 | 15 | composer require dg/twitter-php 16 | 17 | 18 | Usage 19 | ----- 20 | Sign in to the https://twitter.com and register an application from the https://apps.twitter.com page. Remember 21 | to never reveal your consumer secrets. Click on My Access Token link from the sidebar and retrieve your own access 22 | token. Now you have consumer key, consumer secret, access token and access token secret. 23 | 24 | Create object using application and request/access keys 25 | 26 | ```php 27 | use DG\Twitter\Twitter; 28 | 29 | $twitter = new Twitter($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret); 30 | ``` 31 | 32 | The send() method updates your status. The message must be encoded in UTF-8: 33 | 34 | ```php 35 | $twitter->send('I am fine today.'); 36 | ``` 37 | 38 | The load() method returns the 20 most recent status updates 39 | posted by you: 40 | 41 | ```php 42 | $statuses = $twitter->load(Twitter::ME); 43 | ``` 44 | 45 | or posted by you and your friends: 46 | 47 | ```php 48 | $statuses = $twitter->load(Twitter::ME_AND_FRIENDS); 49 | ``` 50 | or most recent mentions for you: 51 | 52 | ```php 53 | $statuses = $twitter->load(Twitter::REPLIES); 54 | ``` 55 | Extracting the information from the channel is easy: 56 | 57 | ```php 58 | foreach ($statuses as $status) { 59 | echo "message: ", Twitter::clickable($status); 60 | echo "posted at " , $status->created_at; 61 | echo "posted by " , $status->user->name; 62 | } 63 | ``` 64 | 65 | The static method `Twitter::clickable()` makes links, mentions and hash tags in status clickable. 66 | 67 | The authenticate() method tests if user credentials are valid: 68 | 69 | ```php 70 | if (!$twitter->authenticate()) { 71 | die('Invalid name or password'); 72 | } 73 | ``` 74 | 75 | The search() method provides searching in twitter statuses: 76 | 77 | ```php 78 | $results = $twitter->search('#nette'); 79 | ``` 80 | 81 | The returned result is a again array of statuses. 82 | 83 | 84 | Error handling 85 | -------------- 86 | 87 | All methods throw a `DG\Twitter\Exception` on error: 88 | 89 | ```php 90 | try { 91 | $statuses = $twitter->load(Twitter::ME); 92 | } catch (DG\Twitter\Exception $e) { 93 | echo "Error: ", $e->getMessage(); 94 | } 95 | ``` 96 | 97 | Additional features 98 | ------------------- 99 | 100 | The `authenticate()` method tests if user credentials are valid: 101 | 102 | ```php 103 | if (!$twitter->authenticate()) { 104 | die('Invalid name or password'); 105 | } 106 | ``` 107 | 108 | Other commands 109 | -------------- 110 | 111 | You can use all commands defined by [Twitter API 1.1](https://dev.twitter.com/rest/public). 112 | For example [GET statuses/retweets_of_me](https://dev.twitter.com/rest/reference/get/statuses/retweets_of_me) 113 | returns the array of most recent tweets authored by the authenticating user: 114 | 115 | ```php 116 | $statuses = $twitter->request('statuses/retweets_of_me', 'GET', ['count' => 20]); 117 | ``` 118 | 119 | Changelog 120 | --------- 121 | v4.1 (11/2019) 122 | - added Delete Method (#68) 123 | - token is optional throughout + supply get() method 124 | 125 | v4.0 (2/2019) 126 | - requires PHP 7.1 and uses its advantages like typehints, strict types etc. 127 | - class Twitter is now DG\Twitter\Twitter 128 | - class TwitterException is now DG\Twitter\Exception 129 | 130 | v3.8 (2/2019) 131 | - Twitter::sendDirectMessage() uses new API 132 | - Twitter::clickable: added support for $status->full_text (#60) 133 | 134 | v3.7 (3/2018) 135 | - minimal required PHP version changed to 5.4 136 | - Twitter::send() added $options 137 | - Twitter::clickable() now works only with statuses and entites 138 | - fixed coding style 139 | 140 | v3.6 (8/2016) 141 | - added loadUserFollowersList() and sendDirectMessage() 142 | - Twitter::send() allows to upload multiple images 143 | - changed http:// to https:// 144 | 145 | v3.5 (12/2014) 146 | - allows to send message starting with @ and upload file at the same time in PHP >= 5.5 147 | 148 | v3.4 (11/2014) 149 | - cache expiration can be specified as string 150 | - fixed some bugs 151 | 152 | v3.3 (3/2014) 153 | - Twitter::send($status, $image) can upload image 154 | - added Twitter::follow() 155 | 156 | v3.2 (1/2014) 157 | - Twitter API uses SSL OAuth 158 | - Twitter::clickable() supports media 159 | - added Twitter::loadUserInfoById() and loadUserFollowers() 160 | - fixed Twitter::destroy() 161 | 162 | v3.1 (3/2013) 163 | - Twitter::load() - added third argument $data 164 | - Twitter::clickable() uses entities; pass as parameter status object, not just text 165 | - added Twitter::$httpOptions for custom cURL configuration 166 | 167 | v3.0 (12/2012) 168 | - updated to Twitter API 1.1. Some stuff deprecated by Twitter was removed: 169 | - removed RSS, ATOM and XML support 170 | - removed Twitter::ALL 171 | - Twitter::load() - removed third argument $page 172 | - Twitter::search() requires authentication and returns different structure 173 | - removed shortening URL using http://is.gd 174 | - changed order of Twitter::request() arguments to $resource, $method, $data 175 | 176 | v2.0 (8/2012) 177 | - added support for OAuth authentication protocol 178 | - added Twitter::clickable() which makes links, @usernames and #hashtags clickable 179 | - installable via `composer require dg/twitter-php` 180 | 181 | v1.0 (7/2008) 182 | - initial release 183 | 184 | 185 | ----- 186 | (c) David Grudl, 2008, 2016 (https://davidgrudl.com) 187 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/src/Twitter.php: -------------------------------------------------------------------------------- 1 | 20, 42 | CURLOPT_SSL_VERIFYPEER => 0, 43 | CURLOPT_USERAGENT => 'Twitter for PHP', 44 | ]; 45 | 46 | /** @var OAuth\Consumer */ 47 | private $consumer; 48 | 49 | /** @var OAuth\Token */ 50 | private $token; 51 | 52 | 53 | /** 54 | * Creates object using consumer and access keys. 55 | * @throws Exception when CURL extension is not loaded 56 | */ 57 | public function __construct(string $consumerKey, string $consumerSecret, string $accessToken = null, string $accessTokenSecret = null) 58 | { 59 | if (!extension_loaded('curl')) { 60 | throw new Exception('PHP extension CURL is not loaded.'); 61 | } 62 | 63 | $this->consumer = new OAuth\Consumer($consumerKey, $consumerSecret); 64 | if ($accessToken && $accessTokenSecret) { 65 | $this->token = new OAuth\Token($accessToken, $accessTokenSecret); 66 | } 67 | } 68 | 69 | 70 | /** 71 | * Tests if user credentials are valid. 72 | * @throws Exception 73 | */ 74 | public function authenticate(): bool 75 | { 76 | try { 77 | $res = $this->request('account/verify_credentials', 'GET'); 78 | return !empty($res->id); 79 | 80 | } catch (Exception $e) { 81 | if ($e->getCode() === 401) { 82 | return false; 83 | } 84 | throw $e; 85 | } 86 | } 87 | 88 | 89 | /** 90 | * Sends message to the Twitter. 91 | * https://dev.twitter.com/rest/reference/post/statuses/update 92 | * @param string|array $mediaPath path to local media file to be uploaded 93 | * @throws Exception 94 | */ 95 | public function send(string $message, $mediaPath = null, array $options = []): stdClass 96 | { 97 | $mediaIds = []; 98 | foreach ((array) $mediaPath as $item) { 99 | $res = $this->request( 100 | 'https://upload.twitter.com/1.1/media/upload.json', 101 | 'POST', 102 | [], 103 | ['media' => $item] 104 | ); 105 | $mediaIds[] = $res->media_id_string; 106 | } 107 | return $this->request( 108 | 'statuses/update', 109 | 'POST', 110 | $options + ['status' => $message, 'media_ids' => implode(',', $mediaIds) ?: null] 111 | ); 112 | } 113 | 114 | 115 | /** 116 | * Sends a direct message to the specified user. 117 | * https://dev.twitter.com/rest/reference/post/direct_messages/new 118 | * @throws Exception 119 | */ 120 | public function sendDirectMessage(string $username, string $message): stdClass 121 | { 122 | return $this->request( 123 | 'direct_messages/events/new', 124 | 'JSONPOST', 125 | ['event' => [ 126 | 'type' => 'message_create', 127 | 'message_create' => [ 128 | 'target' => ['recipient_id' => $this->loadUserInfo($username)->id_str], 129 | 'message_data' => ['text' => $message], 130 | ], 131 | ]] 132 | ); 133 | } 134 | 135 | 136 | /** 137 | * Follows a user on Twitter. 138 | * https://dev.twitter.com/rest/reference/post/friendships/create 139 | * @throws Exception 140 | */ 141 | public function follow(string $username): stdClass 142 | { 143 | return $this->request('friendships/create', 'POST', ['screen_name' => $username]); 144 | } 145 | 146 | 147 | /** 148 | * Returns the most recent statuses. 149 | * https://dev.twitter.com/rest/reference/get/statuses/user_timeline 150 | * @param int $flags timeline (ME | ME_AND_FRIENDS | REPLIES) and optional (RETWEETS) 151 | * @return stdClass[] 152 | * @throws Exception 153 | */ 154 | public function load(int $flags = self::ME, int $count = 20, array $data = null): array 155 | { 156 | static $timelines = [ 157 | self::ME => 'user_timeline', 158 | self::ME_AND_FRIENDS => 'home_timeline', 159 | self::REPLIES => 'mentions_timeline', 160 | ]; 161 | if (!isset($timelines[$flags & 3])) { 162 | throw new \InvalidArgumentException; 163 | } 164 | 165 | return $this->cachedRequest('statuses/' . $timelines[$flags & 3], (array) $data + [ 166 | 'count' => $count, 167 | 'include_rts' => $flags & self::RETWEETS ? 1 : 0, 168 | ]); 169 | } 170 | 171 | 172 | /** 173 | * Returns information of a given user. 174 | * https://dev.twitter.com/rest/reference/get/users/show 175 | * @throws Exception 176 | */ 177 | public function loadUserInfo(string $username): stdClass 178 | { 179 | return $this->cachedRequest('users/show', ['screen_name' => $username]); 180 | } 181 | 182 | 183 | /** 184 | * Returns information of a given user by id. 185 | * https://dev.twitter.com/rest/reference/get/users/show 186 | * @throws Exception 187 | */ 188 | public function loadUserInfoById(string $id): stdClass 189 | { 190 | return $this->cachedRequest('users/show', ['user_id' => $id]); 191 | } 192 | 193 | 194 | /** 195 | * Returns IDs of followers of a given user. 196 | * https://dev.twitter.com/rest/reference/get/followers/ids 197 | * @throws Exception 198 | */ 199 | public function loadUserFollowers(string $username, int $count = 5000, int $cursor = -1, $cacheExpiry = null): stdClass 200 | { 201 | return $this->cachedRequest('followers/ids', [ 202 | 'screen_name' => $username, 203 | 'count' => $count, 204 | 'cursor' => $cursor, 205 | ], $cacheExpiry); 206 | } 207 | 208 | 209 | /** 210 | * Returns list of followers of a given user. 211 | * https://dev.twitter.com/rest/reference/get/followers/list 212 | * @throws Exception 213 | */ 214 | public function loadUserFollowersList(string $username, int $count = 200, int $cursor = -1, $cacheExpiry = null): stdClass 215 | { 216 | return $this->cachedRequest('followers/list', [ 217 | 'screen_name' => $username, 218 | 'count' => $count, 219 | 'cursor' => $cursor, 220 | ], $cacheExpiry); 221 | } 222 | 223 | 224 | /** 225 | * Destroys status. 226 | * @param int|string $id status to be destroyed 227 | * @throws Exception 228 | */ 229 | public function destroy($id) 230 | { 231 | $res = $this->request("statuses/destroy/$id", 'POST'); 232 | return $res->id ?: false; 233 | } 234 | 235 | 236 | /** 237 | * Retrieves a single status. 238 | * @param int|string $id status to be retrieved 239 | * @throws Exception 240 | */ 241 | public function get($id) 242 | { 243 | $res = $this->request("statuses/show/$id", 'GET'); 244 | return $res; 245 | } 246 | 247 | 248 | /** 249 | * Returns tweets that match a specified query. 250 | * https://dev.twitter.com/rest/reference/get/search/tweets 251 | * @param string|array 252 | * @throws Exception 253 | * @return stdClass|stdClass[] 254 | */ 255 | public function search($query, bool $full = false) 256 | { 257 | $res = $this->request('search/tweets', 'GET', is_array($query) ? $query : ['q' => $query]); 258 | return $full ? $res : $res->statuses; 259 | } 260 | 261 | 262 | /** 263 | * Retrieves the top 50 trending topics for a specific WOEID. 264 | * @param int|string $WOEID Where On Earth IDentifier 265 | */ 266 | public function getTrends(int $WOEID): array 267 | { 268 | return $this->request("trends/place.json?id=$WOEID", 'GET'); 269 | } 270 | 271 | 272 | /** 273 | * Process HTTP request. 274 | * @param string $method GET|POST|JSONPOST|DELETE 275 | * @return mixed 276 | * @throws Exception 277 | */ 278 | public function request(string $resource, string $method, array $data = [], array $files = []) 279 | { 280 | if (!strpos($resource, '://')) { 281 | if (!strpos($resource, '.')) { 282 | $resource .= '.json'; 283 | } 284 | $resource = self::API_URL . $resource; 285 | } 286 | 287 | foreach ($data as $key => $val) { 288 | if ($val === null) { 289 | unset($data[$key]); 290 | } 291 | } 292 | 293 | foreach ($files as $key => $file) { 294 | if (!is_file($file)) { 295 | throw new Exception("Cannot read the file $file. Check if file exists on disk and check its permissions."); 296 | } 297 | $data[$key] = new \CURLFile($file); 298 | } 299 | 300 | $headers = ['Expect:']; 301 | 302 | if ($method === 'JSONPOST') { 303 | $method = 'POST'; 304 | $data = json_encode($data); 305 | $headers[] = 'Content-Type: application/json'; 306 | 307 | } elseif (($method === 'GET' || $method === 'DELETE') && $data) { 308 | $resource .= '?' . http_build_query($data, '', '&'); 309 | } 310 | 311 | $request = OAuth\Request::from_consumer_and_token($this->consumer, $this->token, $method, $resource); 312 | $request->sign_request(new OAuth\SignatureMethod_HMAC_SHA1, $this->consumer, $this->token); 313 | $headers[] = $request->to_header(); 314 | 315 | $options = [ 316 | CURLOPT_URL => $resource, 317 | CURLOPT_HEADER => false, 318 | CURLOPT_RETURNTRANSFER => true, 319 | CURLOPT_HTTPHEADER => $headers, 320 | ] + $this->httpOptions; 321 | 322 | if ($method === 'POST') { 323 | $options += [ 324 | CURLOPT_POST => true, 325 | CURLOPT_POSTFIELDS => $data, 326 | CURLOPT_SAFE_UPLOAD => true, 327 | ]; 328 | } elseif ($method === 'DELETE') { 329 | $options += [ 330 | CURLOPT_CUSTOMREQUEST => 'DELETE', 331 | ]; 332 | } 333 | 334 | $curl = curl_init(); 335 | curl_setopt_array($curl, $options); 336 | $result = curl_exec($curl); 337 | if (curl_errno($curl)) { 338 | throw new Exception('Server error: ' . curl_error($curl)); 339 | } 340 | 341 | if (strpos(curl_getinfo($curl, CURLINFO_CONTENT_TYPE), 'application/json') !== false) { 342 | $payload = @json_decode($result, false, 128, JSON_BIGINT_AS_STRING); // intentionally @ 343 | if ($payload === false) { 344 | throw new Exception('Invalid server response'); 345 | } 346 | } 347 | 348 | $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); 349 | if ($code >= 400) { 350 | throw new Exception(isset($payload->errors[0]->message) 351 | ? $payload->errors[0]->message 352 | : "Server error #$code with answer $result", 353 | $code 354 | ); 355 | } elseif ($code === 204) { 356 | $payload = true; 357 | } 358 | 359 | return $payload; 360 | } 361 | 362 | 363 | /** 364 | * Cached HTTP request. 365 | * @return stdClass|stdClass[] 366 | */ 367 | public function cachedRequest(string $resource, array $data = [], $cacheExpire = null) 368 | { 369 | if (!self::$cacheDir) { 370 | return $this->request($resource, 'GET', $data); 371 | } 372 | if ($cacheExpire === null) { 373 | $cacheExpire = self::$cacheExpire; 374 | } 375 | 376 | $cacheFile = self::$cacheDir 377 | . '/twitter.' 378 | . md5($resource . json_encode($data) . serialize([$this->consumer, $this->token])) 379 | . '.json'; 380 | 381 | $cache = @json_decode((string) @file_get_contents($cacheFile)); // intentionally @ 382 | $expiration = is_string($cacheExpire) ? strtotime($cacheExpire) - time() : $cacheExpire; 383 | if ($cache && @filemtime($cacheFile) + $expiration > time()) { // intentionally @ 384 | return $cache; 385 | } 386 | 387 | try { 388 | $payload = $this->request($resource, 'GET', $data); 389 | file_put_contents($cacheFile, json_encode($payload)); 390 | return $payload; 391 | 392 | } catch (Exception $e) { 393 | if ($cache) { 394 | return $cache; 395 | } 396 | throw $e; 397 | } 398 | } 399 | 400 | 401 | /** 402 | * Makes twitter links, @usernames and #hashtags clickable. 403 | */ 404 | public static function clickable(stdClass $status): string 405 | { 406 | $all = []; 407 | foreach ($status->entities->hashtags as $item) { 408 | $all[$item->indices[0]] = ["https://twitter.com/search?q=%23$item->text", "#$item->text", $item->indices[1]]; 409 | } 410 | foreach ($status->entities->urls as $item) { 411 | if (!isset($item->expanded_url)) { 412 | $all[$item->indices[0]] = [$item->url, $item->url, $item->indices[1]]; 413 | } else { 414 | $all[$item->indices[0]] = [$item->expanded_url, $item->display_url, $item->indices[1]]; 415 | } 416 | } 417 | foreach ($status->entities->user_mentions as $item) { 418 | $all[$item->indices[0]] = ["https://twitter.com/$item->screen_name", "@$item->screen_name", $item->indices[1]]; 419 | } 420 | if (isset($status->entities->media)) { 421 | foreach ($status->entities->media as $item) { 422 | $all[$item->indices[0]] = [$item->url, $item->display_url, $item->indices[1]]; 423 | } 424 | } 425 | 426 | krsort($all); 427 | $s = isset($status->full_text) ? $status->full_text : $status->text; 428 | foreach ($all as $pos => $item) { 429 | $s = iconv_substr($s, 0, $pos, 'UTF-8') 430 | . '' . htmlspecialchars($item[1]) . '' 431 | . iconv_substr($s, $item[2], iconv_strlen($s, 'UTF-8'), 'UTF-8'); 432 | } 433 | return $s; 434 | } 435 | } 436 | 437 | 438 | 439 | /** 440 | * An exception generated by Twitter. 441 | */ 442 | class Exception extends \Exception 443 | { 444 | } 445 | -------------------------------------------------------------------------------- /vendor/composer/ClassLoader.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see http://www.php-fig.org/psr/psr-0/ 41 | * @see http://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | // PSR-4 46 | private $prefixLengthsPsr4 = array(); 47 | private $prefixDirsPsr4 = array(); 48 | private $fallbackDirsPsr4 = array(); 49 | 50 | // PSR-0 51 | private $prefixesPsr0 = array(); 52 | private $fallbackDirsPsr0 = array(); 53 | 54 | private $useIncludePath = false; 55 | private $classMap = array(); 56 | private $classMapAuthoritative = false; 57 | private $missingClasses = array(); 58 | private $apcuPrefix; 59 | 60 | public function getPrefixes() 61 | { 62 | if (!empty($this->prefixesPsr0)) { 63 | return call_user_func_array('array_merge', $this->prefixesPsr0); 64 | } 65 | 66 | return array(); 67 | } 68 | 69 | public function getPrefixesPsr4() 70 | { 71 | return $this->prefixDirsPsr4; 72 | } 73 | 74 | public function getFallbackDirs() 75 | { 76 | return $this->fallbackDirsPsr0; 77 | } 78 | 79 | public function getFallbackDirsPsr4() 80 | { 81 | return $this->fallbackDirsPsr4; 82 | } 83 | 84 | public function getClassMap() 85 | { 86 | return $this->classMap; 87 | } 88 | 89 | /** 90 | * @param array $classMap Class to filename map 91 | */ 92 | public function addClassMap(array $classMap) 93 | { 94 | if ($this->classMap) { 95 | $this->classMap = array_merge($this->classMap, $classMap); 96 | } else { 97 | $this->classMap = $classMap; 98 | } 99 | } 100 | 101 | /** 102 | * Registers a set of PSR-0 directories for a given prefix, either 103 | * appending or prepending to the ones previously set for this prefix. 104 | * 105 | * @param string $prefix The prefix 106 | * @param array|string $paths The PSR-0 root directories 107 | * @param bool $prepend Whether to prepend the directories 108 | */ 109 | public function add($prefix, $paths, $prepend = false) 110 | { 111 | if (!$prefix) { 112 | if ($prepend) { 113 | $this->fallbackDirsPsr0 = array_merge( 114 | (array) $paths, 115 | $this->fallbackDirsPsr0 116 | ); 117 | } else { 118 | $this->fallbackDirsPsr0 = array_merge( 119 | $this->fallbackDirsPsr0, 120 | (array) $paths 121 | ); 122 | } 123 | 124 | return; 125 | } 126 | 127 | $first = $prefix[0]; 128 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 129 | $this->prefixesPsr0[$first][$prefix] = (array) $paths; 130 | 131 | return; 132 | } 133 | if ($prepend) { 134 | $this->prefixesPsr0[$first][$prefix] = array_merge( 135 | (array) $paths, 136 | $this->prefixesPsr0[$first][$prefix] 137 | ); 138 | } else { 139 | $this->prefixesPsr0[$first][$prefix] = array_merge( 140 | $this->prefixesPsr0[$first][$prefix], 141 | (array) $paths 142 | ); 143 | } 144 | } 145 | 146 | /** 147 | * Registers a set of PSR-4 directories for a given namespace, either 148 | * appending or prepending to the ones previously set for this namespace. 149 | * 150 | * @param string $prefix The prefix/namespace, with trailing '\\' 151 | * @param array|string $paths The PSR-4 base directories 152 | * @param bool $prepend Whether to prepend the directories 153 | * 154 | * @throws \InvalidArgumentException 155 | */ 156 | public function addPsr4($prefix, $paths, $prepend = false) 157 | { 158 | if (!$prefix) { 159 | // Register directories for the root namespace. 160 | if ($prepend) { 161 | $this->fallbackDirsPsr4 = array_merge( 162 | (array) $paths, 163 | $this->fallbackDirsPsr4 164 | ); 165 | } else { 166 | $this->fallbackDirsPsr4 = array_merge( 167 | $this->fallbackDirsPsr4, 168 | (array) $paths 169 | ); 170 | } 171 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 172 | // Register directories for a new namespace. 173 | $length = strlen($prefix); 174 | if ('\\' !== $prefix[$length - 1]) { 175 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 176 | } 177 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 178 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 179 | } elseif ($prepend) { 180 | // Prepend directories for an already registered namespace. 181 | $this->prefixDirsPsr4[$prefix] = array_merge( 182 | (array) $paths, 183 | $this->prefixDirsPsr4[$prefix] 184 | ); 185 | } else { 186 | // Append directories for an already registered namespace. 187 | $this->prefixDirsPsr4[$prefix] = array_merge( 188 | $this->prefixDirsPsr4[$prefix], 189 | (array) $paths 190 | ); 191 | } 192 | } 193 | 194 | /** 195 | * Registers a set of PSR-0 directories for a given prefix, 196 | * replacing any others previously set for this prefix. 197 | * 198 | * @param string $prefix The prefix 199 | * @param array|string $paths The PSR-0 base directories 200 | */ 201 | public function set($prefix, $paths) 202 | { 203 | if (!$prefix) { 204 | $this->fallbackDirsPsr0 = (array) $paths; 205 | } else { 206 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 207 | } 208 | } 209 | 210 | /** 211 | * Registers a set of PSR-4 directories for a given namespace, 212 | * replacing any others previously set for this namespace. 213 | * 214 | * @param string $prefix The prefix/namespace, with trailing '\\' 215 | * @param array|string $paths The PSR-4 base directories 216 | * 217 | * @throws \InvalidArgumentException 218 | */ 219 | public function setPsr4($prefix, $paths) 220 | { 221 | if (!$prefix) { 222 | $this->fallbackDirsPsr4 = (array) $paths; 223 | } else { 224 | $length = strlen($prefix); 225 | if ('\\' !== $prefix[$length - 1]) { 226 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 227 | } 228 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 229 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 230 | } 231 | } 232 | 233 | /** 234 | * Turns on searching the include path for class files. 235 | * 236 | * @param bool $useIncludePath 237 | */ 238 | public function setUseIncludePath($useIncludePath) 239 | { 240 | $this->useIncludePath = $useIncludePath; 241 | } 242 | 243 | /** 244 | * Can be used to check if the autoloader uses the include path to check 245 | * for classes. 246 | * 247 | * @return bool 248 | */ 249 | public function getUseIncludePath() 250 | { 251 | return $this->useIncludePath; 252 | } 253 | 254 | /** 255 | * Turns off searching the prefix and fallback directories for classes 256 | * that have not been registered with the class map. 257 | * 258 | * @param bool $classMapAuthoritative 259 | */ 260 | public function setClassMapAuthoritative($classMapAuthoritative) 261 | { 262 | $this->classMapAuthoritative = $classMapAuthoritative; 263 | } 264 | 265 | /** 266 | * Should class lookup fail if not found in the current class map? 267 | * 268 | * @return bool 269 | */ 270 | public function isClassMapAuthoritative() 271 | { 272 | return $this->classMapAuthoritative; 273 | } 274 | 275 | /** 276 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 277 | * 278 | * @param string|null $apcuPrefix 279 | */ 280 | public function setApcuPrefix($apcuPrefix) 281 | { 282 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 283 | } 284 | 285 | /** 286 | * The APCu prefix in use, or null if APCu caching is not enabled. 287 | * 288 | * @return string|null 289 | */ 290 | public function getApcuPrefix() 291 | { 292 | return $this->apcuPrefix; 293 | } 294 | 295 | /** 296 | * Registers this instance as an autoloader. 297 | * 298 | * @param bool $prepend Whether to prepend the autoloader or not 299 | */ 300 | public function register($prepend = false) 301 | { 302 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 303 | } 304 | 305 | /** 306 | * Unregisters this instance as an autoloader. 307 | */ 308 | public function unregister() 309 | { 310 | spl_autoload_unregister(array($this, 'loadClass')); 311 | } 312 | 313 | /** 314 | * Loads the given class or interface. 315 | * 316 | * @param string $class The name of the class 317 | * @return bool|null True if loaded, null otherwise 318 | */ 319 | public function loadClass($class) 320 | { 321 | if ($file = $this->findFile($class)) { 322 | includeFile($file); 323 | 324 | return true; 325 | } 326 | } 327 | 328 | /** 329 | * Finds the path to the file where the class is defined. 330 | * 331 | * @param string $class The name of the class 332 | * 333 | * @return string|false The path if found, false otherwise 334 | */ 335 | public function findFile($class) 336 | { 337 | // class map lookup 338 | if (isset($this->classMap[$class])) { 339 | return $this->classMap[$class]; 340 | } 341 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 342 | return false; 343 | } 344 | if (null !== $this->apcuPrefix) { 345 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 346 | if ($hit) { 347 | return $file; 348 | } 349 | } 350 | 351 | $file = $this->findFileWithExtension($class, '.php'); 352 | 353 | // Search for Hack files if we are running on HHVM 354 | if (false === $file && defined('HHVM_VERSION')) { 355 | $file = $this->findFileWithExtension($class, '.hh'); 356 | } 357 | 358 | if (null !== $this->apcuPrefix) { 359 | apcu_add($this->apcuPrefix.$class, $file); 360 | } 361 | 362 | if (false === $file) { 363 | // Remember that this class does not exist. 364 | $this->missingClasses[$class] = true; 365 | } 366 | 367 | return $file; 368 | } 369 | 370 | private function findFileWithExtension($class, $ext) 371 | { 372 | // PSR-4 lookup 373 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 374 | 375 | $first = $class[0]; 376 | if (isset($this->prefixLengthsPsr4[$first])) { 377 | $subPath = $class; 378 | while (false !== $lastPos = strrpos($subPath, '\\')) { 379 | $subPath = substr($subPath, 0, $lastPos); 380 | $search = $subPath . '\\'; 381 | if (isset($this->prefixDirsPsr4[$search])) { 382 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 383 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 384 | if (file_exists($file = $dir . $pathEnd)) { 385 | return $file; 386 | } 387 | } 388 | } 389 | } 390 | } 391 | 392 | // PSR-4 fallback dirs 393 | foreach ($this->fallbackDirsPsr4 as $dir) { 394 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 395 | return $file; 396 | } 397 | } 398 | 399 | // PSR-0 lookup 400 | if (false !== $pos = strrpos($class, '\\')) { 401 | // namespaced class name 402 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 403 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 404 | } else { 405 | // PEAR-like class name 406 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 407 | } 408 | 409 | if (isset($this->prefixesPsr0[$first])) { 410 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 411 | if (0 === strpos($class, $prefix)) { 412 | foreach ($dirs as $dir) { 413 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 414 | return $file; 415 | } 416 | } 417 | } 418 | } 419 | } 420 | 421 | // PSR-0 fallback dirs 422 | foreach ($this->fallbackDirsPsr0 as $dir) { 423 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 424 | return $file; 425 | } 426 | } 427 | 428 | // PSR-0 include paths. 429 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 430 | return $file; 431 | } 432 | 433 | return false; 434 | } 435 | } 436 | 437 | /** 438 | * Scope isolated include. 439 | * 440 | * Prevents access to $this/self from included files. 441 | */ 442 | function includeFile($file) 443 | { 444 | include $file; 445 | } 446 | -------------------------------------------------------------------------------- /vendor/dg/twitter-php/src/OAuth.php: -------------------------------------------------------------------------------- 1 | key = $key; 49 | $this->secret = $secret; 50 | } 51 | 52 | 53 | public function __toString(): string 54 | { 55 | return "OAuthConsumer[key=$this->key,secret=$this->secret]"; 56 | } 57 | } 58 | 59 | 60 | class Token 61 | { 62 | // access tokens and request tokens 63 | public $key; 64 | public $secret; 65 | 66 | 67 | /** 68 | * key = the token 69 | * secret = the token secret 70 | */ 71 | public function __construct(string $key, string $secret) 72 | { 73 | $this->key = $key; 74 | $this->secret = $secret; 75 | } 76 | 77 | 78 | /** 79 | * generates the basic string serialization of a token that a server 80 | * would respond to request_token and access_token calls with 81 | */ 82 | public function to_string(): string 83 | { 84 | return 'oauth_token=' . 85 | Util::urlencode_rfc3986($this->key) . 86 | '&oauth_token_secret=' . 87 | Util::urlencode_rfc3986($this->secret); 88 | } 89 | 90 | 91 | public function __toString(): string 92 | { 93 | return $this->to_string(); 94 | } 95 | } 96 | 97 | 98 | /** 99 | * A class for implementing a Signature Method 100 | * See section 9 ("Signing Requests") in the spec 101 | */ 102 | abstract class SignatureMethod 103 | { 104 | /** 105 | * Needs to return the name of the Signature Method (ie HMAC-SHA1) 106 | */ 107 | abstract public function get_name(): string; 108 | 109 | 110 | /** 111 | * Build up the signature 112 | * NOTE: The output of this function MUST NOT be urlencoded. 113 | * the encoding is handled in OAuthRequest when the final 114 | * request is serialized 115 | */ 116 | abstract public function build_signature(Request $request, Consumer $consumer, ?Token $token): string; 117 | 118 | 119 | /** 120 | * Verifies that a given signature is correct 121 | */ 122 | public function check_signature(Request $request, Consumer $consumer, Token $token, string $signature): bool 123 | { 124 | $built = $this->build_signature($request, $consumer, $token); 125 | return $built == $signature; 126 | } 127 | } 128 | 129 | 130 | /** 131 | * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104] 132 | * where the Signature Base String is the text and the key is the concatenated values (each first 133 | * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&' 134 | * character (ASCII code 38) even if empty. 135 | * - Chapter 9.2 ("HMAC-SHA1") 136 | */ 137 | class SignatureMethod_HMAC_SHA1 extends SignatureMethod 138 | { 139 | public function get_name(): string 140 | { 141 | return 'HMAC-SHA1'; 142 | } 143 | 144 | 145 | public function build_signature(Request $request, Consumer $consumer, ?Token $token): string 146 | { 147 | $base_string = $request->get_signature_base_string(); 148 | $request->base_string = $base_string; 149 | 150 | $key_parts = [ 151 | $consumer->secret, 152 | $token ? $token->secret : '', 153 | ]; 154 | 155 | $key_parts = Util::urlencode_rfc3986($key_parts); 156 | $key = implode('&', $key_parts); 157 | 158 | return base64_encode(hash_hmac('sha1', $base_string, $key, true)); 159 | } 160 | } 161 | 162 | 163 | /** 164 | * The PLAINTEXT method does not provide any security protection and SHOULD only be used 165 | * over a secure channel such as HTTPS. It does not use the Signature Base String. 166 | * - Chapter 9.4 ("PLAINTEXT") 167 | */ 168 | class SignatureMethod_PLAINTEXT extends SignatureMethod 169 | { 170 | public function get_name(): string 171 | { 172 | return 'PLAINTEXT'; 173 | } 174 | 175 | 176 | /** 177 | * oauth_signature is set to the concatenated encoded values of the Consumer Secret and 178 | * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is 179 | * empty. The result MUST be encoded again. 180 | * - Chapter 9.4.1 ("Generating Signatures") 181 | * 182 | * Please note that the second encoding MUST NOT happen in the SignatureMethod, as 183 | * OAuthRequest handles this! 184 | */ 185 | public function build_signature(Request $request, Consumer $consumer, ?Token $token): string 186 | { 187 | $key_parts = [ 188 | $consumer->secret, 189 | $token ? $token->secret : '', 190 | ]; 191 | 192 | $key_parts = Util::urlencode_rfc3986($key_parts); 193 | $key = implode('&', $key_parts); 194 | $request->base_string = $key; 195 | 196 | return $key; 197 | } 198 | } 199 | 200 | 201 | /** 202 | * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in 203 | * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for 204 | * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a 205 | * verified way to the Service Provider, in a manner which is beyond the scope of this 206 | * specification. 207 | * - Chapter 9.3 ("RSA-SHA1") 208 | */ 209 | abstract class SignatureMethod_RSA_SHA1 extends SignatureMethod 210 | { 211 | public function get_name(): string 212 | { 213 | return 'RSA-SHA1'; 214 | } 215 | 216 | 217 | /** 218 | * Up to the SP to implement this lookup of keys. Possible ideas are: 219 | * (1) do a lookup in a table of trusted certs keyed off of consumer 220 | * (2) fetch via http using a url provided by the requester 221 | * (3) some sort of specific discovery code based on request 222 | * 223 | * Either way should return a string representation of the certificate 224 | */ 225 | abstract protected function fetch_public_cert(&$request); 226 | 227 | 228 | /** 229 | * Up to the SP to implement this lookup of keys. Possible ideas are: 230 | * (1) do a lookup in a table of trusted certs keyed off of consumer 231 | * 232 | * Either way should return a string representation of the certificate 233 | */ 234 | abstract protected function fetch_private_cert(&$request); 235 | 236 | 237 | public function build_signature(Request $request, Consumer $consumer, ?Token $token): string 238 | { 239 | $base_string = $request->get_signature_base_string(); 240 | $request->base_string = $base_string; 241 | 242 | // Fetch the private key cert based on the request 243 | $cert = $this->fetch_private_cert($request); 244 | 245 | // Pull the private key ID from the certificate 246 | $privatekeyid = openssl_get_privatekey($cert); 247 | 248 | // Sign using the key 249 | $ok = openssl_sign($base_string, $signature, $privatekeyid); 250 | 251 | // Release the key resource 252 | openssl_free_key($privatekeyid); 253 | 254 | return base64_encode($signature); 255 | } 256 | 257 | 258 | public function check_signature(Request $request, Consumer $consumer, Token $token, string $signature): bool 259 | { 260 | $decoded_sig = base64_decode($signature, true); 261 | 262 | $base_string = $request->get_signature_base_string(); 263 | 264 | // Fetch the public key cert based on the request 265 | $cert = $this->fetch_public_cert($request); 266 | 267 | // Pull the public key ID from the certificate 268 | $publickeyid = openssl_get_publickey($cert); 269 | 270 | // Check the computed signature against the one passed in the query 271 | $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); 272 | 273 | // Release the key resource 274 | openssl_free_key($publickeyid); 275 | 276 | return $ok == 1; 277 | } 278 | } 279 | 280 | 281 | class Request 282 | { 283 | // for debug purposes 284 | public $base_string; 285 | public static $version = '1.0'; 286 | public static $POST_INPUT = 'php://input'; 287 | protected $parameters; 288 | protected $http_method; 289 | protected $http_url; 290 | 291 | 292 | public function __construct(string $http_method, string $http_url, array $parameters = null) 293 | { 294 | $parameters = $parameters ?: []; 295 | $parameters = array_merge(Util::parse_parameters((string) parse_url($http_url, PHP_URL_QUERY)), $parameters); 296 | $this->parameters = $parameters; 297 | $this->http_method = $http_method; 298 | $this->http_url = $http_url; 299 | } 300 | 301 | 302 | /** 303 | * attempt to build up a request from what was passed to the server 304 | */ 305 | public static function from_request(string $http_method = null, string $http_url = null, array $parameters = null): self 306 | { 307 | $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on') 308 | ? 'http' 309 | : 'https'; 310 | $http_url = ($http_url) ? $http_url : $scheme . 311 | '://' . $_SERVER['HTTP_HOST'] . 312 | ':' . 313 | $_SERVER['SERVER_PORT'] . 314 | $_SERVER['REQUEST_URI']; 315 | $http_method = ($http_method) ? $http_method : $_SERVER['REQUEST_METHOD']; 316 | 317 | // We weren't handed any parameters, so let's find the ones relevant to 318 | // this request. 319 | // If you run XML-RPC or similar you should use this to provide your own 320 | // parsed parameter-list 321 | if (!$parameters) { 322 | // Find request headers 323 | $request_headers = Util::get_headers(); 324 | 325 | // Parse the query-string to find GET parameters 326 | $parameters = Util::parse_parameters($_SERVER['QUERY_STRING']); 327 | 328 | // It's a POST request of the proper content-type, so parse POST 329 | // parameters and add those overriding any duplicates from GET 330 | if ($http_method == 'POST' 331 | && isset($request_headers['Content-Type']) 332 | && strstr($request_headers['Content-Type'], 'application/x-www-form-urlencoded') 333 | ) { 334 | $post_data = Util::parse_parameters( 335 | file_get_contents(self::$POST_INPUT) 336 | ); 337 | $parameters = array_merge($parameters, $post_data); 338 | } 339 | 340 | // We have a Authorization-header with OAuth data. Parse the header 341 | // and add those overriding any duplicates from GET or POST 342 | if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') { 343 | $header_parameters = Util::split_header( 344 | $request_headers['Authorization'] 345 | ); 346 | $parameters = array_merge($parameters, $header_parameters); 347 | } 348 | } 349 | 350 | return new self($http_method, $http_url, $parameters); 351 | } 352 | 353 | 354 | /** 355 | * pretty much a helper function to set up the request 356 | */ 357 | public static function from_consumer_and_token(Consumer $consumer, ?Token $token, string $http_method, string $http_url, array $parameters = null): self 358 | { 359 | $parameters = $parameters ?: []; 360 | $defaults = [ 361 | 'oauth_version' => self::$version, 362 | 'oauth_nonce' => self::generate_nonce(), 363 | 'oauth_timestamp' => self::generate_timestamp(), 364 | 'oauth_consumer_key' => $consumer->key, 365 | ]; 366 | if ($token) { 367 | $defaults['oauth_token'] = $token->key; 368 | } 369 | 370 | $parameters = array_merge($defaults, $parameters); 371 | 372 | return new self($http_method, $http_url, $parameters); 373 | } 374 | 375 | 376 | public function set_parameter(string $name, $value, bool $allow_duplicates = true): void 377 | { 378 | if ($allow_duplicates && isset($this->parameters[$name])) { 379 | // We have already added parameter(s) with this name, so add to the list 380 | if (is_scalar($this->parameters[$name])) { 381 | // This is the first duplicate, so transform scalar (string) 382 | // into an array so we can add the duplicates 383 | $this->parameters[$name] = [$this->parameters[$name]]; 384 | } 385 | 386 | $this->parameters[$name][] = $value; 387 | } else { 388 | $this->parameters[$name] = $value; 389 | } 390 | } 391 | 392 | 393 | public function get_parameter(string $name) 394 | { 395 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null; 396 | } 397 | 398 | 399 | public function get_parameters(): array 400 | { 401 | return $this->parameters; 402 | } 403 | 404 | 405 | public function unset_parameter(string $name): void 406 | { 407 | unset($this->parameters[$name]); 408 | } 409 | 410 | 411 | /** 412 | * The request parameters, sorted and concatenated into a normalized string. 413 | */ 414 | public function get_signable_parameters(): string 415 | { 416 | // Grab all parameters 417 | $params = $this->parameters; 418 | 419 | // Remove oauth_signature if present 420 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 421 | if (isset($params['oauth_signature'])) { 422 | unset($params['oauth_signature']); 423 | } 424 | 425 | return Util::build_http_query($params); 426 | } 427 | 428 | 429 | /** 430 | * Returns the base string of this request 431 | * 432 | * The base string defined as the method, the url 433 | * and the parameters (normalized), each urlencoded 434 | * and the concated with &. 435 | */ 436 | public function get_signature_base_string(): string 437 | { 438 | $parts = [ 439 | $this->get_normalized_http_method(), 440 | $this->get_normalized_http_url(), 441 | $this->get_signable_parameters(), 442 | ]; 443 | 444 | $parts = Util::urlencode_rfc3986($parts); 445 | 446 | return implode('&', $parts); 447 | } 448 | 449 | 450 | /** 451 | * just uppercases the http method 452 | */ 453 | public function get_normalized_http_method(): string 454 | { 455 | return strtoupper($this->http_method); 456 | } 457 | 458 | 459 | /** 460 | * parses the url and rebuilds it to be 461 | * scheme://host/path 462 | */ 463 | public function get_normalized_http_url(): string 464 | { 465 | $parts = parse_url($this->http_url); 466 | 467 | $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http'; 468 | $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80'); 469 | $host = (isset($parts['host'])) ? $parts['host'] : ''; 470 | $path = (isset($parts['path'])) ? $parts['path'] : ''; 471 | 472 | if (($scheme == 'https' && $port != '443') 473 | || ($scheme == 'http' && $port != '80')) { 474 | $host = "$host:$port"; 475 | } 476 | return "$scheme://$host$path"; 477 | } 478 | 479 | 480 | /** 481 | * builds a url usable for a GET request 482 | */ 483 | public function to_url(): string 484 | { 485 | $post_data = $this->to_postdata(); 486 | $out = $this->get_normalized_http_url(); 487 | if ($post_data) { 488 | $out .= '?' . $post_data; 489 | } 490 | return $out; 491 | } 492 | 493 | 494 | /** 495 | * builds the data one would send in a POST request 496 | */ 497 | public function to_postdata(): string 498 | { 499 | return Util::build_http_query($this->parameters); 500 | } 501 | 502 | 503 | /** 504 | * builds the Authorization: header 505 | */ 506 | public function to_header(string $realm = null): string 507 | { 508 | $first = true; 509 | if ($realm) { 510 | $out = 'Authorization: OAuth realm="' . Util::urlencode_rfc3986($realm) . '"'; 511 | $first = false; 512 | } else { 513 | $out = 'Authorization: OAuth'; 514 | } 515 | 516 | $total = []; 517 | foreach ($this->parameters as $k => $v) { 518 | if (substr($k, 0, 5) != 'oauth') { 519 | continue; 520 | } 521 | if (is_array($v)) { 522 | throw new Exception('Arrays not supported in headers'); 523 | } 524 | $out .= $first ? ' ' : ','; 525 | $out .= Util::urlencode_rfc3986($k) . '="' . Util::urlencode_rfc3986($v) . '"'; 526 | $first = false; 527 | } 528 | return $out; 529 | } 530 | 531 | 532 | public function __toString(): string 533 | { 534 | return $this->to_url(); 535 | } 536 | 537 | 538 | public function sign_request(SignatureMethod $signature_method, Consumer $consumer, ?Token $token) 539 | { 540 | $this->set_parameter( 541 | 'oauth_signature_method', 542 | $signature_method->get_name(), 543 | false 544 | ); 545 | $signature = $this->build_signature($signature_method, $consumer, $token); 546 | $this->set_parameter('oauth_signature', $signature, false); 547 | } 548 | 549 | 550 | public function build_signature(SignatureMethod $signature_method, Consumer $consumer, ?Token $token) 551 | { 552 | $signature = $signature_method->build_signature($this, $consumer, $token); 553 | return $signature; 554 | } 555 | 556 | 557 | /** 558 | * util function: current timestamp 559 | */ 560 | private static function generate_timestamp(): int 561 | { 562 | return time(); 563 | } 564 | 565 | 566 | /** 567 | * util function: current nonce 568 | */ 569 | private static function generate_nonce(): string 570 | { 571 | $mt = microtime(); 572 | $rand = mt_rand(); 573 | 574 | return md5($mt . $rand); // md5s look nicer than numbers 575 | } 576 | } 577 | 578 | 579 | class Util 580 | { 581 | public static function urlencode_rfc3986($input) 582 | { 583 | if (is_array($input)) { 584 | return array_map([__CLASS__, 'urlencode_rfc3986'], $input); 585 | } elseif (is_scalar($input)) { 586 | return str_replace('+', ' ', str_replace('%7E', '~', rawurlencode((string) $input))); 587 | } else { 588 | return ''; 589 | } 590 | } 591 | 592 | 593 | /** 594 | * This decode function isn't taking into consideration the above 595 | * modifications to the encoding process. However, this method doesn't 596 | * seem to be used anywhere so leaving it as is. 597 | */ 598 | public static function urldecode_rfc3986(string $string): string 599 | { 600 | return urldecode($string); 601 | } 602 | 603 | 604 | /** 605 | * Utility function for turning the Authorization: header into 606 | * parameters, has to do some unescaping 607 | * Can filter out any non-oauth parameters if needed (default behaviour) 608 | */ 609 | public static function split_header(string $header, bool $only_allow_oauth_parameters = true): array 610 | { 611 | $params = []; 612 | if (preg_match_all('/(' . ($only_allow_oauth_parameters ? 'oauth_' : '') . '[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches)) { 613 | foreach ($matches[1] as $i => $h) { 614 | $params[$h] = self::urldecode_rfc3986(empty($matches[3][$i]) ? $matches[4][$i] : $matches[3][$i]); 615 | } 616 | if (isset($params['realm'])) { 617 | unset($params['realm']); 618 | } 619 | } 620 | return $params; 621 | } 622 | 623 | 624 | /** 625 | * helper to try to sort out headers for people who aren't running apache 626 | */ 627 | public static function get_headers(): array 628 | { 629 | if (function_exists('apache_request_headers')) { 630 | // we need this to get the actual Authorization: header 631 | // because apache tends to tell us it doesn't exist 632 | $headers = apache_request_headers(); 633 | 634 | // sanitize the output of apache_request_headers because 635 | // we always want the keys to be Cased-Like-This and arh() 636 | // returns the headers in the same case as they are in the 637 | // request 638 | $out = []; 639 | foreach ($headers as $key => $value) { 640 | $key = str_replace( 641 | ' ', 642 | '-', 643 | ucwords(strtolower(str_replace('-', ' ', $key))) 644 | ); 645 | $out[$key] = $value; 646 | } 647 | } else { 648 | // otherwise we don't have apache and are just going to have to hope 649 | // that $_SERVER actually contains what we need 650 | $out = []; 651 | if (isset($_SERVER['CONTENT_TYPE'])) { 652 | $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; 653 | } 654 | if (isset($_ENV['CONTENT_TYPE'])) { 655 | $out['Content-Type'] = $_ENV['CONTENT_TYPE']; 656 | } 657 | 658 | foreach ($_SERVER as $key => $value) { 659 | if (substr($key, 0, 5) == 'HTTP_') { 660 | // this is chaos, basically it is just there to capitalize the first 661 | // letter of every word that is not an initial HTTP and strip HTTP 662 | // code from przemek 663 | $key = str_replace( 664 | ' ', 665 | '-', 666 | ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))) 667 | ); 668 | $out[$key] = $value; 669 | } 670 | } 671 | } 672 | return $out; 673 | } 674 | 675 | 676 | /** 677 | * This function takes a input like a=b&a=c&d=e and returns the parsed parameters like this 678 | * ['a' => array('b','c'), 'd' => 'e'] 679 | */ 680 | public static function parse_parameters(string $input): array 681 | { 682 | if (!isset($input) || !$input) { 683 | return []; 684 | } 685 | 686 | $pairs = explode('&', $input); 687 | 688 | $parsed_parameters = []; 689 | foreach ($pairs as $pair) { 690 | $split = explode('=', $pair, 2); 691 | $parameter = self::urldecode_rfc3986($split[0]); 692 | $value = isset($split[1]) ? self::urldecode_rfc3986($split[1]) : ''; 693 | 694 | if (isset($parsed_parameters[$parameter])) { 695 | // We have already recieved parameter(s) with this name, so add to the list 696 | // of parameters with this name 697 | 698 | if (is_scalar($parsed_parameters[$parameter])) { 699 | // This is the first duplicate, so transform scalar (string) into an array 700 | // so we can add the duplicates 701 | $parsed_parameters[$parameter] = [$parsed_parameters[$parameter]]; 702 | } 703 | 704 | $parsed_parameters[$parameter][] = $value; 705 | } else { 706 | $parsed_parameters[$parameter] = $value; 707 | } 708 | } 709 | return $parsed_parameters; 710 | } 711 | 712 | 713 | public static function build_http_query(array $params): string 714 | { 715 | if (!$params) { 716 | return ''; 717 | } 718 | 719 | // Urlencode both keys and values 720 | $keys = self::urlencode_rfc3986(array_keys($params)); 721 | $values = self::urlencode_rfc3986(array_values($params)); 722 | $params = array_combine($keys, $values); 723 | 724 | // Parameters are sorted by name, using lexicographical byte value ordering. 725 | // Ref: Spec: 9.1.1 (1) 726 | uksort($params, 'strcmp'); 727 | 728 | $pairs = []; 729 | foreach ($params as $parameter => $value) { 730 | if (is_array($value)) { 731 | // If two or more parameters share the same name, they are sorted by their value 732 | // Ref: Spec: 9.1.1 (1) 733 | // June 12th, 2010 - changed to sort because of issue 164 by hidetaka 734 | sort($value, SORT_STRING); 735 | foreach ($value as $duplicate_value) { 736 | $pairs[] = $parameter . '=' . $duplicate_value; 737 | } 738 | } else { 739 | $pairs[] = $parameter . '=' . $value; 740 | } 741 | } 742 | // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) 743 | // Each name-value pair is separated by an '&' character (ASCII code 38) 744 | return implode('&', $pairs); 745 | } 746 | } 747 | --------------------------------------------------------------------------------