|
1 <?php |
|
2 /** |
|
3 * Zend Framework |
|
4 * |
|
5 * LICENSE |
|
6 * |
|
7 * This source file is subject to the new BSD license that is bundled |
|
8 * with this package in the file LICENSE.txt. |
|
9 * It is also available through the world-wide-web at this URL: |
|
10 * http://framework.zend.com/license/new-bsd |
|
11 * If you did not receive a copy of the license and are unable to |
|
12 * obtain it through the world-wide-web, please send an email |
|
13 * to license@zend.com so we can send you a copy immediately. |
|
14 * |
|
15 * @category Zend |
|
16 * @package Zend_Service_Amazon |
|
17 * @subpackage SimpleDb |
|
18 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
19 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
20 */ |
|
21 |
|
22 /** |
|
23 * @see Zend_Service_Amazon_Abstract |
|
24 */ |
|
25 require_once 'Zend/Service/Amazon/Abstract.php'; |
|
26 |
|
27 /** |
|
28 * @see Zend_Service_Amazon_SimpleDb_Response |
|
29 */ |
|
30 require_once 'Zend/Service/Amazon/SimpleDb/Response.php'; |
|
31 |
|
32 /** |
|
33 * @see Zend_Service_Amazon_SimpleDb_Page |
|
34 */ |
|
35 require_once 'Zend/Service/Amazon/SimpleDb/Page.php'; |
|
36 |
|
37 /** |
|
38 * @see Zend_Service_Amazon_SimpleDb_Attribute |
|
39 */ |
|
40 require_once 'Zend/Service/Amazon/SimpleDb/Attribute.php'; |
|
41 |
|
42 /** |
|
43 * @see Zend_Service_Amazon_SimpleDb_Exception |
|
44 */ |
|
45 require_once 'Zend/Service/Amazon/SimpleDb/Exception.php'; |
|
46 |
|
47 /** |
|
48 * @see Zend_Crypt_Hmac |
|
49 */ |
|
50 require_once 'Zend/Crypt/Hmac.php'; |
|
51 |
|
52 /** |
|
53 * @category Zend |
|
54 * @package Zend_Service_Amazon |
|
55 * @subpackage SimpleDb |
|
56 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
57 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
58 */ |
|
59 class Zend_Service_Amazon_SimpleDb extends Zend_Service_Amazon_Abstract |
|
60 { |
|
61 /* Notes */ |
|
62 // TODO SSL is required |
|
63 |
|
64 /** |
|
65 * The HTTP query server |
|
66 */ |
|
67 protected $_sdbEndpoint = 'sdb.amazonaws.com/'; |
|
68 |
|
69 /** |
|
70 * Period after which HTTP request will timeout in seconds |
|
71 */ |
|
72 protected $_httpTimeout = 10; |
|
73 |
|
74 /** |
|
75 * The API version to use |
|
76 */ |
|
77 protected $_sdbApiVersion = '2009-04-15'; |
|
78 |
|
79 /** |
|
80 * Signature Version |
|
81 */ |
|
82 protected $_signatureVersion = '2'; |
|
83 |
|
84 /** |
|
85 * Signature Encoding Method |
|
86 */ |
|
87 protected $_signatureMethod = 'HmacSHA256'; |
|
88 |
|
89 /** |
|
90 * Create Amazon SimpleDB client. |
|
91 * |
|
92 * @param string $access_key Override the default Access Key |
|
93 * @param string $secret_key Override the default Secret Key |
|
94 * @param string $region Sets the AWS Region |
|
95 * @return void |
|
96 */ |
|
97 public function __construct($accessKey, $secretKey) |
|
98 { |
|
99 parent::__construct($accessKey, $secretKey); |
|
100 $this->setEndpoint("https://" . $this->_sdbEndpoint); |
|
101 } |
|
102 |
|
103 /** |
|
104 * Set SimpleDB endpoint to use |
|
105 * |
|
106 * @param string|Zend_Uri_Http $endpoint |
|
107 * @return Zend_Service_Amazon_SimpleDb |
|
108 */ |
|
109 public function setEndpoint($endpoint) |
|
110 { |
|
111 if(!($endpoint instanceof Zend_Uri_Http)) { |
|
112 $endpoint = Zend_Uri::factory($endpoint); |
|
113 } |
|
114 if(!$endpoint->valid()) { |
|
115 require_once 'Zend/Service/Amazon/SimpleDb/Exception.php'; |
|
116 throw new Zend_Service_Amazon_SimpleDb_Exception("Invalid endpoint supplied"); |
|
117 } |
|
118 $this->_endpoint = $endpoint; |
|
119 return $this; |
|
120 } |
|
121 |
|
122 /** |
|
123 * Get SimpleDB endpoint |
|
124 * |
|
125 * @return Zend_Uri_Http |
|
126 */ |
|
127 public function getEndpoint() |
|
128 { |
|
129 return $this->_endpoint; |
|
130 } |
|
131 |
|
132 /** |
|
133 * Get attributes API method |
|
134 * |
|
135 * @param string $domainName Domain name within database |
|
136 * @param string |
|
137 */ |
|
138 public function getAttributes( |
|
139 $domainName, $itemName, $attributeName = null |
|
140 ) { |
|
141 $params = array(); |
|
142 $params['Action'] = 'GetAttributes'; |
|
143 $params['DomainName'] = $domainName; |
|
144 $params['ItemName'] = $itemName; |
|
145 |
|
146 if (isset($attributeName)) { |
|
147 $params['AttributeName'] = $attributeName; |
|
148 } |
|
149 |
|
150 $response = $this->_sendRequest($params); |
|
151 $document = $response->getSimpleXMLDocument(); |
|
152 |
|
153 $attributeNodes = $document->GetAttributesResult->Attribute; |
|
154 |
|
155 // Return an array of arrays |
|
156 $attributes = array(); |
|
157 foreach($attributeNodes as $attributeNode) { |
|
158 $name = (string)$attributeNode->Name; |
|
159 $valueNodes = $attributeNode->Value; |
|
160 $data = null; |
|
161 if (is_array($valueNodes) && !empty($valueNodes)) { |
|
162 $data = array(); |
|
163 foreach($valueNodes as $valueNode) { |
|
164 $data[] = (string)$valueNode; |
|
165 } |
|
166 } elseif (isset($valueNodes)) { |
|
167 $data = (string)$valueNodes; |
|
168 } |
|
169 if (isset($attributes[$name])) { |
|
170 $attributes[$name]->addValue($data); |
|
171 } else { |
|
172 $attributes[$name] = new Zend_Service_Amazon_SimpleDb_Attribute($itemName, $name, $data); |
|
173 } |
|
174 } |
|
175 return $attributes; |
|
176 } |
|
177 |
|
178 /** |
|
179 * Push attributes |
|
180 * |
|
181 * @param string $domainName |
|
182 * @param string $itemName |
|
183 * @param array|Traverable $attributes |
|
184 * @param array $replace |
|
185 * @return void |
|
186 */ |
|
187 public function putAttributes( |
|
188 $domainName, $itemName, $attributes, $replace = array() |
|
189 ) { |
|
190 $params = array(); |
|
191 $params['Action'] = 'PutAttributes'; |
|
192 $params['DomainName'] = $domainName; |
|
193 $params['ItemName'] = $itemName; |
|
194 |
|
195 $index = 0; |
|
196 foreach ($attributes as $attribute) { |
|
197 $attributeName = $attribute->getName(); |
|
198 foreach ($attribute->getValues() as $value) { |
|
199 $params['Attribute.' . $index . '.Name'] = $attributeName; |
|
200 $params['Attribute.' . $index . '.Value'] = $value; |
|
201 |
|
202 // Check if it should be replaced |
|
203 if(array_key_exists($attributeName, $replace) && $replace[$attributeName]) { |
|
204 $params['Attribute.' . $index . '.Replace'] = 'true'; |
|
205 } |
|
206 $index++; |
|
207 } |
|
208 } |
|
209 |
|
210 // Exception should get thrown if there's an error |
|
211 $response = $this->_sendRequest($params); |
|
212 } |
|
213 |
|
214 /** |
|
215 * Add many attributes at once |
|
216 * |
|
217 * @param array $items |
|
218 * @param string $domainName |
|
219 * @param array $replace |
|
220 * @return void |
|
221 */ |
|
222 public function batchPutAttributes($items, $domainName, array $replace = array()) |
|
223 { |
|
224 |
|
225 $params = array(); |
|
226 $params['Action'] = 'BatchPutAttributes'; |
|
227 $params['DomainName'] = $domainName; |
|
228 |
|
229 $itemIndex = 0; |
|
230 foreach ($items as $name => $attributes) { |
|
231 $params['Item.' . $itemIndex . '.ItemName'] = $name; |
|
232 $attributeIndex = 0; |
|
233 foreach ($attributes as $attribute) { |
|
234 // attribute value cannot be array, so when several items are passed |
|
235 // they are treated as separate values with the same attribute name |
|
236 foreach($attribute->getValues() as $value) { |
|
237 $params['Item.' . $itemIndex . '.Attribute.' . $attributeIndex . '.Name'] = $attribute->getName(); |
|
238 $params['Item.' . $itemIndex . '.Attribute.' . $attributeIndex . '.Value'] = $value; |
|
239 if (isset($replace[$name]) |
|
240 && isset($replace[$name][$attribute->getName()]) |
|
241 && $replace[$name][$attribute->getName()] |
|
242 ) { |
|
243 $params['Item.' . $itemIndex . '.Attribute.' . $attributeIndex . '.Replace'] = 'true'; |
|
244 } |
|
245 $attributeIndex++; |
|
246 } |
|
247 } |
|
248 $itemIndex++; |
|
249 } |
|
250 |
|
251 $response = $this->_sendRequest($params); |
|
252 } |
|
253 |
|
254 /** |
|
255 * Delete attributes |
|
256 * |
|
257 * @param string $domainName |
|
258 * @param string $itemName |
|
259 * @param array $attributes |
|
260 * @return void |
|
261 */ |
|
262 public function deleteAttributes($domainName, $itemName, array $attributes = array()) |
|
263 { |
|
264 $params = array(); |
|
265 $params['Action'] = 'DeleteAttributes'; |
|
266 $params['DomainName'] = $domainName; |
|
267 $params['ItemName'] = $itemName; |
|
268 |
|
269 $attributeIndex = 0; |
|
270 foreach ($attributes as $attribute) { |
|
271 foreach ($attribute->getValues() as $value) { |
|
272 $params['Attribute.' . $attributeIndex . '.Name'] = $attribute->getName(); |
|
273 $params['Attribute.' . $attributeIndex . '.Value'] = $value; |
|
274 $attributeIndex++; |
|
275 } |
|
276 } |
|
277 |
|
278 $response = $this->_sendRequest($params); |
|
279 |
|
280 return true; |
|
281 } |
|
282 |
|
283 /** |
|
284 * List domains |
|
285 * |
|
286 * @param $maxNumberOfDomains int |
|
287 * @param $nextToken int |
|
288 * @return array 0 or more domain names |
|
289 */ |
|
290 public function listDomains($maxNumberOfDomains = 100, $nextToken = null) |
|
291 { |
|
292 $params = array(); |
|
293 $params['Action'] = 'ListDomains'; |
|
294 $params['MaxNumberOfDomains'] = $maxNumberOfDomains; |
|
295 |
|
296 if (null !== $nextToken) { |
|
297 $params['NextToken'] = $nextToken; |
|
298 } |
|
299 $response = $this->_sendRequest($params); |
|
300 |
|
301 $domainNodes = $response->getSimpleXMLDocument()->ListDomainsResult->DomainName; |
|
302 |
|
303 $data = array(); |
|
304 foreach ($domainNodes as $domain) { |
|
305 $data[] = (string)$domain; |
|
306 } |
|
307 |
|
308 $nextTokenNode = $response->getSimpleXMLDocument()->ListDomainsResult->NextToken; |
|
309 $nextToken = (string)$nextTokenNode; |
|
310 $nextToken = (trim($nextToken) === '') ? null : $nextToken; |
|
311 |
|
312 return new Zend_Service_Amazon_SimpleDb_Page($data, $nextToken); |
|
313 } |
|
314 |
|
315 /** |
|
316 * Retrieve domain metadata |
|
317 * |
|
318 * @param $domainName string Name of the domain for which metadata will be requested |
|
319 * @return array Key/value array of metadatum names and values. |
|
320 */ |
|
321 public function domainMetadata($domainName) |
|
322 { |
|
323 $params = array(); |
|
324 $params['Action'] = 'DomainMetadata'; |
|
325 $params['DomainName'] = $domainName; |
|
326 $response = $this->_sendRequest($params); |
|
327 |
|
328 $document = $response->getSimpleXMLDocument(); |
|
329 |
|
330 $metadataNodes = $document->DomainMetadataResult->children(); |
|
331 $metadata = array(); |
|
332 foreach ($metadataNodes as $metadataNode) { |
|
333 $name = $metadataNode->getName(); |
|
334 $metadata[$name] = (string)$metadataNode; |
|
335 } |
|
336 |
|
337 return $metadata; |
|
338 } |
|
339 |
|
340 /** |
|
341 * Create a new domain |
|
342 * |
|
343 * @param $domainName string Valid domain name of the domain to create |
|
344 * @return boolean True if successful, false if not |
|
345 */ |
|
346 public function createDomain($domainName) |
|
347 { |
|
348 $params = array(); |
|
349 $params['Action'] = 'CreateDomain'; |
|
350 $params['DomainName'] = $domainName; |
|
351 $response = $this->_sendRequest($params); |
|
352 return $response->getHttpResponse()->isSuccessful(); |
|
353 } |
|
354 |
|
355 /** |
|
356 * Delete a domain |
|
357 * |
|
358 * @param $domainName string Valid domain name of the domain to delete |
|
359 * @return boolean True if successful, false if not |
|
360 */ |
|
361 public function deleteDomain($domainName) |
|
362 { |
|
363 $params = array(); |
|
364 $params['Action'] = 'DeleteDomain'; |
|
365 $params['DomainName'] = $domainName; |
|
366 $response = $this->_sendRequest($params); |
|
367 return $response->getHttpResponse()->isSuccessful(); |
|
368 } |
|
369 |
|
370 /** |
|
371 * Select items from the database |
|
372 * |
|
373 * @param string $selectExpression |
|
374 * @param null|string $nextToken |
|
375 * @return Zend_Service_Amazon_SimpleDb_Page |
|
376 */ |
|
377 public function select($selectExpression, $nextToken = null) |
|
378 { |
|
379 $params = array(); |
|
380 $params['Action'] = 'Select'; |
|
381 $params['SelectExpression'] = $selectExpression; |
|
382 |
|
383 if (null !== $nextToken) { |
|
384 $params['NextToken'] = $nextToken; |
|
385 } |
|
386 |
|
387 $response = $this->_sendRequest($params); |
|
388 $xml = $response->getSimpleXMLDocument(); |
|
389 |
|
390 $attributes = array(); |
|
391 foreach ($xml->SelectResult->Item as $item) { |
|
392 $itemName = (string)$item->Name; |
|
393 |
|
394 foreach ($item->Attribute as $attribute) { |
|
395 $attributeName = (string)$attribute->Name; |
|
396 |
|
397 $values = array(); |
|
398 foreach ($attribute->Value as $value) { |
|
399 $values[] = (string)$value; |
|
400 } |
|
401 $attributes[$itemName][$attributeName] = new Zend_Service_Amazon_SimpleDb_Attribute($itemName, $attributeName, $values); |
|
402 } |
|
403 } |
|
404 |
|
405 $nextToken = (string)$xml->NextToken; |
|
406 |
|
407 return new Zend_Service_Amazon_SimpleDb_Page($attributes, $nextToken); |
|
408 } |
|
409 |
|
410 /** |
|
411 * Quote SDB value |
|
412 * |
|
413 * Wraps it in '' |
|
414 * |
|
415 * @param string $value |
|
416 * @return string |
|
417 */ |
|
418 public function quote($value) |
|
419 { |
|
420 // wrap in single quotes and convert each ' inside to '' |
|
421 return "'" . str_replace("'", "''", $value) . "'"; |
|
422 } |
|
423 |
|
424 /** |
|
425 * Quote SDB column or table name |
|
426 * |
|
427 * Wraps it in `` |
|
428 * @param string $name |
|
429 * @return string |
|
430 */ |
|
431 public function quoteName($name) |
|
432 { |
|
433 if (preg_match('/^[a-z_$][a-z0-9_$-]*$/i', $name) == false) { |
|
434 throw new Zend_Service_Amazon_SimpleDb_Exception("Invalid name: can contain only alphanumeric characters, \$ and _"); |
|
435 } |
|
436 return "`$name`"; |
|
437 } |
|
438 |
|
439 /** |
|
440 * Sends a HTTP request to the SimpleDB service using Zend_Http_Client |
|
441 * |
|
442 * @param array $params List of parameters to send with the request |
|
443 * @return Zend_Service_Amazon_SimpleDb_Response |
|
444 * @throws Zend_Service_Amazon_SimpleDb_Exception |
|
445 */ |
|
446 protected function _sendRequest(array $params = array()) |
|
447 { |
|
448 // UTF-8 encode all parameters and replace '+' characters |
|
449 foreach ($params as $name => $value) { |
|
450 unset($params[$name]); |
|
451 $params[utf8_encode($name)] = $value; |
|
452 } |
|
453 |
|
454 $params = $this->_addRequiredParameters($params); |
|
455 |
|
456 try { |
|
457 /* @var $request Zend_Http_Client */ |
|
458 $request = self::getHttpClient(); |
|
459 $request->resetParameters(); |
|
460 |
|
461 $request->setConfig(array( |
|
462 'timeout' => $this->_httpTimeout |
|
463 )); |
|
464 |
|
465 |
|
466 $request->setUri($this->getEndpoint()); |
|
467 $request->setMethod(Zend_Http_Client::POST); |
|
468 foreach ($params as $key => $value) { |
|
469 $params_out[] = rawurlencode($key)."=".rawurlencode($value); |
|
470 } |
|
471 $request->setRawData(join('&', $params_out), Zend_Http_Client::ENC_URLENCODED); |
|
472 $httpResponse = $request->request(); |
|
473 } catch (Zend_Http_Client_Exception $zhce) { |
|
474 $message = 'Error in request to AWS service: ' . $zhce->getMessage(); |
|
475 throw new Zend_Service_Amazon_SimpleDb_Exception($message, $zhce->getCode()); |
|
476 } |
|
477 $response = new Zend_Service_Amazon_SimpleDb_Response($httpResponse); |
|
478 $this->_checkForErrors($response); |
|
479 return $response; |
|
480 } |
|
481 |
|
482 /** |
|
483 * Adds required authentication and version parameters to an array of |
|
484 * parameters |
|
485 * |
|
486 * The required parameters are: |
|
487 * - AWSAccessKey |
|
488 * - SignatureVersion |
|
489 * - Timestamp |
|
490 * - Version and |
|
491 * - Signature |
|
492 * |
|
493 * If a required parameter is already set in the <tt>$parameters</tt> array, |
|
494 * it is overwritten. |
|
495 * |
|
496 * @param array $parameters the array to which to add the required |
|
497 * parameters. |
|
498 * |
|
499 * @return array |
|
500 */ |
|
501 protected function _addRequiredParameters(array $parameters) |
|
502 { |
|
503 $parameters['AWSAccessKeyId'] = $this->_getAccessKey(); |
|
504 $parameters['SignatureVersion'] = $this->_signatureVersion; |
|
505 $parameters['Timestamp'] = gmdate('c'); |
|
506 $parameters['Version'] = $this->_sdbApiVersion; |
|
507 $parameters['SignatureMethod'] = $this->_signatureMethod; |
|
508 $parameters['Signature'] = $this->_signParameters($parameters); |
|
509 |
|
510 return $parameters; |
|
511 } |
|
512 |
|
513 /** |
|
514 * Computes the RFC 2104-compliant HMAC signature for request parameters |
|
515 * |
|
516 * This implements the Amazon Web Services signature, as per the following |
|
517 * specification: |
|
518 * |
|
519 * 1. Sort all request parameters (including <tt>SignatureVersion</tt> and |
|
520 * excluding <tt>Signature</tt>, the value of which is being created), |
|
521 * ignoring case. |
|
522 * |
|
523 * 2. Iterate over the sorted list and append the parameter name (in its |
|
524 * original case) and then its value. Do not URL-encode the parameter |
|
525 * values before constructing this string. Do not use any separator |
|
526 * characters when appending strings. |
|
527 * |
|
528 * @param array $parameters the parameters for which to get the signature. |
|
529 * @param string $secretKey the secret key to use to sign the parameters. |
|
530 * |
|
531 * @return string the signed data. |
|
532 */ |
|
533 protected function _signParameters(array $paramaters) |
|
534 { |
|
535 $data = "POST\n"; |
|
536 $data .= $this->getEndpoint()->getHost() . "\n"; |
|
537 $data .= "/\n"; |
|
538 |
|
539 uksort($paramaters, 'strcmp'); |
|
540 unset($paramaters['Signature']); |
|
541 |
|
542 $arrData = array(); |
|
543 foreach ($paramaters as $key => $value) { |
|
544 $value = urlencode($value); |
|
545 $value = str_replace("%7E", "~", $value); |
|
546 $value = str_replace("+", "%20", $value); |
|
547 $arrData[] = urlencode($key) . '=' . $value; |
|
548 } |
|
549 |
|
550 $data .= implode('&', $arrData); |
|
551 |
|
552 require_once 'Zend/Crypt/Hmac.php'; |
|
553 $hmac = Zend_Crypt_Hmac::compute($this->_getSecretKey(), 'SHA256', $data, Zend_Crypt_Hmac::BINARY); |
|
554 |
|
555 return base64_encode($hmac); |
|
556 } |
|
557 |
|
558 /** |
|
559 * Checks for errors responses from Amazon |
|
560 * |
|
561 * @param Zend_Service_Amazon_SimpleDb_Response $response the response object to |
|
562 * check. |
|
563 * |
|
564 * @return void |
|
565 * |
|
566 * @throws Zend_Service_Amazon_SimpleDb_Exception if one or more errors are |
|
567 * returned from Amazon. |
|
568 */ |
|
569 private function _checkForErrors(Zend_Service_Amazon_SimpleDb_Response $response) |
|
570 { |
|
571 $xpath = new DOMXPath($response->getDocument()); |
|
572 $list = $xpath->query('//Error'); |
|
573 if ($list->length > 0) { |
|
574 $node = $list->item(0); |
|
575 $code = $xpath->evaluate('string(Code/text())', $node); |
|
576 $message = $xpath->evaluate('string(Message/text())', $node); |
|
577 throw new Zend_Service_Amazon_SimpleDb_Exception($message, 0, $code); |
|
578 } |
|
579 } |
|
580 } |