· 8 years ago · Feb 02, 2018, 02:44 AM
1<?php
2/**
3 * Lightweight API interface with the Amazon Simple Notification Service
4 *
5 * @author Chris Barr <chris.barr@ntlworld.com>
6 * @link http://aws.amazon.com/sns/
7 * @link http://docs.amazonwebservices.com/sns/latest/api/
8 */
9class AmazonSNS {
10 /** @var string $access_key */
11 private $access_key;
12 /** @var string $secret_key */
13 private $secret_key;
14 /** @var string $protocol */
15 private $protocol = 'https://'; // http is allowed
16 /** @var string $endpoint */
17 private $endpoint = ''; // Defaults to us-east-1
18 /** @var array $endpoints */
19 private $endpoints = array(
20 'us-east-1' => 'sns.us-east-1.amazonaws.com',
21 'us-west-2' => 'sns.us-west-2.amazonaws.com',
22 'us-west-1' => 'sns.us-west-1.amazonaws.com',
23 'eu-west-1' => 'sns.eu-west-1.amazonaws.com',
24 'eu-central-1' => 'ec2.eu-central-1.amazonaws.com',
25 'ap-southeast-1' => 'sns.ap-southeast-1.amazonaws.com',
26 'ap-southeast-2' => 'sns.ap-southeast-2.amazonaws.com',
27 'ap-northeast-1' => 'sns.ap-northeast-1.amazonaws.com',
28 'sa-east-1' => 'sns.sa-east-1.amazonaws.com'
29 );
30 /**
31 * Instantiate the object - set access_key and secret_key and set default region
32 *
33 * @param string $access_key
34 * @param string $secret_key
35 * @param string $region [optional]
36 * @throws InvalidArgumentException
37 */
38 public function __construct($access_key, $secret_key, $region = 'us-east-1') {
39 $this->access_key = $access_key;
40 $this->secret_key = $secret_key;
41 if(empty($this->access_key) || empty($this->secret_key)) {
42 throw new InvalidArgumentException('Must define Amazon access key and secret key');
43 }
44 $this->setRegion($region);
45 }
46 /**
47 * Set the SNS endpoint/region
48 *
49 * @link http://docs.amazonwebservices.com/general/latest/gr/index.html?rande.html
50 * @param string $region
51 * @return string
52 * @throws InvalidArgumentException
53 */
54 public function setRegion($region) {
55 if(!isset($this->endpoints[$region])) {
56 throw new InvalidArgumentException('Region unrecognised');
57 }
58 return $this->endpoint = $this->endpoints[$region];
59 }
60 //
61 // Public interface functions
62 //
63 /**
64 * Add permissions to a topic
65 *
66 * Example:
67 * $AmazonSNS->addPermission('topic:arn:123', 'New Permission', array('987654321000' => 'Publish', '876543210000' => array('Publish', 'SetTopicAttributes')));
68 *
69 * @link http://docs.amazonwebservices.com/sns/latest/api/API_AddPermission.html
70 * @param string $topicArn
71 * @param string $label Unique name of permissions
72 * @param array $permissions [optional] Array of permissions - member ID as keys, actions as values
73 * @return bool
74 * @throws InvalidArgumentException
75 */
76 public function addPermission($topicArn, $label, $permissions = array()) {
77 if(empty($topicArn) || empty($label)) {
78 throw new InvalidArgumentException('Must supply TopicARN and a Label for this permission');
79 }
80 // Add standard params as normal
81 $params = array(
82 'TopicArn' => $topicArn,
83 'Label' => $label
84 );
85 // Compile permissions into separate sequential arrays
86 $memberFlatArray = array();
87 $permissionFlatArray = array();
88 foreach($permissions as $member => $permission) {
89 if(is_array($permission)) {
90 // Array of permissions
91 foreach($permission as $singlePermission) {
92 $memberFlatArray[] = $member;
93 $permissionFlatArray[] = $singlePermission;
94 }
95 }
96 else {
97 // Just a single permission
98 $memberFlatArray[] = $member;
99 $permissionFlatArray[] = $permission;
100 }
101 }
102 // Dummy check
103 if(count($memberFlatArray) !== count($permissionFlatArray)) {
104 // Something went wrong
105 throw new InvalidArgumentException('Mismatch of permissions to users');
106 }
107 // Finally add to params
108 for($x = 1; $x <= count($memberFlatArray); $x++) {
109 $params['ActionName.member.' . $x] = $permissionFlatArray[$x];
110 $params['AWSAccountID.member.' . $x] = $memberFlatArray[$x];
111 }
112 // Finally send request
113 $this->_request('AddPermission', $params);
114 return true;
115 }
116 /**
117 * Confirm a subscription to a topic
118 *
119 * @link http://docs.amazonwebservices.com/sns/latest/api/API_ConfirmSubscription.html
120 * @param string $topicArn
121 * @param string $token
122 * @param bool|null $authenticateOnUnsubscribe [optional]
123 * @return string - SubscriptionARN
124 * @throws InvalidArgumentException
125 */
126 public function confirmSubscription($topicArn, $token, $authenticateOnUnsubscribe = null) {
127 if(empty($topicArn) || empty($token)) {
128 throw new InvalidArgumentException('Must supply a TopicARN and a Token to confirm subscription');
129 }
130 $params = array(
131 'TopicArn' => $topicArn,
132 'Token' => $token
133 );
134 if(!is_null($authenticateOnUnsubscribe)) {
135 $params['AuthenticateOnUnsubscribe'] = $authenticateOnUnsubscribe;
136 }
137 $resultXml = $this->_request('ConfirmSubscription', $params);
138 return strval($resultXml->ConfirmSubscriptionResult->SubscriptionArn);
139 }
140 /**
141 * Create an SNS topic
142 *
143 * @link http://docs.amazonwebservices.com/sns/latest/api/API_CreateTopic.html
144 * @param string $name
145 * @return string - TopicARN
146 * @throws InvalidArgumentException
147 */
148 public function createTopic($name) {
149 if(empty($name)) {
150 throw new InvalidArgumentException('Must supply a Name to create topic');
151 }
152 $resultXml = $this->_request('CreateTopic', array('Name' => $name));
153 return strval($resultXml->CreateTopicResult->TopicArn);
154 }
155 /**
156 * Delete an SNS topic
157 *
158 * @link http://docs.amazonwebservices.com/sns/latest/api/API_DeleteTopic.html
159 * @param string $topicArn
160 * @return bool
161 * @throws InvalidArgumentException
162 */
163 public function deleteTopic($topicArn) {
164 if(empty($topicArn)) {
165 throw new InvalidArgumentException('Must supply a TopicARN to delete a topic');
166 }
167 $this->_request('DeleteTopic', array('TopicArn' => $topicArn));
168 return true;
169 }
170 /**
171 * Get the attributes of a topic like owner, ACL, display name
172 *
173 * @link http://docs.amazonwebservices.com/sns/latest/api/API_GetTopicAttributes.html
174 * @param string $topicArn
175 * @return array
176 * @throws InvalidArgumentException
177 */
178 public function getTopicAttributes($topicArn) {
179 if(empty($topicArn)) {
180 throw new InvalidArgumentException('Must supply a TopicARN to get topic attributes');
181 }
182 $resultXml = $this->_request('GetTopicAttributes', array('TopicArn' => $topicArn));
183 // Get attributes
184 $attributes = $resultXml->GetTopicAttributesResult->Attributes->entry;
185 // Unfortunately cannot use _processXmlToArray here, so process manually
186 $returnArray = array();
187 // Process into array
188 foreach($attributes as $attribute) {
189 // Store attribute key as array key
190 $returnArray[strval($attribute->key)] = strval($attribute->value);
191 }
192 return $returnArray;
193 }
194 /**
195 * List subscriptions that user is subscribed to
196 *
197 * @link http://docs.amazonwebservices.com/sns/latest/api/API_ListSubscriptions.html
198 * @param string|null $nextToken [optional] Token to retrieve next page of results
199 * @return array
200 */
201 public function listSubscriptions($nextToken = null) {
202 $params = array();
203 if(!is_null($nextToken)) {
204 $params['NextToken'] = $nextToken;
205 }
206 $resultXml = $this->_request('ListSubscriptions', $params);
207 // Get subscriptions
208 $subs = $resultXml->ListSubscriptionsResult->Subscriptions->member;
209 return $this->_processXmlToArray($subs);
210 }
211 /**
212 * List subscribers to a topic
213 *
214 * @link http://docs.amazonwebservices.com/sns/latest/api/API_ListSubscriptionsByTopic.html
215 * @param string $topicArn
216 * @param string|null $nextToken [optional] Token to retrieve next page of results
217 * @return array
218 * @throws InvalidArgumentException
219 */
220 public function listSubscriptionsByTopic($topicArn, $nextToken = null) {
221 if(empty($topicArn)) {
222 throw new InvalidArgumentException('Must supply a TopicARN to show subscriptions to a topic');
223 }
224 $params = array(
225 'TopicArn' => $topicArn
226 );
227 if(!is_null($nextToken)) {
228 $params['NextToken'] = $nextToken;
229 }
230 $resultXml = $this->_request('ListSubscriptionsByTopic', $params);
231 // Get subscriptions
232 $subs = $resultXml->ListSubscriptionsByTopicResult->Subscriptions->member;
233 return $this->_processXmlToArray($subs);
234 }
235 /**
236 * List SNS topics
237 *
238 * @link http://docs.amazonwebservices.com/sns/latest/api/API_ListTopics.html
239 * @param string|null $nextToken [optional] Token to retrieve next page of results
240 * @return array
241 */
242 public function listTopics($nextToken = null) {
243 $params = array();
244 if(!is_null($nextToken)) {
245 $params['NextToken'] = $nextToken;
246 }
247 $resultXml = $this->_request('ListTopics', $params);
248 // Get Topics
249 $topics = $resultXml->ListTopicsResult->Topics->member;
250 return $this->_processXmlToArray($topics);
251 }
252 /**
253 * Publish a message to a topic
254 *
255 * @link http://docs.amazonwebservices.com/sns/latest/api/API_Publish.html
256 * @param string $topicArn
257 * @param string $message
258 * @param string $subject [optional] Used when sending emails
259 * @param string $messageStructure [optional] Used when you want to send a different message for each protocol.If you set MessageStructure to json, the value of the Message parameter must: be a syntactically valid JSON object; and contain at least a top-level JSON key of "default" with a value that is a string.
260 * @return string
261 * @throws InvalidArgumentException
262 */
263 public function publish($topicArn, $message, $subject = '', $messageStructure = '') {
264 if(empty($topicArn) || empty($message)) {
265 throw new InvalidArgumentException('Must supply a TopicARN and Message to publish to a topic');
266 }
267 $params = array(
268 'TopicArn' => $topicArn,
269 'Message' => $message
270 );
271 if(!empty($subject)) {
272 $params['Subject'] = $subject;
273 }
274 if(!empty($messageStructure)) {
275 $params['MessageStructure'] = $messageStructure;
276 }
277 $resultXml = $this->_request('Publish', $params);
278 return strval($resultXml->PublishResult->MessageId);
279 }
280 /**
281 * Remove a set of permissions indentified by topic and label that was used when creating permissions
282 *
283 * @link http://docs.amazonwebservices.com/sns/latest/api/API_RemovePermission.html
284 * @param string $topicArn
285 * @param string $label
286 * @return bool
287 * @throws InvalidArgumentException
288 */
289 public function removePermission($topicArn, $label) {
290 if(empty($topicArn) || empty($label)) {
291 throw new InvalidArgumentException('Must supply a TopicARN and Label to remove a permission');
292 }
293 $this->_request('RemovePermission', array('Label' => $label));
294 return true;
295 }
296 /**
297 * Set a single attribute on a topic
298 *
299 * @link http://docs.amazonwebservices.com/sns/latest/api/API_SetTopicAttributes.html
300 * @param string $topicArn
301 * @param string $attrName
302 * @param mixed $attrValue
303 * @return bool
304 * @throws InvalidArgumentException
305 */
306 public function setTopicAttributes($topicArn, $attrName, $attrValue) {
307 if(empty($topicArn) || empty($attrName) || empty($attrValue)) {
308 throw new InvalidArgumentException('Must supply a TopicARN, AttributeName and AttributeValue to set a topic attribute');
309 }
310 $this->_request('SetTopicAttributes', array(
311 'TopicArn' => $topicArn,
312 'AttributeName' => $attrName,
313 'AttributeValue' => $attrValue
314 ));
315 return true;
316 }
317 /**
318 * Subscribe to a topic
319 *
320 * @link http://docs.amazonwebservices.com/sns/latest/api/API_Subscribe.html
321 * @param string $topicArn
322 * @param string $protocol - http/https/email/email-json/sms/sqs
323 * @param string $endpoint
324 * @return bool
325 * @throws InvalidArgumentException
326 */
327 public function subscribe($topicArn, $protocol, $endpoint) {
328 if(empty($topicArn) || empty($protocol) || empty($endpoint)) {
329 throw new InvalidArgumentException('Must supply a TopicARN, Protocol and Endpoint to subscribe to a topic');
330 }
331 $this->_request('Subscribe', array(
332 'TopicArn' => $topicArn,
333 'Protocol' => $protocol,
334 'Endpoint' => $endpoint
335 ));
336 return true;
337 }
338 /**
339 * Unsubscribe a user from a topic
340 *
341 * @link http://docs.amazonwebservices.com/sns/latest/api/API_Unsubscribe.html
342 * @param string $subscriptionArn
343 * @return bool
344 * @throws InvalidArgumentException
345 */
346 public function unsubscribe($subscriptionArn) {
347 if(empty($subscriptionArn)) {
348 throw new InvalidArgumentException('Must supply a SubscriptionARN to unsubscribe from a topic');
349 }
350 $this->_request('Unsubscribe', array('SubscriptionArn' => $subscriptionArn));
351 return true;
352 }
353 /**
354 * Create Platform endpoint
355 *
356 * @link http://docs.aws.amazon.com/sns/latest/api/API_CreatePlatformEndpoint.html
357 * @param string $platformApplicationArn
358 * @param string $token
359 * @param string $userData
360 * @return bool
361 * @throws InvalidArgumentException
362 */
363 public function createPlatformEndpoint($platformApplicationArn, $token, $userData) {
364 if(empty($platformApplicationArn) || empty($token) || empty($userData)) {
365 throw new InvalidArgumentException('Must supply a PlatformApplicationArn,Token & UserData to create platform endpoint');
366 }
367 $response = $this->_request('CreatePlatformEndpoint', array(
368 'PlatformApplicationArn' => $platformApplicationArn,
369 'Token' => $token,
370 'CustomUserData' => $userData
371 ));
372 return strval($response->CreatePlatformEndpointResult->EndpointArn);
373 }
374 /**
375 * Delete endpoint
376 *
377 * @link http://docs.aws.amazon.com/sns/latest/api/API_DeleteEndpoint.html
378 * @param string $deviceArn
379 *
380 * @return bool
381 * @throws InvalidArgumentException
382 */
383 public function deleteEndpoint($deviceArn) {
384 if(empty($deviceArn)) {
385 throw new InvalidArgumentException('Must supply a DeviceARN to remove platform endpoint');
386 }
387 $this->_request('DeleteEndpoint', array(
388 'EndpointArn' => $deviceArn,
389
390 ));
391 return true;
392 }
393 //
394 // Private functions
395 //
396 /**
397 * Perform and process a cURL request
398 *
399 * @param string $action
400 * @param array $params [optional]
401 * @return SimpleXMLElement
402 * @throws SNSException|APIException
403 */
404 private function _request($action, $params = array()) {
405 // Add in required params
406 $params['Action'] = $action;
407 $params['AWSAccessKeyId'] = $this->access_key;
408 $params['Timestamp'] = gmdate('Y-m-d\TH:i:s.000\Z');
409 $params['SignatureVersion'] = 2;
410 $params['SignatureMethod'] = 'HmacSHA256';
411 // Sort and encode into string
412 uksort($params, 'strnatcmp');
413 $queryString = '';
414 foreach ($params as $key => $val) {
415 $queryString .= "&{$key}=".rawurlencode($val);
416 }
417 $queryString = substr($queryString, 1);
418 // Form request string
419 $requestString = "GET\n"
420 . $this->endpoint."\n"
421 . "/\n"
422 . $queryString;
423 // Create signature - Version 2
424 $params['Signature'] = base64_encode(
425 hash_hmac('sha256', $requestString, $this->secret_key, true)
426 );
427 // Finally create request
428 $request = $this->protocol . $this->endpoint . '/?' . http_build_query($params, '', '&');
429 // Instantiate cUrl and perform request
430 $ch = curl_init();
431 curl_setopt($ch, CURLOPT_URL, $request);
432 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
433 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
434 $output = curl_exec($ch);
435 $info = curl_getinfo($ch);
436 // Close cUrl
437 curl_close($ch);
438 // Load XML response
439 $xmlResponse = simplexml_load_string($output);
440 // Check return code
441 if($this->_checkGoodResponse($info['http_code']) === false) {
442 // Response not in 200 range
443 if(isset($xmlResponse->Error)) {
444 // Amazon returned an XML error
445 trigger_error(sprintf(strval($xmlResponse->Error->Code) . ': ' . strval($xmlResponse->Error->Message)), E_USER_WARNING);
446 //throw new SNSException(strval($xmlResponse->Error->Code) . ': ' . strval($xmlResponse->Error->Message), $info['http_code']);
447 throw new SNSException(strval($xmlResponse->Error->Code), $info['http_code']);
448 }
449 else {
450 // Some other problem
451 throw new APIException('There was a problem executing this request', $info['http_code']);
452 }
453 }
454 else {
455 // All good
456 return $xmlResponse;
457 }
458 }
459 /**
460 * Check the curl response code - anything in 200 range
461 *
462 * @param int $code
463 * @return bool
464 */
465 private function _checkGoodResponse($code) {
466 return floor($code / 100) == 2;
467 }
468 /**
469 * Transform the standard AmazonSNS XML array format into a normal array
470 *
471 * @param SimpleXMLElement $xmlArray
472 * @return array
473 */
474 private function _processXmlToArray(SimpleXMLElement $xmlArray) {
475 $returnArray = array();
476 // Process into array
477 foreach($xmlArray as $xmlElement) {
478 $elementArray = array();
479 // Loop through each element
480 foreach($xmlElement as $key => $element) {
481 // Use strval() to make sure no SimpleXMLElement objects remain
482 $elementArray[$key] = strval($element);
483 }
484 // Store array of elements
485 $returnArray[] = $elementArray;
486 }
487 return $returnArray;
488 }
489}
490// Exception thrown if there's a problem with the API
491class APIException extends Exception {}
492// Exception thrown if Amazon returns an error
493class SNSException extends Exception {}