├── .ably └── capabilities.yaml ├── .github └── workflows │ ├── check.yml │ └── features.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── COPYRIGHT ├── LICENSE ├── MAINTAINERS.md ├── Procfile ├── README.md ├── ably-loader.php ├── composer.json ├── demo └── index.php ├── phpunit.xml ├── src ├── AblyRest.php ├── Auth.php ├── Channel.php ├── Channels.php ├── Defaults.php ├── Exceptions │ ├── AblyException.php │ └── AblyRequestException.php ├── Host.php ├── HostCache.php ├── Http.php ├── Log.php ├── Models │ ├── AuthOptions.php │ ├── BaseMessage.php │ ├── BaseOptions.php │ ├── ChannelOptions.php │ ├── CipherParams.php │ ├── ClientOptions.php │ ├── DeviceDetails.php │ ├── DevicePushDetails.php │ ├── ErrorInfo.php │ ├── HttpPaginatedResponse.php │ ├── Message.php │ ├── PaginatedResult.php │ ├── PresenceMessage.php │ ├── PushChannelSubscription.php │ ├── Stats.php │ ├── Stats │ │ ├── ConnectionTypes.php │ │ ├── MessageCount.php │ │ ├── MessageTraffic.php │ │ ├── MessageTypes.php │ │ ├── RequestCount.php │ │ └── ResourceCount.php │ ├── Status │ │ └── ChannelDetails.php │ ├── TokenDetails.php │ ├── TokenParams.php │ ├── TokenRequest.php │ └── Untyped.php ├── Presence.php ├── Push.php ├── PushAdmin.php ├── PushChannelSubscriptions.php ├── PushDeviceRegistrations.php └── Utils │ ├── Crypto.php │ ├── CurlWrapper.php │ ├── Miscellaneous.php │ └── Stringifiable.php └── tests ├── AblyRestRequestTest.php ├── AblyRestTest.php ├── AppStatsTest.php ├── AssertsRegularExpressions.php ├── AuthTest.php ├── ChannelHistoryTest.php ├── ChannelIdempotentTest.php ├── ChannelMessagesTest.php ├── ChannelStatusTest.php ├── ClientIdTest.php ├── ClientOptionsTest.php ├── CryptoTest.php ├── DefaultsTest.php ├── HostCacheTest.php ├── HostTest.php ├── HttpTest.php ├── LogTest.php ├── MiscellaneousTest.php ├── PresenceTest.php ├── PushAdminTest.php ├── PushChannelSubscriptionsTest.php ├── PushDeviceRegistrationsTest.php ├── TokenTest.php ├── TypesTest.php ├── Utils.php ├── UtilsTest.php ├── extensions └── DebugTestListener.php └── factories └── TestApp.php /.ably/capabilities.yaml: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | common-version: 1.2.0 4 | compliance: 5 | Agent Identifier: 6 | Agents: 7 | Authentication: 8 | API Key: 9 | Token: 10 | Callback: 11 | Literal: 12 | URL: 13 | Query Time: 14 | Debugging: 15 | Error Information: 16 | Logs: 17 | Protocol: 18 | JSON: 19 | MessagePack: 20 | REST: 21 | Authentication: 22 | Authorize: 23 | Create Token Request: 24 | Get Client Identifier: 25 | Request Token: 26 | Channel: 27 | Encryption: 28 | Existence Check: 29 | Get: 30 | History: 31 | Iterate: 32 | Name: 33 | Presence: 34 | History: 35 | Member List: 36 | Publish: 37 | Idempotence: 38 | Push Notifications: 39 | List Subscriptions: 40 | Subscribe: 41 | Release: 42 | Status: 43 | Channel Details: # https://github.com/ably/ably-php/pull/159 44 | Opaque Request: 45 | Push Notifications Administration: 46 | Channel Subscription: 47 | List: 48 | List Channels: 49 | Remove: 50 | Save: 51 | Device Registration: 52 | Get: 53 | List: 54 | Remove: 55 | Save: 56 | Publish: 57 | Request Timeout: 58 | Service: 59 | Get Time: 60 | Statistics: 61 | Query: 62 | Service: 63 | Environment: 64 | Fallbacks: 65 | Hosts: 66 | Internet Up Check: 67 | Retry Count: 68 | Retry Duration: 69 | Retry Timeout: 70 | Host: 71 | Testing: 72 | Disable TLS: 73 | TCP Insecure Port: 74 | TCP Secure Port: 75 | Transport: 76 | Connection Open Timeout: 77 | Proxy: 78 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | # Loosely based upon: 2 | # https://github.com/actions/starter-workflows/blob/main/ci/php.yml 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | php-version: [7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, 8.4] 18 | protocol: [ 'json', 'msgpack' ] 19 | ignorePlatformReq: [ '' ] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | submodules: 'recursive' 25 | 26 | - name: Set up PHP ${{ matrix.php-version }} 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: ${{ matrix.php-version }} 30 | ini-values: error_reporting=E_ALL 31 | 32 | - name: Validate composer.json and composer.lock 33 | run: composer validate 34 | 35 | - name: Install dependencies 36 | run: composer install --prefer-dist --no-progress --no-suggest ${{ matrix.ignorePlatformReq }} 37 | 38 | # the test script is configured in composer.json. 39 | # see: https://getcomposer.org/doc/articles/scripts.md 40 | - name: Run test 41 | env: 42 | PROTOCOL: ${{ matrix.protocol }} 43 | run: composer run-script test 44 | -------------------------------------------------------------------------------- /.github/workflows/features.yml: -------------------------------------------------------------------------------- 1 | name: Features 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | uses: ably/features/.github/workflows/sdk-features.yml@main 12 | with: 13 | repository-name: ably-php 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | tmp/ 4 | config.php 5 | /composer.lock 6 | /.phpunit.result.cache 7 | /vendor/ 8 | 9 | .*.swp 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ably-common"] 2 | path = ably-common 3 | url = https://github.com/ably/ably-common 4 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2016-2022 Ably Real-time Ltd (ably.com) 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | This repository is owned by the Ably SDK team. 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-apache2 demo/ -------------------------------------------------------------------------------- /ably-loader.php: -------------------------------------------------------------------------------- 1 | =0.9.1", 9 | "ext-json" : "*", 10 | "ext-curl" : "*", 11 | "ext-openssl" : "*" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "^8.5 || ^9.5" 15 | }, 16 | "license": "Apache-2.0", 17 | "authors": [ 18 | { 19 | "name": "Ably", 20 | "email": "support@ably.com" 21 | } 22 | ], 23 | "autoload": { 24 | "psr-4": { 25 | "Ably\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "tests\\": "tests/" 31 | } 32 | }, 33 | "scripts": { 34 | "test": "vendor/bin/phpunit" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/index.php: -------------------------------------------------------------------------------- 1 | $apiKey, 19 | ); 20 | 21 | if ($host) { 22 | $settings['host'] = $host; 23 | } 24 | 25 | // instantiate Ably 26 | $app = new \Ably\AblyRest($settings); 27 | $channel = $app->channel($channelName); 28 | 29 | if (!empty($_POST)) { 30 | // publish a message 31 | $channel->publish( $eventName, array('handle' => $_POST['handle'], 'message' => $_POST['message']) ); 32 | die(); 33 | } 34 | 35 | // get a list of recent messages and render the interface 36 | $messages = $channel->history( array('direction' => 'backwards') )->items; 37 | 38 | ?> 39 | 40 | 41 | 42 | 43 | 44 | Simple Chat Demo 45 | 46 | 58 | 59 | 60 | 61 |
62 |
63 |

Let's Chat [api_time: time()/1000) ?> | server_time: ]

64 |
65 |
66 |
67 | 68 | 69 |
70 | 71 |
72 |
73 | 74 |
75 | 76 | 77 | 78 |
79 |
80 |
81 |
82 |
    83 | 84 | 85 | timestamp / 1000); 87 | $day = date($date_format, $timestamp); ?> 88 | 89 |
  • 90 | 91 |
  • 92 | data->handle ?>: data->message ?>
  • 93 | 94 |
