Skip to content

Commit 478e1d2

Browse files
committed
Merge branch 'master' into vapid
Conflicts: src/Encryption.php src/Utils.php src/WebPush.php
2 parents 216aaac + dd1da44 commit 478e1d2

File tree

10 files changed

+267
-17
lines changed

10 files changed

+267
-17
lines changed

.travis.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1+
dist: trusty
12
language: php
23

34
php:
45
- 5.6
56
- hhvm
67
- 7.0
78

9+
env:
10+
- TRAVIS_NODE_VERSION="stable"
11+
12+
before_install:
13+
- nvm install node
14+
15+
install:
16+
- npm install [email protected] -g
17+
818
before_script:
919
- composer install --prefer-source -n --no-dev
20+
- "export DISPLAY=:99.0"
21+
- "sh -e /etc/init.d/xvfb start || echo \"Unable to start virtual display.\""
22+
- sleep 3 # give xvfb some time to start
1023

11-
script: phpunit -c phpunit.travis.xml
24+
script:
25+
- web-push-testing-service start example -p 9012
26+
- phpunit -c phpunit.travis.xml
27+
- web-push-testing-service stop example

CONTRIBUTING.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
11
# Contributing to WebPush
22
WebPush is an open source library.
33
Feel free to contribute by submitting a pull request or creating (and solving) issues!
4+
5+
## Running Tests
6+
7+
First, you will need to create your own configuration file by copying
8+
phpunit.dist.xml to phpunit.xml and filling in the fields you need for
9+
testing (i.e. STANDARD_ENDPOINT, GCM_API_KEY etc.).
10+
11+
Then, download [phpunit](https://phpunit.de/) and test with one of the
12+
following commands:
13+
14+
**For All Tests**
15+
`php phpunit.phar`
16+
17+
**For a Specific Test File**
18+
`php phpunit.phar tests/EncryptionTest.php`
19+
20+
**For a Single Test**
21+
`php phpunit.phar . --filter "/::testPadPayload( .*)?$/"` (regex)
22+
23+
Some tests have a custom decorator @skipIfTravis. The reason is that
24+
there's no way in Travis to update the push subscription, so the endpoint
25+
in my phpunit.travis.xml would ultimately expire
26+
(and require a human modification), and the corresponding tests would fail.
27+
But locally, these tests are handy.

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# WebPush
22
> Web Push library for PHP
33
4-
[![Build Status](https://travis-ci.org/Minishlink/web-push.svg?branch=master)](https://travis-ci.org/Minishlink/web-push)
4+
[![Build Status](https://travis-ci.org/web-push-libs/web-push-php.svg?branch=master)](https://travis-ci.org/web-push-libs/web-push-php)
55
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/d60e8eea-aea1-4739-8ce0-a3c3c12c6ccf/mini.png)](https://insight.sensiolabs.com/projects/d60e8eea-aea1-4739-8ce0-a3c3c12c6ccf)
66

77
## Installation
@@ -210,6 +210,7 @@ $browser = $webPush->getBrowser();
210210
The following are available:
211211

212212
- Symfony: [MinishlinkWebPushBundle](https://github.com/Minishlink/web-push-bundle)
213+
- Laravel: [laravel-notification-channels/webpush](https://github.com/laravel-notification-channels/webpush)
213214

214215
Feel free to add your own!
215216

@@ -243,8 +244,5 @@ This library was inspired by the Node.js [marco-c/web-push](https://github.com/m
243244
## Contributing
244245
See [CONTRIBUTING.md](https://github.com/Minishlink/web-push/blob/master/CONTRIBUTING.md).
245246

246-
## Tests
247-
Copy `phpunit.xml` from `phpunit.dist.xml` and fill it with your test endpoints and private keys.
248-
249247
## License
250248
[MIT](https://github.com/Minishlink/web-push/blob/master/LICENSE)

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"type": "library",
44
"description": "Web Push library for PHP",
55
"keywords": ["push", "notifications", "web", "WebPush", "Push API"],
6-
"homepage": "https://github.com/Minishlink/web-push",
6+
"homepage": "https://github.com/web-push-libs/web-push-php",
77
"license": "MIT",
88
"authors": [
99
{

src/Encryption.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ final class Encryption
2727
*/
2828
public static function padPayload($payload, $automatic)
2929
{
30-
$payloadLen = Utils::safe_strlen($payload);
30+
$payloadLen = Utils::safeStrlen($payload);
3131
$padLen = $automatic ? self::MAX_PAYLOAD_LENGTH - $payloadLen : 0;
3232

3333
return pack('n*', $padLen).str_pad($payload, $padLen + $payloadLen, chr(0), STR_PAD_LEFT);
@@ -62,7 +62,7 @@ public static function encrypt($payload, $userPublicKey, $userAuthToken, $native
6262
$userPublicKeyObject = $generator->getPublicKeyFrom($pointUserPublicKey->getX(), $pointUserPublicKey->getY(), $generator->getOrder());
6363

6464
// get shared secret from user public key and local private key
65-
$sharedSecret = hex2bin($math->decHex((string) $userPublicKeyObject->getPoint()->mul($localPrivateKeyObject->getSecret())->getX()));
65+
$sharedSecret = hex2bin($math->decHex(gmp_strval($userPublicKeyObject->getPoint()->mul($localPrivateKeyObject->getSecret())->getX())));
6666

6767
// generate salt
6868
$salt = openssl_random_pseudo_bytes(16);
@@ -143,12 +143,12 @@ private static function hkdf($salt, $ikm, $info, $length)
143143
*/
144144
private static function createContext($clientPublicKey, $serverPublicKey)
145145
{
146-
if (Utils::safe_strlen($clientPublicKey) !== 65) {
146+
if (Utils::safeStrlen($clientPublicKey) !== 65) {
147147
throw new \ErrorException('Invalid client public key length');
148148
}
149149

150150
// This one should never happen, because it's our code that generates the key
151-
if (Utils::safe_strlen($serverPublicKey) !== 65) {
151+
if (Utils::safeStrlen($serverPublicKey) !== 65) {
152152
throw new \ErrorException('Invalid server public key length');
153153
}
154154

@@ -171,7 +171,7 @@ private static function createContext($clientPublicKey, $serverPublicKey)
171171
*/
172172
private static function createInfo($type, $context)
173173
{
174-
if (Utils::safe_strlen($context) !== 135) {
174+
if (Utils::safeStrlen($context) !== 135) {
175175
throw new \ErrorException('Context argument has invalid size');
176176
}
177177

src/Utils.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
class Utils
1515
{
16-
public static function safe_strlen($string)
16+
public static function safeStrlen($string)
1717
{
1818
return mb_strlen($string, '8bit');
1919
}

src/WebPush.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public function __construct(array $auth = array(), $defaultOptions = array(), $t
8282
public function sendNotification($endpoint, $payload = null, $userPublicKey = null, $userAuthToken = null, $flush = false, $options = array())
8383
{
8484
if (isset($payload)) {
85-
if (Utils::safe_strlen($payload) > Encryption::MAX_PAYLOAD_LENGTH) {
85+
if (Utils::safeStrlen($payload) > Encryption::MAX_PAYLOAD_LENGTH) {
8686
throw new \ErrorException('Size of payload must not be greater than '.Encryption::MAX_PAYLOAD_LENGTH.' octets.');
8787
}
8888

@@ -185,11 +185,11 @@ private function prepareAndSend(array $notifications)
185185
$encrypted = Encryption::encrypt($payload, $userPublicKey, $userAuthToken, $this->nativePayloadEncryptionSupport);
186186

187187
$headers = array(
188-
'Content-Length' => Utils::safe_strlen($encrypted['cipherText']),
188+
'Content-Length' => Utils::safeStrlen($encrypted['cipherText']),
189189
'Content-Type' => 'application/octet-stream',
190190
'Content-Encoding' => 'aesgcm',
191-
'Encryption' => 'keyid=p256dh;salt='.$encrypted['salt'],
192-
'Crypto-Key' => 'keyid=p256dh;dh='.$encrypted['localPublicKey'],
191+
'Encryption' => 'salt='.$encrypted['salt'],
192+
'Crypto-Key' => 'dh='.$encrypted['localPublicKey'],
193193
);
194194

195195
$content = $encrypted['cipherText'];

tests/EncryptionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function testPadPayload($payload)
2424
$res = Encryption::padPayload($payload, true);
2525

2626
$this->assertContains('test', $res);
27-
$this->assertEquals(4080, Utils::safe_strlen($res));
27+
$this->assertEquals(4080, Utils::safeStrlen($res));
2828
}
2929

3030
public function payloadProvider()

tests/PushServiceTest.php

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the WebPush library.
5+
*
6+
* (c) Louis Lagrange <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
use Minishlink\WebPush\WebPush;
13+
14+
class PushServiceTest extends PHPUnit_Framework_TestCase
15+
{
16+
private static $portNumber = 9012;
17+
private static $testSuiteId;
18+
private static $testServiceUrl;
19+
private static $gcmSenderId = '759071690750';
20+
private static $gcmApiKey = 'AIzaSyBAU0VfXoskxUSg81K5VgLgwblHbZWe6tA';
21+
22+
/** @var WebPush WebPush with correct api keys */
23+
private $webPush;
24+
25+
/**
26+
* This check forces these tests to only run on Travis.
27+
* If we can reliably start and stop web-push-testing-service and
28+
* detect current OS, we can probably run this automatically
29+
* for Linux and OS X at a later date.
30+
*/
31+
protected function checkRequirements()
32+
{
33+
parent::checkRequirements();
34+
35+
if (!(getenv('TRAVIS') || getenv('CI'))) {
36+
$this->markTestSkipped('This test does not run on Travis.');
37+
}
38+
}
39+
40+
public static function setUpBeforeClass()
41+
{
42+
self::$testServiceUrl = 'http://localhost:'.self::$portNumber;
43+
}
44+
45+
protected function setUp()
46+
{
47+
$startApiCurl = curl_init(self::$testServiceUrl.'/api/start-test-suite/');
48+
curl_setopt_array($startApiCurl, array(
49+
CURLOPT_POST => true,
50+
CURLOPT_POSTFIELDS => array(),
51+
CURLOPT_RETURNTRANSFER => true,
52+
CURLOPT_TIMEOUT => 30,
53+
));
54+
55+
$resp = curl_exec($startApiCurl);
56+
57+
if ($resp) {
58+
$parsedResp = json_decode($resp);
59+
self::$testSuiteId = $parsedResp->{'data'}->{'testSuiteId'};
60+
} else {
61+
echo 'Curl error: ';
62+
echo curl_error($startApiCurl);
63+
64+
throw new Exception('Unable to get a test suite from the '.
65+
'web-push-testing-service');
66+
}
67+
68+
curl_close($startApiCurl);
69+
}
70+
71+
public function browserProvider()
72+
{
73+
return array(
74+
// Web Push
75+
array('chrome', 'stable', array()),
76+
array('chrome', 'beta', array()),
77+
array('chrome', 'unstable', array()),
78+
array('firefox', 'stable', array()),
79+
array('firefox', 'beta', array()),
80+
array('firefox', 'unstable', array()),
81+
// Web Push + GCM
82+
array('chrome', 'stable', array('GCM' => self::$gcmApiKey)),
83+
array('chrome', 'beta', array('GCM' => self::$gcmApiKey)),
84+
array('chrome', 'unstable', array('GCM' => self::$gcmApiKey)),
85+
array('firefox', 'stable', array('GCM' => self::$gcmApiKey)),
86+
array('firefox', 'beta', array('GCM' => self::$gcmApiKey)),
87+
array('firefox', 'unstable', array('GCM' => self::$gcmApiKey)),
88+
// Web Push + VAPID
89+
// Web Push + GCM + VAPID
90+
);
91+
}
92+
93+
/**
94+
* @dataProvider browserProvider
95+
* Run integration tests with browsers
96+
*/
97+
public function testBrowsers($browserId, $browserVersion, $options)
98+
{
99+
$this->webPush = new WebPush($options);
100+
$this->webPush->setAutomaticPadding(false);
101+
102+
$dataString = json_encode(array(
103+
'testSuiteId' => self::$testSuiteId,
104+
'browserName' => $browserId,
105+
'browserVersion' => $browserVersion,
106+
'gcmSenderId' => self::$gcmSenderId,
107+
));
108+
$getSubscriptionCurl = curl_init(self::$testServiceUrl.'/api/get-subscription/');
109+
curl_setopt_array($getSubscriptionCurl, array(
110+
CURLOPT_POST => true,
111+
CURLOPT_POSTFIELDS => $dataString,
112+
CURLOPT_RETURNTRANSFER => true,
113+
CURLOPT_HTTPHEADER => array(
114+
'Content-Type: application/json',
115+
'Content-Length: '.strlen($dataString),
116+
),
117+
CURLOPT_TIMEOUT => 30,
118+
));
119+
120+
$resp = curl_exec($getSubscriptionCurl);
121+
122+
// Close request to clear up some resources
123+
curl_close($getSubscriptionCurl);
124+
125+
$parsedResp = json_decode($resp);
126+
127+
$testId = $parsedResp->{'data'}->{'testId'};
128+
$subscription = $parsedResp->{'data'}->{'subscription'};
129+
$endpoint = $subscription->{'endpoint'};
130+
$keys = $subscription->{'keys'};
131+
$auth = $keys->{'auth'};
132+
$p256dh = $keys->{'p256dh'};
133+
134+
$payload = 'hello';
135+
$getNotificationCurl = null;
136+
try {
137+
$sendResp = $this->webPush->sendNotification($endpoint, $payload, $p256dh, $auth, true);
138+
$this->assertTrue($sendResp);
139+
140+
$dataString = json_encode(array(
141+
'testSuiteId' => self::$testSuiteId,
142+
'testId' => $testId,
143+
));
144+
145+
$getNotificationCurl = curl_init(self::$testServiceUrl.'/api/get-notification-status/');
146+
curl_setopt_array($getNotificationCurl, array(
147+
CURLOPT_POST => true,
148+
CURLOPT_POSTFIELDS => $dataString,
149+
CURLOPT_RETURNTRANSFER => true,
150+
CURLOPT_HTTPHEADER => array(
151+
'Content-Type: application/json',
152+
'Content-Length: '.strlen($dataString),
153+
),
154+
CURLOPT_TIMEOUT => 30,
155+
));
156+
$resp = curl_exec($getNotificationCurl);
157+
158+
$parsedResp = json_decode($resp);
159+
160+
$messages = $parsedResp->{'data'}->{'messages'};
161+
$this->assertEquals(count($messages), 1);
162+
$this->assertEquals($messages[0], $payload);
163+
} catch (Exception $e) {
164+
if (
165+
strpos($endpoint, 'https://android.googleapis.com/gcm/send') === 0 &&
166+
!array_key_exists('GCM', $options)
167+
) {
168+
if ($e->getMessage() !== 'No GCM API Key specified.') {
169+
echo $e;
170+
}
171+
$this->assertEquals($e->getMessage(), 'No GCM API Key specified.');
172+
} else {
173+
if ($getNotificationCurl) {
174+
echo 'Curl error: ';
175+
echo curl_error($getNotificationCurl);
176+
}
177+
throw $e;
178+
}
179+
}
180+
}
181+
182+
protected function tearDown()
183+
{
184+
$dataString = '{ "testSuiteId": '.self::$testSuiteId.' }';
185+
$curl = curl_init(self::$testServiceUrl.'/api/end-test-suite/');
186+
curl_setopt_array($curl, array(
187+
CURLOPT_POST => true,
188+
CURLOPT_POSTFIELDS => $dataString,
189+
CURLOPT_RETURNTRANSFER => true,
190+
CURLOPT_HTTPHEADER => array(
191+
'Content-Type: application/json',
192+
'Content-Length: '.strlen($dataString),
193+
),
194+
CURLOPT_TIMEOUT => 30,
195+
));
196+
$resp = curl_exec($curl);
197+
$parsedResp = json_decode($resp);
198+
199+
self::$testSuiteId = null;
200+
// Close request to clear up some resources
201+
curl_close($curl);
202+
}
203+
204+
public static function tearDownAfterClass()
205+
{
206+
$testingServiceResult = exec(
207+
'web-push-testing-service stop phpunit');
208+
}
209+
}

tests/WebPushTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ public function testFlush()
118118
$this->assertTrue($this->webPush->flush());
119119
}
120120

121+
/**
122+
* @skipIfTravis
123+
*/
121124
public function testSendGCMNotificationWithoutGCMApiKey()
122125
{
123126
$webPush = new WebPush();

0 commit comments

Comments
 (0)