diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e9fa033 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: php +php: + - "5.6" + - "7.0" + - "7.1" +install: composer install +script: + - vendor/bin/phpunit + - vendor/bin/phpcs --standard=PSR2 library/ tests/ diff --git a/README.md b/README.md index a49741b..6bbad73 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Google Cloud Messaging (GCM) PHP Server Library -------------------------------------------- -A PHP library for sending messages to devices registered through Google Cloud Messaging. +A PHP library for sending messages to devices registered through Google Cloud Messaging. [![Build Status](https://travis-ci.org/dommccarty/GCMMessage.svg?branch=master)](https://travis-ci.org/dommccarty/GCMMessage) See: http://developer.android.com/guide/google/gcm/index.html @@ -20,7 +20,7 @@ $message = new GCM\Message( ); $message - ->notification(array("title" => "foo", "body" => "bar")) + ->setNotification(array("title" => "foo", "body" => "bar")) ->setCollapseKey("collapse_key") ->setDelayWhileIdle(true) ->setTtl(123) @@ -40,7 +40,21 @@ try { } if ($response->getFailureCount() > 0) { - $invalidRegistrationIds = $GCMresponse->getInvalidRegistrationIds(); + + if ($response->getExistsInvalidDataKey()) { + //You used a reserved data key + $error_msg = 'Invalid data key in payload. ' . json_encode($message->getNotification()); + throw new Exception($error_msg, Exception::INVALID_DATA_KEY); + } + + if ($response->getExistsMismatchSenderId()) { + //A client sent the wrong senderId when it registered for pushes + $error_msg = 'Mismatch senderId. Problem clients are ' + . json_encode($response->getMismatchSenderIdIds()); + throw new Exception($error_msg, Exception::MISMATCH_SENDER_ID); + } + + $invalidRegistrationIds = $response->getInvalidRegistrationIds(); foreach($invalidRegistrationIds as $invalidRegistrationId) { //Remove $invalidRegistrationId from DB //TODO @@ -53,11 +67,20 @@ try { } catch (GCM\Exception $e) { switch ($e->getCode()) { + + case GCM\Exception::UNKNOWN_ERROR: + if ($e->getMustRetry()) { + $waitSeconds = $e->getWaitSeconds(); + //retry in that many seconds, and use exponential back-off subsequently. + //TODO + break; + } case GCM\Exception::ILLEGAL_API_KEY: case GCM\Exception::AUTHENTICATION_ERROR: - case GCM\Exception::MALFORMED_REQUEST: - case GCM\Exception::UNKNOWN_ERROR: + case GCM\Exception::MALFORMED_REQUEST: case GCM\Exception::MALFORMED_RESPONSE: + case GCM\Exception::INVALID_DATA_KEY: //you used a forbidden key in the notification + case GCM\Exception::MISMATCH_SENDER_ID; //a client sent the wrong senderId when it registered for pushes //Deal with it break; } @@ -89,7 +112,21 @@ try { } if ($response->getFailureCount() > 0) { - $invalidRegistrationIds = $GCMresponse->getInvalidRegistrationIds(); + + if ($response->getExistsInvalidDataKey()) { + //You used a reserved data key + $error_msg = 'Invalid data key in payload. ' . json_encode($message->getNotification()); + throw new Exception($error_msg, Exception::INVALID_DATA_KEY); + } + + if ($response->getExistsMismatchSenderId()) { + //A client sent the wrong senderId when it registered for pushes + $error_msg = 'Mismatch senderId. Problem clients are ' + . json_encode($response->getMismatchSenderIdIds()); + throw new Exception($error_msg, Exception::MISMATCH_SENDER_ID); + } + + $invalidRegistrationIds = $response->getInvalidRegistrationIds(); foreach($invalidRegistrationIds as $invalidRegistrationId) { //Remove $invalidRegistrationId from DB //TODO @@ -102,11 +139,20 @@ try { } catch (GCM\Exception $e) { switch ($e->getCode()) { + + case GCM\Exception::UNKNOWN_ERROR: + if ($e->getMustRetry()) { + $waitSeconds = $e->getWaitSeconds(); + //retry in that many seconds, and use exponential back-off subsequently. + //TODO + break; + } case GCM\Exception::ILLEGAL_API_KEY: case GCM\Exception::AUTHENTICATION_ERROR: - case GCM\Exception::MALFORMED_REQUEST: - case GCM\Exception::UNKNOWN_ERROR: + case GCM\Exception::MALFORMED_REQUEST: case GCM\Exception::MALFORMED_RESPONSE: + case GCM\Exception::INVALID_DATA_KEY: //you used a forbidden key in the notification + case GCM\Exception::MISMATCH_SENDER_ID; //a client sent the wrong senderId when it registered for pushes //Deal with it break; } @@ -131,8 +177,6 @@ $sender = new GCM\Sender("YOUR GOOGLE API KEY", false, "/path/to/cacert.crt"); ChangeLog ---------------------- -* v0.3 - Content-available added (https://github.com/CodeMonkeysRu/GCMMessage/pull/11) -* v0.2 - Notifications added -* v0.1 - Initial release + Licensed under MIT license. diff --git a/composer.json b/composer.json index 235ffbb..bfa44b8 100644 --- a/composer.json +++ b/composer.json @@ -1,24 +1,32 @@ { - "name": "codemonkeys-ru/gcm-message", - "type": "library", - "description": "Google Cloud Messaging (GCM) PHP Server Library", - "keywords": ["GCM", "push message", "android"], - "authors": [ - { - "name": "Vladimir Savenkov", - "email": "ivariable@gmail.com", - "homepage": "http://ivariable.ru", - "role": "Developer" - } - ], - "license": "MIT", - "require": { - "php": ">=5.3.2" - }, - "minimum-stability": "dev", - "autoload": { - "psr-0": { - "CodeMonkeysRu\\GCM": "library/" - } - } + "name": "codemonkeys-ru/gcm-message", + "minimum-stability": "stable", + "type": "library", + "description": "Google Cloud Messaging (GCM) PHP Server Library", + "keywords": [ + "GCM", + "push message", + "android" + ], + "authors": [ + { + "name": "Vladimir Savenkov", + "email": "ivariable@gmail.com", + "homepage": "https://github.com/iVariable", + "role": "Maintainer" + } + ], + "license": "MIT", + "autoload": { + "psr-0": { + "CodeMonkeysRu\\GCM": "library/" + } + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "squizlabs/php_codesniffer": "2.*", + "phpunit/phpunit": "5.7.*" + } } diff --git a/library/CodeMonkeysRu/GCM/Exception.php b/library/CodeMonkeysRu/GCM/Exception.php index 6611539..998af6e 100644 --- a/library/CodeMonkeysRu/GCM/Exception.php +++ b/library/CodeMonkeysRu/GCM/Exception.php @@ -9,5 +9,26 @@ class Exception extends \Exception const MALFORMED_REQUEST = 3; const UNKNOWN_ERROR = 4; const MALFORMED_RESPONSE = 5; - -} \ No newline at end of file + const INVALID_DATA_KEY = 6; + const MISMATCH_SENDER_ID = 7; + + private $mustRetry = false; + private $waitSeconds = null; + + public function setMustRetry($bool) + { + $this->mustRetry = $bool; + } + public function getMustRetry() + { + return $this->mustRetry; + } + public function setWaitSeconds($int) + { + $this->waitSeconds = $int; + } + public function getWaitSeconds() + { + return $this->waitSeconds; + } +} diff --git a/library/CodeMonkeysRu/GCM/Message.php b/library/CodeMonkeysRu/GCM/Message.php index d8bdc60..c7c3815 100644 --- a/library/CodeMonkeysRu/GCM/Message.php +++ b/library/CodeMonkeysRu/GCM/Message.php @@ -113,8 +113,8 @@ public function __construct($registrationIds = null, $data = null, $collapseKey /** * Set multiple fields at once. * - * @param string[] $registrationIds - * @param array|null $data + * @param string[] $registrationIds + * @param array|null $data * @param string|null $collapseKey */ public function bulkSet($registrationIds = array(), $data = null, $collapseKey = null) @@ -222,4 +222,4 @@ public function setContentAvailable($contentAvailable) $this->contentAvailable = $contentAvailable; return $this; } -} \ No newline at end of file +} diff --git a/library/CodeMonkeysRu/GCM/Response.php b/library/CodeMonkeysRu/GCM/Response.php index 208c95e..1e33ee1 100644 --- a/library/CodeMonkeysRu/GCM/Response.php +++ b/library/CodeMonkeysRu/GCM/Response.php @@ -34,6 +34,28 @@ class Response * @var integer */ private $canonicalIds = null; + + /** + * Response headers. + * + * @var string[] + */ + private $responseHeaders = []; + + /** + * Did you use a reserved data key? + * + * @var boolean + */ + private $existsInvalidDataKey = false; + + /** + * Did one of your clients register with the wrong senderId? + * If one of them did, then presumably they all did. + * + * @var boolean + */ + private $existsMismatchSenderId = false; /** * Array of objects representing the status of the messages processed. @@ -43,32 +65,58 @@ class Response * message_id: String representing the message when it was successfully processed. * registration_id: If set, means that GCM processed the message but it has another canonical * registration ID for that device, so sender should replace the IDs on future requests - * (otherwise they might be rejected). This field is never set if there is an error in the request. + * (otherwise they might be rejected). This field is never set if + * there is an error in the request. * error: String describing an error that occurred while processing the message for that recipient. * The possible values are the same as documented in the above table, plus "Unavailable" * (meaning GCM servers were busy and could not process the message for that particular recipient, - * so it could be retried). + * so it could be retried, or the device rate is exceeded and you should wait a bit). * * @var array */ private $results = array(); - public function __construct(Message $message, $responseBody) + public function __construct(Message $message, $responseBody, $responseHeaders) { + $this->responseHeaders = $responseHeaders; + $data = \json_decode($responseBody, true); if ($data === null) { - throw new Exception("Malformed reponse body. ".$responseBody, Exception::MALFORMED_RESPONSE); + throw new Exception( + "Malformed reponse body. ".json_encode($responseHeaders).$responseBody, + Exception::MALFORMED_RESPONSE + ); } $this->multicastId = $data['multicast_id']; $this->failure = $data['failure']; $this->success = $data['success']; $this->canonicalIds = $data['canonical_ids']; + $this->existsInvalidDataKey = false; + $this->existsMismatchSenderId = false; $this->results = array(); + foreach ($message->getRegistrationIds() as $key => $registrationId) { - $this->results[$registrationId] = $data['results'][$key]; + $result = $data['results'][$key]; + if (isset($result['error'])) { + switch ($result['error']) { + case "InvalidDataKey": + $this->existsInvalidDataKey = true; + break; + case "MismatchSenderId": + $this->existsMismatchSenderId = true; + break; + } + } + $this->results[$registrationId] = $result; } + $result = null; } - + + public function getResponseHeaders() + { + return $this->responseHeaders; + } + public function getMulticastId() { return $this->multicastId; @@ -79,10 +127,25 @@ public function getSuccessCount() return $this->success; } + /** + * Both implementation errors and server errors are included here. + * + * @return integer + */ public function getFailureCount() { return $this->failure; } + + public function getExistsInvalidDataKey() + { + return $this->existsInvalidDataKey; + } + + public function getExistsMismatchSenderId() + { + return $this->existsMismatchSenderId; + } public function getNewRegistrationIdsCount() { @@ -105,14 +168,19 @@ public function getNewRegistrationIds() if ($this->getNewRegistrationIdsCount() == 0) { return array(); } - $filteredResults = array_filter($this->results, - function($result) { + $filteredResults = array_filter( + $this->results, + function ($result) { return isset($result['registration_id']); - }); + } + ); - $data = array_map(function($result) { + $data = array_map( + function ($result) { return $result['registration_id']; - }, $filteredResults); + }, + $filteredResults + ); return $data; } @@ -128,8 +196,9 @@ public function getInvalidRegistrationIds() if ($this->getFailureCount() == 0) { return array(); } - $filteredResults = array_filter($this->results, - function($result) { + $filteredResults = array_filter( + $this->results, + function ($result) { return ( isset($result['error']) && @@ -139,11 +208,13 @@ function($result) { ($result['error'] == "InvalidRegistration") ) ); - }); + } + ); return array_keys($filteredResults); } + /** * Returns an array of registration ids for which you must resend a message (?), * cause devices aren't available now. @@ -157,16 +228,48 @@ public function getUnavailableRegistrationIds() if ($this->getFailureCount() == 0) { return array(); } - $filteredResults = array_filter($this->results, - function($result) { + $filteredResults = array_filter( + $this->results, + function ($result) { return ( isset($result['error']) && + ( ($result['error'] == "Unavailable") + || + ($result['error'] == "InternalServerError") + || + ($result['error'] == "DeviceMessageRateExceeded") + ) ); - }); + } + ); return array_keys($filteredResults); } + + /** + * Returns an array of registration ids who registered + * for pushes using the wrong senderId. + * + * @return array + */ + public function getMismatchSenderIdIds() + { + if ($this->getFailureCount() == 0) { + return array(); + } + $filteredResults = array_filter( + $this->results, + function ($result) { + return ( + isset($result['error']) + && + ($result['error'] == "MismatchSenderId") + ); + } + ); -} \ No newline at end of file + return array_keys($filteredResults); + } +} diff --git a/library/CodeMonkeysRu/GCM/Sender.php b/library/CodeMonkeysRu/GCM/Sender.php index 0a0e466..395f363 100644 --- a/library/CodeMonkeysRu/GCM/Sender.php +++ b/library/CodeMonkeysRu/GCM/Sender.php @@ -18,7 +18,7 @@ class Sender /** * Path to CA file (due to cURL 7.10 changes; you can get it from here: http://curl.haxx.se/docs/caextract.html) - * + * * @var string */ private $caInfoPath = false; @@ -44,8 +44,8 @@ public function __construct($serverApiKey, $gcmUrl = false, $caInfoPath = false) /** * Send message to GCM without explicitly created message * - * @param string[] $registrationIds - * @param array|null $data + * @param string[] $registrationIds + * @param array|null $data * @param string|null $collapseKey * * @throws \CodeMonkeysRu\GCM\Exception @@ -61,7 +61,7 @@ public function sendMessage() /** * Send message to GCM * - * @param \CodeMonkeysRu\GCM\Message $message + * @param \CodeMonkeysRu\GCM\Message $message * @throws \CodeMonkeysRu\GCM\Exception * @return \CodeMonkeysRu\GCM\Response */ @@ -74,7 +74,10 @@ public function send(Message $message) //GCM response: Number of messages on bulk (1001) exceeds maximum allowed (1000) if (count($message->getRegistrationIds()) > 1000) { - throw new Exception("Malformed request: Registration Ids exceed the GCM imposed limit of 1000", Exception::MALFORMED_REQUEST); + throw new Exception( + "Malformed request: Registration Ids exceed the GCM imposed limit of 1000", + Exception::MALFORMED_REQUEST + ); } $rawData = $this->formMessageData($message); @@ -90,11 +93,11 @@ public function send(Message $message) $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->gcmUrl); - curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - + curl_setopt($ch, CURLOPT_HEADER, 1); // return HTTP headers with response + if ($this->caInfoPath !== false) { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_CAINFO, $this->caInfoPath); @@ -104,7 +107,18 @@ public function send(Message $message) curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - $resultBody = curl_exec($ch); + $resp = curl_exec($ch); + + if ($resp === false) { + throw new Exception('Error connecting to GCM endpoint: '.curl_error($ch), Exception::UNKNOWN_ERROR); + } + + list($responseHeaders, $resultBody) = explode("\r\n\r\n", $resp, 2); + // $headers now has a string of the HTTP headers + // $resultBody is the body of the HTTP response + + $responseHeaders = explode("\r\n", $responseHeaders); + $resultHttpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); @@ -123,18 +137,31 @@ public function send(Message $message) break; default: + $E = new Exception( + "Unknown error. ".json_encode($responseHeaders)."\n".$resultBody, + Exception::UNKNOWN_ERROR + ); + + foreach ($responseHeaders as $header) { + if (strpos($header, 'Retry-After:') !== false) { + $E->setMustRetry(true); + $E->setWaitSeconds((int) explode(" ", $header)[1]); + break; + } + } + + throw $E; //TODO: Retry-after - throw new Exception("Unknown error. ".$resultBody, Exception::UNKNOWN_ERROR); break; } - return new Response($message, $resultBody); + return new Response($message, $resultBody, $responseHeaders); } /** * Form raw message data for sending to GCM * - * @param \CodeMonkeysRu\GCM\Message $message + * @param \CodeMonkeysRu\GCM\Message $message * @return array */ private function formMessageData(Message $message) @@ -152,7 +179,7 @@ private function formMessageData(Message $message) 'time_to_live' => 'getTtl', 'restricted_package_name' => 'getRestrictedPackageName', 'dry_run' => 'getDryRun', - 'content_available' => 'getContentAvailable', + 'content_available' => 'getContentAvailable' ); foreach ($dataFields as $fieldName => $getter) { @@ -167,15 +194,17 @@ private function formMessageData(Message $message) /** * Validate size of json representation of passed payload * - * @param array $rawData - * @param string $fieldName - * @param int $maxSize + * @param array $rawData + * @param string $fieldName + * @param int $maxSize * @throws \CodeMonkeysRu\GCM\Exception * @return void */ private function validatePayloadSize(array $rawData, $fieldName, $maxSize) { - if (!isset($rawData[$fieldName])) return; + if (!isset($rawData[$fieldName])) { + return; + } if (strlen(json_encode($rawData[$fieldName])) > $maxSize) { throw new Exception( ucfirst($fieldName)." payload is to big (max {$maxSize} bytes)", @@ -183,5 +212,4 @@ private function validatePayloadSize(array $rawData, $fieldName, $maxSize) ); } } - } diff --git a/tests/ExceptionTest.php b/tests/ExceptionTest.php new file mode 100644 index 0000000..5375a30 --- /dev/null +++ b/tests/ExceptionTest.php @@ -0,0 +1,34 @@ +setMustRetry(true); + $exception->setWaitSeconds(120); + + $this->exception = $exception; + } + + public function testGetMustRetry() + { + $this->assertEquals(true, $this->exception->getMustRetry()); + } + + public function testGetWaitSeconds() + { + $this->assertEquals(120, $this->exception->getWaitSeconds()); + } +} diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index c3ea557..db1c676 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -1,20 +1,39 @@ responseHeadersOK = $responseHeadersOK; + $this->responseOK = new \CodeMonkeysRu\GCM\Response($messageOK, $responseBodyOK, $responseHeadersOK); + + $messageInvalidDataKey = new \CodeMonkeysRu\GCM\Message(array(1, 2)); + $responseBodyInvalidDataKey = '{ "multicast_id": 216, + "success": 0, + "failure": 2, + "canonical_ids": 0, + "results": [ + { "error": "InvalidDataKey" }, + { "error": "InvalidDataKey" } + ] + }'; + + $this->responseInvalidDataKey = new \CodeMonkeysRu\GCM\Response( + $messageInvalidDataKey, + $responseBodyInvalidDataKey, + $responseHeadersOK + ); + } - $this->response = new \CodeMonkeysRu\GCM\Response($message, $responseBody); + public function testGetResponseHeaders() + { + $this->assertEquals($this->responseHeadersOK, $this->responseOK->getResponseHeaders()); } public function testGetNewRegistrationIds() { - $this->assertEquals(array(5 => 32), $this->response->getNewRegistrationIds()); + $this->assertEquals(array(5 => 32), $this->responseOK->getNewRegistrationIds()); } public function testGetInvalidRegistrationIds() { - $this->assertEquals(array(3, 6), $this->response->getInvalidRegistrationIds()); + $this->assertEquals(array(3, 6), $this->responseOK->getInvalidRegistrationIds()); } public function testGetUnavailableRegistrationIds() { - $this->assertEquals(array(2), $this->response->getUnavailableRegistrationIds()); + $this->assertEquals(array(2), $this->responseOK->getUnavailableRegistrationIds()); } - -} \ No newline at end of file + + public function testGetExistsMismatchSenderId() + { + $this->assertEquals(true, $this->responseOK->getExistsMismatchSenderId()) + && + $this->assertEquals(false, $this->responseInvalidDataKey->getExistsMismatchSenderId()); + } + public function testGetMismatchSenderIdIds() + { + $this->assertEquals(array(7), $this->responseOK->getMismatchSenderIdIds()); + } + public function testGetExistsInvalidDataKey() + { + $this->assertEquals(true, $this->responseInvalidDataKey->getExistsInvalidDataKey()) + && + $this->assertEquals(false, $this->responseOK->getExistsInvalidDataKey()); + } +} diff --git a/tests/SenderTest.php b/tests/SenderTest.php index f3a0649..b5c6ea1 100644 --- a/tests/SenderTest.php +++ b/tests/SenderTest.php @@ -1,6 +1,10 @@ setNotification($notification); $sender->send($message); } - -} \ No newline at end of file +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2657061..52d1401 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,6 +1,5 @@