95 |
96 |
97 |
98 |
99 |
100 | 101 | 102 | 103 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | src 7 | 8 | 9 | 10 | 11 | tests 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Channel.php: -------------------------------------------------------------------------------- 1 | ably = $ably; 39 | $this->name = $name; 40 | $this->channelPath = "/channels/" . urlencode( $name ); 41 | $this->presence = new Presence( $ably, $this ); 42 | 43 | $this->setOptions( $options ); 44 | } 45 | 46 | /** 47 | * Magic getter for the $presence property 48 | */ 49 | public function __get( $name ) { 50 | if ($name == 'presence') { 51 | return $this->presence; 52 | } 53 | 54 | throw new AblyException( 'Undefined property: '.__CLASS__.'::'.$name ); 55 | } 56 | 57 | /** 58 | * Posts a message to this channel 59 | * @param mixed ... Either a Message, array of Message-s, or (string eventName, string data) 60 | * @throws \Ably\Exceptions\AblyException 61 | */ 62 | public function __publish_request_body($first) { 63 | // Process arguments 64 | $messages = []; 65 | 66 | if ( is_a( $first, 'Ably\Models\Message' ) ) { // single Message 67 | $messages[] = $first; 68 | } else if ( is_array( $first ) ) { // array of Messages 69 | $messages = $first; 70 | } else { 71 | throw new AblyException( 72 | 'Wrong parameters provided, use either Message, array of Messages, or name and data', 40003, 400 73 | ); 74 | } 75 | 76 | // Cipher and Idempotent 77 | $emptyId = true; 78 | foreach ( $messages as $msg ) { 79 | if ( $this->options->cipher ) { 80 | $msg->setCipherParams( $this->options->cipher ); 81 | } 82 | if ( $msg->id ) { 83 | $emptyId = false; 84 | } 85 | } 86 | 87 | if ($emptyId && $this->ably->options->idempotentRestPublishing) { 88 | $baseId = base64_encode( openssl_random_pseudo_bytes(12) ); 89 | foreach ( $messages as $key => $msg ) { 90 | $msg->id = $baseId . ":" . $key; 91 | } 92 | } 93 | 94 | // Serialize 95 | if($this->ably->options->useBinaryProtocol) { 96 | if ( count($messages) == 1) { 97 | $serialized = MessagePack::pack($messages[0]->encodeAsArray(), PackOptions::FORCE_STR); 98 | } else { 99 | $array = []; 100 | foreach ( $messages as $msg ) { 101 | $array[] = $msg->encodeAsArray(); 102 | } 103 | $serialized = MessagePack::pack($array, PackOptions::FORCE_STR); 104 | } 105 | } 106 | else { 107 | if ( count($messages) == 1) { 108 | $serialized = $messages[0]->toJSON(); 109 | } else { 110 | $jsonArray = []; 111 | foreach ( $messages as $msg ) { 112 | $jsonArray[] = $msg->toJSON(); 113 | } 114 | $serialized = '[' . implode( ',', $jsonArray ) . ']'; 115 | } 116 | } 117 | 118 | return $serialized; 119 | } 120 | 121 | public function publish(...$args) { 122 | $first = $args[0]; 123 | $params = []; 124 | 125 | if ( is_string( $first ) ) { // eventName, data[, clientId][, extras] 126 | $msg = new Message(); 127 | $msg->name = $first; 128 | $msg->data = $args[1]; 129 | // TODO RSL1h: Remove clientId/extras extras support for 2.0 130 | $argsn = count($args); 131 | if ( $argsn == 3 ) { 132 | if ( is_string($args[2]) ) 133 | $msg->clientId = $args[2]; 134 | else if ( is_array($args[2]) ) 135 | $msg->extras = $args[2]; 136 | } else if ( $argsn == 4 ) { 137 | $msg->clientId = $args[2]; 138 | $msg->extras = $args[3]; 139 | } 140 | 141 | $request_body = $this->__publish_request_body($msg); 142 | } else { 143 | $request_body = $this->__publish_request_body($first); 144 | if ( count($args) > 1 ) { 145 | $params = $args[1]; 146 | } 147 | } 148 | 149 | $url = $this->channelPath . '/messages'; 150 | if (!empty($params)) { 151 | $url .= '?' . Stringifiable::buildQuery( $params ); 152 | } 153 | 154 | $this->ably->post( $url, $headers = [], $request_body ); 155 | return true; 156 | } 157 | 158 | /** 159 | * Retrieves channel's history of messages 160 | * @param array $params Parameters to be sent with the request 161 | * @return PaginatedResult 162 | */ 163 | public function history( $params = [] ) { 164 | return new PaginatedResult( $this->ably, 'Ably\Models\Message', 165 | $this->getCipherParams(), 166 | 'GET', $this->getPath() . '/messages', 167 | $params ); 168 | } 169 | 170 | /** 171 | * Retrieves current channel active status with no. of publishers, subscribers, presenceMembers etc 172 | * @return ChannelDetails 173 | */ 174 | public function status() { 175 | return ChannelDetails::from($this->ably->get("/channels/" . $this->getName())); 176 | } 177 | 178 | /** 179 | * @return string Channel's name 180 | */ 181 | public function getName() { 182 | return $this->name; 183 | } 184 | 185 | /** 186 | * @return string Channel portion of the request URI 187 | */ 188 | public function getPath() { 189 | return $this->channelPath; 190 | } 191 | 192 | /** 193 | * @return CipherParams|null Cipher params if the channel is encrypted 194 | */ 195 | public function getCipherParams() { 196 | return $this->options->cipher; 197 | } 198 | 199 | /** 200 | * @return ChannelOptions 201 | */ 202 | public function getOptions() { 203 | return $this->options; 204 | } 205 | 206 | /** 207 | * Sets channel options 208 | * @param array|null $options channel options 209 | * @throws AblyException 210 | */ 211 | public function setOptions( $options = [] ) { 212 | $this->options = new ChannelOptions( $options ); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/Channels.php: -------------------------------------------------------------------------------- 1 | ably = $ably; 15 | } 16 | 17 | /** 18 | * Creates a new Channel object for the specified channel if none exists, or returns the existing channel 19 | * Note that if you request the same channel with different parameters, all the instances 20 | * of the channel will be updated. 21 | * @param string $name Name of the channel 22 | * @param array|null $options ChannelOptions for the channel 23 | * @return \Ably\Channel 24 | */ 25 | public function get( $name, $options = null ) { 26 | 27 | if ( isset( $this->channels[$name] ) ) { 28 | if ( !is_null( $options ) ) { 29 | $this->channels[$name]->setOptions( $options ); 30 | } 31 | 32 | return $this->channels[$name]; 33 | } else { 34 | $this->channels[$name] = new Channel( $this->ably, $name, is_null( $options ) ? [] : $options ); 35 | 36 | return $this->channels[$name]; 37 | } 38 | } 39 | 40 | /** 41 | * Releases the channel resource i.e. it’s deleted and can then be garbage collected 42 | * @param string $name Name of the channel 43 | */ 44 | public function release( $name ) { 45 | if ( isset( $this->channels[$name] ) ) { 46 | unset( $this->channels[$name] ); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Defaults.php: -------------------------------------------------------------------------------- 1 | errorInfo = new ErrorInfo(); 27 | $this->errorInfo->message = $message; 28 | $this->errorInfo->code = $code; 29 | $this->errorInfo->statusCode = $statusCode; 30 | } 31 | 32 | public function getStatusCode() { 33 | return $this->errorInfo->statusCode; 34 | } 35 | 36 | // PHP doesn't allow overriding these methods 37 | 38 | // public function getCode() { 39 | // return $this->errorInfo->code; 40 | // } 41 | 42 | // public function getMessage() { 43 | // return $this->errorInfo->message; 44 | // } 45 | } 46 | -------------------------------------------------------------------------------- /src/Exceptions/AblyRequestException.php: -------------------------------------------------------------------------------- 1 | response = $response ? : [ 'headers' => '', 'body' => '' ]; 15 | } 16 | 17 | public function getResponse() { 18 | return $this->response; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Host.php: -------------------------------------------------------------------------------- 1 | primaryHost = $clientOptions->getPrimaryRestHost(); 12 | $this->fallbackHosts = $clientOptions->getFallbackHosts(); 13 | $this->hostCache = new HostCache($clientOptions->fallbackRetryTimeout); 14 | } 15 | 16 | public function fallbackHosts($currentHost) { 17 | if ($currentHost != $this->primaryHost) { 18 | yield $this->primaryHost; 19 | } 20 | $shuffledFallbacks = $this->fallbackHosts; 21 | shuffle($shuffledFallbacks); 22 | foreach ($shuffledFallbacks as $fallbackHost) { 23 | if ($currentHost != $fallbackHost) { 24 | yield $fallbackHost; 25 | } 26 | } 27 | } 28 | 29 | // getPreferredHost - Used to retrieve host in the order 1. Cached host 2. primary host 30 | public function getPreferredHost() { 31 | $host = $this->hostCache->get(); 32 | if (empty($host)) { 33 | return $this->primaryHost; 34 | } 35 | return $host; 36 | } 37 | 38 | public function setPreferredHost($host) { 39 | $this->hostCache->put($host); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/HostCache.php: -------------------------------------------------------------------------------- 1 | timeoutDuration = $timeoutDurationInMs; 17 | } 18 | 19 | public function put($host) 20 | { 21 | $this->host = $host; 22 | $this->expireTime = Miscellaneous::systemTime() + $this->timeoutDuration; 23 | } 24 | 25 | public function get() 26 | { 27 | if (empty($this->host) || Miscellaneous::systemTime() > $this->expireTime) { 28 | return ""; 29 | } 30 | return $this->host; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http.php: -------------------------------------------------------------------------------- 1 | postDataFormat = $clientOptions->useBinaryProtocol ? 'msgpack' : 'json'; 44 | $this->connectTimeout = $clientOptions->httpOpenTimeout; 45 | $this->requestTimeout = $clientOptions->httpRequestTimeout; 46 | $this->curl = new CurlWrapper(); 47 | } 48 | 49 | /** 50 | * Wrapper to do a GET request 51 | * @see Http::request() 52 | */ 53 | public function get( $url, $headers = [], $params = [] ) { 54 | return $this->request( 'GET', $url, $headers, $params ); 55 | } 56 | 57 | /** 58 | * Wrapper to do a POST request 59 | * @see Http::request() 60 | */ 61 | public function post( $url, $headers = [], $params = [] ) { 62 | return $this->request( 'POST', $url, $headers, $params ); 63 | } 64 | 65 | /** 66 | * Wrapper to do a PUT request 67 | * @see Http::request() 68 | */ 69 | public function put( $url, $headers = [], $params = [] ) { 70 | return $this->request( 'PUT', $url, $headers, $params ); 71 | } 72 | 73 | /** 74 | * Wrapper to do a DELETE request 75 | * @see Http::request() 76 | */ 77 | public function delete( $url, $headers = [], $params = [] ) { 78 | return $this->request( 'DELETE', $url, $headers, $params ); 79 | } 80 | 81 | /** 82 | * Wrapper to do a PATCH request 83 | * @see Http::request() 84 | */ 85 | public function patch( $url, $headers = [], $params = [] ) { 86 | return $this->request( 'PATCH', $url, $headers, $params ); 87 | } 88 | 89 | /** 90 | * Executes a cURL request 91 | * @param string $method HTTP method (GET, POST, PUT, DELETE, PATCH, ...) 92 | * @param string $url Absolute URL to make a request on 93 | * @param array $headers HTTP headers to send 94 | * @param array|string $params Array of parameters to submit or a JSON string 95 | * @throws AblyRequestException if the request fails 96 | * @throws AblyRequestTimeoutException if the request times out 97 | * @return array with 'headers' and 'body' fields, body is automatically decoded 98 | */ 99 | public function request( $method, $url, $headers = [], $params = [] ) { 100 | $method = strtoupper($method); 101 | 102 | $ch = $this->curl->init($url); 103 | $this->curl->setOpt( $ch, CURLOPT_CUSTOMREQUEST, $method ); 104 | 105 | if (isset($_SERVER['http_proxy']) && is_string($_SERVER['http_proxy'])) { 106 | $this->curl->setOpt($ch, CURLOPT_PROXY, $_SERVER['http_proxy']); 107 | } elseif (isset($_SERVER['https_proxy']) && is_string($_SERVER['https_proxy'])) { 108 | $this->curl->setOpt($ch, CURLOPT_PROXY, $_SERVER['https_proxy']); 109 | } 110 | 111 | $this->curl->setOpt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->connectTimeout); 112 | $this->curl->setOpt($ch, CURLOPT_TIMEOUT_MS, $this->requestTimeout); 113 | 114 | if (!empty($params)) { 115 | if (is_array( $params )) { 116 | $paramsQuery = http_build_query( $params ); 117 | 118 | if ($method == 'GET' || $method == 'DELETE') { 119 | if ($paramsQuery) { 120 | $url .= '?' . $paramsQuery; 121 | } 122 | $this->curl->setOpt( $ch, CURLOPT_URL, $url ); 123 | } else if ($method == 'POST') { 124 | $this->curl->setOpt( $ch, CURLOPT_POST, true ); 125 | $this->curl->setOpt( $ch, CURLOPT_POSTFIELDS, $paramsQuery ); 126 | } else { 127 | $this->curl->setOpt( $ch, CURLOPT_POSTFIELDS, $paramsQuery ); 128 | } 129 | } else if (is_string( $params )) { // json or msgpack 130 | if ($method == 'POST') { 131 | $this->curl->setOpt( $ch, CURLOPT_POST, true ); 132 | } 133 | 134 | $this->curl->setOpt( $ch, CURLOPT_POSTFIELDS, $params ); 135 | 136 | if ($this->postDataFormat == 'json') { 137 | $headers[] = 'Content-Type: application/json'; 138 | } 139 | elseif ($this->postDataFormat == 'msgpack') { 140 | $headers[] = 'Content-Type: application/x-msgpack'; 141 | } 142 | } else { 143 | throw new AblyRequestException( 'Unknown $params format', -1, -1 ); 144 | } 145 | } 146 | 147 | if (!empty($headers)) { 148 | $this->curl->setOpt( $ch, CURLOPT_HTTPHEADER, $headers ); 149 | } 150 | 151 | $this->curl->setOpt( $ch, CURLOPT_RETURNTRANSFER, true ); 152 | if ( Log::getLogLevel() >= Log::VERBOSE ) { 153 | $this->curl->setOpt( $ch, CURLOPT_VERBOSE, true ); 154 | } 155 | $this->curl->setOpt( $ch, CURLOPT_HEADER, true ); // return response headers 156 | 157 | Log::d( 'cURL command:', $this->curl->getCommand( $ch ) ); 158 | 159 | $raw = $this->curl->exec( $ch ); 160 | $info = $this->curl->getInfo( $ch ); 161 | $err = $this->curl->getErrNo( $ch ); 162 | $errmsg = $err ? $this->curl->getError( $ch ) : ''; 163 | $contentType = $this->curl->getContentType( $ch ); 164 | 165 | $this->curl->close( $ch ); 166 | 167 | if ( $err ) { // a connection error has occured (no data received) 168 | Log::e('cURL error:', $err, $errmsg); 169 | throw new AblyRequestException('cURL error: ' . $errmsg, 50003, 500); // RSC15d, throw timeout error 170 | } 171 | 172 | $resHeaders = substr( $raw, 0, $info['header_size'] ); 173 | $body = substr( $raw, $info['header_size'] ); 174 | 175 | $decodedBody = null; 176 | if(strpos($contentType, 'application/x-msgpack') === 0) { 177 | $decodedBody = MessagePack::unpack($body, PackOptions::FORCE_STR); 178 | 179 | Miscellaneous::deepConvertArrayToObject($decodedBody); 180 | } 181 | elseif(strpos($contentType, 'application/json') === 0) 182 | $decodedBody = json_decode( $body ); 183 | 184 | $response = [ 185 | 'headers' => $resHeaders, 186 | 'body' => $decodedBody ?: $body, 187 | 'info' => $info, 188 | ]; 189 | 190 | Log::v( 'cURL request response:', $info['http_code'], $response ); 191 | 192 | if ( $info['http_code'] < 200 || $info['http_code'] >= 300 ) { 193 | $ablyCode = empty( $decodedBody->error->code ) ? $info['http_code'] * 100 : $decodedBody->error->code * 1; 194 | $errorMessage = empty( $decodedBody->error->message ) ? 'cURL request failed' : $decodedBody->error->message; 195 | throw new AblyRequestException( $errorMessage, $ablyCode, $info['http_code'], $response ); 196 | } 197 | 198 | return $response; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Log.php: -------------------------------------------------------------------------------- 1 | = $level ) { 84 | $function = self::$logCallback; 85 | return $function ? $function( $level, $args ) : self::defaultLogCallback( $level, $args ); 86 | } 87 | } 88 | 89 | /** 90 | * The default logging function 91 | */ 92 | private static function defaultLogCallback( $level, $args ) { 93 | $last = count($args) - 1; 94 | 95 | $timestamp = date( "Y-m-d H:i:s\t" ); 96 | 97 | foreach ($args as $i => $arg) { 98 | if (is_string($arg)) { 99 | file_put_contents( 'php://stdout', $timestamp . $arg . ($i == $last ? "\n" : "\t") ); 100 | } 101 | else if (is_bool($arg)) { 102 | file_put_contents( 'php://stdout', $timestamp . ($arg ? 'true' : 'false') . ($i == $last ? "\n" : "\t") ); 103 | } 104 | else if (is_scalar($arg)) { 105 | file_put_contents( 'php://stdout', $timestamp . $arg . ($i == $last ? "\n" : "\t") ); 106 | } 107 | else { 108 | file_put_contents( 'php://stdout', $timestamp . print_r( $arg, true ). "\n" ); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Models/AuthOptions.php: -------------------------------------------------------------------------------- 1 | tokenDetails ) && !empty( $this->token ) ) { 82 | $this->tokenDetails = new TokenDetails( $this->token ); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Models/BaseMessage.php: -------------------------------------------------------------------------------- 1 | encode() ); 80 | } 81 | 82 | /** 83 | * Populates the message from JSON and automatically decodes data. 84 | * @param string|stdClass $json JSON string or an already decoded object. 85 | * @param bool $keepOriginal When set to true, the message won't be decoded or decrypted 86 | * @throws AblyException 87 | */ 88 | public function fromJSON( $json, $keepOriginal = false ) { 89 | $this->clearFields(); 90 | 91 | if (!is_string( $json )) { 92 | $obj = $json; 93 | } else { 94 | $obj = @json_decode($json); 95 | if (!$obj) { 96 | throw new AblyException( 'Invalid object or JSON encoded object' ); 97 | } 98 | } 99 | 100 | $class = get_class( $this ); 101 | foreach ($obj as $key => $value) { 102 | if (property_exists( $class, $key )) { 103 | $this->$key = $value; 104 | } 105 | } 106 | 107 | if ($keepOriginal) return; 108 | 109 | $this->decode(); 110 | } 111 | 112 | /** 113 | * Creates and returns a new message from the given encoded message like object 114 | * @param stdClass $obj Message-like object 115 | * @param CipherParams|null $cipherParams 116 | */ 117 | public static function fromEncoded( $obj, ?CipherParams $cipherParams = null ) { 118 | $class = get_called_class(); 119 | 120 | $msg = new $class(); 121 | if ($cipherParams != null) { 122 | $msg->setCipherParams( $cipherParams ); 123 | } 124 | 125 | foreach ($obj as $key => $value) { 126 | if (property_exists( $class, $key )) { 127 | $msg->$key = $value; 128 | } 129 | } 130 | 131 | $msg->decode(); 132 | 133 | return $msg; 134 | } 135 | 136 | /** 137 | * Creates and returns a new message from the given encoded message like object 138 | * @param array $objs Array of Message-Like objects 139 | * @param CipherParams|null $cipherParams 140 | */ 141 | public static function fromEncodedArray( $objs, ?CipherParams $cipherParams = null ) { 142 | return array_map( 143 | function( $obj ) use ($cipherParams) { return static::fromEncoded($obj, $cipherParams); }, 144 | $objs 145 | ); 146 | } 147 | 148 | /** 149 | * Returns an encoded message as a stdClass ready for stringifying 150 | */ 151 | protected function encode() { 152 | $msg = new \stdClass(); 153 | 154 | if ($this->id) { 155 | $msg->id = $this->id; 156 | } 157 | 158 | if ($this->clientId) { 159 | $msg->clientId = $this->clientId; 160 | } 161 | 162 | if ($this->extras) { 163 | $msg->extras = $this->extras; 164 | } 165 | 166 | if ($this->encoding) { 167 | $msg->encoding = $this->encoding; 168 | $msg->data = $this->data; 169 | 170 | return $msg; 171 | } 172 | 173 | $isBinary = false; 174 | $encodings = []; 175 | 176 | if ( is_array( $this->data ) || $this->data instanceof \stdClass ) { 177 | $encodings[] = 'json'; 178 | $msg->data = json_encode($this->data); 179 | } else if ( is_string( $this->data ) ){ 180 | if ( mb_check_encoding( $this->data, 'UTF-8' ) ) { // it's a UTF-8 string 181 | $msg->data = $this->data; 182 | } else { // not UTF-8, assuming it's a binary string 183 | $msg->data = $this->data; 184 | $isBinary = true; 185 | } 186 | } else if ( !isset( $this->data ) || $this->data === null ) { 187 | return $msg; 188 | } else { 189 | throw new AblyException( 190 | 'Message data must be either, string, string with binary data, JSON-encodable array or object, or null.', 40003, 400 191 | ); 192 | } 193 | 194 | if ( $this->cipherParams ) { 195 | if ( !$isBinary ) { 196 | $encodings[] = 'utf-8'; 197 | } 198 | 199 | $msg->data = base64_encode( Crypto::encrypt( $msg->data, $this->cipherParams ) ); 200 | $encodings[] = 'cipher+' . $this->cipherParams->getAlgorithmString(); 201 | $encodings[] = 'base64'; 202 | } else { 203 | if ( $isBinary ) { 204 | $msg->data = base64_encode( $this->data ); 205 | $encodings[] = 'base64'; 206 | } 207 | } 208 | 209 | if ( count( $encodings ) ) { 210 | $msg->encoding = implode( '/', $encodings ); 211 | } else { 212 | $msg->encoding = ''; 213 | } 214 | 215 | return $msg; 216 | } 217 | 218 | /** 219 | * Decodes message's data field according to encoding 220 | * @throws AblyException 221 | */ 222 | protected function decode() { 223 | $this->originalData = $this->data; 224 | $this->originalEncoding = $this->encoding; 225 | 226 | if (!empty( $this->encoding )) { 227 | $encodings = explode( '/', $this->encoding ); 228 | 229 | foreach (array_reverse( $encodings ) as $encoding) { 230 | if ($encoding == 'base64') { 231 | $this->data = base64_decode( $this->data ); 232 | 233 | if ($this->data === false) { 234 | throw new AblyException( 'Could not base64-decode message data' ); 235 | } 236 | 237 | array_pop( $encodings ); 238 | } else if ($encoding == 'json') { 239 | $this->data = json_decode( $this->data ); 240 | 241 | if ($this->data === null) { 242 | throw new AblyException( 'Could not JSON-decode message data' ); 243 | } 244 | 245 | array_pop( $encodings ); 246 | } else if (strpos( $encoding, 'cipher+' ) === 0) { 247 | if (!$this->cipherParams) { 248 | Log::e( 'Could not decrypt message data, no cipherParams provided' ); 249 | break; 250 | } 251 | 252 | $data = Crypto::decrypt( $this->data, $this->cipherParams ); 253 | 254 | if ($data === false) { 255 | Log::e( 'Could not decrypt message data' ); 256 | break; 257 | } 258 | 259 | $this->data = $data; 260 | array_pop( $encodings ); 261 | } 262 | } 263 | 264 | $this->encoding = count( $encodings ) ? implode( '/', $encodings ) : null; 265 | } 266 | } 267 | 268 | /** 269 | * Sets all the public fields to null 270 | */ 271 | protected function clearFields() { 272 | $fields = get_object_vars( $this ); 273 | unset( $fields['cipherParams'] ); 274 | 275 | foreach ($fields as $key => $value) { 276 | $this->$key = null; 277 | } 278 | } 279 | 280 | /** 281 | * Sets cipher parameters for this message for automatic encryption and decryption. 282 | * @param CipherParams $cipherParams 283 | */ 284 | public function setCipherParams( CipherParams $cipherParams ) { 285 | $this->cipherParams = $cipherParams; 286 | } 287 | 288 | public function encodeAsArray() { 289 | $encoded = (array)$this->encode(); 290 | 291 | Miscellaneous::deepConvertObjectToArray($encoded); 292 | return $encoded; 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/Models/BaseOptions.php: -------------------------------------------------------------------------------- 1 | $value) { 15 | if (property_exists( $class, $key )) { 16 | $this->$key = $value; 17 | } 18 | } 19 | } 20 | 21 | public function toArray() { 22 | $properties = call_user_func('get_object_vars', $this); 23 | foreach ($properties as $k => $v) { 24 | if ($v === null) { 25 | unset($properties[$k]); 26 | } 27 | } 28 | return $properties; 29 | } 30 | 31 | public function fromJSON( $json ) { 32 | if (!is_string( $json )) { 33 | $obj = $json; 34 | } else { 35 | $obj = @json_decode($json); 36 | if (!$obj) { 37 | throw new AblyException( 'Invalid object or JSON encoded object' ); 38 | } 39 | } 40 | 41 | $class = get_class( $this ); 42 | foreach ($obj as $key => $value) { 43 | if (property_exists( $class, $key )) { 44 | $this->$key = $value; 45 | } 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Models/ChannelOptions.php: -------------------------------------------------------------------------------- 1 | cipher ) ) { 23 | $this->cipher = Crypto::getDefaultParams( $this->cipher ); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Models/CipherParams.php: -------------------------------------------------------------------------------- 1 | algorithm 28 | . ($this->keyLength ? '-' . $this->keyLength : '') 29 | . ($this->mode ? '-' . $this->mode : ''); 30 | } 31 | 32 | public function generateIV() { 33 | $length = openssl_cipher_iv_length( $this->getAlgorithmString() ); 34 | if ( $length > 0 ) { 35 | $this->iv = openssl_random_pseudo_bytes( $length ); 36 | } 37 | } 38 | 39 | public function checkValidAlgorithm() { 40 | $validAlgs = openssl_get_cipher_methods( true ); 41 | return in_array( $this->getAlgorithmString(), $validAlgs ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Models/ClientOptions.php: -------------------------------------------------------------------------------- 1 | 'sandbox-rest.ably.io' 58 | */ 59 | public $environment; 60 | 61 | /** 62 | * @var string[] fallback hosts, used when connection to default host fails, populated automatically 63 | */ 64 | public $fallbackHosts = []; 65 | 66 | /** 67 | * @var integer – default 600000 (10 minutes) the period in milliseconds 68 | * before HTTP requests are retried against the default endpoint 69 | */ 70 | public $fallbackRetryTimeout = 600000; 71 | 72 | /** 73 | * @var \Ably\Models\TokenParams defaultTokenParams – overrides the client library defaults described in TokenParams 74 | */ 75 | public $defaultTokenParams; 76 | 77 | /** 78 | * @var integer Timeout for opening the connection 79 | * Warning: may be rounded down on some OSes and values < 1000 will always fail in that case. 80 | */ 81 | public $httpOpenTimeout = 4000; 82 | 83 | /** 84 | * @var integer connection timeout after which a next fallback host is used 85 | */ 86 | public $httpRequestTimeout = 10000; 87 | 88 | /** 89 | * @var integer Max number of fallback host retries for HTTP requests that fail due to network issues or server problems 90 | */ 91 | public $httpMaxRetryCount = 3; 92 | 93 | /** 94 | * @var integer Max elapsed time in which fallback host retries for HTTP requests will be attempted 95 | */ 96 | public $httpMaxRetryDuration = 15000; 97 | 98 | /** 99 | * @var string a class that should be used for making HTTP connections 100 | * To allow mocking in tests. 101 | */ 102 | public $httpClass = 'Ably\Http'; 103 | 104 | /** 105 | * @var bool defaults to false for clients with version < 1.2, otherwise true 106 | */ 107 | public $idempotentRestPublishing = true; 108 | 109 | /** 110 | * @var string a class that should be used for Auth 111 | * To allow mocking in tests. 112 | */ 113 | public $authClass = 'Ably\Auth'; 114 | 115 | 116 | private function isProductionEnvironment() { 117 | return empty($this->environment) || strcasecmp($this->environment, "production") == 0; 118 | } 119 | 120 | private function isDefaultPort() { 121 | return $this->tls ? $this->tlsPort == Defaults::$tlsPort : $this->port == Defaults::$port; 122 | } 123 | 124 | private function activePort() { 125 | return $this->tls ? $this->tlsPort : $this->port; 126 | } 127 | 128 | private function isDefaultRestHost() { 129 | return $this->restHost == Defaults::$restHost; 130 | } 131 | 132 | public function getPrimaryRestHost() { 133 | if ($this->isDefaultRestHost()) { 134 | return $this->isProductionEnvironment() ? $this->restHost : $this->environment.'-'.$this->restHost; 135 | } 136 | return $this->restHost; 137 | } 138 | 139 | public function getFallbackHosts() { 140 | $fallbacks = $this->fallbackHosts ?? []; 141 | if (empty($this->fallbackHosts) && $this->isDefaultRestHost() && $this->isDefaultPort()) { 142 | $fallbacks = $this->isProductionEnvironment() ? Defaults::$fallbackHosts : Defaults::getEnvironmentFallbackHosts($this->environment); 143 | } 144 | return $fallbacks; 145 | } 146 | 147 | public function getHostUrl($host) { 148 | return ($this-> tls ? 'https://' : 'http://') . $host. ':' .$this->activePort(); 149 | } 150 | 151 | public function __construct( $options = [] ) { 152 | parent::__construct( $options ); 153 | if (empty($this->restHost)) { 154 | $this->restHost = Defaults::$restHost; 155 | } 156 | if (empty($this->port)) { 157 | $this->port = Defaults::$port; 158 | } 159 | if (empty($this->tlsPort)) { 160 | $this->tlsPort = Defaults::$tlsPort; 161 | } 162 | if (empty($this->defaultTokenParams)) { 163 | $this->defaultTokenParams = new TokenParams(); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Models/DeviceDetails.php: -------------------------------------------------------------------------------- 1 | response = $ex->getResponse(); 53 | 54 | if ($ex->getCode() >= 50000) { // all fallback hosts failed, rethrow exception 55 | throw $ex; 56 | } 57 | } 58 | 59 | $this->parseHeaders($this->response['headers']); 60 | 61 | if ($this->statusCode < 200 | $this->statusCode >= 300) { 62 | $this->success = false; 63 | 64 | if ( isset($this->headers['X-Ably-Errorcode']) ) { 65 | $this->errorCode = $this->headers['X-Ably-Errorcode'] * 1; 66 | } 67 | 68 | if ( isset($this->headers['X-Ably-Errormessage']) ) { 69 | $this->errorMessage = $this->headers['X-Ably-Errormessage']; 70 | } 71 | } else { 72 | $this->success = true; 73 | } 74 | } 75 | 76 | private function parseHeaders( $headers ) { 77 | $headers = explode("\n", $headers); 78 | $http = array_shift($headers); 79 | $http = explode(' ', $http); 80 | 81 | $this->statusCode = $http[1] * 1; 82 | $this->headers = []; 83 | 84 | foreach($headers as $header) { 85 | if(!trim($header)) continue; 86 | list($key, $value) = explode(':', $header, 2); 87 | $key = trim($key); 88 | 89 | // Title-Case 90 | $key = preg_replace_callback('/\w+/', function ($match) { 91 | return ucfirst(strtolower($match[0])); 92 | }, $key); 93 | 94 | $this->headers[$key] = trim($value); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Models/Message.php: -------------------------------------------------------------------------------- 1 | name ) && $this->name ) { 21 | $msg->name = $this->name; 22 | } 23 | 24 | if ( isset( $this->connectionKey ) && $this->connectionKey ) { 25 | $msg->connectionKey = $this->connectionKey; 26 | } 27 | 28 | return $msg; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Models/PaginatedResult.php: -------------------------------------------------------------------------------- 1 | ably = $ably; 41 | $this->model = $model; 42 | $this->cipherParams = $cipherParams; 43 | $this->requestHeaders = $headers; 44 | $this->method = $method; 45 | $this->path = $path; 46 | 47 | $response = $this->ably->requestInternal( $method, $path, $headers, $params, $withHeaders = true ); 48 | $this->response = $response; 49 | 50 | $body = isset( $response['body'] ) ? $response['body'] : []; 51 | if ( is_object( $body ) ) $body = [ $body ]; 52 | 53 | if ( is_array( $body ) ) { 54 | 55 | if ( is_null( $model ) ) { 56 | $transformedArray = $body; 57 | } else { 58 | $transformedArray = []; 59 | 60 | foreach ($body as $data) { 61 | 62 | $instance = new $model; 63 | 64 | if ( !method_exists( $model, 'fromJSON' ) ) { 65 | throw new AblyException( 66 | 'Invalid model class provided: ' . $model . 67 | '. The model needs to implement fromJSON method.' 68 | ); 69 | } 70 | if ( !empty( $cipherParams ) ) { 71 | $instance->setCipherParams( $cipherParams ); 72 | } 73 | $instance->fromJSON( $data ); 74 | 75 | $transformedArray[] = $instance; 76 | } 77 | } 78 | 79 | $this->items = $transformedArray; 80 | $this->parsePaginationHeaders( $response['headers'] ); 81 | } 82 | } 83 | 84 | /** 85 | * Fetches the first page of results 86 | * @return PaginatedResult Returns self if the current page is the first 87 | */ 88 | public function first() { 89 | if (isset($this->paginationHeaders['first'])) { 90 | return new PaginatedResult( $this->ably, $this->model, $this->cipherParams, $this->method, $this->paginationHeaders['first'] ); 91 | } else { 92 | return null; 93 | } 94 | } 95 | 96 | /** 97 | * @return boolean Whether there is a first page 98 | */ 99 | public function hasFirst() { 100 | return $this->isPaginated() && isset($this->paginationHeaders['first']); 101 | } 102 | 103 | /** 104 | * Fetches the next page of results 105 | * @return PaginatedResult|null Next page or null if the current page is the last 106 | */ 107 | public function next() { 108 | if ($this->isPaginated() && isset($this->paginationHeaders['next'])) { 109 | return new PaginatedResult( $this->ably, $this->model, $this->cipherParams, $this->method, $this->paginationHeaders['next'] ); 110 | } else { 111 | return null; 112 | } 113 | } 114 | 115 | /** 116 | * @return boolean Whether there is a next page 117 | */ 118 | public function hasNext() { 119 | return $this->isPaginated() && isset($this->paginationHeaders['next']); 120 | } 121 | 122 | /** 123 | * @return boolean Whether the current page is the last, always true for single-page results 124 | */ 125 | public function isLast() { 126 | if (!$this->isPaginated() || !isset($this->paginationHeaders['next']) ) { 127 | return true; 128 | } else { 129 | return false; 130 | } 131 | } 132 | 133 | /** 134 | * @return boolean Whether the fetched results can be paginated (pagination headers received) 135 | */ 136 | public function isPaginated() { 137 | return is_array($this->paginationHeaders) && count($this->paginationHeaders); 138 | } 139 | 140 | /** 141 | * Parses HTTP headers for pagination links 142 | */ 143 | private function parsePaginationHeaders($headers) { 144 | $path = preg_replace('/\/[^\/]*$/', '/', $this->path); 145 | 146 | preg_match_all('/Link: *\<([^\>]*)\>; *rel="([^"]*)"/i', $headers, $matches, PREG_SET_ORDER); 147 | 148 | if (!$matches) return; 149 | 150 | $this->paginationHeaders = []; 151 | foreach ($matches as $m) { 152 | $link = $m[1]; 153 | $rel = $m[2]; 154 | 155 | if (substr($link, 0, 2) != './') { 156 | throw new AblyException( "Server error - only relative URLs are supported in pagination" ); 157 | } 158 | 159 | $link = explode('/', $link); 160 | $link = $path . end($link); 161 | 162 | $this->paginationHeaders[$rel] = $link; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Models/PresenceMessage.php: -------------------------------------------------------------------------------- 1 | deviceId && $this->clientId) { 26 | throw new \InvalidArgumentException( 27 | 'both device and client id given, only one expected' 28 | ); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Models/Stats.php: -------------------------------------------------------------------------------- 1 | clearFields(); 49 | } 50 | /** 51 | * Populates stats from JSON 52 | * @param string|stdClass $json JSON string or an already decoded object. 53 | * @throws AblyException 54 | */ 55 | public function fromJSON( $json ) { 56 | $this->clearFields(); 57 | 58 | if (is_object( $json )) { 59 | $obj = $json; 60 | } 61 | else if(is_array( $json) ){ 62 | $obj = (object)$json; 63 | } 64 | else { 65 | $obj = @json_decode($json); 66 | if (!$obj) { 67 | throw new AblyException( 'Invalid object or JSON encoded object' ); 68 | } 69 | } 70 | 71 | self::deepCopy( $obj, $this ); 72 | } 73 | 74 | protected static function deepCopy( $target, $dst ) { 75 | foreach ( $target as $key => $value ) { 76 | if ( is_object( $value )) { 77 | self::deepCopy( $value, $dst->$key ); 78 | } else { 79 | $dst->$key = $value; 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Sets all the public fields to null 86 | */ 87 | public function clearFields() { 88 | $this->all = new Stats\MessageTypes(); 89 | $this->inbound = new Stats\MessageTraffic(); 90 | $this->outbound = new Stats\MessageTraffic(); 91 | $this->persisted = new Stats\MessageTypes(); 92 | $this->connections = new Stats\ConnectionTypes(); 93 | $this->channels = new Stats\ResourceCount(); 94 | $this->apiRequests = new Stats\RequestCount(); 95 | $this->tokenRequests = new Stats\RequestCount(); 96 | $this->intervalId = ''; 97 | $this->intervalGranularity = ''; 98 | $this->intervalTime = 0; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Models/Stats/ConnectionTypes.php: -------------------------------------------------------------------------------- 1 | all = new ResourceCount(); 20 | $this->plain = new ResourceCount(); 21 | $this->tls = new ResourceCount(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Models/Stats/MessageCount.php: -------------------------------------------------------------------------------- 1 | count = 0; 18 | $this->data = 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Models/Stats/MessageTraffic.php: -------------------------------------------------------------------------------- 1 | all = new MessageTypes(); 22 | $this->realtime = new MessageTypes(); 23 | $this->rest = new MessageTypes(); 24 | $this->webhook = new MessageTypes(); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Models/Stats/MessageTypes.php: -------------------------------------------------------------------------------- 1 | all = new MessageCount(); 20 | $this->messages = new MessageCount(); 21 | $this->presence = new MessageCount(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Models/Stats/RequestCount.php: -------------------------------------------------------------------------------- 1 | failed = 0; 19 | $this->refused = 0; 20 | $this->succeeded = 0; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Models/Stats/ResourceCount.php: -------------------------------------------------------------------------------- 1 | mean = 0; 24 | $this->min = 0; 25 | $this->opened = 0; 26 | $this->peak = 0; 27 | $this->refused = 0; 28 | } 29 | } -------------------------------------------------------------------------------- /src/Models/Status/ChannelDetails.php: -------------------------------------------------------------------------------- 1 | name = $object->name; 32 | $channelDetails->channelId = $object->channelId; 33 | $channelDetails->status = ChannelStatus::from($object->status); 34 | return $channelDetails; 35 | } 36 | } 37 | 38 | /** 39 | * https://docs.ably.io/client-lib-development-guide/features/#CHS1 40 | */ 41 | class ChannelStatus 42 | { 43 | /** 44 | * @var bool 45 | */ 46 | public $isActive; 47 | 48 | /** 49 | * @var ChannelOccupancy 50 | */ 51 | public $occupancy; 52 | 53 | /** 54 | * @param \stdClass 55 | * @return ChannelStatus 56 | */ 57 | static function from($object) { 58 | $channelStatus = new self(); 59 | $channelStatus->isActive = $object->isActive; 60 | $channelStatus->occupancy = ChannelOccupancy::from($object->occupancy); 61 | return $channelStatus; 62 | } 63 | } 64 | 65 | /** 66 | * https://docs.ably.io/client-lib-development-guide/features/#CHO1 67 | */ 68 | class ChannelOccupancy 69 | { 70 | /** 71 | * @var ChannelMetrics 72 | */ 73 | public $metrics; 74 | 75 | /** 76 | * @param \stdClass 77 | * @return ChannelOccupancy 78 | */ 79 | static function from($object) { 80 | $occupancy = new self(); 81 | $occupancy->metrics = ChannelMetrics::from($object->metrics); 82 | return $occupancy; 83 | } 84 | } 85 | 86 | /** 87 | * https://docs.ably.io/client-lib-development-guide/features/#CHM1 88 | */ 89 | class ChannelMetrics 90 | { 91 | /** 92 | * @var int 93 | */ 94 | public $connections; 95 | 96 | /** 97 | * @var int 98 | */ 99 | public $presenceConnections; 100 | 101 | /** 102 | * @var int 103 | */ 104 | public $presenceMembers; 105 | 106 | /** 107 | * @var int 108 | */ 109 | public $presenceSubscribers; 110 | 111 | /** 112 | * @var int 113 | */ 114 | public $publishers; 115 | 116 | /** 117 | * @var int 118 | */ 119 | public $subscribers; 120 | 121 | /** 122 | * @param \stdClass 123 | * @return ChannelMetrics 124 | */ 125 | static function from($object) { 126 | $metrics = new self(); 127 | $metrics->connections = $object->connections; 128 | $metrics->presenceConnections= $object->presenceConnections; 129 | $metrics->presenceMembers = $object->presenceMembers; 130 | $metrics->presenceSubscribers= $object->presenceSubscribers; 131 | $metrics->publishers = $object->publishers; 132 | $metrics->subscribers = $object->subscribers; 133 | return $metrics; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Models/TokenDetails.php: -------------------------------------------------------------------------------- 1 | token = $options; 43 | } else { 44 | parent::__construct( $options ); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Models/TokenParams.php: -------------------------------------------------------------------------------- 1 | capability )) { 44 | $this->capability = (array) $this->capability; 45 | } 46 | 47 | if (is_array( $this->capability )) { 48 | ksort( $this->capability ); 49 | $this->capability = json_encode( $this->capability ); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/Models/TokenRequest.php: -------------------------------------------------------------------------------- 1 | $value ) { 25 | $this->$key = $value; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Presence.php: -------------------------------------------------------------------------------- 1 | ably = $ably; 18 | $this->channel = $channel; 19 | } 20 | 21 | /** 22 | * Retrieves channel's presence data 23 | * @param array $params Parameters to be sent with the request 24 | * @return PaginatedResult 25 | */ 26 | public function get( $params = [] ) { 27 | return new PaginatedResult( $this->ably, 'Ably\Models\PresenceMessage', $this->channel->getCipherParams(), 'GET', $this->channel->getPath() . '/presence', $params ); 28 | } 29 | 30 | /** 31 | * Retrieves channel's history of presence data 32 | * @param array $params Parameters to be sent with the request 33 | * @return PaginatedResult 34 | */ 35 | public function history( $params = [] ) { 36 | return new PaginatedResult( $this->ably, 'Ably\Models\PresenceMessage', $this->channel->getCipherParams(), 'GET', $this->channel->getPath() . '/presence/history', $params ); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Push.php: -------------------------------------------------------------------------------- 1 | ably = $ably; 15 | $this->admin = new PushAdmin( $ably ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/PushAdmin.php: -------------------------------------------------------------------------------- 1 | ably = $ably; 16 | $this->deviceRegistrations = new PushDeviceRegistrations( $ably ); 17 | $this->channelSubscriptions = new PushChannelSubscriptions ( $ably ); 18 | } 19 | 20 | public function publish ( array $recipient, array $data, $returnHeaders = false ) { 21 | if ( empty($recipient) ) { 22 | throw new \InvalidArgumentException('recipient is empty'); 23 | } 24 | 25 | if ( empty($data) ) { 26 | throw new \InvalidArgumentException('data is empty'); 27 | } 28 | 29 | $params = array_merge( $data, [ 'recipient' => $recipient ] ); 30 | $this->ably->post( '/push/publish', [], $params, $returnHeaders ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/PushChannelSubscriptions.php: -------------------------------------------------------------------------------- 1 | ably = $ably; 17 | } 18 | 19 | /** 20 | * Creates a new push channel subscription. Returns a 21 | * PushChannelSubscription object. 22 | * 23 | * @param array $subscription an array with the subscription information 24 | */ 25 | public function save ( $subscription ) { 26 | $obj = new PushChannelSubscription( $subscription ); 27 | $path = '/push/channelSubscriptions' ; 28 | $params = $obj->toArray(); 29 | $body = $this->ably->post( $path, [], $params ); 30 | $body = json_decode(json_encode($body), true); // Convert stdClass to array 31 | return new PushChannelSubscription ( $body ); 32 | } 33 | 34 | /** 35 | * Returns a PaginatedResult object with the list of PushChannelSubscription 36 | * objects, filtered by the given parameters. 37 | * 38 | * @param array $params the parameters used to filter the list 39 | */ 40 | public function list_ (array $params = []) { 41 | $path = '/push/channelSubscriptions'; 42 | return new PaginatedResult( $this->ably, 'Ably\Models\PushChannelSubscription', 43 | $cipher = false, 'GET', $path, $params ); 44 | } 45 | 46 | /** 47 | * Returns a PaginatedResult object with the list of channel names. 48 | * 49 | * @param array $params the parameters used to filter the list 50 | */ 51 | public function listChannels (array $params = []) { 52 | $path = '/push/channels'; 53 | return new PaginatedResult( $this->ably, NULL, 54 | $cipher = false, 'GET', $path, $params ); 55 | } 56 | 57 | /** 58 | * Removes the given channel subscription. 59 | * 60 | * @param string $subscription the id of the device 61 | */ 62 | public function remove ($subscription) { 63 | $params = $subscription->toArray(); 64 | $path = '/push/channelSubscriptions'; 65 | return $this->ably->delete( $path, [], $params, false ); 66 | } 67 | 68 | /** 69 | * Removes the channel subscriptions identified by the given parameters. 70 | * 71 | * @param string $subscription the id of the device 72 | */ 73 | public function removeWhere (array $params = []) { 74 | $path = '/push/channelSubscriptions'; 75 | return $this->ably->delete( $path, [], $params, false ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/PushDeviceRegistrations.php: -------------------------------------------------------------------------------- 1 | ably = $ably; 17 | } 18 | 19 | /** 20 | * Creates or updates the device. Returns a DeviceDetails object. 21 | * 22 | * @param array $device an array with the device information 23 | */ 24 | public function save ( $device ) { 25 | $deviceDetails = new DeviceDetails( $device ); 26 | $path = '/push/deviceRegistrations/' . $deviceDetails->id; 27 | $params = $deviceDetails->toArray(); 28 | $body = $this->ably->put( $path, [], $params ); 29 | $body = json_decode(json_encode($body), true); // Convert stdClass to array 30 | return new DeviceDetails ( $body ); 31 | } 32 | 33 | /** 34 | * Returns a DeviceDetails object if the device id is found or results in 35 | * a not found error if the device cannot be found. 36 | * 37 | * @param string $deviceId the id of the device 38 | */ 39 | public function get ($deviceId) { 40 | $path = '/push/deviceRegistrations/' . $deviceId; 41 | $body = $this->ably->get( $path ); 42 | $body = json_decode(json_encode($body), true); // Convert stdClass to array 43 | return new DeviceDetails ( $body ); 44 | } 45 | 46 | /** 47 | * Returns a PaginatedResult object with the list of DeviceDetails 48 | * objects, filtered by the given parameters. 49 | * 50 | * @param array $params the parameters used to filter the list 51 | */ 52 | public function list_ (array $params = []) { 53 | $path = '/push/deviceRegistrations'; 54 | return new PaginatedResult( $this->ably, 'Ably\Models\DeviceDetails', $cipher = false, 'GET', $path, $params ); 55 | } 56 | 57 | /** 58 | * Deletes the registered device identified by the given device id. 59 | * 60 | * @param string $device_id the id of the device 61 | */ 62 | public function remove ($deviceId, $returnHeaders = false) { 63 | $path = '/push/deviceRegistrations/' . $deviceId; 64 | return $this->ably->delete( $path, [], [], $returnHeaders ); 65 | } 66 | 67 | /** 68 | * Deletes the subscriptions identified by the given parameters. 69 | * 70 | * @param array $params the parameters that identify the subscriptions to remove 71 | */ 72 | public function removeWhere(array $params, $returnHeaders = false) { 73 | $path = '/push/deviceRegistrations'; 74 | return $this->ably->delete( $path, [], $params, $returnHeaders ); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Utils/Crypto.php: -------------------------------------------------------------------------------- 1 | getAlgorithmString(), $cipherParams->key, $raw, $cipherParams->iv ?? '' ); 19 | 20 | if ($ciphertext === false) { 21 | return false; 22 | } 23 | 24 | $iv = $cipherParams->iv; 25 | 26 | self::updateIV( $cipherParams ); 27 | 28 | return $iv.$ciphertext; 29 | } 30 | 31 | /** 32 | * Decrypts payload and returns original data. 33 | * @return string|false Original data as string or string containing binary data, false if unsuccessful. 34 | */ 35 | public static function decrypt( $payload, $cipherParams ) { 36 | $raw = defined( 'OPENSSL_RAW_DATA' ) ? OPENSSL_RAW_DATA : true; 37 | 38 | $ivLength = openssl_cipher_iv_length( $cipherParams->getAlgorithmString() ); 39 | $iv = substr( $payload, 0, $ivLength ); 40 | $ciphertext = substr( $payload, $ivLength ); 41 | return openssl_decrypt( $ciphertext, $cipherParams->getAlgorithmString(), $cipherParams->key, $raw, $iv ); 42 | } 43 | 44 | /** 45 | * Returns default encryption parameters. 46 | * @param $params Array Array containing optional cipher parameters. A `key` must be specified. 47 | * The key may be either a binary string or a base64 encoded string, in which case `'base64Key' => true` must be set. 48 | * `iv` can also be provided as binary or base64 string (`'base64IV' => true`), although you shouldn't need it in most cases. 49 | * @return CipherParams Default encryption parameters. 50 | */ 51 | public static function getDefaultParams( $params ) { 52 | if ( !isset( $params['key'] ) ) throw new AblyException ( 'No key specified.', 40003, 400 ); 53 | 54 | $cipherParams = new CipherParams(); 55 | 56 | if ( isset( $params['base64Key'] ) && $params['base64Key'] ) { 57 | $params['key'] = strtr( $params['key'], '_-', '/+' ); 58 | $params['key'] = base64_decode( $params['key'] ); 59 | } 60 | 61 | $cipherParams->key = $params['key']; 62 | $cipherParams->algorithm = isset( $params['algorithm'] ) ? $params['algorithm'] : 'aes'; 63 | 64 | if ($cipherParams->algorithm == 'aes') { 65 | $cipherParams->mode = isset( $params['mode'] ) ? $params['mode'] : 'cbc'; 66 | $cipherParams->keyLength = isset( $params['keyLength'] ) ? $params['keyLength'] : strlen( $cipherParams->key ) * 8; 67 | 68 | if ( !in_array( $cipherParams->keyLength, [ 128, 256 ] ) ) { 69 | throw new AblyException ( 'Unsupported keyLength. Only 128 and 256 bits are supported.', 40003, 400 ); 70 | } 71 | 72 | if ( $cipherParams->keyLength / 8 != strlen( $cipherParams->key ) ) { 73 | throw new AblyException ( 'keyLength does not match the actual key length.', 40003, 400 ); 74 | } 75 | 76 | if ( !in_array( $cipherParams->getAlgorithmString(), [ 'aes-128-cbc', 'aes-256-cbc' ] ) ) { 77 | throw new AblyException ( 'Unsupported cipher configuration "' . $cipherParams->getAlgorithmString() 78 | . '". The supported configurations are aes-128-cbc and aes-256-cbc', 40003, 400 ); 79 | } 80 | } else { 81 | if ( isset( $params['mode'] ) ) $cipherParams->mode = $params['mode']; 82 | if ( isset( $params['keyLength'] ) ) $cipherParams->keyLength = $params['keyLength']; 83 | 84 | if ( !$cipherParams->checkValidAlgorithm() ) { 85 | throw new AblyException( 'The specified algorithm "'.$cipherParams->getAlgorithmString().'"' 86 | . ' is not supported by openssl. See openssl_get_cipher_methods.', 40003, 400 ); 87 | } 88 | } 89 | 90 | if ( isset( $params['iv'] ) ) { 91 | $cipherParams->iv = $params['iv']; 92 | if ( isset( $params['base64Iv'] ) && $params['base64Iv'] ) { 93 | $cipherParams->iv = strtr( $cipherParams->iv, '_-', '/+' ); 94 | $cipherParams->iv = base64_decode( $cipherParams->iv ); 95 | } 96 | } else { 97 | $cipherParams->generateIV(); 98 | } 99 | 100 | return $cipherParams; 101 | } 102 | 103 | /** 104 | * Generates a random encryption key. 105 | * @param $keyLength|null The length of the key to be generated in bits, defaults to 256. 106 | */ 107 | public static function generateRandomKey( $keyLength = 256 ) { 108 | return openssl_random_pseudo_bytes( $keyLength / 8 ); 109 | } 110 | 111 | /** 112 | * Updates CipherParams' Initialization Vector by encrypting a fixed string 113 | * with current CipherParams state, thus randomizing it. 114 | */ 115 | protected static function updateIV( CipherParams $cipherParams ) { 116 | $raw = defined( 'OPENSSL_RAW_DATA' ) ? OPENSSL_RAW_DATA : true; 117 | 118 | $ivLength = strlen( $cipherParams->iv ?? '' ); 119 | 120 | $cipherParams->iv = openssl_encrypt( str_repeat( ' ', $ivLength ), $cipherParams->getAlgorithmString(), $cipherParams->key, $raw, $cipherParams->iv ?? '' ); 121 | $cipherParams->iv = substr( $cipherParams->iv, 0, $ivLength ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Utils/CurlWrapper.php: -------------------------------------------------------------------------------- 1 | commands[(int) $handle] = [ 14 | 'prefix' => 'curl', 15 | 'command' => ' ', 16 | 'url' => $url ? : '', 17 | ]; 18 | 19 | return $handle; 20 | } 21 | 22 | public function setOpt( $handle, $option, $value ) { 23 | if ( $option == CURLOPT_URL ) { 24 | $this->commands[(int) $handle]['url'] = $value; 25 | } else if ( $option == CURLOPT_POST && $value ) { 26 | $this->commands[(int) $handle]['command'] .= '-X POST '; 27 | } else if ( $option == CURLOPT_CUSTOMREQUEST ) { 28 | $this->commands[(int) $handle]['command'] .= '-X ' . $value . ' '; 29 | } else if ( $option == CURLOPT_POSTFIELDS ) { 30 | $this->commands[(int) $handle]['command'] .= '--data "'. str_replace( '"', '\"', $value ) .'" '; 31 | } else if ( $option == CURLOPT_HTTPHEADER ) { 32 | foreach($value as $header) { 33 | $this->commands[(int) $handle]['command'] .= '-H "' . str_replace( '"', '\"', $header ).'" '; 34 | } 35 | } 36 | 37 | return curl_setopt( $handle, $option, $value ); 38 | } 39 | 40 | public function exec( $handle ) { 41 | return curl_exec( $handle ); 42 | } 43 | 44 | public function close( $handle ) { 45 | unset( $this->commands[(int) $handle] ); 46 | 47 | return curl_close( $handle ); 48 | } 49 | 50 | public function getInfo( $handle ) { 51 | return curl_getinfo( $handle ); 52 | } 53 | 54 | public function getErrNo( $handle ) { 55 | return curl_errno( $handle ); 56 | } 57 | 58 | public function getError( $handle ) { 59 | return curl_error( $handle ); 60 | } 61 | 62 | public function getContentType( $handle ) { 63 | return curl_getinfo( $handle, CURLINFO_CONTENT_TYPE ); 64 | } 65 | 66 | /** 67 | * Retrieve a command pastable to terminal for a handle 68 | */ 69 | public function getCommand( $handle ) { 70 | return $this->commands[(int) $handle]['prefix'] . $this->commands[(int) $handle]['command'] . $this->commands[(int) $handle]['url']; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Utils/Miscellaneous.php: -------------------------------------------------------------------------------- 1 | 0; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Utils/Stringifiable.php: -------------------------------------------------------------------------------- 1 | getOptions(); 15 | self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ 16 | 'key' => self::$testApp->getAppKeyDefault()->string, 17 | ] ) ); 18 | } 19 | 20 | public static function tearDownAfterClass(): void { 21 | self::$testApp->release(); 22 | } 23 | 24 | /** 25 | * Batch publishes messages for given list of channels 26 | * RSC19 27 | * https://ably.com/docs/api/rest-api#batch-publish 28 | * @throws \Ably\Exceptions\AblyRequestException 29 | */ 30 | public function testBatchPublishMultipleChannelsUsingPostRequest() { 31 | 32 | $payload = array( 33 | "channels" => ["channel1", "channel2", "channel3", "channel4"], 34 | "messages" => array( 35 | "id" => "1", 36 | "data" => "foo" 37 | ) 38 | ); 39 | 40 | $batchPublishPaginatedResult = self::$ably->request("POST","/messages", [], $payload); 41 | $this->assertNotNull($batchPublishPaginatedResult); 42 | $this->assertEquals(201, $batchPublishPaginatedResult->statusCode); 43 | $this->assertTrue($batchPublishPaginatedResult->success); 44 | $this->assertNull($batchPublishPaginatedResult->errorCode); 45 | $this->assertNull($batchPublishPaginatedResult->errorMessage); 46 | $this->assertTrue( $batchPublishPaginatedResult->isLast(), 'Expected not to be the last page' ); 47 | 48 | if (self::$ably->options->useBinaryProtocol) { 49 | $this->assertEquals("application/x-msgpack", $batchPublishPaginatedResult->headers["Content-Type"]); 50 | } else { 51 | $this->assertEquals("application/json", $batchPublishPaginatedResult->headers["Content-Type"]); 52 | } 53 | $this->assertCount(4, $batchPublishPaginatedResult->items); 54 | foreach ($batchPublishPaginatedResult->items as $key=> $item) { 55 | $this->assertEquals("channel".($key + 1), $item->channel); 56 | $this->assertEquals(1, $item->messageId); 57 | } 58 | 59 | foreach (["channel1", "channel2", "channel3", "channel4"] as $channelName) { 60 | $channel = self::$ably->channel($channelName); 61 | $paginatedHistory = $channel->history(); 62 | foreach ($paginatedHistory->items as $msg) { 63 | $this->assertEquals("1", $msg->id); 64 | $this->assertEquals("foo", $msg->data); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/AssertsRegularExpressions.php: -------------------------------------------------------------------------------- 1 | getOptions(); 36 | self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ 37 | 'key' => self::$testApp->getAppKeyDefault()->string, 38 | 'idempotentRestPublishing' => true, 39 | ] ) ); 40 | } 41 | 42 | public static function tearDownAfterClass(): void { 43 | self::$testApp->release(); 44 | } 45 | 46 | /** 47 | * RSL1j 48 | */ 49 | public function testMessageSerialization() { 50 | $channel = self::$ably->channel( 'messageSerialization' ); 51 | 52 | $msg = new Message(); 53 | $msg->name = 'name'; 54 | $msg->data = 'data'; 55 | $msg->clientId = 'clientId'; 56 | $msg->id = 'foobar'; 57 | 58 | $body = $channel->__publish_request_body( $msg ); 59 | if(self::$ably->options->useBinaryProtocol) { 60 | $body = MessagePack::unpack($body); 61 | Miscellaneous::deepConvertArrayToObject($body); 62 | } 63 | else 64 | $body = json_decode($body); 65 | 66 | $this->assertTrue( property_exists($body, 'name') ); 67 | $this->assertTrue( property_exists($body, 'data') ); 68 | $this->assertTrue( property_exists($body, 'clientId') ); 69 | $this->assertTrue( property_exists($body, 'id') ); 70 | } 71 | 72 | /** 73 | * RSL1k1 74 | */ 75 | public function testIdempotentLibraryGenerated() { 76 | $channel = self::$ably->channel( 'idempotentLibraryGenerated' ); 77 | 78 | $msg = new Message(); 79 | $msg->name = 'name'; 80 | $msg->data = 'data'; 81 | 82 | $body = $channel->__publish_request_body( $msg ); 83 | if(self::$ably->options->useBinaryProtocol) { 84 | $body = MessagePack::unpack($body); 85 | Miscellaneous::deepConvertArrayToObject($body); 86 | } 87 | else 88 | $body = json_decode($body); 89 | 90 | $id = explode ( ":", $body->id); 91 | $this->assertEquals( count($id), 2); 92 | $this->assertGreaterThanOrEqual( 9, strlen(base64_decode($id[0])) ); 93 | $this->assertEquals( $id[1], "0"); 94 | 95 | $channel->publish($msg); 96 | $messages = $channel->history(); 97 | $this->assertEquals(1, count($messages->items)); 98 | $this->assertEquals($messages->items[0]->id, $msg->id); 99 | } 100 | 101 | /** 102 | * RSL1k2 103 | */ 104 | public function testIdempotentClientSupplied() { 105 | $channel = self::$ably->channel( 'idempotentClientSupplied' ); 106 | 107 | $msg = new Message(); 108 | $msg->name = 'name'; 109 | $msg->data = 'data'; 110 | $msg->id = 'foobar'; 111 | 112 | $body = $channel->__publish_request_body( $msg ); 113 | if(self::$ably->options->useBinaryProtocol) { 114 | $body = MessagePack::unpack($body); 115 | Miscellaneous::deepConvertArrayToObject($body); 116 | } 117 | else 118 | $body = json_decode($body); 119 | 120 | $this->assertEquals( $body->id, "foobar" ); 121 | 122 | $channel->publish($msg); 123 | $messages = $channel->history(); 124 | $this->assertEquals(count($messages->items), 1); 125 | $this->assertEquals($messages->items[0]->id, $msg->id); 126 | } 127 | 128 | /** 129 | * RSL1k3 130 | */ 131 | public function testIdempotentMixedIds() { 132 | $channel = self::$ably->channel( 'idempotentMixedIds' ); 133 | 134 | $messages = []; 135 | 136 | $msg = new Message(); 137 | $msg->name = 'name'; 138 | $msg->data = 'data'; 139 | $msg->id = 'foobar'; 140 | $messages[] = $msg; 141 | 142 | $msg = new Message(); 143 | $msg->name = 'name'; 144 | $msg->data = 'data'; 145 | $messages[] = $msg; 146 | 147 | $body = $channel->__publish_request_body( $messages ); 148 | if(self::$ably->options->useBinaryProtocol) { 149 | $body = MessagePack::unpack($body); 150 | Miscellaneous::deepConvertArrayToObject($body); 151 | } 152 | else 153 | $body = json_decode($body); 154 | 155 | $this->assertEquals( $body[0]->id, "foobar" ); 156 | $this->assertFalse( property_exists($body[1], 'id') ); 157 | 158 | $this->expectException(AblyRequestException::class); 159 | $channel->publish($messages); 160 | } 161 | 162 | /** 163 | * RSL1k4 164 | */ 165 | public function testIdempotentLibraryGeneratedPublish() { 166 | $ably = new AblyRest( array_merge( self::$defaultOptions, [ 167 | 'key' => self::$testApp->getAppKeyDefault()->string, 168 | 'idempotentRestPublishing' => true, 169 | 'httpClass' => 'tests\HttpMockIdempotent', 170 | 'fallbackHosts' => [ 171 | self::$ably->options->getPrimaryRestHost(), 172 | self::$ably->options->getPrimaryRestHost(), 173 | self::$ably->options->getPrimaryRestHost(), 174 | ], 175 | ] ) ); 176 | 177 | $channel = $ably->channel( 'idempotentLibraryGeneratedPublish' ); 178 | 179 | $msg = new Message(); 180 | $msg->name = 'name'; 181 | $msg->data = 'data'; 182 | 183 | $body = $channel->publish( $msg ); 184 | 185 | $messages = $channel->history(); 186 | $this->assertEquals( 1, count($messages->items)); 187 | } 188 | 189 | /** 190 | * RSL1k5 191 | */ 192 | public function testIdempotentClientSuppliedPublish() { 193 | $channel = self::$ably->channel( 'idempotentClientSuppliedPublish' ); 194 | 195 | $msg = new Message(); 196 | $msg->name = 'name'; 197 | $msg->data = 'data'; 198 | $msg->id = 'foobar'; 199 | 200 | $body = $channel->publish( $msg ); 201 | $body = $channel->publish( $msg ); 202 | $body = $channel->publish( $msg ); 203 | 204 | $messages = $channel->history(); 205 | $this->assertEquals( 1, count($messages->items)); 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /tests/ChannelStatusTest.php: -------------------------------------------------------------------------------- 1 | getOptions(); 18 | self::$ably = new AblyRest(array_merge(self::$defaultOptions, [ 19 | 'key' => self::$testApp->getAppKeyDefault()->string, 20 | ])); 21 | } 22 | 23 | public static function tearDownAfterClass(): void 24 | { 25 | self::$testApp->release(); 26 | } 27 | 28 | /** 29 | * @testdox RSL8, CHD1 30 | */ 31 | public function testChannelStatus() 32 | { 33 | $channel = self::$ably->channel('channel1'); 34 | $channelStatus = $channel->status(); 35 | self::assertNotNull($channelStatus->channelId); 36 | self::assertEquals("channel1", $channelStatus->channelId); 37 | self::assertEquals("channel1", $channelStatus->name); 38 | self::assertTrue($channelStatus->status->isActive); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/ClientOptionsTest.php: -------------------------------------------------------------------------------- 1 | getPrimaryRestHost()); 17 | self::assertTrue($clientOptions->tls); 18 | self::assertEquals(443, $clientOptions->tlsPort); 19 | $fallbackHosts = $clientOptions->getFallbackHosts(); 20 | sort($fallbackHosts); 21 | $this->assertEquals(Defaults::$fallbackHosts, $fallbackHosts); 22 | } 23 | 24 | /** 25 | * @testdox RSC15h 26 | */ 27 | public function testWithProductionEnvironment() { 28 | $clientOptions = new ClientOptions(); 29 | $clientOptions->environment = "Production"; 30 | self::assertEquals('rest.ably.io', $clientOptions->getPrimaryRestHost()); 31 | self::assertTrue($clientOptions->tls); 32 | self::assertEquals(443, $clientOptions->tlsPort); 33 | $fallbackHosts = $clientOptions->getFallbackHosts(); 34 | sort($fallbackHosts); 35 | $this->assertEquals(Defaults::$fallbackHosts, $fallbackHosts); 36 | } 37 | 38 | /** 39 | * @testdox RSC15g2 RTC1e 40 | */ 41 | public function testWithCustomEnvironment() { 42 | $clientOptions = new ClientOptions(); 43 | $clientOptions->environment = "sandbox"; 44 | self::assertEquals('sandbox-rest.ably.io', $clientOptions->getPrimaryRestHost()); 45 | self::assertTrue($clientOptions->tls); 46 | self::assertEquals(443, $clientOptions->tlsPort); 47 | $fallbackHosts = $clientOptions->getFallbackHosts(); 48 | sort($fallbackHosts); 49 | $this->assertEquals(Defaults::getEnvironmentFallbackHosts('sandbox'), $fallbackHosts); 50 | } 51 | 52 | /** 53 | * @testdox RSC11b RTN17b RTC1e 54 | */ 55 | public function testWithCustomEnvironmentAndNonDefaultPorts() { 56 | $clientOptions = new ClientOptions(); 57 | $clientOptions->environment = "local"; 58 | $clientOptions->port = 8080; 59 | $clientOptions->tlsPort = 8081; 60 | self::assertEquals('local-rest.ably.io', $clientOptions->getPrimaryRestHost()); 61 | self::assertEquals(8080, $clientOptions->port); 62 | self::assertEquals(8081, $clientOptions->tlsPort); 63 | self::assertTrue($clientOptions->tls); 64 | $fallbackHosts = $clientOptions->getFallbackHosts(); 65 | self::assertEmpty($fallbackHosts); 66 | } 67 | 68 | /** 69 | * @testdox RSC11 70 | */ 71 | public function testWithCustomRestHost() { 72 | $clientOptions = new ClientOptions(); 73 | $clientOptions->restHost = "test.org"; 74 | self::assertEquals('test.org', $clientOptions->getPrimaryRestHost()); 75 | self::assertEquals(80, $clientOptions->port); 76 | self::assertEquals(443, $clientOptions->tlsPort); 77 | self::assertTrue($clientOptions->tls); 78 | $fallbackHosts = $clientOptions->getFallbackHosts(); 79 | self::assertEmpty($fallbackHosts); 80 | } 81 | 82 | /** 83 | * @testdox RSC15g1 84 | */ 85 | public function testWithFallbacks() { 86 | $clientOptions = new ClientOptions(); 87 | $clientOptions->fallbackHosts = ["a.example.com", "b.example.com"]; 88 | self::assertEquals('rest.ably.io', $clientOptions->getPrimaryRestHost()); 89 | self::assertTrue($clientOptions->tls); 90 | self::assertEquals(443, $clientOptions->tlsPort); 91 | $fallbackHosts = $clientOptions->getFallbackHosts(); 92 | sort($fallbackHosts); 93 | self::assertEquals(["a.example.com", "b.example.com"], $fallbackHosts); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/DefaultsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expectedFallbackHosts, $fallbackHosts); 21 | } 22 | 23 | /** 24 | * @testdox RSC15i 25 | */ 26 | public function testEnvironmentFallbackHosts() { 27 | $expectedFallbackHosts = [ 28 | "sandbox-a-fallback.ably-realtime.com", 29 | "sandbox-b-fallback.ably-realtime.com", 30 | "sandbox-c-fallback.ably-realtime.com", 31 | "sandbox-d-fallback.ably-realtime.com", 32 | "sandbox-e-fallback.ably-realtime.com" 33 | ]; 34 | $fallbackHosts = Defaults::getEnvironmentFallbackHosts("sandbox"); 35 | $this->assertEquals($expectedFallbackHosts, $fallbackHosts); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/HostCacheTest.php: -------------------------------------------------------------------------------- 1 | put(Defaults::$restHost); 18 | self::assertEquals("rest.ably.io", $hostCache->get()); 19 | } 20 | 21 | /** 22 | * @testdox RSC15f 23 | */ 24 | public function testExpiredHost() { 25 | $hostCache = new HostCache(999); 26 | $hostCache->put(Defaults::$restHost); 27 | sleep(1); 28 | self::assertEquals("", $hostCache->get()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/HostTest.php: -------------------------------------------------------------------------------- 1 | getPreferredHost(); 28 | self::assertEquals("rest.ably.io", $prefHost); 29 | 30 | $fallbacks = iterator_to_array($restHosts->fallbackHosts($prefHost)); 31 | self::assertNotEquals($expectedFallbackHosts, $fallbacks); 32 | 33 | sort($fallbacks); 34 | self::assertEquals($expectedFallbackHosts, $fallbacks); 35 | } 36 | 37 | /** 38 | * @testdox RSC15a, RSA15e, RSC15f 39 | */ 40 | public function testFallbacksOtherThanPreferredHost() { 41 | $clientOptions = new ClientOptions(); 42 | $restHosts = new Host($clientOptions); 43 | // All expected hosts supposed to be tried upon 44 | $expectedFallbackHosts = [ 45 | "rest.ably.io", 46 | "a.ably-realtime.com", 47 | "c.ably-realtime.com", 48 | "d.ably-realtime.com", 49 | "e.ably-realtime.com", 50 | ]; 51 | 52 | $restHosts->setPreferredHost("b.ably-realtime.com"); 53 | 54 | $prefHost = $restHosts->getPreferredHost(); 55 | self::assertEquals("b.ably-realtime.com", $prefHost); 56 | 57 | $fallbacks = iterator_to_array($restHosts->fallbackHosts($prefHost)); 58 | self::assertEquals("rest.ably.io", $fallbacks[0]); 59 | 60 | sort($fallbacks); 61 | sort($expectedFallbackHosts); 62 | self::assertEquals($expectedFallbackHosts, $fallbacks); 63 | } 64 | 65 | /** 66 | * @testdox RSC15a 67 | */ 68 | public function testGetAllFallbacksWithNoPreferredHost() { 69 | $clientOptions = new ClientOptions(); 70 | $restHosts = new Host($clientOptions); 71 | // All expected hosts supposed to be tried upon 72 | $expectedFallbackHosts = [ 73 | "rest.ably.io", 74 | "b.ably-realtime.com", 75 | "a.ably-realtime.com", 76 | "c.ably-realtime.com", 77 | "d.ably-realtime.com", 78 | "e.ably-realtime.com", 79 | ]; 80 | 81 | $fallbacks = iterator_to_array($restHosts->fallbackHosts("")); 82 | self::assertEquals("rest.ably.io", $fallbacks[0]); 83 | 84 | sort($fallbacks); 85 | sort($expectedFallbackHosts); 86 | self::assertEquals($expectedFallbackHosts, $fallbacks); 87 | } 88 | 89 | /** 90 | * @testdox RSC15e 91 | */ 92 | public function testGetPrimaryHostIfNothingIsCached() { 93 | $clientOptions = new ClientOptions(); 94 | $restHosts = new Host($clientOptions); 95 | $prefHost = $restHosts->getPreferredHost(); 96 | self::assertEquals("rest.ably.io", $prefHost); 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /tests/HttpTest.php: -------------------------------------------------------------------------------- 1 | getOptions(); 22 | self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ 23 | 'key' => self::$testApp->getAppKeyDefault()->string, 24 | ] ) ); 25 | } 26 | 27 | public static function tearDownAfterClass(): void { 28 | self::$testApp->release(); 29 | } 30 | 31 | /** 32 | * Verify that API version is sent in HTTP requests 33 | */ 34 | public function testVersionHeaderPresence() { 35 | $opts = [ 36 | 'key' => 'fake.key:totallyFake', 37 | 'httpClass' => 'tests\HttpMock', 38 | ]; 39 | $ably = new AblyRest( $opts ); 40 | $ably->time(); // make a request 41 | 42 | $curlParams = $ably->http->getCurlLastParams(); 43 | $this->assertContains( 'X-Ably-Version: ' . Defaults::API_VERSION, $curlParams[CURLOPT_HTTPHEADER], 44 | 'Expected Ably version header in HTTP request' ); 45 | 46 | AblyRest::setLibraryFlavourString(); 47 | } 48 | 49 | /** 50 | * Verify proper agent header is set as per RSC7d 51 | */ 52 | public function testAblyAgentHeader() { 53 | $opts = [ 54 | 'key' => 'fake.key:totallyFake', 55 | 'httpClass' => 'tests\HttpMock', 56 | ]; 57 | $ably = new AblyRest( $opts ); 58 | $ably->time(); // make a request 59 | $curlParams = $ably->http->getCurlLastParams(); 60 | 61 | $expectedAgentHeader = 'ably-php/'.Defaults::LIB_VERSION.' '.'php/'.Miscellaneous::getNumeric(phpversion()); 62 | $this->assertContains( 'Ably-Agent: '. $expectedAgentHeader, $curlParams[CURLOPT_HTTPHEADER], 63 | 'Expected Ably agent header in HTTP request' ); 64 | 65 | $ably = new AblyRest( $opts ); 66 | $ably->time(); // make a request 67 | 68 | $curlParams = $ably->http->getCurlLastParams(); 69 | 70 | $this->assertContains( 'Ably-Agent: '. $expectedAgentHeader, $curlParams[CURLOPT_HTTPHEADER], 71 | 'Expected Ably agent header in HTTP request' ); 72 | 73 | AblyRest::setLibraryFlavourString( 'laravel'); 74 | AblyRest::setAblyAgentHeader('customLib', '2.3.5'); 75 | $ably = new AblyRest( $opts ); 76 | $ably->time(); // make a request 77 | 78 | $curlParams = $ably->http->getCurlLastParams(); 79 | 80 | $expectedAgentHeader = 'ably-php/'.Defaults::LIB_VERSION.' '.'php/'.Miscellaneous::getNumeric(phpversion()).' laravel'.' customLib/2.3.5'; 81 | $this->assertContains( 'Ably-Agent: '. $expectedAgentHeader, $curlParams[CURLOPT_HTTPHEADER], 82 | 'Expected Ably agent header in HTTP request' ); 83 | 84 | AblyRest::setLibraryFlavourString(); 85 | } 86 | 87 | /** 88 | * Verify that GET requests are encoded properly (using requestToken) 89 | */ 90 | public function testGET() { 91 | $authParams = [ 92 | 'param1' => '&?#', 93 | 'param2' => 'x', 94 | ]; 95 | $tokenParams = [ 96 | 'clientId' => 'test', 97 | ]; 98 | 99 | $ably = new AblyRest( [ 100 | 'key' => 'fake.key:totallyFake', 101 | 'authUrl' => 'http://test.test/tokenRequest', 102 | 'authParams' => $authParams, 103 | 'authMethod' => 'GET', 104 | 'httpClass' => 'tests\HttpMock', 105 | ] ); 106 | 107 | $expectedParams = array_merge( $authParams, $tokenParams ); 108 | 109 | $ably->auth->requestToken( $tokenParams ); 110 | 111 | $curlParams = $ably->http->getCurlLastParams(); 112 | 113 | $this->assertEquals( 'http://test.test/tokenRequest?'.http_build_query($expectedParams), $curlParams[CURLOPT_URL], 'Expected URL to contain encoded GET parameters' ); 114 | } 115 | 116 | 117 | /** 118 | * Verify that POST requests are encoded properly (using requestToken) 119 | */ 120 | public function testPOST() { 121 | $authParams = [ 122 | 'param1' => '&?#', 123 | 'param2' => 'x', 124 | ]; 125 | $tokenParams = [ 126 | 'clientId' => 'test', 127 | ]; 128 | 129 | $ably = new AblyRest( [ 130 | 'key' => 'fake.key:totallyFake', 131 | 'authUrl' => 'http://test.test/tokenRequest', 132 | 'authParams' => $authParams, 133 | 'authMethod' => 'POST', 134 | 'httpClass' => 'tests\HttpMock', 135 | ] ); 136 | 137 | $expectedParams = array_merge( $authParams, $tokenParams ); 138 | 139 | $ably->auth->requestToken( $tokenParams ); 140 | 141 | $curlParams = $ably->http->getCurlLastParams(); 142 | 143 | $this->assertEquals( 'http://test.test/tokenRequest', $curlParams[CURLOPT_URL], 144 | 'Expected URL to match authUrl' ); 145 | $this->assertEquals( http_build_query($expectedParams), $curlParams[CURLOPT_POSTFIELDS], 146 | 'Expected POST params to contain encoded params' ); 147 | } 148 | 149 | /** 150 | * RSC19 Test basic AblyRest::request functionality 151 | */ 152 | public function testRequestBasic() { 153 | $ably = self::$ably; 154 | 155 | $msg = (object) [ 156 | 'name' => 'testEvent', 157 | 'data' => 'testPayload', 158 | ]; 159 | 160 | $res = $ably->request('POST', '/channels/persisted:test/messages', [], $msg ); 161 | 162 | $this->assertTrue($res->success, 'Expected sending a message via custom request to succeed'); 163 | $this->assertLessThan(300, $res->statusCode, 'Expected statusCode < 300'); 164 | $this->assertEmpty($res->errorCode, 'Expected empty errorCode'); 165 | $this->assertEmpty($res->errorMessage, 'Expected empty errorMessage'); 166 | 167 | $res2 = $ably->request('GET', '/channels/persisted:test/messages'); 168 | 169 | $this->assertTrue($res2->success, 'Expected retrieving the message via custom request to succeed'); 170 | $this->assertLessThan(300, $res2->statusCode, 'Expected statusCode < 300'); 171 | $this->assertArrayHasKey('Content-Type', $res2->headers, 172 | 'Expected headers to be an array containing key `Content-Type`'); 173 | $this->assertEquals(1, count($res2->items), 'Expected to receive 1 message'); 174 | $this->assertEquals($msg->name, $res2->items[0]->name, 175 | 'Expected to receive matching message contents'); 176 | 177 | $res3 = $ably->request('GET', '/this-does-not-exist'); 178 | 179 | $this->assertEquals(404, $res3->statusCode, 'Expected statusCode 404'); 180 | $this->assertEquals(40400, $res3->errorCode, 'Expected errorCode 40400'); 181 | $this->assertNotEmpty($res3->errorMessage, 'Expected errorMessage to be set'); 182 | $this->assertArrayHasKey('X-Ably-Errorcode', $res3->headers, 183 | 'Expected X-Ably-Errorcode header to be present'); 184 | $this->assertArrayHasKey('X-Ably-Errormessage', $res3->headers, 185 | 'Expected X-Ably-Errormessage header to be present'); 186 | } 187 | 188 | /** 189 | * RSC19 - Test that Response handles various returned structures properly 190 | */ 191 | public function testRequestReturnValues() { 192 | $ably = new AblyRest( [ 193 | 'key' => 'fake.key:totallyFake', 194 | 'httpClass' => 'tests\HttpMockReturnData', 195 | ] ); 196 | 197 | // array of objects 198 | $ably->http->setResponseJSONString('[{"test":"one"},{"test":"two"},{"test":"three"}]'); 199 | $res1 = $ably->request('GET', '/get_test_json'); 200 | $this->assertEquals('[{"test":"one"},{"test":"two"},{"test":"three"}]', json_encode($res1->items)); 201 | 202 | // array with single object 203 | $ably->http->setResponseJSONString('[{"test":"yes"}]'); 204 | $res2 = $ably->request('GET', '/get_test_json'); 205 | $this->assertEquals('[{"test":"yes"}]', json_encode($res2->items)); 206 | 207 | // single object - should be returned as array with single object 208 | $ably->http->setResponseJSONString('{"test":"yes"}'); 209 | $res3 = $ably->request('GET', '/get_test_json'); 210 | $this->assertEquals('[{"test":"yes"}]', json_encode($res3->items)); 211 | 212 | // not an object or array - should be returned as empty array 213 | $ably->http->setResponseJSONString('"invalid"'); 214 | $res4 = $ably->request('GET', '/get_test_json'); 215 | $this->assertEquals('[]', json_encode($res4->items)); 216 | } 217 | } 218 | 219 | 220 | class CurlWrapperMock extends CurlWrapper { 221 | public $lastParams; 222 | 223 | public function init( $url = null ) { 224 | $this->lastParams = [ CURLOPT_URL => $url ]; 225 | 226 | return parent::init( $url ); 227 | } 228 | 229 | public function setOpt( $handle, $option, $value ) { 230 | $this->lastParams[$option] = $value; 231 | 232 | return parent::setOpt( $handle, $option, $value ); 233 | } 234 | 235 | /** 236 | * Returns a fake token when tere is `/tokenRequest` in the URL, otherwise returns current time 237 | * wrapped in an array (as does GET /time) without actually making the request. 238 | */ 239 | public function exec( $handle ) { 240 | if (preg_match('/\\/tokenRequest/', $this->lastParams[CURLOPT_URL])) { 241 | return 'tokentokentoken'; 242 | } 243 | 244 | return '[' . round( microtime( true ) * 1000 ) . ']'; 245 | } 246 | 247 | public function getInfo( $handle ) { 248 | return [ 249 | 'http_code' => 200, 250 | 'header_size' => 0, 251 | ]; 252 | } 253 | } 254 | 255 | 256 | class HttpMock extends Http { 257 | public function __construct() { 258 | parent::__construct(new \Ably\Models\ClientOptions()); 259 | $this->curl = new CurlWrapperMock(); 260 | } 261 | 262 | public function getCurlLastParams() { 263 | return $this->curl->lastParams; 264 | } 265 | } 266 | 267 | 268 | class HttpMockReturnData extends Http { 269 | private $responseStr = ''; 270 | public function setResponseJSONString($str) { 271 | $this->responseStr = $str; 272 | } 273 | 274 | public function request($method, $url, $headers = [], $params = []) { 275 | 276 | if ($method == 'GET' && self::endsWith($url, '/get_test_json')) { 277 | return [ 278 | 'headers' => 'HTTP/1.1 200 OK'."\n", 279 | 'body' => json_decode($this->responseStr), 280 | ]; 281 | } else { 282 | return [ 283 | 'headers' => 'HTTP/1.1 404 Not found'."\n", 284 | 'body' => '', 285 | ]; 286 | } 287 | } 288 | 289 | private static function endsWith($haystack, $needle) { 290 | return substr($haystack, -strlen($needle)) == $needle; 291 | } 292 | } 293 | 294 | -------------------------------------------------------------------------------- /tests/LogTest.php: -------------------------------------------------------------------------------- 1 | 'fake.key:totallyFake' 14 | ] ); 15 | } 16 | 17 | private function logMessages() { 18 | Log::v('This is a test verbose message.'); 19 | Log::d('This is a test debug message.'); 20 | Log::w('This is a test warning.'); 21 | Log::e('This is a test error.'); 22 | } 23 | 24 | /** 25 | * Test if logger uses warning level as default 26 | */ 27 | public function testLogDefault() { 28 | $out = ''; 29 | 30 | $opts = [ 31 | 'key' => 'fake.key:veryFake', 32 | 'logHandler' => function( $level, $args ) use ( &$out ) { 33 | $out .= $args[0] . "\n"; 34 | }, 35 | ]; 36 | $ably = new AblyRest( $opts ); 37 | 38 | $this->logMessages(); 39 | 40 | $this->assertIsInt( strpos($out, 'This is a test warning.'), 'Expected warning level to be logged.' ); 41 | $this->assertIsInt( strpos($out, 'This is a test error.'), 'Expected error level to be logged.' ); 42 | $this->assertFalse( strpos($out, 'This is a test verbose message.'), 43 | 'Expected verbose level NOT to be logged.' ); 44 | $this->assertFalse( strpos($out, 'This is a test debug message.'), 'Expected debug level NOT to be logged.' ); 45 | } 46 | 47 | /** 48 | * Test verbose log level with a handler 49 | */ 50 | public function testLogVerbose() { 51 | $out = ''; 52 | 53 | $opts = [ 54 | 'key' => 'fake.key:veryFake', 55 | 'logLevel' => Log::VERBOSE, 56 | 'logHandler' => function( $level, $args ) use ( &$out ) { 57 | $out .= $args[0] . "\n"; 58 | }, 59 | ]; 60 | 61 | $ably = new AblyRest( $opts ); 62 | $this->logMessages(); 63 | 64 | $this->assertIsInt( strpos($out, 'This is a test warning.'), 'Expected warning level to be logged.' ); 65 | $this->assertIsInt( strpos($out, 'This is a test error.'), 'Expected error level to be logged.' ); 66 | $this->assertIsInt( strpos($out, 'This is a test verbose message.'), 'Expected verbose level to be logged.' ); 67 | $this->assertIsInt( strpos($out, 'This is a test debug message.'), 'Expected debug level to be logged.' ); 68 | } 69 | 70 | /** 71 | * Test log level == NONE 72 | */ 73 | public function testLogNone() { 74 | $called = false; 75 | $opts = [ 76 | 'key' => 'fake.key:veryFake', 77 | 'logLevel' => Log::NONE, 78 | 'logHandler' => function( $level, $args ) use ( &$called ) { 79 | $called = true; 80 | }, 81 | ]; 82 | 83 | $ably = new AblyRest( $opts ); 84 | $this->logMessages(); 85 | $this->assertFalse( $called, 'Log handler incorrectly called' ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/MiscellaneousTest.php: -------------------------------------------------------------------------------- 1 | data = $msg; 17 | Miscellaneous::deepConvertObjectToArray($msg_nested); 18 | 19 | //Test if message_nested is an array 20 | $this->AssertIsArray($msg_nested, "Expected msg_nested to be an array"); 21 | 22 | //Test if message_nested contains all keys 23 | $this->AssertArrayHasKey("name", $msg_nested, "Expected msg_nested to be an array containing key `name`"); 24 | $this->AssertArrayHasKey("connectionKey", $msg_nested, "Expected msg_nested to be an array containing key `connectionKey`"); 25 | $this->AssertArrayHasKey("clientId", $msg_nested, "Expected msg_nested to be an array containing key `clientId`"); 26 | $this->AssertArrayHasKey("connectionId", $msg_nested, "Expected msg_nested to be an array containing key `connectionId`"); 27 | $this->AssertArrayHasKey("data", $msg_nested, "Expected msg_nested to be an array containing key `data`"); 28 | $this->AssertArrayHasKey("encoding", $msg_nested, "Expected msg_nested to be an array containing key `data`"); 29 | $this->AssertArrayHasKey("extras", $msg_nested, "Expected msg_nested to be an array containing key `data`"); 30 | $this->AssertArrayHasKey("id", $msg_nested, "Expected msg_nested to be an array containing key `data`"); 31 | $this->AssertArrayHasKey("timestamp", $msg_nested, "Expected msg_nested to be an array containing key `data`"); 32 | $this->AssertArrayHasKey("originalData", $msg_nested, "Expected msg_nested to be an array containing key `data`"); 33 | $this->AssertArrayHasKey("originalEncoding", $msg_nested, "Expected msg_nested to be an array containing key `data`"); 34 | 35 | 36 | //Test if message_nested["data"] is an array 37 | $this->AssertIsArray($msg_nested, "Expected msg_nested to be an array"); 38 | 39 | //Test if message_nested["data"] contains all keys 40 | $this->AssertArrayHasKey("name", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `name`"); 41 | $this->AssertArrayHasKey("connectionKey", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `connectionKey`"); 42 | $this->AssertArrayHasKey("clientId", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `clientId`"); 43 | $this->AssertArrayHasKey("connectionId", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `connectionId`"); 44 | $this->AssertArrayHasKey("data", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `data`"); 45 | $this->AssertArrayHasKey("encoding", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `data`"); 46 | $this->AssertArrayHasKey("extras", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `data`"); 47 | $this->AssertArrayHasKey("id", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `data`"); 48 | $this->AssertArrayHasKey("timestamp", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `data`"); 49 | $this->AssertArrayHasKey("originalData", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `data`"); 50 | $this->AssertArrayHasKey("originalEncoding", $msg_nested["data"], "Expected msg_nested['data'] to be an array containing key `data`"); 51 | 52 | 53 | } 54 | 55 | public function testDeepConvertObjectToArrayFromArray(){ 56 | $object = new Message(); 57 | $array_test = [ 58 | "name" => "test", 59 | "foo" => "bar", 60 | "object" => $object 61 | ]; 62 | Miscellaneous::deepConvertObjectToArray($array_test); 63 | //Test if $array_test is an array 64 | $this->AssertIsArray($array_test, "Expected msg_nested to be an array"); 65 | 66 | //Test if $array_test contains all keys 67 | $this->AssertArrayHasKey("name", $array_test, "Expected msg_nested to be an array containing key `name`"); 68 | $this->AssertArrayHasKey("foo", $array_test, "Expected msg_nested to be an array containing key `connectionKey`"); 69 | $this->AssertArrayHasKey("object", $array_test, "Expected msg_nested to be an array containing key `clientId`"); 70 | 71 | //Test if $array_test["object"] is an array 72 | $this->AssertIsArray($array_test["object"], "Expected array_test['object'] to be an array"); 73 | 74 | //Test if $array_test["object"] contains all keys 75 | $this->AssertArrayHasKey("name", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `name`"); 76 | $this->AssertArrayHasKey("connectionKey", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `connectionKey`"); 77 | $this->AssertArrayHasKey("clientId", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `clientId`"); 78 | $this->AssertArrayHasKey("connectionId", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `connectionId`"); 79 | $this->AssertArrayHasKey("data", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `data`"); 80 | $this->AssertArrayHasKey("encoding", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `data`"); 81 | $this->AssertArrayHasKey("extras", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `data`"); 82 | $this->AssertArrayHasKey("id", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `data`"); 83 | $this->AssertArrayHasKey("timestamp", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `data`"); 84 | $this->AssertArrayHasKey("originalData", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `data`"); 85 | $this->AssertArrayHasKey("originalEncoding", $array_test["object"], "Expected msg_nested['data'] to be an array containing key `data`"); 86 | 87 | 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /tests/PresenceTest.php: -------------------------------------------------------------------------------- 1 | getOptions(); 23 | self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ 24 | 'key' => self::$testApp->getAppKeyDefault()->string, 25 | ] ) ); 26 | 27 | $fixture = self::$testApp->getFixture(); 28 | self::$presenceFixture = $fixture->post_apps->channels[0]->presence; 29 | 30 | $cipherParams = Crypto::getDefaultParams([ 31 | 'key' => $fixture->cipher->key, 32 | 'algorithm' => $fixture->cipher->algorithm, 33 | 'keyLength' => $fixture->cipher->keylength, 34 | 'mode' => $fixture->cipher->mode, 35 | 'iv' => $fixture->cipher->iv, 36 | 'base64Key' => true, 37 | 'base64Iv' => true, 38 | ]); 39 | 40 | $options = [ 41 | 'cipher' => $cipherParams, 42 | ]; 43 | 44 | self::$channel = self::$ably->channel('persisted:presence_fixtures', $options); 45 | } 46 | 47 | public static function tearDownAfterClass(): void { 48 | self::$testApp->release(); 49 | } 50 | 51 | /** 52 | * Compare presence data with fixture 53 | */ 54 | public function testComparePresenceDataWithFixture() { 55 | $presence = self::$channel->presence->get(); 56 | 57 | // verify presence existence and count 58 | $this->assertNotNull( $presence, 'Expected non-null presence data' ); 59 | $this->assertEquals( 6, count($presence->items), 'Expected 6 presence messages' ); 60 | 61 | // verify presence contents 62 | $fixturePresenceMap = []; 63 | foreach (self::$presenceFixture as $entry) { 64 | $fixturePresenceMap[$entry->clientId] = $entry->data; 65 | } 66 | 67 | foreach ($presence->items as $entry) { 68 | $this->assertNotNull( $entry->clientId, 'Expected non-null client ID' ); 69 | $this->assertTrue( 70 | array_key_exists($entry->clientId, $fixturePresenceMap), 71 | 'Expected presence contents to match' 72 | ); 73 | if(self::$ably->options->useBinaryProtocol && $entry->clientId === 'client_encoded'){ 74 | $this->assertEquals( 75 | base64_decode($fixturePresenceMap[$entry->clientId]), $entry->originalData, 76 | 'Expected encrypted presence contents values to be equal match' 77 | ); 78 | } 79 | else { 80 | $this->assertEquals( 81 | $fixturePresenceMap[$entry->clientId], $entry->originalData, 82 | 'Expected presence contents values to be equal match' 83 | ); 84 | } 85 | } 86 | 87 | // verify limit / pagination 88 | $firstPage = self::$channel->presence->get( [ 'limit' => 3, 'direction' => 'forwards' ] ); 89 | 90 | $this->assertEquals( 3, count($firstPage->items), 'Expected 3 presence entries on the 1st page' ); 91 | 92 | $nextPage = $firstPage->next(); 93 | $this->assertEquals( 3, count($nextPage->items), 'Expected 3 presence entries on the 2nd page' ); 94 | $this->assertTrue( $nextPage->isLast(), 'Expected last page' ); 95 | } 96 | 97 | /** 98 | * Compare presence history with fixture 99 | */ 100 | public function testComparePresenceHistoryWithFixture() { 101 | $history = self::$channel->presence->history(); 102 | 103 | // verify history existence and count 104 | $this->assertNotNull( $history, 'Expected non-null history data' ); 105 | $this->assertEquals( 6, count($history->items), 'Expected 6 history entries' ); 106 | 107 | // verify history contents 108 | $fixtureHistoryMap = []; 109 | foreach (self::$presenceFixture as $entry) { 110 | $fixtureHistoryMap[$entry->clientId] = $entry->data; 111 | } 112 | 113 | foreach ($history->items as $entry) { 114 | $this->assertNotNull( $entry->clientId, 'Expected non-null client ID' ); 115 | $this->assertTrue( 116 | isset($fixtureHistoryMap[$entry->clientId]) && $fixtureHistoryMap[$entry->clientId] == $entry->originalData, 117 | 'Expected presence contents to match' 118 | ); 119 | } 120 | 121 | // verify limit / pagination - forwards 122 | $firstPage = self::$channel->presence->history( [ 'limit' => 3, 'direction' => 'forwards' ] ); 123 | 124 | $this->assertEquals( 3, count($firstPage->items), 'Expected 3 presence entries' ); 125 | 126 | $nextPage = $firstPage->next(); 127 | 128 | $this->assertEquals( self::$presenceFixture[0]->clientId, $firstPage->items[0]->clientId, 'Expected least recent presence activity to be the first' ); 129 | $this->assertEquals( self::$presenceFixture[5]->clientId, $nextPage->items[2]->clientId, 'Expected most recent presence activity to be the last' ); 130 | 131 | // verify limit / pagination - backwards (default) 132 | $firstPage = self::$channel->presence->history( [ 'limit' => 3 ] ); 133 | 134 | $this->assertEquals( 3, count($firstPage->items), 'Expected 3 presence entries' ); 135 | 136 | $nextPage = $firstPage->next(); 137 | 138 | $this->assertEquals( self::$presenceFixture[5]->clientId, $firstPage->items[0]->clientId, 'Expected most recent presence activity to be the first' ); 139 | $this->assertEquals( self::$presenceFixture[0]->clientId, $nextPage->items[2]->clientId, 'Expected least recent presence activity to be the last' ); 140 | } 141 | 142 | /* 143 | * Check whether time range queries work properly 144 | */ 145 | public function testPresenceHistoryTimeRange() { 146 | // ensure some time has passed since mock presence data was sent 147 | $delay = 1000; // sleep for 1000ms 148 | usleep($delay * 1000); // in microseconds 149 | 150 | $timeOffset = self::$ably->time() - Miscellaneous::systemTime(); 151 | $now = $timeOffset + Miscellaneous::systemTime(); 152 | 153 | // test with start parameter 154 | try { 155 | $history = self::$channel->presence->history( [ 'start' => $now ] ); 156 | $this->assertEquals( 0, count($history->items), 'Expected 0 presence messages' ); 157 | } catch (AblyRequestException $e) { 158 | $this->fail( 'Start parameter - ' . $e->getMessage() . ', HTTP code: ' . $e->getCode() ); 159 | } 160 | 161 | // test with end parameter 162 | try { 163 | $history = self::$channel->presence->history( [ 'end' => $now ] ); 164 | $this->assertEquals( 6, count($history->items), 'Expected 6 presence messages' ); 165 | } catch (AblyRequestException $e) { 166 | $this->fail( 'End parameter - ' . $e->getMessage() . ', HTTP code: ' . $e->getCode() ); 167 | } 168 | 169 | // test with both start and end parameters - time range: ($now - 500ms) ... $now 170 | try { 171 | $history = self::$channel->presence->history( [ 'start' => $now - ($delay / 2), 'end' => $now ] ); 172 | $this->assertEquals( 0, count($history->items), 'Expected 0 presence messages' ); 173 | } catch (AblyRequestException $e) { 174 | $this->fail( 'Start + end parameter - ' . $e->getMessage() . ', HTTP code: ' . $e->getCode() ); 175 | } 176 | 177 | // test ISO 8601 date format 178 | try { 179 | $history = self::$channel->presence->history( [ 'end' => gmdate('c', intval($now / 1000)) ] ); 180 | $this->assertEquals( 6, count($history->items), 'Expected 6 presence messages' ); 181 | } catch (AblyRequestException $e) { 182 | $this->fail( 'ISO format: ' . $e->getMessage() . ', HTTP code: ' . $e->getCode() ); 183 | } 184 | } 185 | 186 | /** 187 | * Compare presence data with fixture 188 | */ 189 | public function testComparePresenceDataWithFixtureEncrypted() { 190 | $presence = self::$channel->presence->get(); 191 | 192 | // verify presence existence and count 193 | $this->assertNotNull( $presence, 'Expected non-null presence data' ); 194 | $this->assertEquals( 6, count($presence->items), 'Expected 6 presence messages' ); 195 | 196 | // verify presence contents 197 | $messageMap = []; 198 | foreach ($presence->items as $entry) { 199 | $messageMap[$entry->clientId] = $entry->data; 200 | } 201 | 202 | $this->assertEquals( $messageMap['client_decoded'], $messageMap['client_encoded'], 'Expected decrypted and sample data to match' ); 203 | } 204 | 205 | /** 206 | * Ensure clientId and connectionId filters on Presence GET works 207 | */ 208 | public function testFilters() { 209 | $presenceClientFilter = self::$channel->presence->get( [ 'clientId' => 'client_string' ] ); 210 | $this->assertEquals( 1, count($presenceClientFilter->items), 'Expected the clientId filter to return 1 user' ); 211 | 212 | $connId = $presenceClientFilter->items[0]->connectionId; 213 | 214 | $presenceConnFilter1 = self::$channel->presence->get( [ 'connectionId' => $connId ] ); 215 | $this->assertEquals( 6, count($presenceConnFilter1->items), 'Expected the connectionId filter to return 6 users' ); 216 | 217 | $presenceConnFilter2 = self::$channel->presence->get( [ 'connectionId' => '*FAKE CONNECTION ID*' ] ); 218 | $this->assertEquals( 0, count($presenceConnFilter2->items), 'Expected the connectionId filter to return no users' ); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /tests/PushAdminTest.php: -------------------------------------------------------------------------------- 1 | getOptions(); 18 | self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ 19 | 'key' => self::$testApp->getAppKeyDefault()->string, 20 | ] ) ); 21 | } 22 | 23 | public static function tearDownAfterClass(): void { 24 | self::$testApp->release(); 25 | } 26 | 27 | /** 28 | * RSH1a 29 | */ 30 | public function testAdminPublish() { 31 | $channelName = 'pushenabled:push_admin_publish-ok'; 32 | $recipient = [ 33 | 'transportType' => 'ablyChannel', 34 | 'channel' => $channelName, 35 | 'ablyKey' => self::$ably->options->key, 36 | 'ablyUrl' => self::$testApp->server 37 | ]; 38 | $data = [ 'data' => [ 'foo' => 'bar' ] ]; 39 | 40 | self::$ably->push->admin->publish( $recipient, $data , true ); 41 | sleep(5); // It takes some time for the message to show up in the history 42 | $channel = self::$ably->channel($channelName); 43 | $history = $channel->history(); 44 | $this->assertEquals( 1, count($history->items), 'Expected 1 message' ); 45 | } 46 | 47 | public function badValues() { 48 | $recipient = [ 'clientId' => 'ablyChannel' ]; 49 | $data = [ 'data' => [ 'foo' => 'bar' ] ]; 50 | 51 | return [ 52 | [ [], $data ], 53 | [ $recipient, [] ], 54 | ]; 55 | } 56 | 57 | /** 58 | * @dataProvider badValues 59 | */ 60 | public function testAdminPublishInvalid($recipient, $data) { 61 | $this->expectException(InvalidArgumentException::class); 62 | self::$ably->push->admin->publish( $recipient, $data ); 63 | } 64 | 65 | public function errorValues() { 66 | $recipient = [ 'clientId' => 'ablyChannel' ]; 67 | $data = [ 'data' => [ 'foo' => 'bar' ] ]; 68 | 69 | return [ 70 | [ $recipient, [ 'xxx' => 25 ] ], 71 | ]; 72 | } 73 | 74 | /** 75 | * @dataProvider errorValues 76 | */ 77 | public function testAdminPublishError($recipient, $data) { 78 | $this->expectException(AblyRequestException::class); 79 | self::$ably->push->admin->publish( $recipient, $data ); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /tests/PushChannelSubscriptionsTest.php: -------------------------------------------------------------------------------- 1 | random_string(26), 15 | 'clientId' => random_string(12), 16 | 'platform' => 'ios', 17 | 'formFactor' => 'phone', 18 | 'push' => [ 19 | 'recipient' => [ 20 | 'transportType' => 'apns', 21 | 'deviceToken' => '740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad' 22 | ] 23 | ], 24 | 'deviceSecret' => random_string(12), 25 | ]; 26 | } 27 | 28 | 29 | class PushChannelSubscriptionsTest extends \PHPUnit\Framework\TestCase { 30 | protected static $testApp; 31 | protected static $defaultOptions; 32 | protected static $ably; 33 | 34 | public static function setUpBeforeClass(): void { 35 | self::$testApp = new TestApp(); 36 | self::$defaultOptions = self::$testApp->getOptions(); 37 | self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ 38 | 'key' => self::$testApp->getAppKeyDefault()->string, 39 | ] ) ); 40 | } 41 | 42 | public static function tearDownAfterClass(): void { 43 | self::$testApp->release(); 44 | } 45 | 46 | /** 47 | * RSH1c1 48 | */ 49 | public function testList() { 50 | $channel = 'pushenabled:list'; 51 | 52 | // Register devices and subscribe 53 | $deviceIds = []; 54 | $clientIds = []; 55 | foreach(range(1,5) as $index) { 56 | // Register device 57 | $data = deviceData(); 58 | self::$ably->push->admin->deviceRegistrations->save($data); 59 | $deviceIds[] = $data['id']; 60 | // Subscribe 61 | self::$ably->push->admin->channelSubscriptions->save([ 62 | 'channel' => $channel, 63 | 'deviceId' => $data['id'], 64 | ]); 65 | } 66 | foreach(range(1,5) as $index) { 67 | // Register device 68 | $data = deviceData(); 69 | self::$ably->push->admin->deviceRegistrations->save($data); 70 | $clientIds[] = $data['clientId']; 71 | // Subscribe 72 | self::$ably->push->admin->channelSubscriptions->save([ 73 | 'channel' => $channel, 74 | 'clientId' => $data['clientId'], 75 | ]); 76 | } 77 | 78 | $params = [ 'channel' => $channel ]; 79 | $response = self::$ably->push->admin->channelSubscriptions->list_($params); 80 | $this->assertInstanceOf(PaginatedResult::class, $response); 81 | $this->assertGreaterThanOrEqual(10, count($response->items)); 82 | $this->assertInstanceOf(PushChannelSubscription::class, $response->items[0]); 83 | $this->assertContains($response->items[0]->deviceId, $deviceIds); 84 | 85 | // limit 86 | $response = self::$ably->push->admin->channelSubscriptions->list_( 87 | array_merge($params, ['limit' => 2]) 88 | ); 89 | $this->assertEquals(2, count($response->items)); 90 | 91 | // Filter by device id 92 | $deviceId = $deviceIds[0]; 93 | $response = self::$ably->push->admin->channelSubscriptions->list_( 94 | array_merge($params, ['deviceId' => $deviceId]) 95 | ); 96 | $this->assertEquals(1, count($response->items)); 97 | $response = self::$ably->push->admin->channelSubscriptions->list_( 98 | array_merge($params, ['deviceId' => random_string(26)]) 99 | ); 100 | $this->assertEquals(0, count($response->items)); 101 | 102 | // Filter by client id 103 | $clientId = $clientIds[0]; 104 | $response = self::$ably->push->admin->channelSubscriptions->list_( 105 | array_merge($params, ['clientId' => $clientId]) 106 | ); 107 | $this->assertEquals(1, count($response->items)); 108 | $response = self::$ably->push->admin->channelSubscriptions->list_( 109 | array_merge($params, ['clientId' => random_string(12)]) 110 | ); 111 | $this->assertEquals(0, count($response->items)); 112 | } 113 | 114 | 115 | /** 116 | * RSH1c2 117 | */ 118 | public function testListChannels() { 119 | $channelSubscriptions = self::$ably->push->admin->channelSubscriptions; 120 | 121 | // Register several subscriptions 122 | $clientId = random_string(12); 123 | foreach ( ['pushenabled:test1', 'pushenabled:test2', 'pushenabled:test3' ] as $name ) { 124 | $channelSubscriptions->save(['channel' => $name, 'clientId' => $clientId]); 125 | } 126 | 127 | $response = $channelSubscriptions->listChannels(); 128 | $this->assertInstanceOf(PaginatedResult::class, $response); 129 | $this->assertTrue(is_array($response->items)); 130 | $this->assertTrue(is_string($response->items[0])); 131 | $this->assertGreaterThanOrEqual(3, count($response->items)); 132 | 133 | // limit 134 | $response = $channelSubscriptions->listChannels([ 'limit' => 2]); 135 | $this->assertEquals(2, count($response->items)); 136 | } 137 | 138 | 139 | /** 140 | * RSH1c3 141 | */ 142 | public function testSave() { 143 | // Create 144 | $data = deviceData(); 145 | self::$ably->push->admin->deviceRegistrations->save($data); 146 | 147 | // Subscribe 148 | $channelSubscription = self::$ably->push->admin->channelSubscriptions->save([ 149 | 'channel' => 'pushenabled:test', 150 | 'deviceId' => $data['id'], 151 | ]); 152 | $this->assertInstanceOf(PushChannelSubscription::class, $channelSubscription); 153 | $this->assertEquals($channelSubscription->channel, 'pushenabled:test'); 154 | $this->assertEquals($channelSubscription->deviceId, $data['id']); 155 | 156 | // Update, doesn't fail 157 | self::$ably->push->admin->channelSubscriptions->save([ 158 | 'channel' => 'pushenabled:test', 159 | 'deviceId' => $data['id'], 160 | ]); 161 | 162 | // Fail 163 | $clientId = random_string(12); 164 | $this->expectException(\InvalidArgumentException::class); 165 | self::$ably->push->admin->channelSubscriptions->save([ 166 | 'channel' => 'pushenabled:test', 167 | 'deviceId' => $data['id'], 168 | 'clientId' => $clientId, 169 | ]); 170 | } 171 | 172 | public function badValues() { 173 | $data = deviceData(); 174 | return [ 175 | [ [ 'channel' => 'notallowed', 'deviceId' => $data['id'] ] ], 176 | [ [ 'channel' => 'pushenabled:test', 'deviceId' => 'notregistered' ] ] 177 | ]; 178 | } 179 | 180 | /** 181 | * @dataProvider badValues 182 | */ 183 | public function testSaveInvalid($data) { 184 | $this->expectException(AblyException::class); 185 | self::$ably->push->admin->channelSubscriptions->save($data); 186 | } 187 | 188 | 189 | /** 190 | * RSH1c4 191 | */ 192 | public function testRemove() { 193 | $admin = self::$ably->push->admin; 194 | $channelSubscriptions = $admin->channelSubscriptions; 195 | 196 | // Register device 197 | $data = deviceData(); 198 | $admin->deviceRegistrations->save($data); 199 | $deviceId = $data['id']; 200 | $clientId = $data['clientId']; 201 | 202 | // Remove by device id 203 | $subscription = $channelSubscriptions->save([ 204 | 'channel' => 'pushenabled:test', 205 | 'deviceId' => $deviceId, 206 | ]); 207 | 208 | $params = ['deviceId' => $deviceId]; 209 | $response = $channelSubscriptions->list_($params); 210 | $this->assertEquals(1, count($response->items)); 211 | $channelSubscriptions->remove($subscription); 212 | $response = $channelSubscriptions->list_($params); 213 | $this->assertEquals(0, count($response->items)); 214 | 215 | // Remove by client id 216 | $subscription = $channelSubscriptions->save([ 217 | 'channel' => 'pushenabled:test', 218 | 'clientId' => $clientId, 219 | ]); 220 | 221 | $params = ['clientId' => $clientId]; 222 | $response = $channelSubscriptions->list_($params); 223 | $this->assertEquals(1, count($response->items)); 224 | $channelSubscriptions->remove($subscription); 225 | $response = $channelSubscriptions->list_($params); 226 | $this->assertEquals(0, count($response->items)); 227 | 228 | // Remove again, no error 229 | $channelSubscriptions->remove($subscription); 230 | } 231 | 232 | 233 | /** 234 | * RSH1c5 235 | */ 236 | public function testRemoveWhere() { 237 | $admin = self::$ably->push->admin; 238 | $channelSubscriptions = $admin->channelSubscriptions; 239 | 240 | // Register device 241 | $data = deviceData(); 242 | $admin->deviceRegistrations->save($data); 243 | $deviceId = $data['id']; 244 | $clientId = $data['clientId']; 245 | 246 | // Remove by device id 247 | $channelSubscriptions->save([ 248 | 'channel' => 'pushenabled:test', 249 | 'deviceId' => $deviceId, 250 | ]); 251 | 252 | $params = ['deviceId' => $deviceId]; 253 | $response = $channelSubscriptions->list_($params); 254 | $this->assertEquals(1, count($response->items)); 255 | $channelSubscriptions->removeWhere($params); 256 | sleep(3); // Deletion is async: wait a few seconds 257 | $response = $channelSubscriptions->list_($params); 258 | $this->assertEquals(0, count($response->items)); 259 | 260 | // Remove by client id 261 | $channelSubscriptions->save([ 262 | 'channel' => 'pushenabled:test', 263 | 'clientId' => $clientId, 264 | ]); 265 | 266 | $params = ['clientId' => $clientId]; 267 | $response = $channelSubscriptions->list_($params); 268 | $this->assertEquals(1, count($response->items)); 269 | $channelSubscriptions->removeWhere($params); 270 | sleep(3); // Deletion is async: wait a few seconds 271 | $response = $channelSubscriptions->list_($params); 272 | $this->assertEquals(0, count($response->items)); 273 | 274 | // Remove again, no error 275 | $channelSubscriptions->removeWhere($params); 276 | } 277 | 278 | } 279 | -------------------------------------------------------------------------------- /tests/PushDeviceRegistrationsTest.php: -------------------------------------------------------------------------------- 1 | random_string(26), 16 | 'clientId' => random_string(12), 17 | 'platform' => 'ios', 18 | 'formFactor' => 'phone', 19 | 'push' => [ 20 | 'recipient' => [ 21 | 'transportType' => 'apns', 22 | 'deviceToken' => '740f4707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bb78ad' 23 | ] 24 | ], 25 | 'deviceSecret' => random_string(12), 26 | ]; 27 | } 28 | 29 | 30 | class PushDeviceRegistrationsTest extends \PHPUnit\Framework\TestCase { 31 | protected static $testApp; 32 | protected static $defaultOptions; 33 | protected static $ably; 34 | 35 | public static function setUpBeforeClass(): void { 36 | self::$testApp = new TestApp(); 37 | self::$defaultOptions = self::$testApp->getOptions(); 38 | self::$ably = new AblyRest( array_merge( self::$defaultOptions, [ 39 | 'key' => self::$testApp->getAppKeyDefault()->string, 40 | ] ) ); 41 | } 42 | 43 | public static function tearDownAfterClass(): void { 44 | self::$testApp->release(); 45 | } 46 | 47 | /** 48 | * RSH1b1 49 | */ 50 | public function testGet() { 51 | // Save 52 | $data = data(); 53 | self::$ably->push->admin->deviceRegistrations->save($data); 54 | 55 | // Found 56 | $deviceDetails = self::$ably->push->admin->deviceRegistrations->get($data['id']); 57 | $this->assertEquals($data['id'], $deviceDetails->id); 58 | $this->assertEquals($data['platform'], $deviceDetails->platform); 59 | $this->assertEquals($data['formFactor'], $deviceDetails->formFactor); 60 | $this->assertEquals($data['deviceSecret'], $deviceDetails->deviceSecret); 61 | 62 | // Not Found 63 | $this->expectException(AblyException::class); 64 | self::$ably->push->admin->deviceRegistrations->get("not-found"); 65 | } 66 | 67 | /** 68 | * RSH1b2 69 | */ 70 | public function testList() { 71 | $datas = []; 72 | $ids = []; 73 | foreach(range(1,10) as $index) { 74 | $data = data(); 75 | self::$ably->push->admin->deviceRegistrations->save($data); 76 | $datas[] = $data; 77 | $ids[] = $data['id']; 78 | } 79 | 80 | $response = self::$ably->push->admin->deviceRegistrations->list_(); 81 | $this->assertInstanceOf(PaginatedResult::class, $response); 82 | $this->assertGreaterThanOrEqual(10, count($response->items)); 83 | $this->assertInstanceOf(DeviceDetails::class, $response->items[0]); 84 | $response_ids = array_map(function($x) {return $x->id;}, $response->items); 85 | $this->assertContains($ids[0], $response_ids); 86 | 87 | // limit 88 | $response = self::$ably->push->admin->deviceRegistrations->list_([ 'limit' => 2 ]); 89 | $this->assertEquals(count($response->items), 2); 90 | 91 | // pagination 92 | $response = self::$ably->push->admin->deviceRegistrations->list_([ 'limit' => 1 ]); 93 | $this->assertEquals(count($response->items), 1); 94 | $response = $response->next(); 95 | $this->assertEquals(count($response->items), 1); 96 | 97 | // Filter by device id 98 | $first = $datas[0]; 99 | $response = self::$ably->push->admin->deviceRegistrations->list_([ 'deviceId' => $first['id'] ]); 100 | $this->assertEquals(count($response->items), 1); 101 | $response = self::$ably->push->admin->deviceRegistrations->list_([ 'deviceId' => random_string(26) ]); 102 | $this->assertEquals(count($response->items), 0); 103 | 104 | // Filter by client id 105 | $response = self::$ably->push->admin->deviceRegistrations->list_([ 'clientId' => $first['clientId'] ]); 106 | $this->assertEquals(count($response->items), 1); 107 | $response = self::$ably->push->admin->deviceRegistrations->list_([ 'clientId' => random_string(12) ]); 108 | $this->assertEquals(count($response->items), 0); 109 | } 110 | 111 | /** 112 | * RSH1b3 113 | */ 114 | public function testSave() { 115 | $data = data(); 116 | 117 | // Create 118 | $deviceDetails = self::$ably->push->admin->deviceRegistrations->save($data); 119 | $this->assertInstanceOf(DeviceDetails::class, $deviceDetails); 120 | $this->assertEquals($deviceDetails->id, $data['id']); 121 | 122 | // Update 123 | $new_data = array_merge($data, [ 'formFactor' => 'tablet' ]); 124 | $deviceDetails = self::$ably->push->admin->deviceRegistrations->save($new_data); 125 | 126 | // Fail 127 | $this->expectException(AblyException::class); 128 | $new_data = array_merge($data, [ 'deviceSecret' => random_string(12) ]); 129 | self::$ably->push->admin->deviceRegistrations->save($new_data); 130 | } 131 | 132 | public function badValues() { 133 | $data = data(); 134 | return [ 135 | [ 136 | array_merge($data, [ 137 | 'push' => [ 'recipient' => array_merge($data['push']['recipient'], ['transportType' => 'xyz']) ] 138 | ]) 139 | ], 140 | [ array_merge($data, [ 'platform' => 'native' ]) ], 141 | [ array_merge($data, [ 'formFactor' => 'fridge' ]) ], 142 | ]; 143 | } 144 | 145 | /** 146 | * @dataProvider badValues 147 | */ 148 | public function testSaveInvalid($data) { 149 | $this->expectException(AblyRequestException::class); 150 | self::$ably->push->admin->deviceRegistrations->save($data); 151 | } 152 | 153 | 154 | /** 155 | * RSH1b4 156 | */ 157 | public function testRemove() { 158 | $data = data(); 159 | $deviceId = $data['id']; 160 | 161 | // Save 162 | self::$ably->push->admin->deviceRegistrations->save($data); 163 | $deviceDetails = self::$ably->push->admin->deviceRegistrations->get($deviceId); 164 | $this->assertEquals($deviceId, $deviceDetails->id); 165 | 166 | // Remove 167 | $response = self::$ably->push->admin->deviceRegistrations->remove($deviceId, true); 168 | $this->assertEquals($response['info']['http_code'] , 204); 169 | 170 | // Remove again, it doesn't fail 171 | $response = self::$ably->push->admin->deviceRegistrations->remove($deviceId, true); 172 | $this->assertEquals($response['info']['http_code'] , 204); 173 | 174 | // The device is gone 175 | $this->expectException(AblyException::class); 176 | $this->expectExceptionCode(40400); 177 | self::$ably->push->admin->deviceRegistrations->get($deviceId); 178 | } 179 | 180 | 181 | /** 182 | * RSH1b5 183 | */ 184 | public function testRemoveWhere() { 185 | $data = data(); 186 | self::$ably->push->admin->deviceRegistrations->save($data); 187 | 188 | // Exists 189 | $deviceId = $data['id']; 190 | $deviceDetails = self::$ably->push->admin->deviceRegistrations->get($deviceId); 191 | $this->assertEquals($deviceId, $deviceDetails->id); 192 | 193 | // Remove 194 | $response = self::$ably->push->admin->deviceRegistrations->removeWhere([ 'deviceId' => $deviceId ], true); 195 | $this->assertEquals($response['info']['http_code'] , 204); 196 | 197 | // Remove again, no matching params, doesn't fail 198 | $response = self::$ably->push->admin->deviceRegistrations->removeWhere([ 'deviceId' => $deviceId ], true); 199 | $this->assertEquals($response['info']['http_code'] , 204); 200 | 201 | // It's gone 202 | $this->expectException(AblyException::class); 203 | $this->expectExceptionCode(40400); 204 | self::$ably->push->admin->deviceRegistrations->get($deviceId); 205 | } 206 | 207 | public function testRemoveWhereClientId() { 208 | $data = data(); 209 | self::$ably->push->admin->deviceRegistrations->save($data); 210 | 211 | // Exists 212 | $deviceId = $data['id']; 213 | $clientId = $data['clientId']; 214 | $deviceDetails = self::$ably->push->admin->deviceRegistrations->get($deviceId); 215 | $this->assertEquals($clientId, $deviceDetails->clientId); 216 | 217 | // Remove 218 | $response = self::$ably->push->admin->deviceRegistrations->removeWhere([ 'clientId' => $clientId ], true); 219 | $this->assertEquals($response['info']['http_code'] , 204); 220 | 221 | // Remove again, no matching params, doesn't fail 222 | $response = self::$ably->push->admin->deviceRegistrations->removeWhere([ 'clientId' => $clientId ], true); 223 | $this->assertEquals($response['info']['http_code'] , 204); 224 | 225 | // Deletion is async: wait a few seconds 226 | sleep(3); 227 | 228 | // It's gone 229 | $this->expectException(AblyException::class); 230 | $this->expectExceptionCode(40400); 231 | self::$ably->push->admin->deviceRegistrations->get($deviceId); 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /tests/TypesTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( property_exists( $class, $member ), 22 | "Expected class `$class` to contain a field named `$member`." ); 23 | } 24 | } 25 | 26 | protected function verifyClassConstants( $class, $expectedMembers ) { 27 | foreach( $expectedMembers as $member => $value ) { 28 | $this->assertEquals( $value, constant( "$class::$member" ), 29 | "Expected class `$class` to have a constant `$member` with a value of `$value`." 30 | ); 31 | } 32 | } 33 | 34 | protected function verifyObjectTypes( $obj, $expectedTypes ) { 35 | foreach( $obj as $key => $value ) { 36 | if ( gettype( $value ) == 'object' ) { 37 | $this->assertEquals( $expectedTypes[$key], get_class( $value ), 38 | "Expected object (".get_class($obj).") to contain a member `$key` of type `".$expectedTypes[$key]."`." 39 | ); 40 | } else { 41 | $this->assertEquals( $expectedTypes[$key], gettype( $value ), 42 | "Expected object (".get_class($obj).") to contain a member `$key` of type `".$expectedTypes[$key]."`." 43 | ); 44 | } 45 | } 46 | } 47 | 48 | public function testMessageType() { 49 | $this->verifyClassMembers( '\Ably\Models\Message', [ 50 | 'id', 51 | 'clientId', 52 | 'connectionId', 53 | 'connectionKey', 54 | 'name', 55 | 'data', 56 | 'encoding', 57 | 'timestamp', 58 | ] ); 59 | } 60 | 61 | public function testPresenceMessageType() { 62 | $this->verifyClassMembers( '\Ably\Models\PresenceMessage', [ 63 | 'id', 64 | 'action', 65 | 'clientId', 66 | 'connectionId', 67 | 'data', 68 | 'encoding', 69 | 'timestamp', 70 | 'memberKey' 71 | ] ); 72 | 73 | $this->verifyClassConstants( '\Ably\Models\PresenceMessage', [ 74 | 'ABSENT' => 0, 75 | 'PRESENT' => 1, 76 | 'ENTER' => 2, 77 | 'LEAVE' => 3, 78 | 'UPDATE' => 4 79 | ] ); 80 | } 81 | 82 | public function testTokenRequestType() { 83 | $this->verifyClassMembers( '\Ably\Models\TokenRequest', [ 84 | 'keyName', 85 | 'clientId', 86 | 'nonce', 87 | 'mac', 88 | 'capability', 89 | 'ttl', 90 | ] ); 91 | } 92 | 93 | public function testTokenDetailsType() { 94 | $this->verifyClassMembers( '\Ably\Models\TokenDetails', [ 95 | 'token', 96 | 'expires', 97 | 'issued', 98 | 'capability', 99 | 'clientId', 100 | ] ); 101 | } 102 | 103 | public function testStatsType() { 104 | $this->verifyClassMembers( '\Ably\Models\Stats', [ 105 | 'all', 106 | 'apiRequests', 107 | 'channels', 108 | 'connections', 109 | 'inbound', 110 | 'intervalGranularity', 111 | 'intervalId', 112 | 'intervalTime', 113 | 'outbound', 114 | 'persisted', 115 | 'tokenRequests' 116 | ] ); 117 | } 118 | 119 | public function testErrorInfoType() { 120 | $this->verifyClassMembers( '\Ably\Models\ErrorInfo', [ 121 | 'code', 122 | 'statusCode', 123 | 'message', 124 | ] ); 125 | } 126 | 127 | public function testClientOptionsType() { 128 | $this->verifyClassMembers( '\Ably\Models\ClientOptions', [ 129 | 'clientId', 130 | 'logLevel', 131 | 'logHandler', 132 | 'tls', 133 | 'useBinaryProtocol', 134 | 'key', 135 | 'token', 136 | 'tokenDetails', 137 | 'useTokenAuth', 138 | 'authCallback', 139 | 'authUrl', 140 | 'authMethod', 141 | 'authHeaders', 142 | 'authParams', 143 | 'queryTime', 144 | 'environment', 145 | 'restHost', 146 | 'port', 147 | 'tlsPort', 148 | 'httpOpenTimeout', 149 | 'httpRequestTimeout', 150 | 'httpMaxRetryCount', 151 | 'idempotentRestPublishing', 152 | ] ); 153 | 154 | $co = new \Ably\Models\ClientOptions(); 155 | $this->assertEquals( 4000, $co->httpOpenTimeout ); 156 | $this->assertEquals( 10000, $co->httpRequestTimeout ); 157 | $this->assertEquals( 3, $co->httpMaxRetryCount ); 158 | $this->assertEquals( 15000, $co->httpMaxRetryDuration ); 159 | } 160 | 161 | // TO3n 162 | public function testClientOptionsIdempotent() 163 | { 164 | // Test default value 165 | $co = new \Ably\Models\ClientOptions(); 166 | if (Defaults::API_VERSION <= '1.1') { 167 | $this->assertEquals( false, $co->idempotentRestPublishing ); 168 | } else { 169 | $this->assertEquals( true, $co->idempotentRestPublishing ); 170 | } 171 | 172 | // Test explicit value 173 | $co = new \Ably\Models\ClientOptions( array( 'idempotentRestPublishing' => true ) ); 174 | $this->assertEquals( true, $co->idempotentRestPublishing ); 175 | 176 | $co = new \Ably\Models\ClientOptions( array( 'idempotentRestPublishing' => false ) ); 177 | $this->assertEquals( false, $co->idempotentRestPublishing ); 178 | } 179 | 180 | public function testAuthOptionsType() { 181 | $this->verifyClassMembers( '\Ably\Models\ClientOptions', [ 182 | 'key', 183 | 'authCallback', 184 | 'authUrl', 185 | 'authMethod', 186 | 'authHeaders', 187 | 'authParams', 188 | 'queryTime', 189 | ] ); 190 | } 191 | 192 | public function testTokenParamsType() { 193 | $this->verifyClassMembers( '\Ably\Models\TokenParams', [ 194 | 'ttl', 195 | 'capability', 196 | 'clientId', 197 | 'timestamp', 198 | ] ); 199 | } 200 | 201 | public function testChannelOptionsType() { 202 | $this->verifyClassMembers( '\Ably\Models\ChannelOptions', [ 203 | 'cipher', 204 | ] ); 205 | } 206 | 207 | public function testCipherParamsType() { 208 | $this->verifyClassMembers( '\Ably\Models\CipherParams', [ 209 | 'algorithm', 210 | 'key', 211 | 'keyLength', 212 | 'mode' 213 | ] ); 214 | } 215 | 216 | public function testStatsTypes() { 217 | $stats = new \Ably\Models\Stats(); 218 | $this->verifyObjectTypes( $stats, [ 219 | 'all' => 'Ably\Models\Stats\MessageTypes', 220 | 'inbound' => 'Ably\Models\Stats\MessageTraffic', 221 | 'outbound' => 'Ably\Models\Stats\MessageTraffic', 222 | 'persisted' => 'Ably\Models\Stats\MessageTypes', 223 | 'connections' => 'Ably\Models\Stats\ConnectionTypes', 224 | 'channels' => 'Ably\Models\Stats\ResourceCount', 225 | 'apiRequests' => 'Ably\Models\Stats\RequestCount', 226 | 'tokenRequests' => 'Ably\Models\Stats\RequestCount', 227 | 'intervalId' => 'string', 228 | 'intervalGranularity' => 'string', 229 | 'intervalTime' => 'integer', 230 | ] ); 231 | 232 | // verify MessageTypes 233 | $this->verifyObjectTypes( $stats->all, [ 234 | 'all' => 'Ably\Models\Stats\MessageCount', 235 | 'messages' => 'Ably\Models\Stats\MessageCount', 236 | 'presence' => 'Ably\Models\Stats\MessageCount', 237 | ] ); 238 | 239 | // verify MessageCount 240 | $this->verifyObjectTypes( $stats->all->all, [ 241 | 'count' => 'integer', 242 | 'data' => 'integer', 243 | ] ); 244 | 245 | // verify MessageTraffic 246 | $this->verifyObjectTypes( $stats->inbound, [ 247 | 'all' => 'Ably\Models\Stats\MessageTypes', 248 | 'realtime' => 'Ably\Models\Stats\MessageTypes', 249 | 'rest' => 'Ably\Models\Stats\MessageTypes', 250 | 'webhook' => 'Ably\Models\Stats\MessageTypes', 251 | ] ); 252 | 253 | // verify ConnectionTypes 254 | $this->verifyObjectTypes( $stats->connections, [ 255 | 'all' => 'Ably\Models\Stats\ResourceCount', 256 | 'plain' => 'Ably\Models\Stats\ResourceCount', 257 | 'tls' => 'Ably\Models\Stats\ResourceCount', 258 | ] ); 259 | 260 | // verify ResourceCount 261 | $this->verifyObjectTypes( $stats->connections->all, [ 262 | 'mean' => 'integer', 263 | 'min' => 'integer', 264 | 'opened' => 'integer', 265 | 'peak' => 'integer', 266 | 'refused' => 'integer', 267 | ] ); 268 | 269 | // verify RequestCount 270 | $this->verifyObjectTypes( $stats->apiRequests, [ 271 | 'failed' => 'integer', 272 | 'refused' => 'integer', 273 | 'succeeded' => 'integer', 274 | ] ); 275 | } 276 | 277 | public function testHttpPaginatedResponseType() { 278 | $this->verifyClassMembers( '\Ably\Models\HttpPaginatedResponse', [ 279 | 'items', 280 | 'statusCode', 281 | 'success', 282 | 'errorCode', 283 | 'errorMessage', 284 | 'headers', 285 | ] ); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /tests/Utils.php: -------------------------------------------------------------------------------- 1 | assertEquals('1.2.9', $numericVersion); 10 | 11 | $numericVersion = Miscellaneous::getNumeric('4.6-macos-t'); 12 | $this->assertEquals('4.6', $numericVersion); 13 | 14 | $numericVersion = Miscellaneous::getNumeric('7.2.34-28+ubuntu20.04.1+deb.sury.org+1'); 15 | $this->assertEquals('7.2.34', $numericVersion); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/extensions/DebugTestListener.php: -------------------------------------------------------------------------------- 1 | getName() . "\"\n"; 12 | } 13 | 14 | public function endTestSuite(PHPUnit\Framework\TestSuite $suite): void 15 | { 16 | echo "\n\n"; 17 | } 18 | 19 | public function startTest(PHPUnit\Framework\Test $test): void 20 | { 21 | echo "\n" . $test->getName().'... '; 22 | } 23 | 24 | public function endTest(PHPUnit\Framework\Test $test, $time): void 25 | { 26 | echo "(" . round($time * 1000) . " ms)"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/factories/TestApp.php: -------------------------------------------------------------------------------- 1 | options = $settings; 35 | 36 | $this->server = $clientOpts->getHostUrl($clientOpts->getPrimaryRestHost()); 37 | $this->init(); 38 | 39 | return $this; 40 | } 41 | 42 | private function init() { 43 | 44 | $this->fixture = json_decode ( file_get_contents( __DIR__ . self::$fixtureFile, 1 ) ); 45 | 46 | if (!$this->fixture) { 47 | echo 'Unable to read fixture file'; 48 | exit(1); 49 | } 50 | 51 | $raw = $this->request( 'POST', $this->server . '/apps', [], json_encode( $this->fixture->post_apps ) ); 52 | $response = json_decode( $raw ); 53 | 54 | if ($response === null) { 55 | echo 'Could not connect to API.'; 56 | exit(1); 57 | } 58 | 59 | $this->appId = $response->appId; 60 | 61 | foreach ($response->keys as $key) { 62 | $obj = new stdClass(); 63 | $obj->appId = $this->appId; 64 | $obj->id = $key->id; 65 | $obj->value = $key->value; 66 | $obj->name = $this->appId . '.' . $key->id; 67 | $obj->string = $this->appId . '.' . $key->id . ':' . $key->value; 68 | $obj->capability = $key->capability; 69 | 70 | $this->appKeys[] = $obj; 71 | } 72 | } 73 | 74 | public function release() { 75 | if (!empty($this->options)) { 76 | $headers = [ 'authorization: Basic ' . base64_encode( $this->getAppKeyDefault()->string ) ]; 77 | $this->request( 'DELETE', $this->server . '/apps/' . $this->appId, $headers ); 78 | $this->options = null; 79 | } 80 | } 81 | 82 | public function getFixture() { 83 | return $this->fixture; 84 | } 85 | 86 | public function getOptions() { 87 | return $this->options; 88 | } 89 | 90 | public function getAppId() { 91 | return $this->appId; 92 | } 93 | 94 | public function getAppKeyDefault() { 95 | return $this->appKeys[0]; 96 | } 97 | 98 | public function getAppKeyWithCapabilities() { 99 | return $this->appKeys[1]; 100 | } 101 | 102 | private function request( $mode, $url, $headers = [], $params = '' ) { 103 | $ch = curl_init($url); 104 | 105 | curl_setopt($ch, CURLOPT_FAILONERROR, true); // Required for HTTP error codes to be reported via call to curl_error($ch) 106 | 107 | if ( $mode == 'DELETE') curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'DELETE' ); 108 | if ( $mode == 'POST' ) curl_setopt ( $ch, CURLOPT_POST, 1 ); 109 | 110 | if (!empty($params)) { 111 | curl_setopt( $ch, CURLOPT_POSTFIELDS, $params ); 112 | array_push( $headers, 'Accept: application/json', 'Content-Type: application/json' ); 113 | } 114 | 115 | if (!empty($headers)) { 116 | curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); 117 | } 118 | 119 | curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); 120 | if ($this->debugRequests) { 121 | curl_setopt( $ch, CURLOPT_VERBOSE, 1 ); 122 | } 123 | 124 | $raw = curl_exec($ch); 125 | 126 | if (curl_errno($ch)) { 127 | var_dump(curl_error($ch)); // Prints curl request error if exists 128 | } 129 | 130 | curl_close ($ch); 131 | 132 | if ($this->debugRequests) { 133 | var_dump($raw); 134 | } 135 | 136 | return $raw; 137 | } 138 | } 139 | --------------------------------------------------------------------------------