File indexing completed on 2024-05-26 06:03:26

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Service
0017  * @subpackage Amazon_Sqs
0018  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0019  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0020  * @version    $Id$
0021  */
0022 
0023 /**
0024  * @see Zend_Service_Amazon_Abstract
0025  */
0026 // require_once 'Zend/Service/Amazon/Abstract.php';
0027 
0028 /**
0029  * @see Zend_Crypt_Hmac
0030  */
0031 // require_once 'Zend/Crypt/Hmac.php';
0032 
0033 /**
0034  * Class for connecting to the Amazon Simple Queue Service (SQS)
0035  *
0036  * @category   Zend
0037  * @package    Zend_Service
0038  * @subpackage Amazon_Sqs
0039  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0040  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0041  * @see        http://aws.amazon.com/sqs/ Amazon Simple Queue Service
0042  */
0043 class Zend_Service_Amazon_Sqs extends Zend_Service_Amazon_Abstract
0044 {
0045     /**
0046      * Default timeout for createQueue() function
0047      */
0048     const CREATE_TIMEOUT_DEFAULT = 30;
0049 
0050     /**
0051      * HTTP end point for the Amazon SQS service
0052      */
0053     protected $_sqsEndpoint = 'queue.amazonaws.com';
0054 
0055     /**
0056      * The API version to use
0057      */
0058     protected $_sqsApiVersion = '2009-02-01';
0059 
0060     /**
0061      * Signature Version
0062      */
0063     protected $_sqsSignatureVersion = '2';
0064 
0065     /**
0066      * Signature Encoding Method
0067      */
0068     protected $_sqsSignatureMethod = 'HmacSHA256';
0069 
0070     protected $_sqsEndpoints = array('us-east-1' => 'sqs.us-east-1.amazonaws.com',
0071                                      'us-west-1' => 'sqs.us-west-1.amazonaws.com',
0072                                      'eu-west-1' => 'sqs.eu-west-1.amazonaws.com',
0073                                      'ap-southeast-1' => 'sqs.ap-southeast-1.amazonaws.com',
0074                                      'ap-northeast-1' => 'sqs.ap-northeast-1.amazonaws.com');
0075     /**
0076      * Constructor
0077      *
0078      * The default region is us-east-1. Use the region to set it to one of the regions that is build-in into ZF.
0079      * To add a new AWS region use the setEndpoint() method.
0080      *
0081      * @param string $accessKey
0082      * @param string $secretKey
0083      * @param string $region
0084      */
0085     public function __construct($accessKey = null, $secretKey = null, $region = null)
0086     {
0087         parent::__construct($accessKey, $secretKey, $region);
0088         
0089         if (null !== $region) {
0090             $this->_setEndpoint($region);
0091         }
0092     }
0093 
0094     /**
0095      * Set SQS endpoint
0096      *
0097      * Checks and sets endpoint if region exists in $_sqsEndpoints. If a new SQS region is added by amazon,
0098      * please use the setEndpoint function to set it.
0099      *
0100      * @param  string  $region region
0101      * @throws Zend_Service_Amazon_Sqs_Exception
0102      */
0103     protected function _setEndpoint($region)
0104     {
0105         if (array_key_exists($region, $this->_sqsEndpoints)) {
0106             $this->_sqsEndpoint = $this->_sqsEndpoints[$region];
0107         } else {
0108             throw new Zend_Service_Amazon_Sqs_Exception('Invalid SQS region specified.');
0109         }
0110     }
0111     
0112     /**
0113      * Set SQS endpoint
0114      *
0115      * You can set SQS to on of the build-in regions. If the region does not exsist it will be added.
0116      *
0117      * @param  string  $region region
0118      * @throws Zend_Service_Amazon_Sqs_Exception
0119      */
0120     public function setEndpoint($region)
0121     {
0122         if (!empty($region)) {
0123             if (array_key_exists($region, $this->_sqsEndpoints)) {
0124                 $this->_sqsEndpoint = $this->_sqsEndpoints[$region];
0125             } else {
0126                 $this->_sqsEndpoints[$region] = "sqs.$region.amazonaws.com";
0127                 $this->_sqsEndpoint = $this->_sqsEndpoints[$region];
0128             }
0129         } else {
0130             throw new Zend_Service_Amazon_Sqs_Exception('Empty region specified.');
0131         }
0132     }
0133 
0134     /**
0135      * Get the SQS endpoint
0136      * 
0137      * @return string 
0138      */
0139     public function getEndpoint()
0140     {
0141         return $this->_sqsEndpoint;
0142     }
0143 
0144     /**
0145      * Get possible SQS endpoints
0146      *
0147      * Since there is not an SQS webserive to get all possible endpoints, a hardcoded list is available.
0148      * For the actual region list please check:
0149      * http://docs.amazonwebservices.com/AWSSimpleQueueService/2009-02-01/APIReference/index.html?QueueServiceWsdlArticle.html
0150      *
0151      * @param  string  $region region
0152      * @return array
0153      */
0154     public function getEndpoints()
0155     {
0156         return $this->_sqsEndpoints;
0157     }
0158     
0159     /**
0160      * Create a new queue
0161      *
0162      * Visibility timeout is how long a message is left in the queue "invisible"
0163      * to other readers.  If the message is acknowleged (deleted) before the
0164      * timeout, then the message is deleted.  However, if the timeout expires
0165      * then the message will be made available to other queue readers.
0166      *
0167      * @param  string  $queue_name queue name
0168      * @param  integer $timeout    default visibility timeout
0169      * @return string|boolean
0170      * @throws Zend_Service_Amazon_Sqs_Exception
0171      */
0172     public function create($queue_name, $timeout = null)
0173     {
0174         $params = array();
0175         $params['QueueName'] = $queue_name;
0176         $timeout = ($timeout === null) ? self::CREATE_TIMEOUT_DEFAULT : (int)$timeout;
0177         $params['DefaultVisibilityTimeout'] = $timeout;
0178 
0179         $retry_count = 0;
0180 
0181         do {
0182             $retry  = false;
0183             $result = $this->_makeRequest(null, 'CreateQueue', $params);
0184 
0185             if (!isset($result->CreateQueueResult->QueueUrl)
0186                 || empty($result->CreateQueueResult->QueueUrl)
0187             ) {
0188                 if ($result->Error->Code == 'AWS.SimpleQueueService.QueueNameExists') {
0189                     return false;
0190                 } elseif ($result->Error->Code == 'AWS.SimpleQueueService.QueueDeletedRecently') {
0191                     // Must sleep for 60 seconds, then try re-creating the queue
0192                     sleep(60);
0193                     $retry = true;
0194                     $retry_count++;
0195                 } else {
0196                     // require_once 'Zend/Service/Amazon/Sqs/Exception.php';
0197                     throw new Zend_Service_Amazon_Sqs_Exception($result->Error->Code);
0198                 }
0199             } else {
0200                 return (string) $result->CreateQueueResult->QueueUrl;
0201             }
0202 
0203         } while ($retry);
0204 
0205         return false;
0206     }
0207 
0208     /**
0209      * Delete a queue and all of it's messages
0210      *
0211      * Returns false if the queue is not found, true if the queue exists
0212      *
0213      * @param  string  $queue_url queue URL
0214      * @return boolean
0215      * @throws Zend_Service_Amazon_Sqs_Exception
0216      */
0217     public function delete($queue_url)
0218     {
0219         $result = $this->_makeRequest($queue_url, 'DeleteQueue');
0220 
0221         if ($result->Error->Code !== null) {
0222             // require_once 'Zend/Service/Amazon/Sqs/Exception.php';
0223             throw new Zend_Service_Amazon_Sqs_Exception($result->Error->Code);
0224         }
0225 
0226         return true;
0227     }
0228 
0229     /**
0230      * Get an array of all available queues
0231      *
0232      * @return array
0233      * @throws Zend_Service_Amazon_Sqs_Exception
0234      */
0235     public function getQueues()
0236     {
0237         $result = $this->_makeRequest(null, 'ListQueues');
0238 
0239         if (isset($result->Error)) {
0240             // require_once 'Zend/Service/Amazon/Sqs/Exception.php';
0241             throw new Zend_Service_Amazon_Sqs_Exception($result->Error->Code);
0242         }
0243 
0244         if (!isset($result->ListQueuesResult->QueueUrl)
0245             || empty($result->ListQueuesResult->QueueUrl)
0246         ) {
0247             return array();
0248         }
0249 
0250         $queues = array();
0251         foreach ($result->ListQueuesResult->QueueUrl as $queue_url) {
0252             $queues[] = (string)$queue_url;
0253         }
0254 
0255         return $queues;
0256     }
0257 
0258     /**
0259      * Return the approximate number of messages in the queue
0260      *
0261      * @param  string  $queue_url Queue URL
0262      * @return integer
0263      * @throws Zend_Service_Amazon_Sqs_Exception
0264      */
0265     public function count($queue_url)
0266     {
0267         return (int)$this->getAttribute($queue_url, 'ApproximateNumberOfMessages');
0268     }
0269 
0270     /**
0271      * Send a message to the queue
0272      *
0273      * @param  string $queue_url Queue URL
0274      * @param  string $message   Message to send to the queue
0275      * @return string            Message ID
0276      * @throws Zend_Service_Amazon_Sqs_Exception
0277      */
0278     public function send($queue_url, $message)
0279     {
0280         $params = array();
0281         $params['MessageBody'] = urlencode($message);
0282 
0283         $checksum = md5($params['MessageBody']);
0284 
0285         $result = $this->_makeRequest($queue_url, 'SendMessage', $params);
0286 
0287         if (!isset($result->SendMessageResult->MessageId)
0288             || empty($result->SendMessageResult->MessageId)
0289         ) {
0290             // require_once 'Zend/Service/Amazon/Sqs/Exception.php';
0291             throw new Zend_Service_Amazon_Sqs_Exception($result->Error->Code);
0292         } else if ((string) $result->SendMessageResult->MD5OfMessageBody != $checksum) {
0293             // require_once 'Zend/Service/Amazon/Sqs/Exception.php';
0294             throw new Zend_Service_Amazon_Sqs_Exception('MD5 of body does not match message sent');
0295         }
0296 
0297         return (string) $result->SendMessageResult->MessageId;
0298     }
0299 
0300     /**
0301      * Get messages in the queue
0302      *
0303      * @param  string  $queue_url    Queue name
0304      * @param  integer $max_messages Maximum number of messages to return
0305      * @param  integer $timeout      Visibility timeout for these messages
0306      * @return array
0307      * @throws Zend_Service_Amazon_Sqs_Exception
0308      */
0309     public function receive($queue_url, $max_messages = null, $timeout = null)
0310     {
0311         $params = array();
0312 
0313         // If not set, the visibility timeout on the queue is used
0314         if ($timeout !== null) {
0315             $params['VisibilityTimeout'] = (int)$timeout;
0316         }
0317 
0318         // SQS will default to only returning one message
0319         if ($max_messages !== null) {
0320             $params['MaxNumberOfMessages'] = (int)$max_messages;
0321         }
0322 
0323         $result = $this->_makeRequest($queue_url, 'ReceiveMessage', $params);
0324 
0325         if (isset($result->Error)) {
0326             // require_once 'Zend/Service/Amazon/Sqs/Exception.php';
0327             throw new Zend_Service_Amazon_Sqs_Exception($result->Error->Code);
0328         }
0329 
0330         if (!isset($result->ReceiveMessageResult->Message)
0331             || empty($result->ReceiveMessageResult->Message)
0332         ) {
0333             // no messages found
0334             return array();
0335         }
0336 
0337         $data = array();
0338         foreach ($result->ReceiveMessageResult->Message as $message) {
0339             $data[] = array(
0340                 'message_id' => (string)$message->MessageId,
0341                 'handle'     => (string)$message->ReceiptHandle,
0342                 'md5'        => (string)$message->MD5OfBody,
0343                 'body'       => urldecode((string)$message->Body),
0344             );
0345         }
0346 
0347         return $data;
0348     }
0349 
0350     /**
0351      * Delete a message from the queue
0352      *
0353      * Returns true if the message is deleted, false if the deletion is
0354      * unsuccessful.
0355      *
0356      * @param  string $queue_url  Queue URL
0357      * @param  string $handle     Message handle as returned by SQS
0358      * @return boolean
0359      * @throws Zend_Service_Amazon_Sqs_Exception
0360      */
0361     public function deleteMessage($queue_url, $handle)
0362     {
0363         $params = array();
0364         $params['ReceiptHandle'] = (string)$handle;
0365 
0366         $result = $this->_makeRequest($queue_url, 'DeleteMessage', $params);
0367 
0368         if (isset($result->Error->Code)
0369             && !empty($result->Error->Code)
0370         ) {
0371             return false;
0372         }
0373 
0374         // Will always return true unless ReceiptHandle is malformed
0375         return true;
0376     }
0377 
0378     /**
0379      * Get the attributes for the queue
0380      *
0381      * @param  string $queue_url  Queue URL
0382      * @param  string $attribute
0383      * @return string
0384      * @throws Zend_Service_Amazon_Sqs_Exception
0385      */
0386     public function getAttribute($queue_url, $attribute = 'All')
0387     {
0388         $params = array();
0389         $params['AttributeName'] = $attribute;
0390 
0391         $result = $this->_makeRequest($queue_url, 'GetQueueAttributes', $params);
0392 
0393         if (!isset($result->GetQueueAttributesResult->Attribute)
0394             || empty($result->GetQueueAttributesResult->Attribute)
0395         ) {
0396             // require_once 'Zend/Service/Amazon/Sqs/Exception.php';
0397             throw new Zend_Service_Amazon_Sqs_Exception($result->Error->Code);
0398         }
0399 
0400         if(count($result->GetQueueAttributesResult->Attribute) > 1) {
0401             $attr_result = array();
0402             foreach($result->GetQueueAttributesResult->Attribute as $attribute) {
0403                 $attr_result[(string)$attribute->Name] = (string)$attribute->Value;
0404             }
0405             return $attr_result;
0406         } else {
0407             return (string) $result->GetQueueAttributesResult->Attribute->Value;
0408         }
0409     }
0410 
0411     /**
0412      * Make a request to Amazon SQS
0413      *
0414      * @param  string           $queue  Queue Name
0415      * @param  string           $action SQS action
0416      * @param  array            $params
0417      * @return SimpleXMLElement
0418      */
0419     private function _makeRequest($queue_url, $action, $params = array())
0420     {
0421         $params['Action'] = $action;
0422         $params = $this->addRequiredParameters($queue_url, $params);
0423 
0424         if ($queue_url === null) {
0425             $queue_url = '/';
0426         }
0427 
0428         $client = self::getHttpClient();
0429 
0430         switch ($action) {
0431             case 'ListQueues':
0432             case 'CreateQueue':
0433                 $client->setUri('http://'.$this->_sqsEndpoint);
0434                 break;
0435             default:
0436                 $client->setUri($queue_url);
0437                 break;
0438         }
0439 
0440         $retry_count = 0;
0441 
0442         do {
0443             $retry = false;
0444 
0445             $client->resetParameters();
0446             $client->setParameterGet($params);
0447 
0448             $response = $client->request('GET');
0449 
0450             $response_code = $response->getStatus();
0451 
0452             // Some 5xx errors are expected, so retry automatically
0453             if ($response_code >= 500 && $response_code < 600 && $retry_count <= 5) {
0454                 $retry = true;
0455                 $retry_count++;
0456                 sleep($retry_count / 4 * $retry_count);
0457             }
0458         } while ($retry);
0459 
0460         unset($client);
0461 
0462         return new SimpleXMLElement($response->getBody());
0463     }
0464 
0465     /**
0466      * Adds required authentication and version parameters to an array of
0467      * parameters
0468      *
0469      * The required parameters are:
0470      * - AWSAccessKey
0471      * - SignatureVersion
0472      * - Timestamp
0473      * - Version and
0474      * - Signature
0475      *
0476      * If a required parameter is already set in the <tt>$parameters</tt> array,
0477      * it is overwritten.
0478      *
0479      * @param  string $queue_url  Queue URL
0480      * @param  array  $parameters the array to which to add the required
0481      *                            parameters.
0482      * @return array
0483      */
0484     protected function addRequiredParameters($queue_url, array $parameters)
0485     {
0486         $parameters['AWSAccessKeyId']   = $this->_getAccessKey();
0487         $parameters['SignatureVersion'] = $this->_sqsSignatureVersion;
0488         $parameters['Timestamp']        = gmdate('Y-m-d\TH:i:s\Z', time()+10);
0489         $parameters['Version']          = $this->_sqsApiVersion;
0490         $parameters['SignatureMethod']  = $this->_sqsSignatureMethod;
0491         $parameters['Signature']        = $this->_signParameters($queue_url, $parameters);
0492 
0493         return $parameters;
0494     }
0495 
0496     /**
0497      * Computes the RFC 2104-compliant HMAC signature for request parameters
0498      *
0499      * This implements the Amazon Web Services signature, as per the following
0500      * specification:
0501      *
0502      * 1. Sort all request parameters (including <tt>SignatureVersion</tt> and
0503      *    excluding <tt>Signature</tt>, the value of which is being created),
0504      *    ignoring case.
0505      *
0506      * 2. Iterate over the sorted list and append the parameter name (in its
0507      *    original case) and then its value. Do not URL-encode the parameter
0508      *    values before constructing this string. Do not use any separator
0509      *    characters when appending strings.
0510      *
0511      * @param  string $queue_url  Queue URL
0512      * @param  array  $parameters the parameters for which to get the signature.
0513      *
0514      * @return string the signed data.
0515      */
0516     protected function _signParameters($queue_url, array $paramaters)
0517     {
0518         $data = "GET\n";
0519         $data .= $this->_sqsEndpoint . "\n";
0520         if ($queue_url !== null) {
0521             $data .= parse_url($queue_url, PHP_URL_PATH);
0522         }
0523         else {
0524             $data .= '/';
0525         }
0526         $data .= "\n";
0527 
0528         uksort($paramaters, 'strcmp');
0529         unset($paramaters['Signature']);
0530 
0531         $arrData = array();
0532         foreach($paramaters as $key => $value) {
0533             $arrData[] = $key . '=' . str_replace('%7E', '~', urlencode($value));
0534         }
0535 
0536         $data .= implode('&', $arrData);
0537 
0538         $hmac = Zend_Crypt_Hmac::compute($this->_getSecretKey(), 'SHA256', $data, Zend_Crypt_Hmac::BINARY);
0539 
0540         return base64_encode($hmac);
0541     }
0542 }