├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── data └── safariPushPackage.base │ ├── icon.iconset │ ├── icon_128x128.png │ ├── icon_128x128@2x.png │ ├── icon_16x16.png │ ├── icon_16x16@2x.png │ ├── icon_32x32.png │ └── icon_32x32@2x.png │ └── website.json ├── phpunit.xml.dist ├── src └── JWage │ └── APNS │ ├── APNSMessage.php │ ├── Certificate.php │ ├── Client.php │ ├── Payload.php │ ├── Safari │ ├── Package.php │ ├── PackageGenerator.php │ ├── PackageManifester.php │ └── PackageSigner.php │ ├── Sender.php │ └── SocketClient.php └── tests ├── JWage └── APNS │ └── Tests │ ├── APNSMessageTest.php │ ├── CertificateTest.php │ ├── ClientTest.php │ ├── PayloadTest.php │ ├── Safari │ └── PackageGeneratorTest.php │ ├── SenderTest.php │ └── SocketClientTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | before_script: composer install 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jonathan H. Wage 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP APNS 2 | ======== 3 | 4 | PHP Apple Push Notification Service Library 5 | 6 | [![Build Status](https://secure.travis-ci.org/jwage/php-apns.png?branch=master)](http://travis-ci.org/jwage/php-apns) 7 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/jwage/php-apns/badges/quality-score.png?s=98f9260f6488ed20d32d21106dc349c1eac26a35)](https://scrutinizer-ci.com/g/jwage/php-apns/) 8 | [![Code Coverage](https://scrutinizer-ci.com/g/jwage/php-apns/badges/coverage.png?s=cd50bac60d2699353b0d9ffef3b7a1a4f6568f70)](https://scrutinizer-ci.com/g/jwage/php-apns/) 9 | [![Latest Stable Version](https://poser.pugx.org/jwage/php-apns/v/stable.png)](https://packagist.org/packages/jwage/php-apns) 10 | [![Total Downloads](https://poser.pugx.org/jwage/php-apns/downloads.png)](https://packagist.org/packages/jwage/php-apns) 11 | [![Dependency Status](https://www.versioneye.com/php/jwage:php-apns/1.0.0/badge.png)](https://www.versioneye.com/php/jwage:php-apns/1.0.0) 12 | 13 | 14 | ## Install 15 | 16 | Install PHP APNS using composer: 17 | 18 | composer require jwage/php-apns 19 | 20 | ## Generate Safari Notification Package 21 | 22 | ```php 23 | use JWage\APNS\Certificate; 24 | use JWage\APNS\Safari\PackageGenerator; 25 | 26 | $certificate = new Certificate(file_get_contents('apns.p12'), 'certpassword'); 27 | $packageGenerator = new PackageGenerator( 28 | $certificate, '/base/pushPackage/path', 'yourdomain.com' 29 | ); 30 | 31 | // returns JWage\APNS\Safari\Package instance 32 | $package = $packageGenerator->createPushPackageForUser('userid'); 33 | 34 | // send zip file to the browser 35 | echo $package->getZipPath(); 36 | ``` 37 | 38 | ## Sending Notifications 39 | 40 | ```php 41 | use JWage\APNS\Certificate; 42 | use JWage\APNS\Client; 43 | use JWage\APNS\Sender; 44 | use JWage\APNS\SocketClient; 45 | 46 | $certificate = new Certificate(file_get_contents('apns.pem')); 47 | $socketClient = new SocketClient($certificate, 'gateway.push.apple.com', 2195); 48 | $client = new Client($socketClient); 49 | $sender = new Sender($client); 50 | 51 | $sender->send('devicetoken', 'Title of push', 'Body of push', 'http://deeplink.com'); 52 | ``` 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwage/php-apns", 3 | "type": "library", 4 | "description": "Object Oriented PHP Apple Push Notification Integration Library", 5 | "keywords": ["apple", "notifications", "apns"], 6 | "homepage": "http://github.com/jwage/php-apns", 7 | "license": "MIT", 8 | "authors": [ 9 | {"name": "Jonathan H. Wage", "email": "jonwage@gmail.com"} 10 | ], 11 | "autoload": { 12 | "psr-0": {"JWage\\APNS": "src/"} 13 | }, 14 | "require": { 15 | "php": ">=5.3.0", 16 | "ext-openssl": "*", 17 | "ext-zip": "*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /data/safariPushPackage.base/icon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwage/php-apns/75d8b0f4722cf4342a42d0fc4e0619c32550b955/data/safariPushPackage.base/icon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /data/safariPushPackage.base/icon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwage/php-apns/75d8b0f4722cf4342a42d0fc4e0619c32550b955/data/safariPushPackage.base/icon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /data/safariPushPackage.base/icon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwage/php-apns/75d8b0f4722cf4342a42d0fc4e0619c32550b955/data/safariPushPackage.base/icon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /data/safariPushPackage.base/icon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwage/php-apns/75d8b0f4722cf4342a42d0fc4e0619c32550b955/data/safariPushPackage.base/icon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /data/safariPushPackage.base/icon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwage/php-apns/75d8b0f4722cf4342a42d0fc4e0619c32550b955/data/safariPushPackage.base/icon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /data/safariPushPackage.base/icon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwage/php-apns/75d8b0f4722cf4342a42d0fc4e0619c32550b955/data/safariPushPackage.base/icon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /data/safariPushPackage.base/website.json: -------------------------------------------------------------------------------- 1 | { 2 | "websiteName": "{{ websiteName }}", 3 | "websitePushID": "{{ websitePushId }}", 4 | "allowedDomains": ["http://{{ host }}", "https://{{ host }}"], 5 | "urlFormatString": "http://{{ host }}/%@", 6 | "authenticationToken": "{{ userId }}", 7 | "webServiceURL": "https://{{ host }}/safari_push_notifications/{{ userId }}" 8 | } 9 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/JWage/APNS/Tests/ 7 | 8 | 9 | 10 | 11 | 12 | ./src/JWage/APNS/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/JWage/APNS/APNSMessage.php: -------------------------------------------------------------------------------- 1 | deviceToken = $deviceToken; 30 | $this->payload = $payload; 31 | } 32 | 33 | /** 34 | * Returns a binary message that the Apple Push Notification Service understands. 35 | * 36 | * @return string $binaryMessage 37 | */ 38 | public function getBinaryMessage() 39 | { 40 | $encodedPayload = $this->jsonEncode($this->payload->getPayload()); 41 | 42 | return chr(0). 43 | chr(0). 44 | chr(32). 45 | pack('H*', $this->deviceToken). 46 | chr(0).chr(strlen($encodedPayload)). 47 | $encodedPayload; 48 | } 49 | 50 | /** 51 | * @param array $payload 52 | * @return string $payloadJson 53 | */ 54 | private function jsonEncode(array $payload) 55 | { 56 | return json_encode($payload); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/JWage/APNS/Certificate.php: -------------------------------------------------------------------------------- 1 | certificateString = (string) $certificateString; 26 | $this->password = $password; 27 | } 28 | 29 | /** 30 | * Gets the certificate string. 31 | * 32 | * @return string $certificateString 33 | */ 34 | public function getCertificateString() 35 | { 36 | return $this->certificateString; 37 | } 38 | 39 | /** 40 | * Gets the certificate password. 41 | * 42 | * @return string $password 43 | */ 44 | public function getPassword() 45 | { 46 | return $this->password; 47 | } 48 | 49 | /** 50 | * Writes the certificate to the given file path. 51 | * 52 | * @param string $path 53 | */ 54 | public function writeTo($path) 55 | { 56 | file_put_contents($path, $this->certificateString); 57 | } 58 | 59 | /** 60 | * Writes the certificate to a temporary file and returns the path. 61 | * 62 | * @return string $path 63 | */ 64 | public function writeToTmp() 65 | { 66 | $path = tempnam(sys_get_temp_dir(), 'cert_'); 67 | 68 | $this->writeTo($path); 69 | 70 | return $path; 71 | } 72 | 73 | /** 74 | * @return string 75 | */ 76 | public function __toString() 77 | { 78 | return $this->certificateString; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/JWage/APNS/Client.php: -------------------------------------------------------------------------------- 1 | socketClient = $socketClient; 20 | } 21 | 22 | /** 23 | * Send Payload instance to a given device token. 24 | * 25 | * @param string $deviceToken The device token to send payload to. 26 | * @param \JWage\APNS\Payload $payload The payload to send to device token. 27 | */ 28 | public function sendPayload($deviceToken, Payload $payload) 29 | { 30 | return $this->socketClient->write( 31 | $this->createApnMessage($deviceToken, $payload)->getBinaryMessage() 32 | ); 33 | } 34 | 35 | /** 36 | * Creates an APNSMessage instance for the given device token and payload. 37 | * 38 | * @param string $deviceToken 39 | * @param \JWage\APNS\Payload $payload 40 | * @return \JWage\APNS\APNSMessage 41 | */ 42 | protected function createApnMessage($deviceToken, Payload $payload) 43 | { 44 | return new APNSMessage($deviceToken, $payload); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/JWage/APNS/Payload.php: -------------------------------------------------------------------------------- 1 | title = $title; 32 | $this->body = $body; 33 | $this->deepLink = $deepLink; 34 | } 35 | 36 | /** 37 | * Gets payload array structure. 38 | * 39 | * @return array $payload 40 | */ 41 | public function getPayload() 42 | { 43 | return array( 44 | 'aps' => array( 45 | 'alert' => array( 46 | 'title' => $this->title, 47 | 'body' => $this->body, 48 | ), 49 | 'url-args' => array( 50 | $this->deepLink, 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/JWage/APNS/Safari/Package.php: -------------------------------------------------------------------------------- 1 | packageDir = $packageDir; 42 | $this->userId = $userId; 43 | $this->zipPath = sprintf('%s.zip', $packageDir); 44 | } 45 | 46 | /** 47 | * Gets path to the zip package directory. 48 | * 49 | * @return string $packageDir 50 | */ 51 | public function getPackageDir() 52 | { 53 | return $this->packageDir; 54 | } 55 | 56 | /** 57 | * Gets the user id this package is for. 58 | * 59 | * @return string $userId 60 | */ 61 | public function getUserId() 62 | { 63 | return $this->userId; 64 | } 65 | 66 | /** 67 | * Gets path to the zip package. 68 | * 69 | * @return string $zipPath 70 | */ 71 | public function getZipPath() 72 | { 73 | return $this->zipPath; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/JWage/APNS/Safari/PackageGenerator.php: -------------------------------------------------------------------------------- 1 | certificate = $certificate; 53 | $this->basePushPackagePath = $basePushPackagePath; 54 | $this->host = $host; 55 | $this->websiteName = $websiteName; 56 | $this->websitePushId = $websitePushId; 57 | } 58 | 59 | /** 60 | * Create a safari website push notification package for the given User. 61 | * 62 | * @param string $userId User id to create package for. 63 | * @return \JWage\APNS\Safari\Package $package Package instance. 64 | */ 65 | public function createPushPackageForUser($userId) 66 | { 67 | $packageDir = sprintf('/%s/pushPackage%s.%s', sys_get_temp_dir(), time(), $userId); 68 | $package = $this->createPackage($packageDir, $userId); 69 | 70 | $this->generatePackage($package); 71 | 72 | return $package; 73 | } 74 | 75 | private function generatePackage(Package $package) 76 | { 77 | $packageDir = $package->getPackageDir(); 78 | $zipPath = $package->getZipPath(); 79 | 80 | if (!is_dir($packageDir)) { 81 | mkdir($packageDir); 82 | } 83 | 84 | $this->copyPackageFiles($package); 85 | $this->createPackageManifest($package); 86 | $this->createPackageSignature($package); 87 | 88 | $zip = $this->createZipArchive(); 89 | 90 | if (!$zip->open($zipPath, ZipArchive::CREATE)) { 91 | throw new ErrorException(sprintf('Could not open package "%s"', $zipPath)); 92 | } 93 | 94 | $packageFiles = Package::$packageFiles; 95 | $packageFiles[] = 'manifest.json'; 96 | $packageFiles[] = 'signature'; 97 | 98 | foreach ($packageFiles as $packageFile) { 99 | $filePath = sprintf('%s/%s', $packageDir, $packageFile); 100 | 101 | if (!file_exists($filePath)) { 102 | throw new ErrorException(sprintf('File does not exist "%s"', $filePath)); 103 | } 104 | 105 | $zip->addFile($filePath, $packageFile); 106 | } 107 | 108 | if (false === $zip->close()) { 109 | throw new ErrorException(sprintf('Could not save package "%s"', $zipPath)); 110 | } 111 | } 112 | 113 | private function copyPackageFiles(Package $package) 114 | { 115 | $packageDir = $package->getPackageDir(); 116 | 117 | mkdir($packageDir . '/icon.iconset'); 118 | 119 | foreach (Package::$packageFiles as $rawFile) { 120 | $filePath = sprintf('%s/%s', $packageDir, $rawFile); 121 | 122 | copy(sprintf('%s/%s', $this->basePushPackagePath, $rawFile), $filePath); 123 | 124 | if ($rawFile === 'website.json') { 125 | $websiteJson = file_get_contents($filePath); 126 | $websiteJson = str_replace('{{ userId }}', $package->getUserId(), $websiteJson); 127 | $websiteJson = str_replace('{{ host }}', $this->host, $websiteJson); 128 | $websiteJson = str_replace('{{ websiteName }}', $this->websiteName, $websiteJson); 129 | $websiteJson = str_replace('{{ websitePushId }}', $this->websitePushId, $websiteJson); 130 | file_put_contents($filePath, $websiteJson); 131 | } 132 | } 133 | } 134 | 135 | private function createPackageManifest(Package $package) 136 | { 137 | return $this->createPackageManifester()->createManifest($package); 138 | } 139 | 140 | private function createPackageSignature(Package $package) 141 | { 142 | return $this->createPackageSigner()->createPackageSignature( 143 | $this->certificate, $package 144 | ); 145 | } 146 | 147 | protected function createPackageSigner() 148 | { 149 | return new PackageSigner(); 150 | } 151 | 152 | protected function createPackageManifester() 153 | { 154 | return new PackageManifester(); 155 | } 156 | 157 | protected function createZipArchive() 158 | { 159 | return new ZipArchive(); 160 | } 161 | 162 | /** 163 | * @param string $packageDir 164 | * @param string $userId 165 | */ 166 | protected function createPackage($packageDir, $userId) 167 | { 168 | return new Package($packageDir, $userId); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/JWage/APNS/Safari/PackageManifester.php: -------------------------------------------------------------------------------- 1 | getPackageDir(), $rawFile); 18 | $manifestData[$rawFile] = sha1(file_get_contents($filePath)); 19 | } 20 | 21 | $manifestJsonPath = sprintf('%s/manifest.json', $package->getPackageDir()); 22 | $manifestJson = json_encode((object) $manifestData); 23 | 24 | file_put_contents($manifestJsonPath, $manifestJson); 25 | 26 | return $manifestJsonPath; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/JWage/APNS/Safari/PackageSigner.php: -------------------------------------------------------------------------------- 1 | getCertificateString(); 22 | $certPassword = $certificate->getPassword(); 23 | 24 | $certs = array(); 25 | 26 | if (!openssl_pkcs12_read($pkcs12, $certs, $certPassword)) { 27 | throw new RuntimeException('Failed to create signature.'); 28 | } 29 | 30 | $signaturePath = sprintf('%s/signature', $package->getPackageDir()); 31 | $manifestJsonPath = sprintf('%s/manifest.json', $package->getPackageDir()); 32 | 33 | // Sign the manifest.json file with the private key from the certificate 34 | $certData = openssl_x509_read($certs['cert']); 35 | $privateKey = openssl_pkey_get_private($certs['pkey'], $certPassword); 36 | openssl_pkcs7_sign($manifestJsonPath, $signaturePath, $certData, $privateKey, array(), PKCS7_BINARY | PKCS7_DETACHED); 37 | 38 | // Convert the signature from PEM to DER 39 | $signaturePem = file_get_contents($signaturePath); 40 | $matches = array(); 41 | 42 | if (!preg_match('~Content-Disposition:[^\n]+\s*?([A-Za-z0-9+=/\r\n]+)\s*?-----~', $signaturePem, $matches)) { 43 | throw new ErrorException('Failed to extract content from signature pem.'); 44 | } 45 | 46 | $signatureDer = base64_decode($matches[1]); 47 | file_put_contents($signaturePath, $signatureDer); 48 | 49 | return $signaturePath; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/JWage/APNS/Sender.php: -------------------------------------------------------------------------------- 1 | client = $client; 20 | } 21 | 22 | /** 23 | * Sends a safari website push notification to the given deviceToken. 24 | * 25 | * @param string $deviceToken 26 | * @param string $title 27 | * @param string $body 28 | * @param string $deepLink 29 | */ 30 | public function send($deviceToken, $title, $body, $deepLink = null) 31 | { 32 | return $this->client->sendPayload( 33 | $deviceToken, $this->createPayload($title, $body, $deepLink) 34 | ); 35 | } 36 | 37 | /** 38 | * Creates a Payload instance. 39 | * 40 | * @param string $title 41 | * @param string $body 42 | * @param string $deepLink 43 | * @return \JWage\APNS\Payload 44 | */ 45 | private function createPayload($title, $body, $deepLink = null) 46 | { 47 | return new Payload($title, $body, $deepLink); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/JWage/APNS/SocketClient.php: -------------------------------------------------------------------------------- 1 | certificate = $certificate; 51 | $this->host = $host; 52 | $this->port = $port; 53 | } 54 | 55 | public function __destruct() 56 | { 57 | if (is_resource($this->apnsResource)) { 58 | fclose($this->apnsResource); 59 | } 60 | } 61 | 62 | /** 63 | * Writes a binary message to apns. 64 | * 65 | * @param string $binaryMessage 66 | * @return integer Returns the number of bytes written, or FALSE on error. 67 | */ 68 | public function write($binaryMessage) 69 | { 70 | if (strlen($binaryMessage) > self::PAYLOAD_MAX_BYTES) { 71 | throw new \InvalidArgumentException( 72 | sprintf('The maximum size allowed for a notification payload is %s bytes; Apple Push Notification Service refuses any notification that exceeds this limit.', self::PAYLOAD_MAX_BYTES) 73 | ); 74 | } 75 | 76 | return fwrite($this->getApnsResource(), $binaryMessage); 77 | } 78 | 79 | /** 80 | * @return Resource 81 | */ 82 | protected function getApnsResource() 83 | { 84 | if (!is_resource($this->apnsResource)) { 85 | $this->apnsResource = $this->createStreamClient(); 86 | } 87 | 88 | return $this->apnsResource; 89 | } 90 | 91 | /** 92 | * @return Resource 93 | */ 94 | protected function createStreamContext() 95 | { 96 | $streamContext = stream_context_create(); 97 | stream_context_set_option($streamContext, 'ssl', 'local_cert', $this->certificate->writeToTmp()); 98 | 99 | return $streamContext; 100 | } 101 | 102 | /** 103 | * @return Resource 104 | */ 105 | protected function createStreamClient() 106 | { 107 | $address = $this->getSocketAddress(); 108 | 109 | $client = @stream_socket_client( 110 | $address, 111 | $this->error, 112 | $this->errorString, 113 | 2, 114 | STREAM_CLIENT_CONNECT, 115 | $this->createStreamContext() 116 | ); 117 | 118 | if (!$client) { 119 | throw new ErrorException( 120 | sprintf('Failed to create stream socket client to "%s". %s', $address, $this->errorString), $this->error 121 | ); 122 | } 123 | 124 | return $client; 125 | } 126 | 127 | /** 128 | * @return string $socketAddress 129 | */ 130 | protected function getSocketAddress() 131 | { 132 | return sprintf('ssl://%s:%s', $this->host, $this->port); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/JWage/APNS/Tests/APNSMessageTest.php: -------------------------------------------------------------------------------- 1 | deviceToken = '97213C2CA2146AF258B098611394FD6943FA730FF65E6797A85D3A0DC713A84C'; 18 | $this->payload = new Payload('title', 'body', 'deep link'); 19 | $this->apnsMessage = new APNSMessage($this->deviceToken, $this->payload); 20 | } 21 | 22 | public function testGetBinaryMessage() 23 | { 24 | $expectedBinaryMessage = $this->createBinaryMessage( 25 | $this->deviceToken, $this->payload->getPayload() 26 | ); 27 | 28 | $binaryMessage = $this->apnsMessage->getBinaryMessage(); 29 | $this->assertEquals($expectedBinaryMessage, $binaryMessage); 30 | } 31 | 32 | private function createBinaryMessage($deviceToken, array $payload) 33 | { 34 | $encodedPayload = json_encode($payload); 35 | 36 | return chr(0). 37 | chr(0). 38 | chr(32). 39 | pack('H*', $deviceToken). 40 | chr(0).chr(strlen($encodedPayload)). 41 | $encodedPayload; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/JWage/APNS/Tests/CertificateTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('', $certificate->getCertificateString()); 14 | 15 | $certificate = new Certificate('test'); 16 | $this->assertEquals('test', $certificate->getCertificateString()); 17 | } 18 | 19 | public function testPassword() 20 | { 21 | $certificate = new Certificate(null); 22 | $this->assertNull($certificate->getPassword()); 23 | 24 | $certificate = new Certificate(null, 'password'); 25 | $this->assertEquals('password', $certificate->getPassword()); 26 | } 27 | 28 | public function testToString() 29 | { 30 | $certificate = new Certificate('test'); 31 | $this->assertEquals('test', (string) $certificate); 32 | } 33 | 34 | public function testWriteTo() 35 | { 36 | $path = '/tmp/certificate_test_'.time(); 37 | $certificate = new Certificate('test'); 38 | $certificate->writeTo($path); 39 | $this->assertFileExists($path); 40 | $this->assertEquals('test', file_get_contents($path)); 41 | } 42 | 43 | public function testWriteToTmp() 44 | { 45 | $certificate = new Certificate('test'); 46 | $path = $certificate->writeToTmp(); 47 | $this->assertFileExists($path); 48 | $this->assertEquals('test', file_get_contents($path)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/JWage/APNS/Tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | socketClient = $this->getMockBuilder('JWage\APNS\SocketClient') 18 | ->disableOriginalConstructor() 19 | ->getMock(); 20 | $this->client = new ClientStub($this->socketClient); 21 | } 22 | 23 | public function testSendPayload() 24 | { 25 | $apnsMessage = $this->getMockBuilder('JWage\APNS\ApnsMessage') 26 | ->disableOriginalConstructor() 27 | ->getMock(); 28 | 29 | $this->client->setApnsMessage($apnsMessage); 30 | 31 | $apnsMessage->expects($this->once()) 32 | ->method('getBinaryMessage') 33 | ->will($this->returnValue('test binary message')); 34 | 35 | $this->socketClient->expects($this->once()) 36 | ->method('write') 37 | ->with('test binary message') 38 | ->will($this->returnValue('success')); 39 | 40 | $payload = new Payload('title', 'body', 'deep link'); 41 | $this->client->sendPayload('97213C2CA2146AF258B098611394FD6943FA730FF65E6797A85D3A0DC713A84C', $payload); 42 | } 43 | } 44 | 45 | class ClientStub extends Client 46 | { 47 | private $apnsMessage; 48 | 49 | public function setApnsMessage(ApnsMessage $apnsMessage) 50 | { 51 | $this->apnsMessage = $apnsMessage; 52 | } 53 | 54 | protected function createApnMessage($deviceToken, Payload $payload) 55 | { 56 | return $this->apnsMessage; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/JWage/APNS/Tests/PayloadTest.php: -------------------------------------------------------------------------------- 1 | payload = new Payload('title', 'body', 'deep link'); 15 | } 16 | 17 | public function testGetPayload() 18 | { 19 | $expectedPayload = array( 20 | 'aps' => array( 21 | 'alert' => array( 22 | 'title' => 'title', 23 | 'body' => 'body', 24 | ), 25 | 'url-args' => array( 26 | 'deep link' 27 | ), 28 | ), 29 | ); 30 | $payload = $this->payload->getPayload(); 31 | $this->assertEquals($expectedPayload, $payload); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/JWage/APNS/Tests/Safari/PackageGeneratorTest.php: -------------------------------------------------------------------------------- 1 | basePushPackagePath = realpath(__DIR__.'/../../../../../data/safariPushPackage.base'); 21 | 22 | $this->certificate = new Certificate('', 'password'); 23 | $this->packageGenerator = new PackageGeneratorStub( 24 | $this->certificate, $this->basePushPackagePath, 'host.com', 'WebsiteName', 'web.com.domain' 25 | ); 26 | } 27 | 28 | public function testCreatePackageForUser() 29 | { 30 | $package = $this->packageGenerator->createPushPackageForUser('userId'); 31 | $this->assertInstanceOf('JWage\APNS\Safari\Package', $package); 32 | $this->assertTrue(file_exists($package->getZipPath())); 33 | 34 | $zip = new ZipArchive(); 35 | $res = $zip->open($package->getZipPath()); 36 | if ($res === true) { 37 | $extractTo = sprintf('%s/extract_path', sys_get_temp_dir()); 38 | $zip->extractTo($extractTo); 39 | $zip->close(); 40 | 41 | $files = glob($extractTo.'/*'); 42 | $this->assertEquals(4, count($files)); 43 | 44 | $iconPath = sprintf('%s/icon.iconset', $extractTo); 45 | $icons = glob(sprintf('%s/*.png', $iconPath)); 46 | $this->assertEquals(6, count($icons)); 47 | 48 | $manifestJsonPath = sprintf('%s/manifest.json', $extractTo); 49 | $expectedManifest = array ( 50 | 'icon.iconset/icon_16x16.png' => '6b192031f23d78db69155261257590cb81b1a6d7', 51 | 'icon.iconset/icon_16x16@2x.png' => '8de653dd0a03f2300f9756c79b0c8bce3abd922b', 52 | 'icon.iconset/icon_32x32.png' => 'a604fd29a5a7bce5d73fe08a75793e8903172c3f', 53 | 'icon.iconset/icon_32x32@2x.png' => 'da343420793428ad803d7ae435e76e3293e60f21', 54 | 'icon.iconset/icon_128x128.png' => 'c958eb6f34a5f6455d2f4b3c85b3bcde30208b4e', 55 | 'icon.iconset/icon_128x128@2x.png' => '529d000f332ad65d284db541a7b5955fa03fb9e7', 56 | 'website.json' => 'bcafd546fd3e1ea7e94c8ccef3fbb089117a31d3', 57 | ); 58 | $this->assertEquals(json_encode($expectedManifest), file_get_contents($manifestJsonPath)); 59 | 60 | $signaturePath = sprintf('%s/signature', $extractTo); 61 | $this->assertEquals('test signature', file_get_contents($signaturePath)); 62 | 63 | $expectedWebsiteJson = 64 | '{ 65 | "websiteName": "WebsiteName", 66 | "websitePushID": "web.com.domain", 67 | "allowedDomains": ["http://host.com", "https://host.com"], 68 | "urlFormatString": "http://host.com/%@", 69 | "authenticationToken": "userId", 70 | "webServiceURL": "https://host.com/safari_push_notifications/userId" 71 | } 72 | '; 73 | 74 | $websiteJsonPath = sprintf('%s/website.json', $extractTo); 75 | $this->assertEquals($expectedWebsiteJson, file_get_contents($websiteJsonPath)); 76 | } else { 77 | $this->fail('Could not extract zip package'); 78 | } 79 | } 80 | } 81 | 82 | class PackageSignerStub extends PackageSigner 83 | { 84 | public function createPackageSignature(Certificate $certificate, Package $package) 85 | { 86 | $signaturePath = sprintf('%s/signature', $package->getPackageDir()); 87 | file_put_contents($signaturePath, 'test signature'); 88 | } 89 | } 90 | 91 | class PackageGeneratorStub extends PackageGenerator 92 | { 93 | protected function createPackageSigner() 94 | { 95 | return new PackageSignerStub(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/JWage/APNS/Tests/SenderTest.php: -------------------------------------------------------------------------------- 1 | client = $this->getMockBuilder('JWage\APNS\Client') 17 | ->disableOriginalConstructor() 18 | ->getMock(); 19 | $this->sender = new Sender($this->client); 20 | } 21 | 22 | public function testSend() 23 | { 24 | $payload = new Payload('title', 'body', 'deep link'); 25 | $this->client->expects($this->once()) 26 | ->method('sendPayload') 27 | ->with('device token', $payload); 28 | $this->sender->send('device token', 'title', 'body', 'deep link'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/JWage/APNS/Tests/SocketClientTest.php: -------------------------------------------------------------------------------- 1 | testPath = tempnam(sys_get_temp_dir(), 'tmp_'); 18 | $this->certificate = new Certificate(''); 19 | $this->socketClient = new SocketClientStub($this->certificate, 'host', 1234); 20 | $this->socketClient->setTestPath($this->testPath); 21 | } 22 | 23 | public function testWrite() 24 | { 25 | $this->socketClient->write('test'); 26 | $this->assertEquals('test', file_get_contents($this->testPath)); 27 | } 28 | 29 | /** 30 | * @expectedException \InvalidArgumentException 31 | * @expectedExceptionMessage The maximum size allowed for a notification payload is 256 bytes; Apple Push Notification Service refuses any notification that exceeds this limit. 32 | */ 33 | public function testWriteMaxBytes() 34 | { 35 | $deviceToken = '97213C2CA2146AF258B098611394FD6943FA730FF65E6797A85D3A0DC713A84C'; 36 | 37 | $payload = array( 38 | 'aps' => array( 39 | 'alert' => array( 40 | 'title' => 'Website', 41 | 'body' => 'Jonathan H. Wage joined your website. This is a really long push notification body. So long it probably won\'t work! Not long enough! Perfect!', 42 | ), 43 | 'url-args' => array( 44 | 'http://google.com', 45 | ), 46 | ), 47 | ); 48 | 49 | $encodedPayload = json_encode($payload); 50 | 51 | $payload = chr(0). 52 | chr(0). 53 | chr(32). 54 | pack('H*', $deviceToken). 55 | chr(0).chr(strlen($encodedPayload)). 56 | $encodedPayload; 57 | 58 | // write binary string to file 59 | $path = sprintf('%s/php_apns_test_write_max_bytes', sys_get_temp_dir()); 60 | file_put_contents($path, $payload); 61 | 62 | $this->assertEquals(filesize($path), strlen($payload), 'Compare result of filesize() to strlen()'); 63 | 64 | $this->socketClient->write($payload); 65 | } 66 | 67 | public function testCreateStreamContext() 68 | { 69 | $streamContext = $this->socketClient->getTestCreateStreamContext(); 70 | $this->assertTrue(is_resource($streamContext)); 71 | } 72 | 73 | public function testGetSocketAddress() 74 | { 75 | $this->assertEquals('ssl://host:1234', $this->socketClient->getTestSocketAddress()); 76 | } 77 | 78 | /** 79 | * @expectedException ErrorException 80 | * @expectedExceptionMessage Failed to create stream socket client to "ssl://somethingthatdoesnotexist:100". php_network_getaddresses: getaddrinfo failed: Name or service not known 81 | */ 82 | public function testConnectThrowsException() 83 | { 84 | $socketClient = new SocketClient($this->certificate, 'somethingthatdoesnotexist', 100); 85 | $socketClient->write('test'); 86 | } 87 | } 88 | 89 | class SocketClientStub extends SocketClient 90 | { 91 | protected $testPath; 92 | 93 | public function setTestPath($testPath) 94 | { 95 | $this->testPath = $testPath; 96 | } 97 | 98 | public function getTestCreateStreamContext() 99 | { 100 | return parent::createStreamContext(); 101 | } 102 | 103 | public function getTestSocketAddress() 104 | { 105 | return parent::getSocketAddress(); 106 | } 107 | 108 | protected function createStreamClient() 109 | { 110 | return fopen($this->testPath, 'r+'); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('JWage\Test', __DIR__); 8 | $loader->register(); 9 | }); 10 | --------------------------------------------------------------------------------