|
|
@@ -0,0 +1,389 @@
|
|
|
+<?php
|
|
|
+/**
|
|
|
+ * Zend Framework
|
|
|
+ *
|
|
|
+ * LICENSE
|
|
|
+ *
|
|
|
+ * This source file is subject to the new BSD license that is bundled
|
|
|
+ * with this package in the file LICENSE.txt.
|
|
|
+ * It is also available through the world-wide-web at this URL:
|
|
|
+ * http://framework.zend.com/license/new-bsd
|
|
|
+ * If you did not receive a copy of the license and are unable to
|
|
|
+ * obtain it through the world-wide-web, please send an email
|
|
|
+ * to license@zend.com so we can send you a copy immediately.
|
|
|
+ *
|
|
|
+ * @category Zend
|
|
|
+ * @package Zend_Mobile
|
|
|
+ * @subpackage Zend_Mobile_Push
|
|
|
+ * @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
|
|
|
+ * @license http://framework.zend.com/license/new-bsd New BSD License
|
|
|
+ * @version $Id$
|
|
|
+ */
|
|
|
+
|
|
|
+/** Zend_Mobile_Push_Abstract **/
|
|
|
+require_once 'Zend/Mobile/Push/Abstract.php';
|
|
|
+
|
|
|
+/** Zend_Mobile_Push_Message_Apns **/
|
|
|
+require_once 'Zend/Mobile/Push/Message/Apns.php';
|
|
|
+
|
|
|
+/**
|
|
|
+ * APNS Push
|
|
|
+ *
|
|
|
+ * @category Zend
|
|
|
+ * @package Zend_Mobile
|
|
|
+ * @subpackage Zend_Mobile_Push
|
|
|
+ * @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
|
|
|
+ * @license http://framework.zend.com/license/new-bsd New BSD License
|
|
|
+ * @version $Id$
|
|
|
+ */
|
|
|
+class Zend_Mobile_Push_Apns extends Zend_Mobile_Push_Abstract
|
|
|
+{
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @const int apple server uri constants
|
|
|
+ */
|
|
|
+ const SERVER_SANDBOX_URI = 0;
|
|
|
+ const SERVER_PRODUCTION_URI = 1;
|
|
|
+ const SERVER_FEEDBACK_SANDBOX_URI = 2;
|
|
|
+ const SERVER_FEEDBACK_PRODUCTION_URI = 3;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Apple Server URI's
|
|
|
+ *
|
|
|
+ * @var array
|
|
|
+ */
|
|
|
+ protected $_serverUriList = array(
|
|
|
+ 'ssl://gateway.sandbox.push.apple.com:2195',
|
|
|
+ 'ssl://gateway.push.apple.com:2195',
|
|
|
+ 'ssl://feedback.push.apple.com:2196',
|
|
|
+ 'ssl://feedback.sandbox.push.apple.com:2196'
|
|
|
+ );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Current Environment
|
|
|
+ *
|
|
|
+ * @var int
|
|
|
+ */
|
|
|
+ protected $_currentEnv;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Socket
|
|
|
+ *
|
|
|
+ * @var resource
|
|
|
+ */
|
|
|
+ protected $_socket;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Certificate
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $_certificate;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Certificate Passphrase
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $_certificatePassphrase;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get Certficiate
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ public function getCertificate()
|
|
|
+ {
|
|
|
+ return $this->_certificate;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Set Certificate
|
|
|
+ *
|
|
|
+ * @param string $cert
|
|
|
+ * @return Zend_Mobile_Push_Apns
|
|
|
+ * @throws Zend_Mobile_Push_Exception
|
|
|
+ */
|
|
|
+ public function setCertificate($cert)
|
|
|
+ {
|
|
|
+ if (!is_string($cert)) {
|
|
|
+ throw new Zend_Mobile_Push_Exception('$cert must be a string');
|
|
|
+ }
|
|
|
+ if (!file_exists($cert)) {
|
|
|
+ throw new Zend_Mobile_Push_Exception('$cert must be a valid path to the certificate');
|
|
|
+ }
|
|
|
+ $this->_certificate = $cert;
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get Certificate Passphrase
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ public function getCertificatePassphrase()
|
|
|
+ {
|
|
|
+ return $this->_certificatePassphrase;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Set Certificate Passphrase
|
|
|
+ *
|
|
|
+ * @param string $passphrase
|
|
|
+ * @return Zend_Mobile_Push_Apns
|
|
|
+ * @throws Zend_Mobile_Push_Exception
|
|
|
+ */
|
|
|
+ public function setCertificatePassphrase($passphrase)
|
|
|
+ {
|
|
|
+ if (!is_string($passphrase)) {
|
|
|
+ throw new Zend_Mobile_Push_Exception('$passphrase must be a string');
|
|
|
+ }
|
|
|
+ $this->_certificatePassphrase = $passphrase;
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Connect to Socket
|
|
|
+ *
|
|
|
+ * @param string $uri
|
|
|
+ * @return bool
|
|
|
+ * @throws Zend_Mobile_Push_Exception_ServerUnavailable
|
|
|
+ */
|
|
|
+ protected function _connect($uri)
|
|
|
+ {
|
|
|
+ $ssl = array(
|
|
|
+ 'local_cert' => $this->_certificate,
|
|
|
+ );
|
|
|
+ if ($this->_certificatePassphrase) {
|
|
|
+ $ssl['passphrase'] = $this->_certificatePassphrase;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->_socket = stream_socket_client($uri,
|
|
|
+ $errno,
|
|
|
+ $errstr,
|
|
|
+ ini_get('default_socket_timeout'),
|
|
|
+ STREAM_CLIENT_CONNECT,
|
|
|
+ stream_context_create(array(
|
|
|
+ 'ssl' => $ssl,
|
|
|
+ ))
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!is_resource($this->_socket)) {
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/ServerUnavailable.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_ServerUnavailable(sprintf('Unable to connect: %s: %d (%s)',
|
|
|
+ $uri,
|
|
|
+ $errno,
|
|
|
+ $errstr
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ stream_set_blocking($this->_socket, 0);
|
|
|
+ stream_set_write_buffer($this->_socket, 0);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Read from the Socket Server
|
|
|
+ *
|
|
|
+ * @param int $length
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function _read($length) {
|
|
|
+ $data = false;
|
|
|
+ if (!feof($this->_socket)) {
|
|
|
+ $data = fread($this->_socket, $length);
|
|
|
+ }
|
|
|
+ return $data;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Write to the Socket Server
|
|
|
+ *
|
|
|
+ * @param string $payload
|
|
|
+ * @return int
|
|
|
+ */
|
|
|
+ protected function _write($payload) {
|
|
|
+ return @fwrite($this->_socket, $payload);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Connect to the Push Server
|
|
|
+ *
|
|
|
+ * @param string $env
|
|
|
+ * @return Zend_Mobile_Push_Abstract
|
|
|
+ * @throws Zend_Mobile_Push_Exception
|
|
|
+ * @throws Zend_Mobile_Push_Exception_ServerUnavailable
|
|
|
+ */
|
|
|
+ public function connect($env = self::SERVER_PRODUCTION_URI)
|
|
|
+ {
|
|
|
+ if ($this->_isConnected) {
|
|
|
+ if ($this->_currentEnv == self::SERVER_PRODUCTION_URI) {
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+ $this->close();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isset($this->_serverUriList[$env])) {
|
|
|
+ throw new Zend_Mobile_Push_Exception('$env is not a valid environment');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$this->_certificate) {
|
|
|
+ throw new Zend_Mobile_Push_Exception('A certificate must be set prior to calling ::connect');
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->_connect($this->_serverUriList[$env]);
|
|
|
+
|
|
|
+ $this->_currentEnv = $env;
|
|
|
+ $this->_isConnected = true;
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Feedback
|
|
|
+ *
|
|
|
+ * @return array array w/ key = token and value = time
|
|
|
+ * @throws Zend_Mobile_Push_Exception
|
|
|
+ * @throws Zend_Mobile_Push_Exception_ServerUnavailable
|
|
|
+ */
|
|
|
+ public function feedback()
|
|
|
+ {
|
|
|
+ if (!$this->_isConnected ||
|
|
|
+ !in_array($this->_currentEnv,
|
|
|
+ array(self::SERVER_FEEDBACK_SANDBOX_URI, self::SERVER_FEEDBACK_PRODUCTION_URI))) {
|
|
|
+ $this->connect(self::SERVER_FEEDBACK_PRODUCTION_URI);
|
|
|
+ }
|
|
|
+
|
|
|
+ $tokens = array();
|
|
|
+ while ($token = $this->_read(38)) {
|
|
|
+ if (strlen($token) < 38) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $token = unpack('Ntime/ntokenLength/H*token', $token);
|
|
|
+ if (!isset($tokens[$token['token']]) || $tokens[$token['token']] < $token['time']) {
|
|
|
+ $tokens[$token['token']] = $token['time'];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $tokens;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Send Message
|
|
|
+ *
|
|
|
+ * @param Zend_Mobile_Push_Message_Apns $message
|
|
|
+ * @return boolean
|
|
|
+ * @throws Zend_Mobile_Push_Exception
|
|
|
+ * @throws Zend_Mobile_Push_Exception_ServerUnavailable
|
|
|
+ * @throws Zend_Mobile_Push_Exception_InvalidToken
|
|
|
+ * @throws Zend_Mobile_Push_Exception_InvalidTopic
|
|
|
+ * @throws Zend_Mobile_Push_Exception_InvalidPayload
|
|
|
+ */
|
|
|
+ public function send(Zend_Mobile_Push_Message_Abstract $message)
|
|
|
+ {
|
|
|
+ if (!$message->validate()) {
|
|
|
+ throw new Zend_Mobile_Push_Exception('The message is not valid.');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$this->_isConnected || !in_array($this->_currentEnv, array(
|
|
|
+ self::SERVER_SANDBOX_URI,
|
|
|
+ self::SERVER_PRODUCTION_URI))) {
|
|
|
+ $this->connect(self::SERVER_PRODUCTION_URI);
|
|
|
+ }
|
|
|
+
|
|
|
+ $payload = array('aps' => array());
|
|
|
+
|
|
|
+ $alert = $message->getAlert();
|
|
|
+ foreach ($alert as $k => $v) {
|
|
|
+ if ($v == null) {
|
|
|
+ unset($alert[$k]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!empty($alert)) {
|
|
|
+ $payload['aps']['alert'] = $alert;
|
|
|
+ }
|
|
|
+ $payload['aps']['badge'] = $message->getBadge();
|
|
|
+ $payload['aps']['sound'] = $message->getSound();
|
|
|
+
|
|
|
+ foreach($message->getCustomData() as $k => $v) {
|
|
|
+ $payload[$k] = $v;
|
|
|
+ }
|
|
|
+ $payload = json_encode($payload);
|
|
|
+
|
|
|
+ $expire = $message->getExpire();
|
|
|
+ if ($expire > 0) {
|
|
|
+ $expire += time();
|
|
|
+ }
|
|
|
+ $id = $message->getId();
|
|
|
+ if (empty($id)) {
|
|
|
+ $id = time();
|
|
|
+ }
|
|
|
+
|
|
|
+ $payload = pack('CNNnH*', 1, $id, $expire, 32, $message->getToken())
|
|
|
+ . pack('n', strlen($payload))
|
|
|
+ . $payload;
|
|
|
+ $ret = $this->_write($payload);
|
|
|
+ if ($ret === false) {
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/ServerUnavailable.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_ServerUnavailable('Unable to send message');
|
|
|
+ }
|
|
|
+ // check for errors from apple
|
|
|
+ $err = $this->_read(1024);
|
|
|
+ if (strlen($err) > 0) {
|
|
|
+ $err = unpack('Ccmd/Cerrno/Nid', $err);
|
|
|
+ switch ($err['errno']) {
|
|
|
+ case 0:
|
|
|
+ return true;
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ throw new Zend_Mobile_Push_Exception('A processing error has occurred on the apple push notification server.');
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/InvalidToken.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_InvalidToken('Missing token; you must set a token for the message.');
|
|
|
+ break;
|
|
|
+ case 3:
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/InvalidTopic.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_InvalidTopic('Missing id; you must set an id for the message.');
|
|
|
+ break;
|
|
|
+ case 4:
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/InvalidPayload.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_InvalidPayload('Missing message; the message must always have some content.');
|
|
|
+ break;
|
|
|
+ case 5:
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/InvalidToken.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_InvalidToken('Bad token. This token is too big and is not a regular apns token.');
|
|
|
+ break;
|
|
|
+ case 6:
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/InvalidTopic.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_InvalidTopic('The message id is too big; reduce the size of the id.');
|
|
|
+ break;
|
|
|
+ case 7:
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/InvalidPayload.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_InvalidPayload('The message is too big; reduce the size of the message.');
|
|
|
+ break;
|
|
|
+ case 8:
|
|
|
+ require_once 'Zend/Mobile/Push/Exception/InvalidToken.php';
|
|
|
+ throw new Zend_Mobile_Push_Exception_InvalidToken('Bad token. Remove this token from being sent to again.');
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ throw new Zend_Mobile_Push_Exception(sprintf('An unknown error occurred: %d', $err['errno']));
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Close Connection
|
|
|
+ *
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ public function close()
|
|
|
+ {
|
|
|
+ if ($this->_isConnected && is_resource($this->_socket)) {
|
|
|
+ fclose($this->_socket);
|
|
|
+ }
|
|
|
+ $this->_isConnected = false;
|
|
|
+ }
|
|
|
+}
|