├── CHANGELOG.md ├── LICENSE ├── composer.json └── src └── PushNotifications.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## 2.0.0 8 | ### Added 9 | - Added GitHub actions; 10 | - Added types where possible; 11 | ### Changed 12 | - Updated composer packages 13 | - Updated composer.json (#37, #38); 14 | ### Removed 15 | - Dropped support for old PHP versions; 16 | - Dropped Travis CI; 17 | 18 | ## 1.1.2 19 | ### Changed 20 | - Replaced `array_key_exists` with `property_exists` for compatibility with 21 | PHP 7.4 22 | 23 | ## 1.1.1 24 | ### Changed 25 | - Allow compatibility with guzzlehttp 7.0 in composer json & added tests to verify this 26 | 27 | ## 1.1.0 28 | ### Added 29 | - Support for "Authenticated Users" feature: `publishToUsers`, `generateToken` and `deleteUser` 30 | ### Changed 31 | - `publish` renamed to `publishToInterests` (`publish` method deprecated). 32 | 33 | ## 1.0.0 34 | ### Added 35 | - Changelog for GA release 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pusher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pusher/pusher-push-notifications", 3 | "version": "2.0", 4 | "license": "MIT", 5 | "require": { 6 | "php": ">=8.0", 7 | "guzzlehttp/guzzle": "^7.0", 8 | "firebase/php-jwt": "^6.0", 9 | "ext-mbstring": "*" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^9.0", 13 | "symfony/yaml": "^5.0 || ^6.0", 14 | "doctrine/instantiator": "1.4.0", 15 | "overtrue/phplint": "^4.0 || ^5.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Pusher\\PushNotifications\\": "src/" 20 | } 21 | }, 22 | "scripts": { 23 | "phplint": "./vendor/bin/phplint ./ --exclude=vendor --no-cache" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/PushNotifications.php: -------------------------------------------------------------------------------- 1 | options)) { 25 | throw new \Exception("Required 'instanceId' in Pusher\PushNotifications constructor options"); 26 | } 27 | if (!is_string($this->options["instanceId"])) { 28 | throw new \Exception("'instanceId' must be a string"); 29 | } 30 | if ($this->options["instanceId"] === "") { 31 | throw new \Exception("'instanceId' cannot be the empty string"); 32 | } 33 | 34 | if (!array_key_exists("secretKey", $this->options)) { 35 | throw new \Exception("Required 'secretKey' in Pusher\PushNotifications constructor options"); 36 | } 37 | if (!is_string($this->options["secretKey"])) { 38 | throw new \Exception("'secretKey' must be a string"); 39 | } 40 | if ($this->options["secretKey"] === "") { 41 | throw new \Exception("'secretKey' cannot be the empty string"); 42 | } 43 | 44 | if (!array_key_exists("endpoint", $this->options)) { 45 | $this->options["endpoint"] = "https://" . $options["instanceId"] . ".pushnotifications.pusher.com"; 46 | } else { 47 | if (!is_string($this->options["endpoint"])) { 48 | throw new \Exception("'endpoint' must be a string"); 49 | } 50 | if ($this->options["endpoint"] === "") { 51 | throw new \Exception("'endpoint' cannot be the empty string"); 52 | } 53 | } 54 | 55 | if (!$client) { 56 | $this->client = new GuzzleHttp\Client(); 57 | } else { 58 | $this->client = $client; 59 | } 60 | } 61 | 62 | private function makeRequest(string $method, string $path, array $pathParams, array|null $body = null): mixed { 63 | $escapedPathParams = []; 64 | foreach ($pathParams as $k => $v) { 65 | $escapedPathParams[$k] = urlencode($v); 66 | } 67 | 68 | $endpoint = $this->options["endpoint"]; 69 | $interpolatedPath = strtr($path, $escapedPathParams); 70 | $url = $endpoint . $interpolatedPath; 71 | 72 | try { 73 | $response = $this->client->request( 74 | $method, 75 | $url, 76 | [ 77 | "headers" => [ 78 | "Authorization" => "Bearer " . $this->options["secretKey"], 79 | "X-Pusher-Library" => "pusher-push-notifications-php " . PushNotifications::SDK_VERSION 80 | ], 81 | "json" => $body 82 | ] 83 | ); 84 | } catch (\GuzzleHttp\Exception\BadResponseException $e) { 85 | $response = $e->GetResponse(); 86 | $parsedResponse = json_decode($response->GetBody()); 87 | $badJSON = $parsedResponse === null; 88 | if ( 89 | $badJSON || 90 | !property_exists($parsedResponse, 'error') || 91 | !property_exists($parsedResponse, 'description') 92 | ) { 93 | throw new \Exception("An unexpected server error has occurred"); 94 | } 95 | throw new \Exception("{$parsedResponse->error}: {$parsedResponse->description}"); 96 | } 97 | 98 | $parsedResponse = json_decode($response->GetBody()); 99 | 100 | return $parsedResponse; 101 | } 102 | 103 | /** 104 | * @param array $interests 105 | * @param array $publishRequest 106 | * @return mixed 107 | * @throws \Exception 108 | */ 109 | public function publishToInterests(array $interests, array $publishRequest): mixed { 110 | if (count($interests) === 0) { 111 | throw new \Exception("Publishes must target at least one interest"); 112 | } 113 | if (count($interests) > PushNotifications::MAX_INTERESTS) { 114 | throw new \Exception("Number of interests exceeds maximum of " . PushNotifications::MAX_INTERESTS); 115 | } 116 | 117 | foreach($interests as $interest) { 118 | if (!is_string($interest)) { 119 | throw new \Exception("Interest \"$interest\" is not a string"); 120 | } 121 | if (mb_strlen($interest) > PushNotifications::MAX_INTEREST_LENGTH) { 122 | throw new \Exception("Interest \"$interest\" is longer than the maximum length of " . PushNotifications::MAX_INTEREST_LENGTH . " chars."); 123 | } 124 | if ( $interest === '' ) { 125 | throw new \Exception("Interest names cannot be the empty string"); 126 | } 127 | if (!preg_match(PushNotifications::INTEREST_REGEX, $interest)) { 128 | throw new \Exception(implode([ 129 | "Interest \"$interest\" contains a forbidden character.", 130 | " Allowed characters are: ASCII upper/lower-case letters,", 131 | " numbers or one of _=@,.;-" 132 | ])); 133 | } 134 | } 135 | 136 | $publishRequest['interests'] = $interests; 137 | $path = '/publish_api/v1/instances/INSTANCE_ID/publishes/interests'; 138 | $pathParams = [ 139 | 'INSTANCE_ID' => $this->options["instanceId"] 140 | ]; 141 | $response = $this->makeRequest("POST", $path, $pathParams, $publishRequest); 142 | 143 | if ($response === null) { 144 | throw new \Exception("An unexpected server error has occurred"); 145 | } 146 | 147 | return $response; 148 | } 149 | 150 | public function publishToUsers(array $userIds, array $publishRequest): mixed { 151 | if (count($userIds) === 0) { 152 | throw new \Exception("Publishes must target at least one user"); 153 | } 154 | if (count($userIds) > PushNotifications::MAX_USERS) { 155 | throw new \Exception("Number of user ids exceeds maximum of " . PushNotifications::MAX_USERS); 156 | } 157 | 158 | foreach($userIds as $userId) { 159 | $this->checkUserId($userId); 160 | } 161 | 162 | $publishRequest['users'] = $userIds; 163 | $path = '/publish_api/v1/instances/INSTANCE_ID/publishes/users'; 164 | $pathParams = [ 165 | 'INSTANCE_ID' => $this->options["instanceId"] 166 | ]; 167 | $response = $this->makeRequest("POST", $path, $pathParams, $publishRequest); 168 | 169 | if ($response === null) { 170 | throw new \Exception("An unexpected server error has occurred"); 171 | } 172 | 173 | return $response; 174 | } 175 | 176 | public function deleteUser(string $userId): void { 177 | $this->checkUserId($userId); 178 | 179 | $path = '/customer_api/v1/instances/INSTANCE_ID/users/USER_ID'; 180 | $pathParams = [ 181 | 'INSTANCE_ID' => $this->options["instanceId"], 182 | 'USER_ID' => $userId 183 | ]; 184 | $this->makeRequest("DELETE", $path, $pathParams); 185 | } 186 | 187 | public function generateToken(string $userId): array { 188 | $this->checkUserId($userId); 189 | 190 | $instanceId = $this->options["instanceId"]; 191 | $secretKey = $this->options["secretKey"]; 192 | 193 | $issuer = "https://$instanceId.pushnotifications.pusher.com"; 194 | $claims = [ 195 | "iss" => $issuer, 196 | "sub" => $userId, 197 | "exp" => time() + PushNotifications::AUTH_TOKEN_DURATION_SECS 198 | ]; 199 | 200 | $token = JWT::encode($claims, $secretKey, 'HS256'); 201 | 202 | return [ 203 | "token" => $token 204 | ]; 205 | } 206 | 207 | private function checkUserId(string $userId): void { 208 | if ($userId === '') { 209 | throw new \Exception("User id cannot be the empty string"); 210 | } 211 | if (mb_strlen($userId) > PushNotifications::MAX_USER_ID_LENGTH) { 212 | throw new \Exception("User id \"$userId\" is longer than the maximum length of " . PushNotifications::MAX_USER_ID_LENGTH . " chars."); 213 | } 214 | } 215 | 216 | public function getClient(): GuzzleHttp\Client 217 | { 218 | return $this->client; 219 | } 220 | } 221 | --------------------------------------------------------------------------------