├── .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 |
80 |
81 |
82 |
83 |
84 |
85 | timestamp / 1000);
87 | $day = date($date_format, $timestamp); ?>
88 |
89 | = $day ?>
90 |
91 | = gmdate('h:i a', $timestamp) ?>
92 | = $message->data->handle ?>: = $message->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 |
--------------------------------------------------------------------------------