diff --git a/BreakingChanges.md b/BreakingChanges.md new file mode 100644 index 0000000..db55af7 --- /dev/null +++ b/BreakingChanges.md @@ -0,0 +1,10 @@ +Tracking Breaking changes in 1.0.0 + +* Removed `ServiceBuilder.php`, moved static builder methods into `BlobRestProxy`, `TableRestProxy`, `QueueRestProxy` and `FileRestProxy`. +* Moved method `SharedAccessSignatureHelper::generateBlobServiceSharedAccessSignatureToken()` into `BlobSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateTableServiceSharedAccessSignatureToken()` into `TableSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateQueueServiceSharedAccessSignatureToken()` into `QueueSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateFileServiceSharedAccessSignatureToken()` into `FileSharedAccessSignatureHelper`. +* `CommonMiddleWare` constructor requires storage service version as parameter now. +* `AccessPolicy` class is now an abstract class, added children classes `BlobAccessPolicy`, `ContainerAccessPolicy`, `TableAccessPolicy`, `QueueAccessPolicy`, `FileAccessPolicy` and `ShareAccessPolicy`. +* Deprecated PHP 5.5 support. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5a54677 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +This [repository](https://github.com/azure/azure-storage-common-php) is currently used for releasing only, please go to [azure-storage-php](https://github.com/azure/azure-storage-php) for submitting issues or contribution. \ No newline at end of file diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..d6c1b12 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,11 @@ +2018.01 - version 1.0.0 + +* Removed `ServiceBuilder.php`, moved static builder methods into `BlobRestProxy`, `TableRestProxy`, `QueueRestProxy` and `FileRestProxy`. +* Moved method `SharedAccessSignatureHelper::generateBlobServiceSharedAccessSignatureToken()` into `BlobSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateTableServiceSharedAccessSignatureToken()` into `TableSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateQueueServiceSharedAccessSignatureToken()` into `QueueSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateFileServiceSharedAccessSignatureToken()` into `FileSharedAccessSignatureHelper`. +* `CommonMiddleWare` constructor requires storage service version as parameter now. +* `AccessPolicy` class is now an abstract class, added children classes `BlobAccessPolicy`, `ContainerAccessPolicy`, `TableAccessPolicy`, `QueueAccessPolicy`, `FileAccessPolicy` and `ShareAccessPolicy`. +* Fixed a bug that `Utilities::allZero()` will return true for non-zero data chunks. +* Deprecated PHP 5.5 support. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 2107107..7928860 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ - MIT License +The MIT License (MIT) - Copyright (c) Microsoft Corporation. All rights reserved. +Copyright (c) 2016 Microsoft Corporation - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 72f1506..013fef7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,9 @@ +# Microsoft Azure Storage Common PHP Client Library -# Contributing +This project provides a set of common PHP code shared by Azure Storage Blob, Table, Queue and File PHP client libraries. For how to access Microsoft Azure Storage services (blobs, tables, queues and files), please refer to the [Microsoft Azure Storage PHP Client Library](https://github.com/Azure/azure-storage-php). For documentation on how to host PHP applications on Microsoft Azure, please see the [Microsoft Azure PHP Developer Center](http://www.windowsazure.com/en-us/develop/php/). -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.microsoft.com. +[![Latest Stable Version](https://poser.pugx.org/microsoft/azure-storage-common/v/stable)](https://packagist.org/packages/microsoft/azure-storage-common) -When you submit a pull request, a CLA-bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +> **Note** +> +> * This [repository](https://github.com/azure/azure-storage-common-php) is currently used for releasing only, please go to [azure-storage-php](https://github.com/azure/azure-storage-php) for submitting issues or contribution. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..292b3d1 --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "microsoft/azure-storage-common", + "version": "1.0.0", + "description": "This project provides a set of common code shared by Azure Storage Blob, Table, Queue and File PHP client libraries.", + "keywords": [ "php", "azure", "storage", "sdk", "common" ], + "license": "MIT", + "authors": [ + { + "name": "Azure Storage PHP Client Library", + "email": "dmsh@microsoft.com" + } + ], + "require": { + "php": ">=5.6.0", + "guzzlehttp/guzzle": "~6.0" + }, + "autoload": { + "psr-4": { + "MicrosoftAzure\\Storage\\Common\\": "src/Common" + } + } +} \ No newline at end of file diff --git a/src/Common/CloudConfigurationManager.php b/src/Common/CloudConfigurationManager.php new file mode 100644 index 0000000..62aebdb --- /dev/null +++ b/src/Common/CloudConfigurationManager.php @@ -0,0 +1,155 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\ConnectionStringSource; + +/** + * Configuration manager for accessing Windows Azure settings. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CloudConfigurationManager +{ + private static $_isInitialized = false; + private static $_sources; + + /** + * Restrict users from creating instances from this class + */ + private function __construct() + { + } + + /** + * Initializes the connection string source providers. + * + * @return void + */ + private static function _init() + { + if (!self::$_isInitialized) { + self::$_sources = array(); + + // Get list of default connection string sources. + $default = ConnectionStringSource::getDefaultSources(); + foreach ($default as $name => $provider) { + self::$_sources[$name] = $provider; + } + + self::$_isInitialized = true; + } + } + + /** + * Gets a connection string from all available sources. + * + * @param string $key The connection string key name. + * + * @return string If the key does not exist return null. + */ + public static function getConnectionString($key) + { + Validate::canCastAsString($key, 'key'); + + self::_init(); + $value = null; + + foreach (self::$_sources as $source) { + $value = call_user_func_array($source, array($key)); + + if (!empty($value)) { + break; + } + } + + return $value; + } + + /** + * Registers a new connection string source provider. If the source to get + * registered is a default source, only the name of the source is required. + * + * @param string $name The source name. + * @param callable $provider The source callback. + * @param boolean $prepend When true, the $provider is processed first when + * calling getConnectionString. When false (the default) the $provider is + * processed after the existing callbacks. + * + * @return void + */ + public static function registerSource($name, $provider = null, $prepend = false) + { + Validate::canCastAsString($name, 'name'); + Validate::notNullOrEmpty($name, 'name'); + + self::_init(); + $default = ConnectionStringSource::getDefaultSources(); + + // Try to get callback if the user is trying to register a default source. + $provider = Utilities::tryGetValue($default, $name, $provider); + + Validate::notNullOrEmpty($provider, 'callback'); + + if ($prepend) { + self::$_sources = array_merge( + array($name => $provider), + self::$_sources + ); + } else { + self::$_sources[$name] = $provider; + } + } + + /** + * Unregisters a connection string source. + * + * @param string $name The source name. + * + * @return callable + */ + public static function unregisterSource($name) + { + Validate::canCastAsString($name, 'name'); + Validate::notNullOrEmpty($name, 'name'); + + self::_init(); + + $sourceCallback = Utilities::tryGetValue(self::$_sources, $name); + + if (!is_null($sourceCallback)) { + unset(self::$_sources[$name]); + } + + return $sourceCallback; + } +} diff --git a/src/Common/Exceptions/InvalidArgumentTypeException.php b/src/Common/Exceptions/InvalidArgumentTypeException.php new file mode 100644 index 0000000..8aca35f --- /dev/null +++ b/src/Common/Exceptions/InvalidArgumentTypeException.php @@ -0,0 +1,55 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Exceptions; + +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Exception thrown if an argument type does not match with the expected type. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Exceptions + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class InvalidArgumentTypeException extends \InvalidArgumentException +{ + /** + * Constructor. + * + * @param string $validType The valid type that should be provided by the user. + * @param string $name The parameter name. + * + * @return InvalidArgumentTypeException + */ + public function __construct($validType, $name = null) + { + parent::__construct( + sprintf(Resources::INVALID_PARAM_MSG, $name, $validType) + ); + } +} diff --git a/src/Common/Exceptions/ServiceException.php b/src/Common/Exceptions/ServiceException.php new file mode 100644 index 0000000..40045f1 --- /dev/null +++ b/src/Common/Exceptions/ServiceException.php @@ -0,0 +1,177 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Exceptions; + +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use Psr\Http\Message\ResponseInterface; + +/** + * Fires when the response code is incorrect. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Exceptions + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ServiceException extends \LogicException +{ + private $response; + private $errorText; + private $errorMessage; + + /** + * Constructor + * + * @param ResponseInterface $response The response received that causes the + * exception. + * + * @internal + * + * @return ServiceException + */ + public function __construct(ResponseInterface $response) + { + parent::__construct( + sprintf( + Resources::AZURE_ERROR_MSG, + $response->getStatusCode(), + $response->getReasonPhrase(), + $response->getBody() + ) + ); + $this->code = $response->getStatusCode(); + $this->response = $response; + $this->errorText = $response->getReasonPhrase(); + $this->errorMessage = self::parseErrorMessage($response); + } + + /** + * Error message to be parsed. + * + * @param ResponseInterface $response The response with a response body. + * + * @internal + * + * @return string + */ + protected static function parseErrorMessage(ResponseInterface $response) + { + //try to parse using xml serializer, if failed, return the whole body + //as the error message. + $serializer = new XmlSerializer(); + $errorMessage = ''; + try { + $internalErrors = libxml_use_internal_errors(true); + $parsedArray = $serializer->unserialize($response->getBody()); + $messages = array(); + foreach (libxml_get_errors() as $error) { + $messages[] = $error->message; + } + if (!empty($messages)) { + throw new \Exception( + sprintf(Resources::ERROR_CANNOT_PARSE_XML, implode('; ', $messages)) + ); + } + libxml_use_internal_errors($internalErrors); + if (array_key_exists(Resources::XTAG_MESSAGE, $parsedArray)) { + $errorMessage = $parsedArray[Resources::XTAG_MESSAGE]; + } else { + $errorMessage = $response->getBody(); + } + } catch (\Exception $e) { + $errorMessage = $response->getBody(); + } + return $errorMessage; + } + + /** + * Gets error text. + * + * @return string + */ + public function getErrorText() + { + return $this->errorText; + } + + /** + * Gets detailed error message. + * + * @return string + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * Gets the request ID of the failure. + * + * @return string + */ + public function getRequestID() + { + $requestID = ''; + if (array_key_exists( + Resources::X_MS_REQUEST_ID, + $this->getResponse()->getHeaders() + )) { + $requestID = $this->getResponse() + ->getHeaders()[Resources::X_MS_REQUEST_ID][0]; + } + return $requestID; + } + + /** + * Gets the Date of the failure. + * + * @return string + */ + public function getDate() + { + $date = ''; + if (array_key_exists( + Resources::DATE, + $this->getResponse()->getHeaders() + )) { + $date = $this->getResponse() + ->getHeaders()[Resources::DATE][0]; + } + return $date; + } + + /** + * Gets the response of the failue. + * + * @return string + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/src/Common/Internal/ACLBase.php b/src/Common/Internal/ACLBase.php new file mode 100644 index 0000000..5526dba --- /dev/null +++ b/src/Common/Internal/ACLBase.php @@ -0,0 +1,260 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\Models\AccessPolicy; +use MicrosoftAzure\Storage\Common\Models\SignedIdentifier; +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; + +/** + * Provide base class for service ACLs. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +abstract class ACLBase +{ + private $signedIdentifiers = array(); + private $resourceType = ''; + + /** + * Create an AccessPolicy object by resource type. + * + * @return AccessPolicy + */ + abstract protected static function createAccessPolicy(); + + /** + * Validate if the resource type for the class. + * + * @param string $resourceType the resource type to be validated. + * + * @throws \InvalidArgumentException + * + * @internal + * + * @return void + */ + abstract protected static function validateResourceType($resourceType); + + /** + * Converts signed identifiers to array representation for XML serialization + * + * @internal + * + * @return array + */ + public function toArray() + { + $array = array(); + + foreach ($this->getSignedIdentifiers() as $value) { + $array[] = $value->toArray(); + } + + return $array; + } + + /** + * Converts this signed identifiers to XML representation. + * + * @param XmlSerializer $xmlSerializer The XML serializer. + * + * @internal + * + * @return string + */ + public function toXml(XmlSerializer $serializer) + { + $properties = array( + XmlSerializer::DEFAULT_TAG => Resources::XTAG_SIGNED_IDENTIFIER, + XmlSerializer::ROOT_NAME => Resources::XTAG_SIGNED_IDENTIFIERS + ); + + return $serializer->serialize($this->toArray(), $properties); + } + + /** + * Construct the signed identifiers from a given parsed XML in array + * representation. + * + * @param array|null $parsed The parsed XML into array representation. + * + * @internal + * + * @return void + */ + public function fromXmlArray(array $parsed = null) + { + $this->setSignedIdentifiers(array()); + + // Initialize signed identifiers. + if (!empty($parsed) && + is_array($parsed[Resources::XTAG_SIGNED_IDENTIFIER]) + ) { + $entries = $parsed[Resources::XTAG_SIGNED_IDENTIFIER]; + $temp = Utilities::getArray($entries); + + foreach ($temp as $value) { + $accessPolicy = $value[Resources::XTAG_ACCESS_POLICY]; + $startString = urldecode( + $accessPolicy[Resources::XTAG_SIGNED_START] + ); + $expiryString = urldecode( + $accessPolicy[Resources::XTAG_SIGNED_EXPIRY] + ); + $start = Utilities::convertToDateTime($startString); + $expiry = Utilities::convertToDateTime($expiryString); + $permission = $accessPolicy[Resources::XTAG_SIGNED_PERMISSION]; + $id = $value[Resources::XTAG_SIGNED_ID]; + $this->addSignedIdentifier($id, $start, $expiry, $permission); + } + } + } + + /** + * Gets the type of resource this ACL relate to. + * + * @internal + * + * @return string + */ + protected function getResourceType() + { + return $this->resourceType; + } + + /** + * Set the type of resource this ACL relate to. + * + * @internal + * + * @return void + */ + protected function setResourceType($resourceType) + { + static::validateResourceType($resourceType); + $this->resourceType = $resourceType; + } + + /** + * Add a signed identifier to the ACL. + * + * @param string $id A unique id for this signed identifier. + * @param \DateTime $start The time at which the Shared Access + * Signature becomes valid. If omitted, start + * time for this call is assumed to be the + * time when the service receives the + * request. + * @param \DateTime $expiry The time at which the Shared Access + * Signature becomes invalid. + * @param string $permissions The permissions associated with the Shared + * Access Signature. The user is restricted to + * operations allowed by the permissions. + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/establishing-a-stored-access-policy + */ + public function addSignedIdentifier( + $id, + \DateTime $start, + \DateTime $expiry, + $permissions + ) { + Validate::canCastAsString($id, 'id'); + if ($start != null) { + Validate::isDate($start); + } + Validate::isDate($expiry); + Validate::canCastAsString($permissions, 'permissions'); + + $accessPolicy = static::createAccessPolicy(); + $accessPolicy->setStart($start); + $accessPolicy->setExpiry($expiry); + $accessPolicy->setPermission($permissions); + + $signedIdentifier = new SignedIdentifier(); + $signedIdentifier->setId($id); + $signedIdentifier->setAccessPolicy($accessPolicy); + + // Remove the signed identifier with the same ID. + $this->removeSignedIdentifier($id); + + // There can be no more than 5 signed identifiers at the same time. + Validate::isTrue( + count($this->getSignedIdentifiers()) < 5, + Resources::ERROR_TOO_MANY_SIGNED_IDENTIFIERS + ); + + $this->signedIdentifiers[] = $signedIdentifier; + } + + /** + * Remove the signed identifier with given ID. + * + * @param string $id The ID of the signed identifier to be removed. + * + * @return boolean + */ + public function removeSignedIdentifier($id) + { + Validate::canCastAsString($id, 'id'); + //var_dump($this->signedIdentifiers); + for ($i = 0; $i < count($this->signedIdentifiers); ++$i) { + if ($this->signedIdentifiers[$i]->getId() == $id) { + array_splice($this->signedIdentifiers, $i, 1); + return true; + } + } + + return false; + } + + /** + * Gets signed identifiers. + * + * @return array + */ + public function getSignedIdentifiers() + { + return $this->signedIdentifiers; + } + + public function setSignedIdentifiers(array $signedIdentifiers) + { + // There can be no more than 5 signed identifiers at the same time. + Validate::isTrue( + count($signedIdentifiers) <= 5, + Resources::ERROR_TOO_MANY_SIGNED_IDENTIFIERS + ); + $this->signedIdentifiers = $signedIdentifiers; + } +} diff --git a/src/Common/Internal/Authentication/IAuthScheme.php b/src/Common/Internal/Authentication/IAuthScheme.php new file mode 100644 index 0000000..be50dc8 --- /dev/null +++ b/src/Common/Internal/Authentication/IAuthScheme.php @@ -0,0 +1,52 @@ + + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Authentication; + +use GuzzleHttp\Psr7\Request; + +/** + * Interface for azure authentication schemes. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Authentication + * @author Azure Storage PHP SDK + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +interface IAuthScheme +{ + /** + * Signs a request. + * + * @param \GuzzleHttp\Psr7\Request $request HTTP request object. + * + * @abstract + * + * @return \GuzzleHttp\Psr7\Request + */ + public function signRequest(Request $request); +} diff --git a/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php b/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php new file mode 100644 index 0000000..a021639 --- /dev/null +++ b/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php @@ -0,0 +1,96 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Authentication; + +use GuzzleHttp\Psr7\Request; +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Base class for azure authentication schemes. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Authentication + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SharedAccessSignatureAuthScheme implements IAuthScheme +{ + /** + * The sas token + */ + protected $sasToken; + + /** + * Constructor. + * + * @param string $sasToken shared access signature token. + * + */ + public function __construct($sasToken) + { + // Remove '?' in front of the SAS token if existing + $this->sasToken = str_replace('?', '', $sasToken, $i); + + if ($i > 1) { + throw new \InvalidArgumentException( + sprintf( + Resources::INVALID_SAS_TOKEN, + $sasToken + ) + ); + } + } + + /** + * Adds authentication header to the request headers. + * + * @param \GuzzleHttp\Psr7\Request $request HTTP request object. + * + * @abstract + * + * @return \GuzzleHttp\Psr7\Request + */ + public function signRequest(Request $request) + { + // initial URI + $uri = $request->getUri(); + + // new query values from SAS token + $queryValues = explode('&', $this->sasToken); + + // append SAS token query values to existing URI + foreach ($queryValues as $queryField) { + list($key, $value) = explode('=', $queryField); + + $uri = \GuzzleHttp\Psr7\Uri::withQueryValue($uri, $key, $value); + } + + // replace URI + return $request->withUri($uri, true); + } +} diff --git a/src/Common/Internal/Authentication/SharedKeyAuthScheme.php b/src/Common/Internal/Authentication/SharedKeyAuthScheme.php new file mode 100644 index 0000000..d9a88bf --- /dev/null +++ b/src/Common/Internal/Authentication/SharedKeyAuthScheme.php @@ -0,0 +1,316 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Authentication; + +use GuzzleHttp\Psr7\Request; +use MicrosoftAzure\Storage\Common\Internal\Http\HttpFormatter; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Provides shared key authentication scheme for blob and queue. For more info + * check: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Authentication + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SharedKeyAuthScheme implements IAuthScheme +{ + /** + * The account name + */ + protected $accountName; + + /** + * The account key + */ + protected $accountKey; + + /** + * The included headers + */ + protected $includedHeaders; + + /** + * Constructor. + * + * @param string $accountName storage account name. + * @param string $accountKey storage account primary or secondary key. + * + * @return SharedKeyAuthScheme + */ + public function __construct($accountName, $accountKey) + { + $this->accountKey = $accountKey; + $this->accountName = $accountName; + + $this->includedHeaders = array(); + $this->includedHeaders[] = Resources::CONTENT_ENCODING; + $this->includedHeaders[] = Resources::CONTENT_LANGUAGE; + $this->includedHeaders[] = Resources::CONTENT_LENGTH; + $this->includedHeaders[] = Resources::CONTENT_MD5; + $this->includedHeaders[] = Resources::CONTENT_TYPE; + $this->includedHeaders[] = Resources::DATE; + $this->includedHeaders[] = Resources::IF_MODIFIED_SINCE; + $this->includedHeaders[] = Resources::IF_MATCH; + $this->includedHeaders[] = Resources::IF_NONE_MATCH; + $this->includedHeaders[] = Resources::IF_UNMODIFIED_SINCE; + $this->includedHeaders[] = Resources::RANGE; + } + + /** + * Computes the authorization signature for blob and queue shared key. + * + * @param array $headers request headers. + * @param string $url reuqest url. + * @param array $queryParams query variables. + * @param string $httpMethod request http method. + * + * @see Blob and Queue Services (Shared Key Authentication) at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return string + */ + protected function computeSignature( + array $headers, + $url, + array $queryParams, + $httpMethod + ) { + $canonicalizedHeaders = $this->computeCanonicalizedHeaders($headers); + + $canonicalizedResource = $this->computeCanonicalizedResource( + $url, + $queryParams + ); + + $stringToSign = array(); + $stringToSign[] = strtoupper($httpMethod); + + foreach ($this->includedHeaders as $header) { + $stringToSign[] = Utilities::tryGetValue($headers, $header); + } + + if (count($canonicalizedHeaders) > 0) { + $stringToSign[] = implode("\n", $canonicalizedHeaders); + } + + $stringToSign[] = $canonicalizedResource; + $stringToSign = implode("\n", $stringToSign); + + return $stringToSign; + } + + /** + * Returns authorization header to be included in the request. + * + * @param array $headers request headers. + * @param string $url reuqest url. + * @param array $queryParams query variables. + * @param string $httpMethod request http method. + * + * @see Specifying the Authorization Header section at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return string + */ + public function getAuthorizationHeader( + array $headers, + $url, + array $queryParams, + $httpMethod + ) { + $signature = $this->computeSignature( + $headers, + $url, + $queryParams, + $httpMethod + ); + + return 'SharedKey ' . $this->accountName . ':' . base64_encode( + hash_hmac('sha256', $signature, base64_decode($this->accountKey), true) + ); + } + + /** + * Computes canonicalized headers for headers array. + * + * @param array $headers request headers. + * + * @see Constructing the Canonicalized Headers String section at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return array + */ + protected function computeCanonicalizedHeaders($headers) + { + $canonicalizedHeaders = array(); + $normalizedHeaders = array(); + $validPrefix = Resources::X_MS_HEADER_PREFIX; + + if (is_null($normalizedHeaders)) { + return $canonicalizedHeaders; + } + + foreach ($headers as $header => $value) { + // Convert header to lower case. + $header = strtolower($header); + + // Retrieve all headers for the resource that begin with x-ms-, + // including the x-ms-date header. + if (Utilities::startsWith($header, $validPrefix)) { + // Unfold the string by replacing any breaking white space + // (meaning what splits the headers, which is \r\n) with a single + // space. + $value = str_replace("\r\n", ' ', $value); + + // Trim any white space around the colon in the header. + $value = ltrim($value); + $header = rtrim($header); + + $normalizedHeaders[$header] = $value; + } + } + + // Sort the headers lexicographically by header name, in ascending order. + // Note that each header may appear only once in the string. + ksort($normalizedHeaders); + + foreach ($normalizedHeaders as $key => $value) { + $canonicalizedHeaders[] = $key . ':' . $value; + } + + return $canonicalizedHeaders; + } + + /** + * Computes canonicalized resources from URL using Table formar + * + * @param string $url request url. + * @param array $queryParams request query variables. + * + * @see Constructing the Canonicalized Resource String section at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return string + */ + protected function computeCanonicalizedResourceForTable($url, $queryParams) + { + $queryParams = array_change_key_case($queryParams); + + // 1. Beginning with an empty string (""), append a forward slash (/), + // followed by the name of the account that owns the accessed resource. + $canonicalizedResource = '/' . $this->accountName; + + // 2. Append the resource's encoded URI path, without any query parameters. + $canonicalizedResource .= parse_url($url, PHP_URL_PATH); + + // 3. The query string should include the question mark and the comp + // parameter (for example, ?comp=metadata). No other parameters should + // be included on the query string. + if (array_key_exists(Resources::QP_COMP, $queryParams)) { + $canonicalizedResource .= '?' . Resources::QP_COMP . '='; + $canonicalizedResource .= $queryParams[Resources::QP_COMP]; + } + + return $canonicalizedResource; + } + + /** + * Computes canonicalized resources from URL. + * + * @param string $url request url. + * @param array $queryParams request query variables. + * + * @see Constructing the Canonicalized Resource String section at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return string + */ + protected function computeCanonicalizedResource($url, $queryParams) + { + $queryParams = array_change_key_case($queryParams); + + // 1. Beginning with an empty string (""), append a forward slash (/), + // followed by the name of the account that owns the accessed resource. + $canonicalizedResource = '/' . $this->accountName; + + // 2. Append the resource's encoded URI path, without any query parameters. + $canonicalizedResource .= parse_url($url, PHP_URL_PATH); + + // 3. Retrieve all query parameters on the resource URI, including the comp + // parameter if it exists. + // 4. Sort the query parameters lexicographically by parameter name, in + // ascending order. + if (count($queryParams) > 0) { + ksort($queryParams); + } + + // 5. Convert all parameter names to lowercase. + // 6. URL-decode each query parameter name and value. + // 7. Append each query parameter name and value to the string in the + // following format: + // parameter-name:parameter-value + // 9. Group query parameters + // 10. Append a new line character (\n) after each name-value pair. + foreach ($queryParams as $key => $value) { + // $value must already be ordered lexicographically + // See: ServiceRestProxy::groupQueryValues + $canonicalizedResource .= "\n" . $key . ':' . $value; + } + + return $canonicalizedResource; + } + + /** + * Adds authentication header to the request headers. + * + * @param \GuzzleHttp\Psr7\Request $request HTTP request object. + * + * @abstract + * + * @return \GuzzleHttp\Psr7\Request + */ + public function signRequest(Request $request) + { + $requestHeaders = HttpFormatter::formatHeaders($request->getHeaders()); + + $signedKey = $this->getAuthorizationHeader( + $requestHeaders, + $request->getUri(), + \GuzzleHttp\Psr7\parse_query( + $request->getUri()->getQuery() + ), + $request->getMethod() + ); + + return $request->withHeader(Resources::AUTHENTICATION, $signedKey); + } +} diff --git a/src/Common/Internal/ConnectionStringParser.php b/src/Common/Internal/ConnectionStringParser.php new file mode 100644 index 0000000..75e6f6d --- /dev/null +++ b/src/Common/Internal/ConnectionStringParser.php @@ -0,0 +1,335 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Helper methods for parsing connection strings. The rules for formatting connection + * strings are defined here: + * www.connectionstrings.com/articles/show/important-rules-for-connection-strings + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ConnectionStringParser +{ + const EXPECT_KEY = 'ExpectKey'; + const EXPECT_ASSIGNMENT = 'ExpectAssignment'; + const EXPECT_VALUE = 'ExpectValue'; + const EXPECT_SEPARATOR = 'ExpectSeparator'; + + private $_argumentName; + private $_value; + private $_pos; + private $_state; + + /** + * Parses the connection string into a collection of key/value pairs. + * + * @param string $argumentName Name of the argument to be used in error + * messages. + * @param string $connectionString Connection string. + * + * @return array + */ + public static function parseConnectionString($argumentName, $connectionString) + { + Validate::canCastAsString($argumentName, 'argumentName'); + Validate::notNullOrEmpty($argumentName, 'argumentName'); + Validate::canCastAsString($connectionString, 'connectionString'); + Validate::notNullOrEmpty($connectionString, 'connectionString'); + + $parser = new ConnectionStringParser($argumentName, $connectionString); + return $parser->_parse(); + } + + /** + * Initializes the object. + * + * @param string $argumentName Name of the argument to be used in error + * messages. + * @param string $value Connection string. + */ + private function __construct($argumentName, $value) + { + $this->_argumentName = $argumentName; + $this->_value = $value; + $this->_pos = 0; + $this->_state = ConnectionStringParser::EXPECT_KEY; + } + + /** + * Parses the connection string. + * + * @return array + * + * @throws \RuntimeException + */ + private function _parse() + { + $key = null; + $value = null; + $connectionStringValues = array(); + + while (true) { + $this->_skipWhiteSpaces(); + + if ($this->_pos == strlen($this->_value) + && $this->_state != ConnectionStringParser::EXPECT_VALUE + ) { + // Not stopping after the end has been reached and a value is + // expected results in creating an empty value, which we expect. + break; + } + + switch ($this->_state) { + case ConnectionStringParser::EXPECT_KEY: + $key = $this->_extractKey(); + $this->_state = ConnectionStringParser::EXPECT_ASSIGNMENT; + break; + + case ConnectionStringParser::EXPECT_ASSIGNMENT: + $this->_skipOperator('='); + $this->_state = ConnectionStringParser::EXPECT_VALUE; + break; + + case ConnectionStringParser::EXPECT_VALUE: + $value = $this->_extractValue(); + $this->_state = + ConnectionStringParser::EXPECT_SEPARATOR; + $connectionStringValues[$key] = $value; + $key = null; + $value = null; + break; + + default: + $this->_skipOperator(';'); + $this->_state = ConnectionStringParser::EXPECT_KEY; + break; + } + } + + // Must end parsing in the valid state (expected key or separator) + if ($this->_state == ConnectionStringParser::EXPECT_ASSIGNMENT) { + throw $this->_createException( + $this->_pos, + Resources::MISSING_CONNECTION_STRING_CHAR, + '=' + ); + } + + return $connectionStringValues; + } + + /** + *Generates an invalid connection string exception with the detailed error + * message. + * + * @param integer $position The position of the error. + * @param string $errorString The short error formatting string. + * + * @return \RuntimeException + */ + private function _createException($position, $errorString) + { + $arguments = func_get_args(); + + // Remove first and second arguments (position and error string) + unset($arguments[0], $arguments[1]); + + // Create a short error message. + $errorString = vsprintf($errorString, $arguments); + + // Add position. + $errorString = sprintf( + Resources::ERROR_PARSING_STRING, + $errorString, + $position + ); + + // Create final error message. + $errorString = sprintf( + Resources::INVALID_CONNECTION_STRING, + $this->_argumentName, + $errorString + ); + + return new \RuntimeException($errorString); + } + + /** + * Skips whitespaces at the current position. + * + * @return void + */ + private function _skipWhiteSpaces() + { + while ($this->_pos < strlen($this->_value) + && ctype_space($this->_value[$this->_pos]) + ) { + $this->_pos++; + } + } + + /** + * Extracts the key's value. + * + * @return string + */ + private function _extractValue() + { + $value = Resources::EMPTY_STRING; + + if ($this->_pos < strlen($this->_value)) { + $ch = $this->_value[$this->_pos]; + + if ($ch == '"' || $ch == '\'') { + // Value is contained between double quotes or skipped single quotes. + $this->_pos++; + $value = $this->_extractString($ch); + } else { + $firstPos = $this->_pos; + $isFound = false; + + while ($this->_pos < strlen($this->_value) && !$isFound) { + $ch = $this->_value[$this->_pos]; + + if ($ch == ';') { + $isFound = true; + } else { + $this->_pos++; + } + } + + $value = rtrim( + substr($this->_value, $firstPos, $this->_pos - $firstPos) + ); + } + } + + return $value; + } + + /** + * Extracts key at the current position. + * + * @return string + */ + private function _extractKey() + { + $key = null; + $firstPos = $this->_pos; + $ch = $this->_value[$this->_pos]; + + if ($ch == '"' || $ch == '\'') { + $this->_pos++; + $key = $this->_extractString($ch); + } elseif ($ch == ';' || $ch == '=') { + // Key name was expected. + throw $this->_createException( + $firstPos, + Resources::ERROR_CONNECTION_STRING_MISSING_KEY + ); + } else { + while ($this->_pos < strlen($this->_value)) { + $ch = $this->_value[$this->_pos]; + + // At this point we've read the key, break. + if ($ch == '=') { + break; + } + + $this->_pos++; + } + $key = rtrim(substr($this->_value, $firstPos, $this->_pos - $firstPos)); + } + + if (strlen($key) == 0) { + // Empty key name. + throw $this->_createException( + $firstPos, + Resources::ERROR_CONNECTION_STRING_EMPTY_KEY + ); + } + + return $key; + } + + /** + * Extracts the string until the given quotation mark. + * + * @param string $quote The quotation mark terminating the string. + * + * @return string + */ + private function _extractString($quote) + { + $firstPos = $this->_pos; + + while ($this->_pos < strlen($this->_value) + && $this->_value[$this->_pos] != $quote + ) { + $this->_pos++; + } + + if ($this->_pos == strlen($this->_value)) { + // Runaway string. + throw $this->_createException( + $this->_pos, + Resources::ERROR_CONNECTION_STRING_MISSING_CHARACTER, + $quote + ); + } + + return substr($this->_value, $firstPos, $this->_pos++ - $firstPos); + } + + /** + * Skips specified operator. + * + * @param string $operatorChar The operator character. + * + * @return void + * + * @throws \RuntimeException + */ + private function _skipOperator($operatorChar) + { + if ($this->_value[$this->_pos] != $operatorChar) { + // Character was expected. + throw $this->_createException( + $this->_pos, + Resources::MISSING_CONNECTION_STRING_CHAR, + $operatorChar + ); + } + + $this->_pos++; + } +} diff --git a/src/Common/Internal/ConnectionStringSource.php b/src/Common/Internal/ConnectionStringSource.php new file mode 100644 index 0000000..468d379 --- /dev/null +++ b/src/Common/Internal/ConnectionStringSource.php @@ -0,0 +1,83 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Holder for default connection string sources used in CloudConfigurationManager. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ConnectionStringSource +{ + private static $_defaultSources; + private static $_isInitialized; + const ENVIRONMENT_SOURCE = 'environment_source'; + + /** + * Initializes the default sources. + * + * @return void + */ + private static function _init() + { + if (!self::$_isInitialized) { + self::$_defaultSources = array( + self::ENVIRONMENT_SOURCE => array(__CLASS__, 'environmentSource') + ); + self::$_isInitialized = true; + } + } + + /** + * Gets a connection string value from the system environment. + * + * @param string $key The connection string name. + * + * @return string + */ + public static function environmentSource($key) + { + Validate::canCastAsString($key, 'key'); + + return getenv($key); + } + + /** + * Gets list of default sources. + * + * @return array + */ + public static function getDefaultSources() + { + self::_init(); + return self::$_defaultSources; + } +} diff --git a/src/Common/Internal/Http/HttpCallContext.php b/src/Common/Internal/Http/HttpCallContext.php new file mode 100644 index 0000000..80273e2 --- /dev/null +++ b/src/Common/Internal/Http/HttpCallContext.php @@ -0,0 +1,430 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Http; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; + +/** + * Holds basic elements for making HTTP call. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Http + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class HttpCallContext +{ + private $_method; + private $_headers; + private $_queryParams; + private $_postParameters; + private $_uri; + private $_path; + private $_statusCodes; + private $_body; + private $_serviceOptions; + + /** + * Default constructor. + */ + public function __construct() + { + $this->_method = null; + $this->_body = null; + $this->_path = null; + $this->_uri = null; + $this->_queryParams = array(); + $this->_postParameters = array(); + $this->_statusCodes = array(); + $this->_headers = array(); + $this->_serviceOptions = new ServiceOptions(); + } + + /** + * Gets method. + * + * @return string + */ + public function getMethod() + { + return $this->_method; + } + + /** + * Sets method. + * + * @param string $method The method value. + * + * @return void + */ + public function setMethod($method) + { + Validate::canCastAsString($method, 'method'); + + $this->_method = $method; + } + + /** + * Gets headers. + * + * @return array + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Sets headers. + * + * Ignores the header if its value is empty. + * + * @param array $headers The headers value. + * + * @return void + */ + public function setHeaders(array $headers) + { + $this->_headers = array(); + foreach ($headers as $key => $value) { + $this->addHeader($key, $value); + } + } + + /** + * Gets queryParams. + * + * @return array + */ + public function getQueryParameters() + { + return $this->_queryParams; + } + + /** + * Sets queryParams. + * + * Ignores the query variable if its value is empty. + * + * @param array $queryParams The queryParams value. + * + * @return void + */ + public function setQueryParameters(array $queryParams) + { + $this->_queryParams = array(); + foreach ($queryParams as $key => $value) { + $this->addQueryParameter($key, $value); + } + } + + /** + * Gets uri. + * + * @return string + */ + public function getUri() + { + return $this->_uri; + } + + /** + * Sets uri. + * + * @param string $uri The uri value. + * + * @return void + */ + public function setUri($uri) + { + Validate::canCastAsString($uri, 'uri'); + + $this->_uri = $uri; + } + + /** + * Gets path. + * + * @return string + */ + public function getPath() + { + return $this->_path; + } + + /** + * Sets path. + * + * @param string $path The path value. + * + * @return void + */ + public function setPath($path) + { + Validate::canCastAsString($path, 'path'); + + $this->_path = $path; + } + + /** + * Gets statusCodes. + * + * @return array + */ + public function getStatusCodes() + { + return $this->_statusCodes; + } + + /** + * Sets statusCodes. + * + * @param array $statusCodes The statusCodes value. + * + * @return void + */ + public function setStatusCodes(array $statusCodes) + { + $this->_statusCodes = array(); + foreach ($statusCodes as $value) { + $this->addStatusCode($value); + } + } + + /** + * Gets body. + * + * @return string + */ + public function getBody() + { + return $this->_body; + } + + /** + * Sets body. + * + * @param string $body The body value. + * + * @return void + */ + public function setBody($body) + { + Validate::canCastAsString($body, 'body'); + + $this->_body = $body; + } + + /** + * Adds or sets header pair. + * + * @param string $name The HTTP header name. + * @param string $value The HTTP header value. + * + * @return void + */ + public function addHeader($name, $value) + { + Validate::canCastAsString($name, 'name'); + Validate::canCastAsString($value, 'value'); + + $this->_headers[$name] = $value; + } + + /** + * Adds or sets header pair. + * + * Ignores header if it's value satisfies empty(). + * + * @param string $name The HTTP header name. + * @param string $value The HTTP header value. + * + * @return void + */ + public function addOptionalHeader($name, $value) + { + Validate::canCastAsString($name, 'name'); + Validate::canCastAsString($value, 'value'); + + if (!empty($value)) { + $this->_headers[$name] = $value; + } + } + + /** + * Removes header from the HTTP request headers. + * + * @param string $name The HTTP header name. + * + * @return void + */ + public function removeHeader($name) + { + Validate::canCastAsString($name, 'name'); + Validate::notNullOrEmpty($name, 'name'); + + unset($this->_headers[$name]); + } + + /** + * Adds or sets query parameter pair. + * + * @param string $name The URI query parameter name. + * @param string $value The URI query parameter value. + * + * @return void + */ + public function addQueryParameter($name, $value) + { + Validate::canCastAsString($name, 'name'); + Validate::canCastAsString($value, 'value'); + + $this->_queryParams[$name] = $value; + } + + /** + * Gets HTTP POST parameters. + * + * @return array + */ + public function getPostParameters() + { + return $this->_postParameters; + } + + /** + * Sets HTTP POST parameters. + * + * @param array $postParameters The HTTP POST parameters. + * + * @return void + */ + public function setPostParameters(array $postParameters) + { + Validate::isArray($postParameters, 'postParameters'); + $this->_postParameters = $postParameters; + } + + /** + * Adds or sets query parameter pair. + * + * Ignores query parameter if it's value satisfies empty(). + * + * @param string $name The URI query parameter name. + * @param string $value The URI query parameter value. + * + * @return void + */ + public function addOptionalQueryParameter($name, $value) + { + Validate::canCastAsString($name, 'name'); + Validate::canCastAsString($value, 'value'); + + if (!empty($value)) { + $this->_queryParams[$name] = $value; + } + } + + /** + * Adds status code to the expected status codes. + * + * @param integer $statusCode The expected status code. + * + * @return void + */ + public function addStatusCode($statusCode) + { + Validate::isInteger($statusCode, 'statusCode'); + + $this->_statusCodes[] = $statusCode; + } + + /** + * Gets header value. + * + * @param string $name The header name. + * + * @return mixed + */ + public function getHeader($name) + { + return Utilities::tryGetValue($this->_headers, $name); + } + + /** + * Gets the saved service options + * + * @return ServiceOptions + */ + public function getServiceOptions() + { + if ($this->_serviceOptions == null) { + $this->_serviceOptions = new ServiceOptions(); + } + return $this->_serviceOptions; + } + + /** + * Sets the service options + * + * @param ServiceOptions $serviceOptions the service options to be set. + * + * @return void + */ + public function setServiceOptions(ServiceOptions $serviceOptions) + { + $this->_serviceOptions = $serviceOptions; + } + + /** + * Converts the context object to string. + * + * @return string + */ + public function __toString() + { + $headers = Resources::EMPTY_STRING; + $uri = $this->_uri; + + if ($uri[strlen($uri)-1] != '/') { + $uri = $uri.'/'; + } + + foreach ($this->_headers as $key => $value) { + $headers .= "$key: $value\n"; + } + + $str = "$this->_method $uri$this->_path HTTP/1.1\n$headers\n"; + $str .= "$this->_body"; + + return $str; + } +} diff --git a/src/Common/Internal/Http/HttpFormatter.php b/src/Common/Internal/Http/HttpFormatter.php new file mode 100644 index 0000000..bfb83df --- /dev/null +++ b/src/Common/Internal/Http/HttpFormatter.php @@ -0,0 +1,59 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Http; + +/** + * Helper class to format the http headers + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Http + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class HttpFormatter +{ + /** + * Convert a http headers array into an uniformed format for further process + * + * @param array $headers headers for format + * + * @return array + */ + public static function formatHeaders(array $headers) + { + $result = array(); + foreach ($headers as $key => $value) { + if (is_array($value) && count($value) == 1) { + $result[strtolower($key)] = $value[0]; + } else { + $result[strtolower($key)] = $value; + } + } + + return $result; + } +} diff --git a/src/Common/Internal/MetadataTrait.php b/src/Common/Internal/MetadataTrait.php new file mode 100644 index 0000000..4a432b1 --- /dev/null +++ b/src/Common/Internal/MetadataTrait.php @@ -0,0 +1,142 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Trait implementing common logic for metadata, last-modified and etag. The + * code is shared for multiple REST APIs. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait MetadataTrait +{ + private $lastModified; + private $etag; + private $metadata; + + /** + * Any operation that modifies the share or its properties or metadata + * updates the last modified time. Operations on files do not affect the + * last modified time of the share. + * + * @return \DateTime. + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets share lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + $this->lastModified = $lastModified; + } + + /** + * The entity tag for the share. If the request version is 2011-08-18 or + * newer, the ETag value will be in quotes. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets share etag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + $this->etag = $etag; + } + + /** + * Gets user defined metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->metadata; + } + + /** + * Sets user defined metadata. This metadata should be added without the + * header prefix (x-ms-meta-*). + * + * @param array $metadata user defined metadata object in array form. + * + * @return void + */ + protected function setMetadata(array $metadata) + { + $this->metadata = $metadata; + } + + /** + * Create an instance using the response headers from the API call. + * + * @param array $responseHeaders The array contains all the response headers + * + * @internal + * + * @return GetShareMetadataResult + */ + public static function createMetadataResult(array $responseHeaders) + { + $result = new static(); + $metadata = Utilities::getMetadataArray($responseHeaders); + $date = Utilities::tryGetValueInsensitive( + Resources::LAST_MODIFIED, + $responseHeaders + ); + $date = Utilities::rfc1123ToDateTime($date); + $result->setETag(Utilities::tryGetValueInsensitive( + Resources::ETAG, + $responseHeaders + )); + $result->setMetadata($metadata); + $result->setLastModified($date); + + return $result; + } +} diff --git a/src/Common/Internal/Middlewares/CommonRequestMiddleware.php b/src/Common/Internal/Middlewares/CommonRequestMiddleware.php new file mode 100644 index 0000000..bb59bf8 --- /dev/null +++ b/src/Common/Internal/Middlewares/CommonRequestMiddleware.php @@ -0,0 +1,132 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Middlewares; + +use MicrosoftAzure\Storage\Common\Middlewares\MiddlewareBase; +use MicrosoftAzure\Storage\Common\Internal\Authentication\IAuthScheme; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use Psr\Http\Message\RequestInterface; + +/** + * CommonRequestMiddleware is the middleware used to add the necessary headers + * and to sign the request with provided authentication scheme. This middleware + * is by default applied to each of the request. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CommonRequestMiddleware extends MiddlewareBase +{ + private $authenticationScheme; + private $headers; + private $msVersion; + private $userAgent; + + /** + * Creates CommonRequestMiddleware with the passed scheme and headers to + * be added. + * + * @param IAuthScheme $authenticationScheme The authentication scheme. + * @param string $storageAPIVersion Azure Storage Service API version, + * like '2016-05-31'. + * @param string $serviceSDKVersion Like '1.0.1' or '1.2.0'. + * @param array $headers The headers to be added. + */ + public function __construct( + IAuthScheme $authenticationScheme = null, + $storageAPIVersion, + $serviceSDKVersion, + array $headers = array() + ) { + $this->authenticationScheme = $authenticationScheme; + $this->msVersion = $storageAPIVersion; + $this->userAgent = self::getUserAgent($serviceSDKVersion); + $this->headers = $headers; + } + + /** + * Add the provided headers, the date, then sign the request using the + * authentication scheme, and return it. + * + * @param RequestInterface $request un-signed request. + * + * @return RequestInterface + */ + protected function onRequest(RequestInterface $request) + { + $result = $request; + + //Adding headers. + foreach ($this->headers as $key => $value) { + $headers = $result->getHeaders(); + if (!array_key_exists($key, $headers)) { + $result = $result->withHeader($key, $value); + } + } + + //rewriting version and user-agent. + $result = $result->withHeader( + Resources::X_MS_VERSION, + $this->msVersion + ); + $result = $result->withHeader( + Resources::USER_AGENT, + $this->userAgent + ); + + //Adding date. + $date = gmdate(Resources::AZURE_DATE_FORMAT, time()); + $result = $result->withHeader(Resources::DATE, $date); + + //Adding request-ID if not specified by the user. + if (!$result->hasHeader(Resources::X_MS_REQUEST_ID)) { + $result = $result->withHeader(Resources::X_MS_REQUEST_ID, \uniqid()); + } + //Sign the request if authentication scheme is not null. + $request = $this->authenticationScheme == null ? + $request : $this->authenticationScheme->signRequest($result); + return $request; + } + + /** + * Gets the user agent string used in request header. + * + * @param $serviceSDKVersion + * + * @return string + */ + private static function getUserAgent($serviceSDKVersion) + { + // e.g. User-Agent: Azure-Storage/1.0.1-1.1.1 (PHP 5.5.32)/WINNT + return 'Azure-Storage/' . $serviceSDKVersion . '-' . + Resources::COMMON_SDK_VERSION . + ' (PHP ' . PHP_VERSION . ')' . '/' . php_uname("s"); + } +} diff --git a/src/Common/Internal/Resources.php b/src/Common/Internal/Resources.php new file mode 100644 index 0000000..ece6d7d --- /dev/null +++ b/src/Common/Internal/Resources.php @@ -0,0 +1,408 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Project resources. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Resources +{ + // @codingStandardsIgnoreStart + + // Connection strings + const USE_DEVELOPMENT_STORAGE_NAME = 'UseDevelopmentStorage'; + const DEVELOPMENT_STORAGE_PROXY_URI_NAME = 'DevelopmentStorageProxyUri'; + const DEFAULT_ENDPOINTS_PROTOCOL_NAME = 'DefaultEndpointsProtocol'; + const ACCOUNT_NAME_NAME = 'AccountName'; + const ACCOUNT_KEY_NAME = 'AccountKey'; + const SAS_TOKEN_NAME = 'SharedAccessSignature'; + const BLOB_ENDPOINT_NAME = 'BlobEndpoint'; + const QUEUE_ENDPOINT_NAME = 'QueueEndpoint'; + const TABLE_ENDPOINT_NAME = 'TableEndpoint'; + const FILE_ENDPOINT_NAME = 'FileEndpoint'; + const SHARED_ACCESS_SIGNATURE_NAME = 'SharedAccessSignature'; + const DEV_STORE_NAME = 'devstoreaccount1'; + const DEV_STORE_KEY = 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='; + const BLOB_BASE_DNS_NAME = 'blob.core.windows.net'; + const QUEUE_BASE_DNS_NAME = 'queue.core.windows.net'; + const TABLE_BASE_DNS_NAME = 'table.core.windows.net'; + const FILE_BASE_DNS_NAME = 'file.core.windows.net'; + const DEV_STORE_CONNECTION_STRING = 'BlobEndpoint=127.0.0.1:10000;QueueEndpoint=127.0.0.1:10001;TableEndpoint=127.0.0.1:10002;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='; + const SUBSCRIPTION_ID_NAME = 'SubscriptionID'; + const CERTIFICATE_PATH_NAME = 'CertificatePath'; + const SECONDARY_STRING = '-secondary'; + const PRIMARY_STRING = '-primary'; + + // Messages + const INVALID_FUNCTION_NAME = 'The class %s does not have a function named %s.'; + const INVALID_TYPE_MSG = 'The provided variable should be of type: '; + const INVALID_META_MSG = 'Metadata cannot contain newline characters.'; + const AZURE_ERROR_MSG = "Fail:\nCode: %s\nValue: %s\ndetails (if any): %s."; + const NOT_IMPLEMENTED_MSG = 'This method is not implemented.'; + const NULL_OR_EMPTY_MSG = "'%s' can't be NULL or empty."; + const NULL_MSG = "'%s' can't be NULL."; + const INVALID_URL_MSG = 'Provided URL is invalid.'; + const INVALID_HT_MSG = 'The header type provided is invalid.'; + const INVALID_VERSION_MSG = 'Server does not support any known protocol versions.'; + const INVALID_EXC_OBJ_MSG = 'Exception object type should be ServiceException.'; + const INVALID_PARAM_MSG = "The provided variable '%s' should be of type '%s'"; + const INVALID_VALUE_MSG = "The provided variable '%s' has unexpected value. Reason: '%s'"; + const INVALID_STRING_LENGTH = "The provided variable '%s' should be of %s characters long"; + const INVALID_SVC_PROP_MSG = 'The provided service properties is invalid.'; + const UNKNOWN_SRILZER_MSG = 'The provided serializer type is unknown'; + const INVALID_CREATE_SERVICE_OPTIONS_MSG = 'Must provide valid location or affinity group.'; + const INVALID_UPDATE_SERVICE_OPTIONS_MSG = 'Must provide either description or label.'; + const INVALID_CONFIG_MSG = 'Config object must be of type Configuration'; + const INVALID_CONFIG_URI = "The provided URI '%s' is invalid. It has to pass the check 'filter_var(, FILTER_VALIDATE_URL)'."; + const INVALID_CONFIG_VALUE = "The provided config value '%s' does not belong to the valid values subset:\n%s"; + const INVALID_ACCOUNT_KEY_FORMAT = "The provided account key '%s' is not a valid base64 string. It has to pass the check 'base64_decode(, true)'."; + const MISSING_CONNECTION_STRING_SETTINGS = "The provided connection string '%s' does not have complete configuration settings."; + const INVALID_CONNECTION_STRING_SETTING_KEY = "The setting key '%s' is not found in the expected configuration setting keys:\n%s"; + const INVALID_CERTIFICATE_PATH = "The provided certificate path '%s' is invalid."; + const INSTANCE_TYPE_VALIDATION_MSG = 'The type of %s is %s but is expected to be %s.'; + const INVALID_MESSAGE_OBJECT_TO_SERIALIZE = 'The given object does not have required methods, so it could not be serialized.'; + const MISSING_CONNECTION_STRING_CHAR = "Missing %s character"; + const ERROR_PARSING_STRING = "'%s' at position %d."; + const INVALID_CONNECTION_STRING = "Argument '%s' is not a valid connection string: '%s'"; + const ERROR_CONNECTION_STRING_MISSING_KEY = 'Missing key name'; + const ERROR_CONNECTION_STRING_EMPTY_KEY = 'Empty key name'; + const ERROR_CONNECTION_STRING_MISSING_CHARACTER = "Missing %s character"; + const ERROR_EMPTY_SETTINGS = 'No keys were found in the connection string'; + const MISSING_LOCK_LOCATION_MSG = 'The lock location of the brokered message is missing.'; + const INVALID_SAS_TOKEN = 'The shared access signatures (SAS) provided is not valid \'%s\''; + const INVALID_SLOT = "The provided deployment slot '%s' is not valid. Only 'staging' and 'production' are accepted."; + const INVALID_DEPLOYMENT_LOCATOR_MSG = 'A slot or deployment name must be provided.'; + const INVALID_CHANGE_MODE_MSG = "The change mode must be 'Auto' or 'Manual'. Use Mode class constants for that purpose."; + const INVALID_DEPLOYMENT_STATUS_MSG = "The change mode must be 'Running' or 'Suspended'. Use DeploymentStatus class constants for that purpose."; + const ERROR_OAUTH_GET_ACCESS_TOKEN = 'Unable to get oauth access token for endpoint \'%s\', account name \'%s\''; + const ERROR_OAUTH_SERVICE_MISSING = 'OAuth service missing for account name \'%s\''; + const ERROR_METHOD_NOT_FOUND = 'Method \'%s\' not found in object class \'%s\''; + const ERROR_INVALID_DATE_STRING = 'Parameter \'%s\' is not a date formatted string \'%s\''; + const ERROR_FILE_COULD_NOT_BE_OPENED = 'Error: file with given path could not be opened or created.'; + const INVALID_PARAM_GENERAL = 'The provided parameter \'%s\' is invalid'; + const INVALID_NEGATIVE_PARAM = 'The provided parameter \'%s\' should be positive number.'; + const SIGNED_SERVICE_INVALID_VALIDATION_MSG = 'The signed service should only be a combination of the letters b(lob) q(ueue) t(able) or f(ile).'; + const SIGNED_RESOURCE_TYPE_INVALID_VALIDATION_MSG = 'The signed resource type should only be a combination of the letters s(ervice) c(container) or o(bject).'; + const STRING_NOT_WITH_GIVEN_COMBINATION = 'The string should only be a combination of the letters %s.'; + const SIGNED_PROTOCOL_INVALID_VALIDATION_MSG = 'The signed protocol is invalid: possible values are https or https,http.'; + const ERROR_RESOURCE_TYPE_NOT_SUPPORTED = 'The given resource type cannot be recognized or is not supported.'; + const ERROR_TOO_MANY_SIGNED_IDENTIFIERS = 'There can be at most 5 signed identifiers at the same time.'; + const INVALID_PERMISSION_PROVIDED = 'Invalid permission provided, the permission of resource type \'%s\' can only be of \'%s\''; + const INVALID_RESOURCE_TYPE = 'Provided resource type is invalid.'; + const ERROR_KEY_NOT_EXIST = "The key '%s' does not exist in the given array."; + const RESOURCE_RANGE_LENGTH_MUST_SET = "The start and end/length of the range must be set."; + const INVALID_ACCEPT_CONTENT_TYPE = "The given accept content type is not valid."; + const ERROR_CANNOT_PARSE_XML = "Cannot parse XML, reasons: %s"; + const INVALID_SCHEME = 'HTTP scheme can only be string \'http\' or \'https\'.'; + + // HTTP Headers + const X_MS_HEADER_PREFIX = 'x-ms-'; + const X_MS_META_HEADER_PREFIX = 'x-ms-meta-'; + const X_MS_VERSION = 'x-ms-version'; + const X_MS_DATE = 'x-ms-date'; + const X_MS_COPY_ACTION = 'x-ms-copy-action'; + const X_MS_COPY_ID = 'x-ms-copy-id'; + const X_MS_COPY_COMPLETION_TIME = 'x-ms-copy-completion-time'; + const X_MS_COPY_STATUS = 'x-ms-copy-status'; + const X_MS_COPY_STATUS_DESCRIPTION = 'x-ms-copy-status-description'; + const X_MS_COPY_SOURCE = 'x-ms-copy-source'; + const X_MS_COPY_PROGRESS = 'x-ms-copy-progress'; + const X_MS_RANGE = 'x-ms-range'; + const X_MS_RANGE_GET_CONTENT_MD5 = 'x-ms-range-get-content-md5'; + const X_MS_DELETE_SNAPSHOTS = 'x-ms-delete-snapshots'; + const X_MS_SNAPSHOT = 'x-ms-snapshot'; + const X_MS_SOURCE_IF_MODIFIED_SINCE = 'x-ms-source-if-modified-since'; + const X_MS_SOURCE_IF_UNMODIFIED_SINCE = 'x-ms-source-if-unmodified-since'; + const X_MS_SOURCE_IF_MATCH = 'x-ms-source-if-match'; + const X_MS_SOURCE_IF_NONE_MATCH = 'x-ms-source-if-none-match'; + const X_MS_SOURCE_LEASE_ID = 'x-ms-source-lease-id'; + const X_MS_CONTINUATION_NEXTTABLENAME = 'x-ms-continuation-nexttablename'; + const X_MS_CONTINUATION_NEXTPARTITIONKEY = 'x-ms-continuation-nextpartitionkey'; + const X_MS_CONTINUATION_NEXTROWKEY = 'x-ms-continuation-nextrowkey'; + const X_MS_REQUEST_ID = 'x-ms-request-id'; + const X_MS_CONTINUATION_LOCATION_MODE = 'x-ms-continuation-location-mode'; + const X_MS_TYPE = 'x-ms-type'; + const X_MS_CONTENT_LENGTH = 'x-ms-content-length'; + const X_MS_CACHE_CONTROL = 'x-ms-cache-control'; + const X_MS_CONTENT_TYPE = 'x-ms-content-type'; + const X_MS_CONTENT_MD5 = 'x-ms-content-md5'; + const X_MS_CONTENT_ENCODING = 'x-ms-content-encoding'; + const X_MS_CONTENT_LANGUAGE = 'x-ms-content-language'; + const X_MS_CONTENT_DISPOSITION = 'x-ms-content-disposition'; + const X_MS_WRITE = 'x-ms-write'; + const ETAG = 'etag'; + const LAST_MODIFIED = 'last-modified'; + const DATE = 'date'; + const AUTHENTICATION = 'authorization'; + const WRAP_AUTHORIZATION = 'WRAP access_token="%s"'; + const CONTENT_ENCODING = 'content-encoding'; + const CONTENT_LANGUAGE = 'content-language'; + const CONTENT_LENGTH = 'content-length'; + const CONTENT_LENGTH_NO_SPACE = 'contentlength'; + const CONTENT_MD5 = 'content-md5'; + const CONTENT_TYPE = 'content-type'; + const CONTENT_ID = 'content-id'; + const CONTENT_RANGE = 'content-range'; + const CACHE_CONTROL = 'cache-control'; + const CONTENT_DISPOSITION = 'content-disposition'; + const IF_MODIFIED_SINCE = 'if-modified-since'; + const IF_MATCH = 'if-match'; + const IF_NONE_MATCH = 'if-none-match'; + const IF_UNMODIFIED_SINCE = 'if-unmodified-since'; + const RANGE = 'range'; + const DATA_SERVICE_VERSION = 'dataserviceversion'; + const MAX_DATA_SERVICE_VERSION = 'maxdataserviceversion'; + const ACCEPT_HEADER = 'accept'; + const ACCEPT_CHARSET = 'accept-charset'; + const USER_AGENT = 'User-Agent'; + const PREFER = 'Prefer'; + + // HTTP Methods + const HTTP_GET = 'GET'; + const HTTP_PUT = 'PUT'; + const HTTP_POST = 'POST'; + const HTTP_HEAD = 'HEAD'; + const HTTP_DELETE = 'DELETE'; + const HTTP_MERGE = 'MERGE'; + + // Misc + const EMPTY_STRING = ''; + const SEPARATOR = ','; + const AZURE_DATE_FORMAT = 'D, d M Y H:i:s T'; + const TIMESTAMP_FORMAT = 'Y-m-d H:i:s'; + const EMULATED = 'EMULATED'; + const EMULATOR_BLOB_URI = '127.0.0.1:10000'; + const EMULATOR_QUEUE_URI = '127.0.0.1:10001'; + const EMULATOR_TABLE_URI = '127.0.0.1:10002'; + const ASTERISK = '*'; + const SERVICE_MANAGEMENT_URL = 'https://management.core.windows.net'; + const HTTP_SCHEME = 'http'; + const HTTPS_SCHEME = 'https'; + const SETTING_NAME = 'SettingName'; + const SETTING_CONSTRAINT = 'SettingConstraint'; + const DEV_STORE_URI = 'http://127.0.0.1'; + const SERVICE_URI_FORMAT = "%s://%s.%s"; + const WRAP_ENDPOINT_URI_FORMAT = "https://%s-sb.accesscontrol.windows.net/WRAPv0.9"; + const MB_IN_BYTES_1 = 1048576; + const MB_IN_BYTES_4 = 4194304; + const MB_IN_BYTES_32 = 33554432; + const MB_IN_BYTES_64 = 67108864; + const MB_IN_BYTES_128 = 134217728; + const MB_IN_BYTES_256 = 268435456; + const MB_IN_BYTES_100 = 104857600; + const GB_IN_BYTES = 1073741824; + const GB_IN_BYTES_200 = 214748364800; + const MAX_BLOB_BLOCKS = 50000; + const MAX_BLOCK_BLOB_SIZE = 5242880000000; + const RETURN_CONTENT = 'return-content'; + const NUMBER_OF_CONCURRENCY = 25;//Guzzle's default value + const DEFAULT_NUMBER_OF_RETRIES = 3; + const DEAFULT_RETRY_INTERVAL = 1000;//Milliseconds + + // Header values + const COMMON_SDK_VERSION = '1.0.0'; + const INT32_MAX = 2147483647; + const INT32_MIN = -2147483648; + + // Query parameter names + const QP_ENTRIES = 'Entries'; + const QP_PREFIX = 'Prefix'; + const QP_MAX_RESULTS = 'MaxResults'; + const QP_METADATA = 'Metadata'; + const QP_MARKER = 'Marker'; + const QP_NEXT_MARKER = 'NextMarker'; + const QP_COMP = 'comp'; + const QP_INCLUDE = 'include'; + const QP_TIMEOUT = 'timeout'; + const QP_REST_TYPE = 'restype'; + const QP_SNAPSHOT = 'snapshot'; + const QP_COPY_ID = 'copyid'; + const QP_NAME = 'Name'; + const QP_PROPERTIES = 'Properties'; + const QP_LAST_MODIFIED = 'Last-Modified'; + const QP_ETAG = 'Etag'; + const QP_QUOTA = 'Quota'; + const QP_CONTENT_LENGTH = 'Content-Length'; + + // Request body content types + const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded'; + const BINARY_FILE_TYPE = 'application/octet-stream'; + const HTTP_TYPE = 'application/http'; + const MULTIPART_MIXED_TYPE = 'multipart/mixed'; + + // Common used XML tags + const XTAG_ATTRIBUTES = '@attributes'; + const XTAG_NAMESPACE = '@namespace'; + const XTAG_LABEL = 'Label'; + const XTAG_NAME = 'Name'; + const XTAG_DESCRIPTION = 'Description'; + const XTAG_LOCATION = 'Location'; + const XTAG_AFFINITY_GROUP = 'AffinityGroup'; + const XTAG_HOSTED_SERVICES = 'HostedServices'; + const XTAG_STORAGE_SERVICES = 'StorageServices'; + const XTAG_STORAGE_SERVICE = 'StorageService'; + const XTAG_DISPLAY_NAME = 'DisplayName'; + const XTAG_SERVICE_NAME = 'ServiceName'; + const XTAG_URL = 'Url'; + const XTAG_ID = 'ID'; + const XTAG_STATUS = 'Status'; + const XTAG_HTTP_STATUS_CODE = 'HttpStatusCode'; + const XTAG_CODE = 'Code'; + const XTAG_MESSAGE = 'Message'; + const XTAG_STORAGE_SERVICE_PROPERTIES = 'StorageServiceProperties'; + const XTAG_SERVICE_ENDPOINT = 'ServiceEndpoint'; + const XTAG_ENDPOINT = 'Endpoint'; + const XTAG_ENDPOINTS = 'Endpoints'; + const XTAG_PRIMARY = 'Primary'; + const XTAG_SECONDARY = 'Secondary'; + const XTAG_KEY_TYPE = 'KeyType'; + const XTAG_STORAGE_SERVICE_KEYS = 'StorageServiceKeys'; + const XTAG_ERROR = 'Error'; + const XTAG_HOSTED_SERVICE = 'HostedService'; + const XTAG_HOSTED_SERVICE_PROPERTIES = 'HostedServiceProperties'; + const XTAG_CREATE_HOSTED_SERVICE = 'CreateHostedService'; + const XTAG_CREATE_STORAGE_SERVICE_INPUT = 'CreateStorageServiceInput'; + const XTAG_UPDATE_STORAGE_SERVICE_INPUT = 'UpdateStorageServiceInput'; + const XTAG_CREATE_AFFINITY_GROUP = 'CreateAffinityGroup'; + const XTAG_UPDATE_AFFINITY_GROUP = 'UpdateAffinityGroup'; + const XTAG_UPDATE_HOSTED_SERVICE = 'UpdateHostedService'; + const XTAG_PACKAGE_URL = 'PackageUrl'; + const XTAG_CONFIGURATION = 'Configuration'; + const XTAG_START_DEPLOYMENT = 'StartDeployment'; + const XTAG_TREAT_WARNINGS_AS_ERROR = 'TreatWarningsAsError'; + const XTAG_CREATE_DEPLOYMENT = 'CreateDeployment'; + const XTAG_DEPLOYMENT_SLOT = 'DeploymentSlot'; + const XTAG_PRIVATE_ID = 'PrivateID'; + const XTAG_ROLE_INSTANCE_LIST = 'RoleInstanceList'; + const XTAG_UPGRADE_DOMAIN_COUNT = 'UpgradeDomainCount'; + const XTAG_ROLE_LIST = 'RoleList'; + const XTAG_SDK_VERSION = 'SdkVersion'; + const XTAG_INPUT_ENDPOINT_LIST = 'InputEndpointList'; + const XTAG_LOCKED = 'Locked'; + const XTAG_ROLLBACK_ALLOWED = 'RollbackAllowed'; + const XTAG_UPGRADE_STATUS = 'UpgradeStatus'; + const XTAG_UPGRADE_TYPE = 'UpgradeType'; + const XTAG_CURRENT_UPGRADE_DOMAIN_STATE = 'CurrentUpgradeDomainState'; + const XTAG_CURRENT_UPGRADE_DOMAIN = 'CurrentUpgradeDomain'; + const XTAG_ROLE_NAME = 'RoleName'; + const XTAG_INSTANCE_NAME = 'InstanceName'; + const XTAG_INSTANCE_STATUS = 'InstanceStatus'; + const XTAG_INSTANCE_UPGRADE_DOMAIN = 'InstanceUpgradeDomain'; + const XTAG_INSTANCE_FAULT_DOMAIN = 'InstanceFaultDomain'; + const XTAG_INSTANCE_SIZE = 'InstanceSize'; + const XTAG_INSTANCE_STATE_DETAILS = 'InstanceStateDetails'; + const XTAG_INSTANCE_ERROR_CODE = 'InstanceErrorCode'; + const XTAG_OS_VERSION = 'OsVersion'; + const XTAG_ROLE_INSTANCE = 'RoleInstance'; + const XTAG_ROLE = 'Role'; + const XTAG_INPUT_ENDPOINT = 'InputEndpoint'; + const XTAG_VIP = 'Vip'; + const XTAG_PORT = 'Port'; + const XTAG_DEPLOYMENT = 'Deployment'; + const XTAG_DEPLOYMENTS = 'Deployments'; + const XTAG_REGENERATE_KEYS = 'RegenerateKeys'; + const XTAG_SWAP = 'Swap'; + const XTAG_PRODUCTION = 'Production'; + const XTAG_SOURCE_DEPLOYMENT = 'SourceDeployment'; + const XTAG_CHANGE_CONFIGURATION = 'ChangeConfiguration'; + const XTAG_MODE = 'Mode'; + const XTAG_UPDATE_DEPLOYMENT_STATUS = 'UpdateDeploymentStatus'; + const XTAG_ROLE_TO_UPGRADE = 'RoleToUpgrade'; + const XTAG_FORCE = 'Force'; + const XTAG_UPGRADE_DEPLOYMENT = 'UpgradeDeployment'; + const XTAG_UPGRADE_DOMAIN = 'UpgradeDomain'; + const XTAG_WALK_UPGRADE_DOMAIN = 'WalkUpgradeDomain'; + const XTAG_ROLLBACK_UPDATE_OR_UPGRADE = 'RollbackUpdateOrUpgrade'; + const XTAG_CONTAINER_NAME = 'ContainerName'; + const XTAG_ACCOUNT_NAME = 'AccountName'; + const XTAG_LOGGING = 'Logging'; + const XTAG_HOUR_METRICS = 'HourMetrics'; + const XTAG_MINUTE_METRICS = 'MinuteMetrics'; + const XTAG_CORS = 'Cors'; + const XTAG_CORS_RULE = 'CorsRule'; + const XTAG_ALLOWED_ORIGINS = 'AllowedOrigins'; + const XTAG_ALLOWED_METHODS = 'AllowedMethods'; + const XTAG_ALLOWED_HEADERS = 'AllowedHeaders'; + const XTAG_EXPOSED_HEADERS = 'ExposedHeaders'; + const XTAG_MAX_AGE_IN_SECONDS = 'MaxAgeInSeconds'; + const XTAG_SIGNED_IDENTIFIERS = 'SignedIdentifiers'; + const XTAG_SIGNED_IDENTIFIER = 'SignedIdentifier'; + const XTAG_ACCESS_POLICY = 'AccessPolicy'; + const XTAG_SIGNED_START = 'Start'; + const XTAG_SIGNED_EXPIRY = 'Expiry'; + const XTAG_SIGNED_PERMISSION = 'Permission'; + const XTAG_SIGNED_ID = 'Id'; + const XTAG_DEFAULT_SERVICE_VERSION = 'DefaultServiceVersion'; + const XTAG_GEO_REPLICATION = 'GeoReplication'; + const XTAG_LAST_SYNC_TIME = 'LastSyncTime'; + const XTAG_PAGE_RANGE = 'PageRange'; + const XTAG_CLEAR_RANGE = 'ClearRange'; + const XTAG_RANGE_START = 'Start'; + const XTAG_RANGE_END = 'End'; + + // PHP URL Keys + const PHP_URL_SCHEME = 'scheme'; + const PHP_URL_HOST = 'host'; + const PHP_URL_PORT = 'port'; + const PHP_URL_USER = 'user'; + const PHP_URL_PASS = 'pass'; + const PHP_URL_PATH = 'path'; + const PHP_URL_QUERY = 'query'; + const PHP_URL_FRAGMENT = 'fragment'; + + // Status Codes + const STATUS_OK = 200; + const STATUS_CREATED = 201; + const STATUS_ACCEPTED = 202; + const STATUS_NO_CONTENT = 204; + const STATUS_PARTIAL_CONTENT = 206; + const STATUS_MOVED_PERMANENTLY = 301; + + // Resource Types + const RESOURCE_TYPE_BLOB = 'b'; + const RESOURCE_TYPE_CONTAINER = 'c'; + const RESOURCE_TYPE_QUEUE = 'q'; + const RESOURCE_TYPE_TABLE = 't'; + const RESOURCE_TYPE_SHARE = 's'; + const RESOURCE_TYPE_FILE = 'f'; + + // Request Options String + const ROS_LOCATION_MODE = 'location_mode'; + const ROS_SECONDARY_URI = 'secondary_uri'; + const ROS_PRIMARY_URI = 'primary_uri'; + const ROS_DECODE_CONTENT = 'decode_content'; + const ROS_STREAM = 'stream'; + const ROS_HANDLER = 'requestHandler'; + + // @codingStandardsIgnoreEnd +} diff --git a/src/Common/Internal/RestProxy.php b/src/Common/Internal/RestProxy.php new file mode 100644 index 0000000..28c257e --- /dev/null +++ b/src/Common/Internal/RestProxy.php @@ -0,0 +1,131 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\Internal\IMiddleware; + +/** + * Base class for all REST proxies. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RestProxy +{ + /** + * @var array + */ + private $middlewares; + + /** + * @var Serialization\ISerializer + */ + protected $dataSerializer; + + /** + * Initializes new RestProxy object. + * + * @param Serialization\ISerializer $dataSerializer The data serializer. + */ + public function __construct(Serialization\ISerializer $dataSerializer = null) + { + $this->middlewares = array(); + $this->dataSerializer = $dataSerializer; + //For logging the request and responses. + // $this->middlewares[] = new HistoryMiddleware('.\\messages.log'); + } + + /** + * Gets middlewares that will be handling the request and response. + * + * @return array + */ + public function getMiddlewares() + { + return $this->middlewares; + } + + /** + * Push a new middleware into the middlewares array. The newly added + * middleware will be the most inner middleware when executed. + * + * @param callable|IMiddleware $middleware the middleware to be added. + * + * @return void + */ + public function pushMiddleware($middleware) + { + $this->middlewares[] = $middleware; + } + + /** + * Adds optional query parameter. + * + * Doesn't add the value if it satisfies empty(). + * + * @param array &$queryParameters The query parameters. + * @param string $key The query variable name. + * @param string $value The query variable value. + * + * @return void + */ + protected function addOptionalQueryParam(array &$queryParameters, $key, $value) + { + Validate::isArray($queryParameters, 'queryParameters'); + Validate::canCastAsString($key, 'key'); + Validate::canCastAsString($value, 'value'); + + if (!is_null($value) && Resources::EMPTY_STRING !== $value) { + $queryParameters[$key] = $value; + } + } + + /** + * Adds optional header. + * + * Doesn't add the value if it satisfies empty(). + * + * @param array &$headers The HTTP header parameters. + * @param string $key The HTTP header name. + * @param string $value The HTTP header value. + * + * @return void + */ + protected function addOptionalHeader(array &$headers, $key, $value) + { + Validate::isArray($headers, 'headers'); + Validate::canCastAsString($key, 'key'); + Validate::canCastAsString($value, 'value'); + + if (!is_null($value) && Resources::EMPTY_STRING !== $value) { + $headers[$key] = $value; + } + } +} diff --git a/src/Common/Internal/Serialization/ISerializer.php b/src/Common/Internal/Serialization/ISerializer.php new file mode 100644 index 0000000..9999536 --- /dev/null +++ b/src/Common/Internal/Serialization/ISerializer.php @@ -0,0 +1,70 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Serialization; + +/** + * The serialization interface. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Serialization + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +interface ISerializer +{ + /** + * Serialize an object into a XML. + * + * @param Object $targetObject The target object to be serialized. + * @param string $rootName The name of the root. + * + * @return string + */ + public static function objectSerialize($targetObject, $rootName); + + /** + * Serializes given array. The array indices must be string to use them as + * as element name. + * + * @param array $array The object to serialize represented in array. + * @param array $properties The used properties in the serialization process. + * + * @return string + */ + public function serialize(array $array, array $properties = null); + + + /** + * Unserializes given serialized string. + * + * @param string $serialized The serialized object in string representation. + * + * @return array + */ + public function unserialize($serialized); +} diff --git a/src/Common/Internal/Serialization/JsonSerializer.php b/src/Common/Internal/Serialization/JsonSerializer.php new file mode 100644 index 0000000..6083f7c --- /dev/null +++ b/src/Common/Internal/Serialization/JsonSerializer.php @@ -0,0 +1,96 @@ + + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Serialization; + +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Perform JSON serialization / deserialization + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Serialization + * @author Azure Storage PHP SDK + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class JsonSerializer implements ISerializer +{ + /** + * Serialize an object with specified root element name. + * + * @param object $targetObject The target object. + * @param string $rootName The name of the root element. + * + * @return string + */ + public static function objectSerialize($targetObject, $rootName) + { + Validate::notNull($targetObject, 'targetObject'); + Validate::canCastAsString($rootName, 'rootName'); + + $contianer = new \stdClass(); + + $contianer->$rootName = $targetObject; + + return json_encode($contianer); + } + + /** + * Serializes given array. The array indices must be string to use them as + * as element name. + * + * @param array $array The object to serialize represented in array. + * @param array $properties The used properties in the serialization process. + * + * @return string + */ + public function serialize(array $array = null, array $properties = null) + { + Validate::isArray($array, 'array'); + + return json_encode($array); + } + + /** + * Unserializes given serialized string to array. + * + * @param string $serialized The serialized object in string representation. + * + * @return array + */ + public function unserialize($serialized) + { + Validate::canCastAsString($serialized, 'serialized'); + + $json = json_decode($serialized); + if ($json && !is_array($json)) { + return get_object_vars($json); + } else { + return $json; + } + } +} diff --git a/src/Common/Internal/Serialization/MessageSerializer.php b/src/Common/Internal/Serialization/MessageSerializer.php new file mode 100644 index 0000000..2df7d7e --- /dev/null +++ b/src/Common/Internal/Serialization/MessageSerializer.php @@ -0,0 +1,178 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Serialization; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use GuzzleHttp\Exception\RequestException; + +/** + * Provides functionality to serialize a message to a string. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Serialization + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class MessageSerializer +{ + /** + * Serialize a message to a string. The message object must be either a type + * of \Exception, or have following methods implemented. + * getHeaders() + * getProtocolVersion() + * (getUri() && getMethod()) || (getStatusCode() && getReasonPhrase()) + * + * @param object $message The message to be serialized. + * + * @return string + */ + public static function objectSerialize($targetObject) + { + //if the object is of exception type, serialize it using the methods + //without checking the methods. + if ($targetObject instanceof RequestException) { + return self::serializeRequestException($targetObject); + } elseif ($targetObject instanceof \Exception) { + return self::serializeException($targetObject); + } + + Validate::methodExists($targetObject, 'getHeaders', 'targetObject'); + Validate::methodExists($targetObject, 'getProtocolVersion', 'targetObject'); + + // Serialize according to the implemented method. + if (method_exists($targetObject, 'getUri') && + method_exists($targetObject, 'getMethod')) { + return self::serializeRequest($targetObject); + } elseif (method_exists($targetObject, 'getStatusCode') && + method_exists($targetObject, 'getReasonPhrase')) { + return self::serializeResponse($targetObject); + } else { + throw new \InvalidArgumentException( + Resources::INVALID_MESSAGE_OBJECT_TO_SERIALIZE + ); + } + } + + /** + * Serialize the request type that implemented the following methods: + * getHeaders() + * getProtocolVersion() + * getUri() + * getMethod() + * + * @param object $request The request to be serialized. + * + * @return string + */ + private static function serializeRequest($request) + { + $headers = $request->getHeaders(); + $version = $request->getProtocolVersion(); + $uri = $request->getUri(); + $method = $request->getMethod(); + + $resultString = "Request:\n"; + $resultString .= "URI: {$uri}\nHTTP Version: {$version}\nMethod: {$method}\n"; + $resultString .= self::serializeHeaders($headers); + + return $resultString; + } + + /** + * Serialize the response type that implemented the following methods: + * getHeaders() + * getProtocolVersion() + * getStatusCode() + * getReasonPhrase() + * + * @param object $response The response to be serialized + * + * @return string + */ + private static function serializeResponse($response) + { + $headers = $response->getHeaders(); + $version = $response->getProtocolVersion(); + $status = $response->getStatusCode(); + $reason = $response->getReasonPhrase(); + + $resultString = "Response:\n"; + $resultString .= "Status Code: {$status}\nReason: {$reason}\n"; + $resultString .= "HTTP Version: {$version}\n"; + $resultString .= self::serializeHeaders($headers); + + return $resultString; + } + + /** + * Serialize the message headers. + * + * @param array $headers The headers to be serialized. + * + * @return string + */ + private static function serializeHeaders(array $headers) + { + $resultString = "Headers:\n"; + foreach ($headers as $key => $value) { + $resultString .= sprintf("%s: %s\n", $key, $value[0]); + } + + return $resultString; + } + + /** + * Serialize the request exception. + * + * @param RequestException $e the request exception to be serialized. + * + * @return string + */ + private static function serializeRequestException(RequestException $e) + { + $resultString = sprintf("Reason:\n%s\n", $e); + if ($e->hasResponse()) { + $resultString .= self::serializeResponse($e->getResponse()); + } + + return $resultString; + } + + /** + * Serialize the general exception + * + * @param \Exception $e general exception to be serialized. + * + * @return string + */ + private static function serializeException(\Exception $e) + { + return sprintf("Reason:\n%s\n", $e); + } +} diff --git a/src/Common/Internal/Serialization/XmlSerializer.php b/src/Common/Internal/Serialization/XmlSerializer.php new file mode 100644 index 0000000..df0e23d --- /dev/null +++ b/src/Common/Internal/Serialization/XmlSerializer.php @@ -0,0 +1,245 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Serialization; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Short description + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Serialization + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class XmlSerializer implements ISerializer +{ + const STANDALONE = 'standalone'; + const ROOT_NAME = 'rootName'; + const DEFAULT_TAG = 'defaultTag'; + + /** + * Converts a SimpleXML object to an Array recursively + * ensuring all sub-elements are arrays as well. + * + * @param string $sxml The SimpleXML object. + * @param array $arr The array into which to store results. + * + * @return array + */ + private function sxml2arr($sxml, array $arr = null) + { + foreach ((array) $sxml as $key => $value) { + if (is_object($value) || (is_array($value))) { + $arr[$key] = $this->sxml2arr($value); + } else { + $arr[$key] = $value; + } + } + + return $arr; + } + + /** + * Takes an array and produces XML based on it. + * + * @param XMLWriter $xmlw XMLWriter object that was previously instanted + * and is used for creating the XML. + * @param array $data Array to be converted to XML. + * @param string $defaultTag Default XML tag to be used if none specified. + * + * @return void + */ + private function arr2xml(\XMLWriter $xmlw, array $data, $defaultTag = null) + { + foreach ($data as $key => $value) { + if ($key === Resources::XTAG_ATTRIBUTES) { + foreach ($value as $attributeName => $attributeValue) { + $xmlw->writeAttribute($attributeName, $attributeValue); + } + } elseif (is_array($value)) { + if (!is_int($key)) { + if ($key != Resources::EMPTY_STRING) { + $xmlw->startElement($key); + } else { + $xmlw->startElement($defaultTag); + } + } + + $this->arr2xml($xmlw, $value); + + if (!is_int($key)) { + $xmlw->endElement(); + } + } else { + $xmlw->writeElement($key, $value); + } + } + } + + /** + * Gets the attributes of a specified object if get attributes + * method is exposed. + * + * @param object $targetObject The target object. + * @param array $methodArray The array of method of the target object. + * + * @return mixed + */ + private static function getInstanceAttributes($targetObject, array $methodArray) + { + foreach ($methodArray as $method) { + if ($method->name == 'getAttributes') { + $classProperty = $method->invoke($targetObject); + return $classProperty; + } + } + return null; + } + + /** + * Serialize an object with specified root element name. + * + * @param object $targetObject The target object. + * @param string $rootName The name of the root element. + * + * @return string + */ + public static function objectSerialize($targetObject, $rootName) + { + Validate::notNull($targetObject, 'targetObject'); + Validate::canCastAsString($rootName, 'rootName'); + $xmlWriter = new \XmlWriter(); + $xmlWriter->openMemory(); + $xmlWriter->setIndent(true); + $reflectionClass = new \ReflectionClass($targetObject); + $methodArray = $reflectionClass->getMethods(); + $attributes = self::getInstanceAttributes( + $targetObject, + $methodArray + ); + + $xmlWriter->startElement($rootName); + if (!is_null($attributes)) { + foreach (array_keys($attributes) as $attributeKey) { + $xmlWriter->writeAttribute( + $attributeKey, + $attributes[$attributeKey] + ); + } + } + + foreach ($methodArray as $method) { + if ((strpos($method->name, 'get') === 0) + && $method->isPublic() + && ($method->name != 'getAttributes') + ) { + $variableName = substr($method->name, 3); + $variableValue = $method->invoke($targetObject); + if (!empty($variableValue)) { + if (gettype($variableValue) === 'object') { + $xmlWriter->writeRaw( + XmlSerializer::objectSerialize( + $variableValue, + $variableName + ) + ); + } else { + $xmlWriter->writeElement($variableName, $variableValue); + } + } + } + } + $xmlWriter->endElement(); + return $xmlWriter->outputMemory(true); + } + + /** + * Serializes given array. The array indices must be string to use them as + * as element name. + * + * @param array $array The object to serialize represented in array. + * @param array $properties The used properties in the serialization process. + * + * @return string + */ + public function serialize(array $array, array $properties = null) + { + $xmlVersion = '1.0'; + $xmlEncoding = 'UTF-8'; + $standalone = Utilities::tryGetValue($properties, self::STANDALONE); + $defaultTag = Utilities::tryGetValue($properties, self::DEFAULT_TAG); + $rootName = Utilities::tryGetValue($properties, self::ROOT_NAME); + $docNamespace = Utilities::tryGetValue( + $array, + Resources::XTAG_NAMESPACE, + null + ); + + if (!is_array($array)) { + return false; + } + + $xmlw = new \XmlWriter(); + $xmlw->openMemory(); + $xmlw->setIndent(true); + $xmlw->startDocument($xmlVersion, $xmlEncoding, $standalone); + + if (is_null($docNamespace)) { + $xmlw->startElement($rootName); + } else { + foreach ($docNamespace as $uri => $prefix) { + $xmlw->startElementNS($prefix, $rootName, $uri); + break; + } + } + + unset($array[Resources::XTAG_NAMESPACE]); + self::arr2xml($xmlw, $array, $defaultTag); + + $xmlw->endElement(); + + return $xmlw->outputMemory(true); + } + + /** + * Unserializes given serialized string. + * + * @param string $serialized The serialized object in string representation. + * + * @return array + */ + public function unserialize($serialized) + { + $sxml = new \SimpleXMLElement($serialized); + + return $this->sxml2arr($sxml); + } +} diff --git a/src/Common/Internal/ServiceRestProxy.php b/src/Common/Internal/ServiceRestProxy.php new file mode 100644 index 0000000..ff06bc7 --- /dev/null +++ b/src/Common/Internal/ServiceRestProxy.php @@ -0,0 +1,635 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\Exceptions\ServiceException; +use MicrosoftAzure\Storage\Common\Internal\RetryMiddlewareFactory; +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; +use MicrosoftAzure\Storage\Common\Internal\Http\HttpCallContext; +use MicrosoftAzure\Storage\Common\Internal\Middlewares\MiddlewareBase; +use MicrosoftAzure\Storage\Common\Middlewares\MiddlewareStack; +use MicrosoftAzure\Storage\Common\LocationMode; +use GuzzleHttp\Promise\EachPromise; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7; +use Psr\Http\Message\ResponseInterface; + +/** + * Base class for all services rest proxies. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ServiceRestProxy extends RestProxy +{ + private $accountName; + private $psrPrimaryUri; + private $psrSecondaryUri; + private $options; + private $client; + + /** + * Initializes new ServiceRestProxy object. + * + * @param string $primaryUri The storage account + * primary uri. + * @param string $secondaryUri The storage account + * secondary uri. + * @param string $accountName The name of the account. + * @param array $options Array of options for + * the service + */ + public function __construct( + $primaryUri, + $secondaryUri, + $accountName, + array $options = [] + ) { + $primaryUri = Utilities::appendDelimiter($primaryUri, '/'); + $secondaryUri = Utilities::appendDelimiter($secondaryUri, '/'); + + $dataSerializer = new XmlSerializer(); + parent::__construct($dataSerializer); + + $this->accountName = $accountName; + $this->psrPrimaryUri = new Uri($primaryUri); + $this->psrSecondaryUri = new Uri($secondaryUri); + $this->options = array_merge(array('http' => array()), $options); + $this->client = self::createClient($this->options['http']); + } + + /** + * Create a Guzzle client for future usage. + * + * @param array $options Optional parameters for the client. + * + * @return Client + */ + private static function createClient(array $options) + { + $verify = true; + //Disable SSL if proxy has been set, and set the proxy in the client. + $proxy = getenv('HTTP_PROXY'); + // For testing with Fiddler + // $proxy = 'localhost:8888'; + // $verify = false; + if (!empty($proxy)) { + $options['proxy'] = $proxy; + } + + return (new \GuzzleHttp\Client( + array_merge( + $options, + array( + "defaults" => array( + "allow_redirects" => true, + "exceptions" => true, + "decode_content" => true, + ), + 'cookies' => true, + 'verify' => $verify, + ) + ) + )); + } + + /** + * Gets the account name. + * + * @return string + */ + public function getAccountName() + { + return $this->accountName; + } + + /** + * Create a middleware stack with given middleware. + * + * @param ServiceOptions $serviceOptions The options user passed in. + * + * @return MiddlewareStack + */ + protected function createMiddlewareStack(ServiceOptions $serviceOptions) + { + //If handler stack is not defined by the user, create a default + //middleware stack. + $stack = null; + if (array_key_exists('stack', $this->options['http'])) { + $stack = $this->options['http']['stack']; + } elseif ($serviceOptions->getMiddlewareStack() != null) { + $stack = $serviceOptions->getMiddlewareStack(); + } else { + $stack = new MiddlewareStack(); + } + + //Push all the middlewares specified in the $serviceOptions to the + //handlerstack. + if ($serviceOptions->getMiddlewares() != array()) { + foreach ($serviceOptions->getMiddlewares() as $middleware) { + $stack->push($middleware); + } + } + + //Push all the middlewares specified in the $options to the + //handlerstack. + if (array_key_exists('middlewares', $this->options)) { + foreach ($this->options['middlewares'] as $middleware) { + $stack->push($middleware); + } + } + + //Push all the middlewares specified in $this->middlewares to the + //handlerstack. + foreach ($this->getMiddlewares() as $middleware) { + $stack->push($middleware); + } + + return $stack; + } + + /** + * Send the requests concurrently. Number of concurrency can be modified + * by inserting a new key/value pair with the key 'number_of_concurrency' + * into the $requestOptions of $serviceOptions. Return only the promise. + * + * @param callable $generator the generator function to generate + * request upon fulfillment + * @param int $statusCode The expected status code for each of the + * request generated by generator. + * @param ServiceOptions $options The service options for the concurrent + * requests. + * + * @return \GuzzleHttp\Promise\Promise|\GuzzleHttp\Promise\PromiseInterface + */ + protected function sendConcurrentAsync( + callable $generator, + $statusCode, + ServiceOptions $options + ) { + $client = $this->client; + $middlewareStack = $this->createMiddlewareStack($options); + + $sendAsync = function ($request, $options) use ($client) { + if ($request->getMethod() == 'HEAD') { + $options['decode_content'] = false; + } + return $client->sendAsync($request, $options); + }; + + $handler = $middlewareStack->apply($sendAsync); + + $requestOptions = $this->generateRequestOptions($options, $handler); + + $promises = \call_user_func( + function () use ( + $generator, + $handler, + $requestOptions + ) { + while (is_callable($generator) && ($request = $generator())) { + yield \call_user_func($handler, $request, $requestOptions); + } + } + ); + + $eachPromise = new EachPromise($promises, [ + 'concurrency' => $options->getNumberOfConcurrency(), + 'fulfilled' => function ($response, $index) use ($statusCode) { + //the promise is fulfilled, evaluate the response + self::throwIfError( + $response, + $statusCode + ); + }, + 'rejected' => function ($reason, $index) { + //Still rejected even if the retry logic has been applied. + //Throwing exception. + throw $reason; + } + ]); + + return $eachPromise->promise(); + } + + + /** + * Create the request to be sent. + * + * @param string $method The method of the HTTP request + * @param array $headers The header field of the request + * @param array $queryParams The query parameter of the request + * @param array $postParameters The HTTP POST parameters + * @param string $path URL path + * @param string $body Request body + * + * @return \GuzzleHttp\Psr7\Request + */ + protected function createRequest( + $method, + array $headers, + array $queryParams, + array $postParameters, + $path, + $locationMode, + $body = Resources::EMPTY_STRING + ) { + if ($locationMode == LocationMode::SECONDARY_ONLY || + $locationMode == LocationMode::SECONDARY_THEN_PRIMARY) { + $uri = $this->psrSecondaryUri; + } else { + $uri = $this->psrPrimaryUri; + } + + //Append the path, not replacing it. + if ($path != null) { + $exPath = $uri->getPath(); + if ($exPath != '') { + //Remove the duplicated slash in the path. + if ($path != '' && $path[0] == '/') { + $path = $exPath . substr($path, 1); + } else { + $path = $exPath . $path; + } + } + $uri = $uri->withPath($path); + } + + // add query parameters into headers + if ($queryParams != null) { + $queryString = Psr7\build_query($queryParams); + $uri = $uri->withQuery($queryString); + } + + // add post parameters into bodys + $actualBody = null; + if (empty($body)) { + if (empty($headers['content-type'])) { + $headers['content-type'] = 'application/x-www-form-urlencoded'; + $actualBody = Psr7\build_query($postParameters); + } + } else { + $actualBody = $body; + } + + $request = new Request( + $method, + $uri, + $headers, + $actualBody + ); + + //add content-length to header + $bodySize = $request->getBody()->getSize(); + if ($bodySize > 0) { + $request = $request->withHeader('content-length', $bodySize); + } + return $request; + } + + /** + * Create promise of sending HTTP request with the specified parameters. + * + * @param string $method HTTP method used in the request + * @param array $headers HTTP headers. + * @param array $queryParams URL query parameters. + * @param array $postParameters The HTTP POST parameters. + * @param string $path URL path + * @param array|int $expected Expected Status Codes. + * @param string $body Request body + * @param ServiceOptions $serviceOptions Service options + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + protected function sendAsync( + $method, + array $headers, + array $queryParams, + array $postParameters, + $path, + $expected = Resources::STATUS_OK, + $body = Resources::EMPTY_STRING, + ServiceOptions $serviceOptions = null + ) { + if ($serviceOptions == null) { + $serviceOptions = new ServiceOptions(); + } + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_TIMEOUT, + $serviceOptions->getTimeout() + ); + + $request = $this->createRequest( + $method, + $headers, + $queryParams, + $postParameters, + $path, + $serviceOptions->getLocationMode(), + $body + ); + + $client = $this->client; + + $middlewareStack = $this->createMiddlewareStack($serviceOptions); + + $sendAsync = function ($request, $options) use ($client) { + return $client->sendAsync($request, $options); + }; + + $handler = $middlewareStack->apply($sendAsync); + + $requestOptions = + $this->generateRequestOptions($serviceOptions, $handler); + + if ($request->getMethod() == 'HEAD') { + $requestOptions[Resources::ROS_DECODE_CONTENT] = false; + } + + $promise = \call_user_func($handler, $request, $requestOptions); + + return $promise->then( + function ($response) use ($expected, $requestOptions) { + self::throwIfError( + $response, + $expected + ); + + return self::addLocationHeaderToResponse( + $response, + $requestOptions[Resources::ROS_LOCATION_MODE] + ); + }, + function ($reason) use ($expected) { + if (!($reason instanceof RequestException)) { + throw $reason; + } + $response = $reason->getResponse(); + if ($response != null) { + self::throwIfError( + $response, + $expected + ); + } else { + //if could not get response but promise rejected, throw reason. + throw $reason; + } + return $response; + } + ); + } + + /** + * Generate the request options using the given service options and stored + * information. + * + * @param ServiceOptions $serviceOptions The service options used to + * generate request options. + * @param callable $handler The handler used to send the + * request. + * @return array + */ + protected function generateRequestOptions( + ServiceOptions $serviceOptions, + callable $handler + ) { + $result = array(); + $result[Resources::ROS_LOCATION_MODE] = $serviceOptions->getLocationMode(); + $result[Resources::ROS_STREAM] = $serviceOptions->getIsStreaming(); + $result[Resources::ROS_DECODE_CONTENT] = $serviceOptions->getDecodeContent(); + $result[Resources::ROS_HANDLER] = $handler; + $result[Resources::ROS_SECONDARY_URI] = $this->getPsrSecondaryUri(); + $result[Resources::ROS_PRIMARY_URI] = $this->getPsrPrimaryUri(); + + return $result; + } + + /** + * Sends the context. + * + * @param HttpCallContext $context The context of the request. + * @return \GuzzleHttp\Psr7\Response + */ + protected function sendContext(HttpCallContext $context) + { + return $this->sendContextAsync($context)->wait(); + } + + /** + * Creates the promise to send the context. + * + * @param HttpCallContext $context The context of the request. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + protected function sendContextAsync(HttpCallContext $context) + { + return $this->sendAsync( + $context->getMethod(), + $context->getHeaders(), + $context->getQueryParameters(), + $context->getPostParameters(), + $context->getPath(), + $context->getStatusCodes(), + $context->getBody(), + $context->getServiceOptions() + ); + } + + /** + * Throws ServiceException if the received status code is not expected. + * + * @param ResponseInterface $response The response received + * @param array|int $expected The expected status codes. + * + * @return void + * + * @throws ServiceException + */ + public static function throwIfError(ResponseInterface $response, $expected) + { + $expectedStatusCodes = is_array($expected) ? $expected : array($expected); + + if (!in_array($response->getStatusCode(), $expectedStatusCodes)) { + throw new ServiceException($response); + } + } + + /** + * Adds HTTP POST parameter to the specified + * + * @param array $postParameters An array of HTTP POST parameters. + * @param string $key The key of a HTTP POST parameter. + * @param string $value the value of a HTTP POST parameter. + * + * @return array + */ + public function addPostParameter( + array $postParameters, + $key, + $value + ) { + Validate::isArray($postParameters, 'postParameters'); + $postParameters[$key] = $value; + return $postParameters; + } + + /** + * Groups set of values into one value separated with Resources::SEPARATOR + * + * @param array $values array of values to be grouped. + * + * @return string + */ + public static function groupQueryValues(array $values) + { + Validate::isArray($values, 'values'); + $joined = Resources::EMPTY_STRING; + + sort($values); + + foreach ($values as $value) { + if (!is_null($value) && !empty($value)) { + $joined .= $value . Resources::SEPARATOR; + } + } + + return trim($joined, Resources::SEPARATOR); + } + + /** + * Adds metadata elements to headers array + * + * @param array $headers HTTP request headers + * @param array $metadata user specified metadata + * + * @return array + */ + protected function addMetadataHeaders(array $headers, array $metadata = null) + { + Utilities::validateMetadata($metadata); + + $metadata = $this->generateMetadataHeaders($metadata); + $headers = array_merge($headers, $metadata); + + return $headers; + } + + /** + * Generates metadata headers by prefixing each element with 'x-ms-meta'. + * + * @param array $metadata user defined metadata. + * + * @return array + */ + public function generateMetadataHeaders(array $metadata = null) + { + $metadataHeaders = array(); + + if (is_array($metadata) && !is_null($metadata)) { + foreach ($metadata as $key => $value) { + $headerName = Resources::X_MS_META_HEADER_PREFIX; + if (strpos($value, "\r") !== false + || strpos($value, "\n") !== false + ) { + throw new \InvalidArgumentException(Resources::INVALID_META_MSG); + } + + // Metadata name is case-presrved and case insensitive + $headerName .= $key; + $metadataHeaders[$headerName] = $value; + } + } + + return $metadataHeaders; + } + + /** + * Get the primary URI in PSR form. + * + * @return Uri + */ + public function getPsrPrimaryUri() + { + return $this->psrPrimaryUri; + } + + /** + * Get the secondary URI in PSR form. + * + * @return Uri + */ + public function getPsrSecondaryUri() + { + return $this->psrSecondaryUri; + } + + /** + * Adds the header that indicates the location mode to the response header. + * + * @return ResponseInterface + */ + private static function addLocationHeaderToResponse( + ResponseInterface $response, + $locationMode + ) { + //If the response already has this header, return itself. + if ($response->hasHeader(Resources::X_MS_CONTINUATION_LOCATION_MODE)) { + return $response; + } + //Otherwise, add the header that indicates the endpoint to be used if + //continuation token is used for subsequent request. Notice that if the + //response does not have location header set at the moment, it means + //that the user have not set a retry middleware. + if ($locationMode == LocationMode::PRIMARY_THEN_SECONDARY) { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + LocationMode::PRIMARY_ONLY + ); + } elseif ($locationMode == LocationMode::SECONDARY_THEN_PRIMARY) { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + LocationMode::SECONDARY_ONLY + ); + } elseif ($locationMode == LocationMode::SECONDARY_ONLY || + $locationMode == LocationMode::PRIMARY_ONLY) { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + $locationMode + ); + } + return $response; + } +} diff --git a/src/Common/Internal/ServiceRestTrait.php b/src/Common/Internal/ServiceRestTrait.php new file mode 100644 index 0000000..fb03776 --- /dev/null +++ b/src/Common/Internal/ServiceRestTrait.php @@ -0,0 +1,259 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\LocationMode; +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; +use MicrosoftAzure\Storage\Common\Models\ServiceProperties; +use MicrosoftAzure\Storage\Common\Models\GetServicePropertiesResult; +use MicrosoftAzure\Storage\Common\Models\GetServiceStatsResult; + +/** + * Trait implementing common REST API for all the services, including the + * following: + * Get/Set Service Properties + * Get service stats + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait ServiceRestTrait +{ + /** + * Gets the properties of the service. + * + * @param ServiceOptions $options The optional parameters. + * + * @return \MicrosoftAzure\Storage\Common\Models\GetServicePropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452239.aspx + */ + public function getServiceProperties( + ServiceOptions $options = null + ) { + return $this->getServicePropertiesAsync($options)->wait(); + } + + /** + * Creates promise to get the properties of the service. + * + * @param ServiceOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452239.aspx + */ + public function getServicePropertiesAsync( + ServiceOptions $options = null + ) { + $method = Resources::HTTP_GET; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = Resources::EMPTY_STRING; + + if (is_null($options)) { + $options = new ServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'service' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'properties' + ); + + $dataSerializer = $this->dataSerializer; + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) use ($dataSerializer) { + $parsed = $dataSerializer->unserialize($response->getBody()); + return GetServicePropertiesResult::create($parsed); + }, null); + } + + /** + * Sets the properties of the service. + * + * It's recommended to use getServiceProperties, alter the returned object and + * then use setServiceProperties with this altered object. + * + * @param ServiceProperties $serviceProperties The service properties. + * @param ServiceOptions $options The optional parameters. + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452235.aspx + */ + public function setServiceProperties( + ServiceProperties $serviceProperties, + ServiceOptions $options = null + ) { + $this->setServicePropertiesAsync($serviceProperties, $options)->wait(); + } + + /** + * Creates the promise to set the properties of the service. + * + * It's recommended to use getServiceProperties, alter the returned object and + * then use setServiceProperties with this altered object. + * + * @param ServiceProperties $serviceProperties The service properties. + * @param ServiceOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452235.aspx + */ + public function setServicePropertiesAsync( + ServiceProperties $serviceProperties, + ServiceOptions $options = null + ) { + Validate::isTrue( + $serviceProperties instanceof ServiceProperties, + Resources::INVALID_SVC_PROP_MSG + ); + + $method = Resources::HTTP_PUT; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = Resources::EMPTY_STRING; + $body = $serviceProperties->toXml($this->dataSerializer); + + if (is_null($options)) { + $options = new ServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'service' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'properties' + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_TYPE, + Resources::URL_ENCODED_CONTENT_TYPE + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_ACCEPTED, + $body, + $options + ); + } + + /** + * Retrieves statistics related to replication for the service. The operation + * will only be sent to secondary location endpoint. + * + * @param ServiceOptions|null $options The options this operation sends with. + * + * @return GetServiceStatsResult + */ + public function getServiceStats(ServiceOptions $options = null) + { + return $this->getServiceStatsAsync($options)->wait(); + } + + /** + * Creates promise that retrieves statistics related to replication for the + * service. The operation will only be sent to secondary location endpoint. + * + * @param ServiceOptions|null $options The options this operation sends with. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function getServiceStatsAsync(ServiceOptions $options = null) + { + $method = Resources::HTTP_GET; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = Resources::EMPTY_STRING; + + if (is_null($options)) { + $options = new ServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'service' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'stats' + ); + + $dataSerializer = $this->dataSerializer; + + $options->setLocationMode(LocationMode::SECONDARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) use ($dataSerializer) { + $parsed = $dataSerializer->unserialize($response->getBody()); + return GetServiceStatsResult::create($parsed); + }, null); + } +} diff --git a/src/Common/Internal/ServiceSettings.php b/src/Common/Internal/ServiceSettings.php new file mode 100644 index 0000000..82767e2 --- /dev/null +++ b/src/Common/Internal/ServiceSettings.php @@ -0,0 +1,288 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Base class for all REST services settings. + * + * Derived classes must implement the following members: + * 1- $isInitialized: A static property that indicates whether the class's static + * members have been initialized. + * 2- init(): A protected static method that initializes static members. + * 3- $validSettingKeys: A static property that contains valid setting keys for this + * service. + * 4- createFromConnectionString($connectionString): A public static function that + * takes a connection string and returns the created settings object. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +abstract class ServiceSettings +{ + /** + * Throws an exception if the connection string format does not match any of the + * available formats. + * + * @param string $connectionString The invalid formatted connection string. + * + * @return void + * + * @throws \RuntimeException + */ + protected static function noMatch($connectionString) + { + throw new \RuntimeException( + sprintf(Resources::MISSING_CONNECTION_STRING_SETTINGS, $connectionString) + ); + } + + /** + * Parses the connection string and then validate that the parsed keys belong to + * the $validSettingKeys + * + * @param string $connectionString The user provided connection string. + * + * @return array The tokenized connection string keys. + * + * @throws \RuntimeException + */ + protected static function parseAndValidateKeys($connectionString) + { + // Initialize the static values if they are not initialized yet. + if (!static::$isInitialized) { + static::init(); + static::$isInitialized = true; + } + + $tokenizedSettings = ConnectionStringParser::parseConnectionString( + 'connectionString', + $connectionString + ); + + // Assure that all given keys are valid. + foreach ($tokenizedSettings as $key => $value) { + if (!Utilities::inArrayInsensitive($key, static::$validSettingKeys)) { + throw new \RuntimeException( + sprintf( + Resources::INVALID_CONNECTION_STRING_SETTING_KEY, + $key, + implode("\n", static::$validSettingKeys) + ) + ); + } + } + + return $tokenizedSettings; + } + + /** + * Creates an anonymous function that acts as predicate. + * + * @param array $requirements The array of conditions to satisfy. + * @param boolean $isRequired Either these conditions are all required or all + * optional. + * @param boolean $atLeastOne Indicates that at least one requirement must + * succeed. + * + * @return callable + */ + protected static function getValidator( + array $requirements, + $isRequired, + $atLeastOne + ) { + // @codingStandardsIgnoreStart + + return function ($userSettings) use ($requirements, $isRequired, $atLeastOne) { + $oneFound = false; + $result = array_change_key_case($userSettings); + foreach ($requirements as $requirement) { + $settingName = strtolower($requirement[Resources::SETTING_NAME]); + + // Check if the setting name exists in the provided user settings. + if (array_key_exists($settingName, $result)) { + // Check if the provided user setting value is valid. + $validationFunc = $requirement[Resources::SETTING_CONSTRAINT]; + $isValid = $validationFunc($result[$settingName]); + + if ($isValid) { + // Remove the setting as indicator for successful validation. + unset($result[$settingName]); + $oneFound = true; + } + } else { + // If required then fail because the setting does not exist + if ($isRequired) { + return null; + } + } + } + + if ($atLeastOne) { + // At least one requirement must succeed, otherwise fail. + return $oneFound ? $result : null; + } else { + return $result; + } + }; + + // @codingStandardsIgnoreEnd + } + + /** + * Creates at lease one succeed predicate for the provided list of requirements. + * + * @return callable + */ + protected static function atLeastOne() + { + $allSettings = func_get_args(); + return self::getValidator($allSettings, false, true); + } + + /** + * Creates an optional predicate for the provided list of requirements. + * + * @return callable + */ + protected static function optional() + { + $optionalSettings = func_get_args(); + return self::getValidator($optionalSettings, false, false); + } + + /** + * Creates an required predicate for the provided list of requirements. + * + * @return callable + */ + protected static function allRequired() + { + $requiredSettings = func_get_args(); + return self::getValidator($requiredSettings, true, false); + } + + /** + * Creates a setting value condition using the passed predicate. + * + * @param string $name The setting key name. + * @param callable $predicate The setting value predicate. + * + * @return array + */ + protected static function settingWithFunc($name, $predicate) + { + $requirement = array(); + $requirement[Resources::SETTING_NAME] = $name; + $requirement[Resources::SETTING_CONSTRAINT] = $predicate; + + return $requirement; + } + + /** + * Creates a setting value condition that validates it is one of the + * passed valid values. + * + * @param string $name The setting key name. + * + * @return array + */ + protected static function setting($name) + { + $validValues = func_get_args(); + + // Remove $name argument. + unset($validValues[0]); + + $validValuesCount = func_num_args(); + + $predicate = function ($settingValue) use ($validValuesCount, $validValues) { + if (empty($validValues)) { + // No restrictions, succeed, + return true; + } + + // Check to find if the $settingValue is valid or not. The index must + // start from 1 as unset deletes the value but does not update the array + // indecies. + for ($index = 1; $index < $validValuesCount; $index++) { + if ($settingValue == $validValues[$index]) { + // $settingValue is found in valid values set, succeed. + return true; + } + } + + throw new \RuntimeException( + sprintf( + Resources::INVALID_CONFIG_VALUE, + $settingValue, + implode("\n", $validValues) + ) + ); + + // $settingValue is missing in valid values set, fail. + return false; + }; + + return self::settingWithFunc($name, $predicate); + } + + /** + * Tests to see if a given list of settings matches a set of filters exactly. + * + * @param array $settings The settings to check. + * + * @return boolean If any filter returns null, false. If there are any settings + * left over after all filters are processed, false. Otherwise true. + */ + protected static function matchedSpecification(array $settings) + { + $constraints = func_get_args(); + + // Remove first element which corresponds to $settings + unset($constraints[0]); + + foreach ($constraints as $constraint) { + $remainingSettings = $constraint($settings); + + if (is_null($remainingSettings)) { + return false; + } else { + $settings = $remainingSettings; + } + } + + if (empty($settings)) { + return true; + } + + return false; + } +} diff --git a/src/Common/Internal/StorageServiceSettings.php b/src/Common/Internal/StorageServiceSettings.php new file mode 100644 index 0000000..ed136b6 --- /dev/null +++ b/src/Common/Internal/StorageServiceSettings.php @@ -0,0 +1,604 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Represents the settings used to sign and access a request against the storage + * service. For more information about storage service connection strings check this + * page: http://msdn.microsoft.com/en-us/library/ee758697 + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class StorageServiceSettings extends ServiceSettings +{ + private $name; + private $key; + private $sas; + private $blobEndpointUri; + private $queueEndpointUri; + private $tableEndpointUri; + private $fileEndpointUri; + private $blobSecondaryEndpointUri; + private $queueSecondaryEndpointUri; + private $tableSecondaryEndpointUri; + private $fileSecondaryEndpointUri; + + private static $devStoreAccount; + private static $useDevelopmentStorageSetting; + private static $developmentStorageProxyUriSetting; + private static $defaultEndpointsProtocolSetting; + private static $accountNameSetting; + private static $accountKeySetting; + private static $sasTokenSetting; + private static $blobEndpointSetting; + private static $queueEndpointSetting; + private static $tableEndpointSetting; + private static $fileEndpointSetting; + + /** + * If initialized or not + * @internal + */ + protected static $isInitialized = false; + + /** + * Valid setting keys + * @internal + */ + protected static $validSettingKeys = array(); + + /** + * Initializes static members of the class. + * + * @return void + */ + protected static function init() + { + self::$useDevelopmentStorageSetting = self::setting( + Resources::USE_DEVELOPMENT_STORAGE_NAME, + 'true' + ); + + self::$developmentStorageProxyUriSetting = self::settingWithFunc( + Resources::DEVELOPMENT_STORAGE_PROXY_URI_NAME, + Validate::getIsValidUri() + ); + + self::$defaultEndpointsProtocolSetting = self::setting( + Resources::DEFAULT_ENDPOINTS_PROTOCOL_NAME, + 'http', + 'https' + ); + + self::$accountNameSetting = self::setting(Resources::ACCOUNT_NAME_NAME); + + self::$accountKeySetting = self::settingWithFunc( + Resources::ACCOUNT_KEY_NAME, + // base64_decode will return false if the $key is not in base64 format. + function ($key) { + $isValidBase64String = base64_decode($key, true); + if ($isValidBase64String) { + return true; + } else { + throw new \RuntimeException( + sprintf(Resources::INVALID_ACCOUNT_KEY_FORMAT, $key) + ); + } + } + ); + + self::$sasTokenSetting = self::setting(Resources::SAS_TOKEN_NAME); + + self::$blobEndpointSetting = self::settingWithFunc( + Resources::BLOB_ENDPOINT_NAME, + Validate::getIsValidUri() + ); + + self::$queueEndpointSetting = self::settingWithFunc( + Resources::QUEUE_ENDPOINT_NAME, + Validate::getIsValidUri() + ); + + self::$tableEndpointSetting = self::settingWithFunc( + Resources::TABLE_ENDPOINT_NAME, + Validate::getIsValidUri() + ); + + self::$fileEndpointSetting = self::settingWithFunc( + Resources::FILE_ENDPOINT_NAME, + Validate::getIsValidUri() + ); + + self::$validSettingKeys[] = Resources::USE_DEVELOPMENT_STORAGE_NAME; + self::$validSettingKeys[] = Resources::DEVELOPMENT_STORAGE_PROXY_URI_NAME; + self::$validSettingKeys[] = Resources::DEFAULT_ENDPOINTS_PROTOCOL_NAME; + self::$validSettingKeys[] = Resources::ACCOUNT_NAME_NAME; + self::$validSettingKeys[] = Resources::ACCOUNT_KEY_NAME; + self::$validSettingKeys[] = Resources::SAS_TOKEN_NAME; + self::$validSettingKeys[] = Resources::BLOB_ENDPOINT_NAME; + self::$validSettingKeys[] = Resources::QUEUE_ENDPOINT_NAME; + self::$validSettingKeys[] = Resources::TABLE_ENDPOINT_NAME; + self::$validSettingKeys[] = Resources::FILE_ENDPOINT_NAME; + } + + /** + * Creates new storage service settings instance. + * + * @param string $name The storage service name. + * @param string $key The storage service key. + * @param string $blobEndpointUri The storage service blob + * endpoint. + * @param string $queueEndpointUri The storage service queue + * endpoint. + * @param string $tableEndpointUri The storage service table + * endpoint. + * @param string $fileEndpointUri The storage service file + * endpoint. + * @param string $blobSecondaryEndpointUri The storage service secondary + * blob endpoint. + * @param string $queueSecondaryEndpointUri The storage service secondary + * queue endpoint. + * @param string $tableSecondaryEndpointUri The storage service secondary + * table endpoint. + * @param string $fileSecondaryEndpointUri The storage service secondary + * file endpoint. + * @param string $sas The storage service SAS token. + */ + public function __construct( + $name, + $key, + $blobEndpointUri, + $queueEndpointUri, + $tableEndpointUri, + $fileEndpointUri, + $blobSecondaryEndpointUri = null, + $queueSecondaryEndpointUri = null, + $tableSecondaryEndpointUri = null, + $fileSecondaryEndpointUri = null, + $sas = null + ) { + $this->name = $name; + $this->key = $key; + $this->sas = $sas; + $this->blobEndpointUri = $blobEndpointUri; + $this->queueEndpointUri = $queueEndpointUri; + $this->tableEndpointUri = $tableEndpointUri; + $this->fileEndpointUri = $fileEndpointUri; + $this->blobSecondaryEndpointUri = $blobSecondaryEndpointUri; + $this->queueSecondaryEndpointUri = $queueSecondaryEndpointUri; + $this->tableSecondaryEndpointUri = $tableSecondaryEndpointUri; + $this->fileSecondaryEndpointUri = $fileSecondaryEndpointUri; + } + + /** + * Returns a StorageServiceSettings with development storage credentials using + * the specified proxy Uri. + * + * @param string $proxyUri The proxy endpoint to use. + * + * @return StorageServiceSettings + */ + private static function getDevelopmentStorageAccount($proxyUri) + { + if (is_null($proxyUri)) { + return self::developmentStorageAccount(); + } + + $scheme = parse_url($proxyUri, PHP_URL_SCHEME); + $host = parse_url($proxyUri, PHP_URL_HOST); + $prefix = $scheme . "://" . $host; + + return new StorageServiceSettings( + Resources::DEV_STORE_NAME, + Resources::DEV_STORE_KEY, + $prefix . ':10000/devstoreaccount1/', + $prefix . ':10001/devstoreaccount1/', + $prefix . ':10002/devstoreaccount1/', + null + ); + } + + /** + * Gets a StorageServiceSettings object that references the development storage + * account. + * + * @return StorageServiceSettings + */ + public static function developmentStorageAccount() + { + if (is_null(self::$devStoreAccount)) { + self::$devStoreAccount = self::getDevelopmentStorageAccount( + Resources::DEV_STORE_URI + ); + } + + return self::$devStoreAccount; + } + + /** + * Gets the default service endpoint using the specified protocol and account + * name. + * + * @param string $scheme The scheme of the service end point. + * @param string $accountName The account name of the service. + * @param string $dns The service DNS. + * @param bool $isSecondary If generating secondary endpoint. + * + * @return string + */ + private static function getServiceEndpoint( + $scheme, + $accountName, + $dns, + $isSecondary = false + ) { + if ($isSecondary) { + $accountName .= Resources::SECONDARY_STRING; + } + return sprintf( + Resources::SERVICE_URI_FORMAT, + $scheme, + $accountName, + $dns + ); + } + + /** + * Creates StorageServiceSettings object given endpoints uri. + * + * @param array $settings The service settings. + * @param string $blobEndpointUri The blob endpoint uri. + * @param string $queueEndpointUri The queue endpoint uri. + * @param string $tableEndpointUri The table endpoint uri. + * @param string $fileEndpointUri The file endpoint uri. + * @param string $blobSecondaryEndpointUri The blob secondary endpoint uri. + * @param string $queueSecondaryEndpointUri The queue secondary endpoint uri. + * @param string $tableSecondaryEndpointUri The table secondary endpoint uri. + * @param string $fileSecondaryEndpointUri The file secondary endpoint uri. + * + * @return StorageServiceSettings + */ + private static function createStorageServiceSettings( + array $settings, + $blobEndpointUri = null, + $queueEndpointUri = null, + $tableEndpointUri = null, + $fileEndpointUri = null, + $blobSecondaryEndpointUri = null, + $queueSecondaryEndpointUri = null, + $tableSecondaryEndpointUri = null, + $fileSecondaryEndpointUri = null + ) { + $blobEndpointUri = Utilities::tryGetValueInsensitive( + Resources::BLOB_ENDPOINT_NAME, + $settings, + $blobEndpointUri + ); + $queueEndpointUri = Utilities::tryGetValueInsensitive( + Resources::QUEUE_ENDPOINT_NAME, + $settings, + $queueEndpointUri + ); + $tableEndpointUri = Utilities::tryGetValueInsensitive( + Resources::TABLE_ENDPOINT_NAME, + $settings, + $tableEndpointUri + ); + $fileEndpointUri = Utilities::tryGetValueInsensitive( + Resources::FILE_ENDPOINT_NAME, + $settings, + $fileEndpointUri + ); + $accountName = Utilities::tryGetValueInsensitive( + Resources::ACCOUNT_NAME_NAME, + $settings + ); + $accountKey = Utilities::tryGetValueInsensitive( + Resources::ACCOUNT_KEY_NAME, + $settings + ); + $sasToken = Utilities::tryGetValueInsensitive( + Resources::SAS_TOKEN_NAME, + $settings + ); + + return new StorageServiceSettings( + $accountName, + $accountKey, + $blobEndpointUri, + $queueEndpointUri, + $tableEndpointUri, + $fileEndpointUri, + $blobSecondaryEndpointUri, + $queueSecondaryEndpointUri, + $tableSecondaryEndpointUri, + $fileSecondaryEndpointUri, + $sasToken + ); + } + + /** + * Creates a StorageServiceSettings object from the given connection string. + * + * @param string $connectionString The storage settings connection string. + * + * @return StorageServiceSettings + */ + public static function createFromConnectionString($connectionString) + { + $tokenizedSettings = self::parseAndValidateKeys($connectionString); + + // Devstore case + $matchedSpecs = self::matchedSpecification( + $tokenizedSettings, + self::allRequired(self::$useDevelopmentStorageSetting), + self::optional(self::$developmentStorageProxyUriSetting) + ); + if ($matchedSpecs) { + $proxyUri = Utilities::tryGetValueInsensitive( + Resources::DEVELOPMENT_STORAGE_PROXY_URI_NAME, + $tokenizedSettings + ); + + return self::getDevelopmentStorageAccount($proxyUri); + } + + // Automatic case + $matchedSpecs = self::matchedSpecification( + $tokenizedSettings, + self::allRequired( + self::$defaultEndpointsProtocolSetting, + self::$accountNameSetting, + self::$accountKeySetting + ), + self::optional( + self::$blobEndpointSetting, + self::$queueEndpointSetting, + self::$tableEndpointSetting, + self::$fileEndpointSetting + ) + ); + if ($matchedSpecs) { + $scheme = Utilities::tryGetValueInsensitive( + Resources::DEFAULT_ENDPOINTS_PROTOCOL_NAME, + $tokenizedSettings + ); + $accountName = Utilities::tryGetValueInsensitive( + Resources::ACCOUNT_NAME_NAME, + $tokenizedSettings + ); + return self::createStorageServiceSettings( + $tokenizedSettings, + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::BLOB_BASE_DNS_NAME + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::QUEUE_BASE_DNS_NAME + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::TABLE_BASE_DNS_NAME + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::FILE_BASE_DNS_NAME + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::BLOB_BASE_DNS_NAME, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::QUEUE_BASE_DNS_NAME, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::TABLE_BASE_DNS_NAME, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::FILE_BASE_DNS_NAME, + true + ) + ); + } + + // Explicit case for AccountName/AccountKey combination + $matchedSpecs = self::matchedSpecification( + $tokenizedSettings, + self::atLeastOne( + self::$blobEndpointSetting, + self::$queueEndpointSetting, + self::$tableEndpointSetting, + self::$fileEndpointSetting + ), + self::allRequired( + self::$accountNameSetting, + self::$accountKeySetting + ) + ); + if ($matchedSpecs) { + return self::createStorageServiceSettings($tokenizedSettings); + } + + // Explicit case for SAS token + $matchedSpecs = self::matchedSpecification( + $tokenizedSettings, + self::atLeastOne( + self::$blobEndpointSetting, + self::$queueEndpointSetting, + self::$tableEndpointSetting, + self::$fileEndpointSetting + ), + self::allRequired( + self::$sasTokenSetting + ) + ); + if ($matchedSpecs) { + return self::createStorageServiceSettings($tokenizedSettings); + } + + self::noMatch($connectionString); + } + + /** + * Gets storage service name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Gets storage service key. + * + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * Checks if there is a SAS token. + * + * @return boolean + */ + public function hasSasToken() + { + return !empty($this->sas); + } + + /** + * Gets storage service SAS token. + * + * @return string + */ + public function getSasToken() + { + return $this->sas; + } + + /** + * Gets storage service blob endpoint uri. + * + * @return string + */ + public function getBlobEndpointUri() + { + return $this->blobEndpointUri; + } + + /** + * Gets storage service queue endpoint uri. + * + * @return string + */ + public function getQueueEndpointUri() + { + return $this->queueEndpointUri; + } + + /** + * Gets storage service table endpoint uri. + * + * @return string + */ + public function getTableEndpointUri() + { + return $this->tableEndpointUri; + } + + /** + * Gets storage service file endpoint uri. + * + * @return string + */ + public function getFileEndpointUri() + { + return $this->fileEndpointUri; + } + + /** + * Gets storage service secondary blob endpoint uri. + * + * @return string + */ + public function getBlobSecondaryEndpointUri() + { + return $this->blobSecondaryEndpointUri; + } + + /** + * Gets storage service secondary queue endpoint uri. + * + * @return string + */ + public function getQueueSecondaryEndpointUri() + { + return $this->queueSecondaryEndpointUri; + } + + /** + * Gets storage service secondary table endpoint uri. + * + * @return string + */ + public function getTableSecondaryEndpointUri() + { + return $this->tableSecondaryEndpointUri; + } + + /** + * Gets storage service secondary file endpoint uri. + * + * @return string + */ + public function getFileSecondaryEndpointUri() + { + return $this->fileSecondaryEndpointUri; + } +} diff --git a/src/Common/Internal/Utilities.php b/src/Common/Internal/Utilities.php new file mode 100644 index 0000000..f24d3d5 --- /dev/null +++ b/src/Common/Internal/Utilities.php @@ -0,0 +1,907 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use GuzzleHttp\Psr7\Stream; + +/** + * Utilities for the project + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Utilities +{ + /** + * Returns the specified value of the $key passed from $array and in case that + * this $key doesn't exist, the default value is returned. + * + * @param array $array The array to be used. + * @param mixed $key The array key. + * @param mixed $default The value to return if $key is not found in $array. + * + * @return mixed + */ + public static function tryGetValue($array, $key, $default = null) + { + return (!is_null($array)) && is_array($array) && array_key_exists($key, $array) + ? $array[$key] + : $default; + } + + /** + * Adds a url scheme if there is no scheme. Return null if input URL is null. + * + * @param string $url The URL. + * @param string $scheme The scheme. By default HTTP + * + * @return string + */ + public static function tryAddUrlScheme($url, $scheme = 'http') + { + if ($url == null) { + return $url; + } + + $urlScheme = parse_url($url, PHP_URL_SCHEME); + + if (empty($urlScheme)) { + $url = "$scheme://" . $url; + } + + return $url; + } + + /** + * Parse storage account name from an endpoint url. + * + * @param string $url The endpoint $url + * + * @return string + */ + public static function tryParseAccountNameFromUrl($url) + { + $host = parse_url($url, PHP_URL_HOST); + + // first token of the url host is account name + return explode('.', $host)[0]; + } + + /** + * Parse secondary endpoint url string from a primary endpoint url. + * + * Return null if the primary endpoint url is invalid. + * + * @param string $uri The primary endpoint url string. + * + * @return null|string + */ + public static function tryGetSecondaryEndpointFromPrimaryEndpoint($uri) + { + $splitTokens = explode('.', $uri); + if (count($splitTokens) > 0 && $splitTokens[0] != '') { + $schemaAccountToken = $splitTokens[0]; + $schemaAccountSplitTokens = explode('/', $schemaAccountToken); + if (count($schemaAccountSplitTokens) > 0 && + $schemaAccountSplitTokens[0] != '') { + $accountName = $schemaAccountSplitTokens[ + count($schemaAccountSplitTokens) - 1 + ]; + $schemaAccountSplitTokens[count($schemaAccountSplitTokens) - 1] = + $accountName . Resources::SECONDARY_STRING; + + $splitTokens[0] = implode('/', $schemaAccountSplitTokens); + $secondaryUri = implode('.', $splitTokens); + return $secondaryUri; + } + } + return null; + } + + /** + * tries to get nested array with index name $key from $array. + * + * Returns empty array object if the value is NULL. + * + * @param string $key The index name. + * @param array $array The array object. + * + * @return array + */ + public static function tryGetArray($key, array $array) + { + return Utilities::getArray(Utilities::tryGetValue($array, $key)); + } + + /** + * Adds the given key/value pair into array if the value doesn't satisfy empty(). + * + * This function just validates that the given $array is actually array. If it's + * NULL the function treats it as array. + * + * @param string $key The key. + * @param string $value The value. + * @param array &$array The array. If NULL will be used as array. + * + * @return void + */ + public static function addIfNotEmpty($key, $value, array &$array) + { + if (!is_null($array)) { + Validate::isArray($array, 'array'); + } + + if (!empty($value)) { + $array[$key] = $value; + } + } + + /** + * Returns the specified value of the key chain passed from $array and in case + * that key chain doesn't exist, null is returned. + * + * @param array $array Array to be used. + * + * @return mixed + */ + public static function tryGetKeysChainValue(array $array) + { + $arguments = func_get_args(); + $numArguments = func_num_args(); + + $currentArray = $array; + for ($i = 1; $i < $numArguments; $i++) { + if (is_array($currentArray)) { + if (array_key_exists($arguments[$i], $currentArray)) { + $currentArray = $currentArray[$arguments[$i]]; + } else { + return null; + } + } else { + return null; + } + } + + return $currentArray; + } + + /** + * Checks if the passed $string starts with $prefix + * + * @param string $string word to seaech in + * @param string $prefix prefix to be matched + * @param boolean $ignoreCase true to ignore case during the comparison; + * otherwise, false + * + * @return boolean + */ + public static function startsWith($string, $prefix, $ignoreCase = false) + { + if ($ignoreCase) { + $string = strtolower($string); + $prefix = strtolower($prefix); + } + return ($prefix == substr($string, 0, strlen($prefix))); + } + + /** + * Returns grouped items from passed $var + * + * @param array $var item to group + * + * @return array + */ + public static function getArray(array $var) + { + if (is_null($var) || empty($var)) { + return array(); + } + + foreach ($var as $value) { + if ((gettype($value) == 'object') + && (get_class($value) == 'SimpleXMLElement') + ) { + return (array) $var; + } elseif (!is_array($value)) { + return array($var); + } + } + + return $var; + } + + /** + * Unserializes the passed $xml into array. + * + * @param string $xml XML to be parsed. + * + * @return array + */ + public static function unserialize($xml) + { + $sxml = new \SimpleXMLElement($xml); + + return self::_sxml2arr($sxml); + } + + /** + * Converts a SimpleXML object to an Array recursively + * ensuring all sub-elements are arrays as well. + * + * @param string $sxml SimpleXML object + * @param array $arr Array into which to store results + * + * @return array + */ + private static function _sxml2arr($sxml, array $arr = null) + { + foreach ((array) $sxml as $key => $value) { + if (is_object($value) || (is_array($value))) { + $arr[$key] = self::_sxml2arr($value); + } else { + $arr[$key] = $value; + } + } + + return $arr; + } + + /** + * Serializes given array into xml. The array indices must be string to use + * them as XML tags. + * + * @param array $array object to serialize represented in array. + * @param string $rootName name of the XML root element. + * @param string $defaultTag default tag for non-tagged elements. + * @param string $standalone adds 'standalone' header tag, values 'yes'/'no' + * + * @return string + */ + public static function serialize( + array $array, + $rootName, + $defaultTag = null, + $standalone = null + ) { + $xmlVersion = '1.0'; + $xmlEncoding = 'UTF-8'; + + if (!is_array($array)) { + return false; + } + + $xmlw = new \XmlWriter(); + $xmlw->openMemory(); + $xmlw->startDocument($xmlVersion, $xmlEncoding, $standalone); + + $xmlw->startElement($rootName); + + self::_arr2xml($xmlw, $array, $defaultTag); + + $xmlw->endElement(); + + return $xmlw->outputMemory(true); + } + + /** + * Takes an array and produces XML based on it. + * + * @param XMLWriter $xmlw XMLWriter object that was previously instanted + * and is used for creating the XML. + * @param array $data Array to be converted to XML + * @param string $defaultTag Default XML tag to be used if none specified. + * + * @return void + */ + private static function _arr2xml( + \XMLWriter $xmlw, + array $data, + $defaultTag = null + ) { + foreach ($data as $key => $value) { + if (strcmp($key, '@attributes') == 0) { + foreach ($value as $attributeName => $attributeValue) { + $xmlw->writeAttribute($attributeName, $attributeValue); + } + } elseif (is_array($value)) { + if (!is_int($key)) { + if ($key != Resources::EMPTY_STRING) { + $xmlw->startElement($key); + } else { + $xmlw->startElement($defaultTag); + } + } + + self::_arr2xml($xmlw, $value); + + if (!is_int($key)) { + $xmlw->endElement(); + } + continue; + } else { + $xmlw->writeElement($key, $value); + } + } + } + + /** + * Converts string into boolean value. + * + * @param string $obj boolean value in string format. + * @param bool $skipNull If $skipNull is set, will return NULL directly + * when $obj is NULL. + * + * @return bool + */ + public static function toBoolean($obj, $skipNull = false) + { + if ($skipNull && is_null($obj)) { + return null; + } + + return filter_var($obj, FILTER_VALIDATE_BOOLEAN); + } + + /** + * Converts string into boolean value. + * + * @param bool $obj boolean value to convert. + * + * @return string + */ + public static function booleanToString($obj) + { + return $obj ? 'true' : 'false'; + } + + /** + * Converts a given date string into \DateTime object + * + * @param string $date windows azure date ins string representation. + * + * @return \DateTime + */ + public static function rfc1123ToDateTime($date) + { + $timeZone = new \DateTimeZone('GMT'); + $format = Resources::AZURE_DATE_FORMAT; + + return \DateTime::createFromFormat($format, $date, $timeZone); + } + + /** + * Generate ISO 8601 compliant date string in UTC time zone + * + * @param \DateTimeInterface $date The date value to convert + * + * @return string + */ + public static function isoDate(\DateTimeInterface $date) + { + $date = clone $date; + $date = $date->setTimezone(new \DateTimeZone('UTC')); + + return str_replace('+00:00', 'Z', $date->format('c')); + } + + /** + * Converts a DateTime object into an Edm.DaeTime value in UTC timezone, + * represented as a string. + * + * @param mixed $value The datetime value. + * + * @return string + */ + public static function convertToEdmDateTime($value) + { + if (empty($value)) { + return $value; + } + + if (is_string($value)) { + $value = self::convertToDateTime($value); + } + + Validate::isDate($value); + + $cloned = clone $value; + $cloned->setTimezone(new \DateTimeZone('UTC')); + return str_replace('+0000', 'Z', $cloned->format("Y-m-d\TH:i:s.u0O")); + } + + /** + * Converts a string to a \DateTime object. Returns false on failure. + * + * @param string $value The string value to parse. + * + * @return \DateTime + */ + public static function convertToDateTime($value) + { + if ($value instanceof \DateTime) { + return $value; + } + + if (substr($value, -1) == 'Z') { + $value = substr($value, 0, strlen($value) - 1); + } + + return new \DateTime($value, new \DateTimeZone('UTC')); + } + + /** + * Converts string to stream handle. + * + * @param string $string The string contents. + * + * @return resource + */ + public static function stringToStream($string) + { + return fopen('data://text/plain,' . urlencode($string), 'rb'); + } + + /** + * Sorts an array based on given keys order. + * + * @param array $array The array to sort. + * @param array $order The keys order array. + * + * @return array + */ + public static function orderArray(array $array, array $order) + { + $ordered = array(); + + foreach ($order as $key) { + if (array_key_exists($key, $array)) { + $ordered[$key] = $array[$key]; + } + } + + return $ordered; + } + + /** + * Checks if a value exists in an array. The comparison is done in a case + * insensitive manner. + * + * @param string $needle The searched value. + * @param array $haystack The array. + * + * @return boolean + */ + public static function inArrayInsensitive($needle, array $haystack) + { + return in_array(strtolower($needle), array_map('strtolower', $haystack)); + } + + /** + * Checks if the given key exists in the array. The comparison is done in a case + * insensitive manner. + * + * @param string $key The value to check. + * @param array $search The array with keys to check. + * + * @return boolean + */ + public static function arrayKeyExistsInsensitive($key, array $search) + { + return array_key_exists(strtolower($key), array_change_key_case($search)); + } + + /** + * Returns the specified value of the $key passed from $array and in case that + * this $key doesn't exist, the default value is returned. The key matching is + * done in a case insensitive manner. + * + * @param string $key The array key. + * @param array $haystack The array to be used. + * @param mixed $default The value to return if $key is not found in $array. + * + * @return mixed + */ + public static function tryGetValueInsensitive($key, $haystack, $default = null) + { + $array = array_change_key_case($haystack); + return Utilities::tryGetValue($array, strtolower($key), $default); + } + + /** + * Returns a string representation of a version 4 GUID, which uses random + * numbers.There are 6 reserved bits, and the GUIDs have this format: + * xxxxxxxx-xxxx-4xxx-[8|9|a|b]xxx-xxxxxxxxxxxx + * where 'x' is a hexadecimal digit, 0-9a-f. + * + * See http://tools.ietf.org/html/rfc4122 for more information. + * + * Note: This function is available on all platforms, while the + * com_create_guid() is only available for Windows. + * + * @return string A new GUID. + */ + public static function getGuid() + { + // @codingStandardsIgnoreStart + + return sprintf( + '%04x%04x-%04x-%04x-%02x%02x-%04x%04x%04x', + mt_rand(0, 65535), + mt_rand(0, 65535), // 32 bits for "time_low" + mt_rand(0, 65535), // 16 bits for "time_mid" + mt_rand(0, 4096) + 16384, // 16 bits for "time_hi_and_version", with + // the most significant 4 bits being 0100 + // to indicate randomly generated version + mt_rand(0, 64) + 128, // 8 bits for "clock_seq_hi", with + // the most significant 2 bits being 10, + // required by version 4 GUIDs. + mt_rand(0, 256), // 8 bits for "clock_seq_low" + mt_rand(0, 65535), // 16 bits for "node 0" and "node 1" + mt_rand(0, 65535), // 16 bits for "node 2" and "node 3" + mt_rand(0, 65535) // 16 bits for "node 4" and "node 5" + ); + + // @codingStandardsIgnoreEnd + } + + /** + * Creates a list of objects of type $class from the provided array using static + * create method. + * + * @param array $parsed The object in array representation + * @param string $class The class name. Must have static method create. + * + * @return array + */ + public static function createInstanceList(array $parsed, $class) + { + $list = array(); + + foreach ($parsed as $value) { + $list[] = $class::create($value); + } + + return $list; + } + + /** + * Takes a string and return if it ends with the specified character/string. + * + * @param string $haystack The string to search in. + * @param string $needle postfix to match. + * @param boolean $ignoreCase Set true to ignore case during the comparison; + * otherwise, false + * + * @return boolean + */ + public static function endsWith($haystack, $needle, $ignoreCase = false) + { + if ($ignoreCase) { + $haystack = strtolower($haystack); + $needle = strtolower($needle); + } + $length = strlen($needle); + if ($length == 0) { + return true; + } + + return (substr($haystack, -$length) === $needle); + } + + /** + * Get id from entity object or string. + * If entity is object than validate type and return $entity->$method() + * If entity is string than return this string + * + * @param object|string $entity Entity with id property + * @param string $type Entity type to validate + * @param string $method Methods that gets id (getId by default) + * + * @return string + */ + public static function getEntityId($entity, $type, $method = 'getId') + { + if (is_string($entity)) { + return $entity; + } else { + Validate::isA($entity, $type, 'entity'); + Validate::methodExists($entity, $method, $type); + + return $entity->$method(); + } + } + + /** + * Generate a pseudo-random string of bytes using a cryptographically strong + * algorithm. + * + * @param int $length Length of the string in bytes + * + * @return string|boolean Generated string of bytes on success, or FALSE on + * failure. + */ + public static function generateCryptoKey($length) + { + return openssl_random_pseudo_bytes($length); + } + + /** + * Convert base 256 number to decimal number. + * + * @param string $number Base 256 number + * + * @return string Decimal number + */ + public static function base256ToDec($number) + { + Validate::canCastAsString($number, 'number'); + + $result = 0; + $base = 1; + for ($i = strlen($number) - 1; $i >= 0; $i--) { + $result = bcadd($result, bcmul(ord($number[$i]), $base)); + $base = bcmul($base, 256); + } + + return $result; + } + + /** + * To evaluate if the stream is larger than a certain size. To restore + * the stream, it has to be seekable, so will return true if the stream + * is not seekable. + * @param Stream $stream The stream to be evaluated. + * @param int $size The size if the string is larger than. + * + * @return boolean true if the stream is larger than the given size. + */ + public static function isStreamLargerThanSizeOrNotSeekable(Stream $stream, $size) + { + Validate::isInteger($size, 'size'); + Validate::isTrue( + $stream instanceof Stream, + sprintf(Resources::INVALID_PARAM_MSG, 'stream', 'Guzzle\Stream') + ); + $result = true; + if ($stream->isSeekable()) { + $position = $stream->tell(); + try { + $stream->seek($size); + } catch (\RuntimeException $e) { + $pos = strpos( + $e->getMessage(), + 'to seek to stream position ' + ); + if ($pos == null) { + throw $e; + } + $result = false; + } + if ($stream->eof()) { + $result = false; + } elseif ($stream->read(1) == '') { + $result = false; + } + $stream->seek($position); + } + return $result; + } + + /** + * Gets metadata array by parsing them from given headers. + * + * @param array $headers HTTP headers containing metadata elements. + * + * @return array + */ + public static function getMetadataArray(array $headers) + { + $metadata = array(); + foreach ($headers as $key => $value) { + $isMetadataHeader = Utilities::startsWith( + strtolower($key), + Resources::X_MS_META_HEADER_PREFIX + ); + + if ($isMetadataHeader) { + // Metadata name is case-presrved and case insensitive + $MetadataName = str_ireplace( + Resources::X_MS_META_HEADER_PREFIX, + Resources::EMPTY_STRING, + $key + ); + $metadata[$MetadataName] = $value; + } + } + + return $metadata; + } + + /** + * Validates the provided metadata array. + * + * @param array $metadata The metadata array. + * + * @return void + */ + public static function validateMetadata(array $metadata = null) + { + if (!is_null($metadata)) { + Validate::isArray($metadata, 'metadata'); + } else { + $metadata = array(); + } + + foreach ($metadata as $key => $value) { + Validate::canCastAsString($key, 'metadata key'); + Validate::canCastAsString($value, 'metadata value'); + } + } + + /** + * Append the content to file. + * @param string $path The file to append to. + * @param string $content The content to append. + * + * @return void + */ + public static function appendToFile($path, $content) + { + $resource = @fopen($path, 'a+'); + if ($resource != null) { + fwrite($resource, $content); + fclose($resource); + } + } + + /** + * Check if all the bytes are zero. + * + * @param string $content The content. + * @return bool + */ + public static function allZero($content) + { + $size = strlen($content); + + // If all Zero, skip this range + for ($i = 0; $i < $size; $i++) { + if (ord($content[$i]) != 0) { + return false; + } + } + + return true; + } + + /** + * Append the delimiter to the string. The delimiter will not be added if + * the string already ends with this delimiter. + * + * @param string $string The string to add delimiter to. + * @param string $delimiter The delimiter to be added. + * + * @return string + */ + public static function appendDelimiter($string, $delimiter) + { + if (!self::endsWith($string, $delimiter)) { + $string .= $delimiter; + } + + return $string; + } + + /** + * Static function used to determine if the request is performed against + * secondary endpoint. + * + * @param Psr\Http\Message\RequestInterface $request The request performed. + * @param array $options The options of the + * request. Must contain + * Resources::ROS_SECONDARY_URI + * + * @return boolean + */ + public static function requestSentToSecondary( + \Psr\Http\Message\RequestInterface $request, + array $options + ) { + $uri = $request->getUri(); + $secondaryUri = $options[Resources::ROS_SECONDARY_URI]; + $isSecondary = false; + if (strpos((string)$uri, (string)$secondaryUri) !== false) { + $isSecondary = true; + } + return $isSecondary; + } + + /** + * Gets the location value from the headers. + * + * @param array $headers request/response headers. + * + * @return string + */ + public static function getLocationFromHeaders(array $headers) + { + $value = Utilities::tryGetValue( + $headers, + Resources::X_MS_CONTINUATION_LOCATION_MODE + ); + + $result = ''; + if (\is_string($value)) { + $result = $value; + } elseif (!empty($value)) { + $result = $value[0]; + } + return $result; + } + + /** + * Gets if the value is a double value or string representation of a double + * value + * + * @param mixed $value The value to be verified. + * + * @return boolean + */ + public static function isDouble($value) + { + return is_numeric($value) && is_double($value + 0); + } + + /** + * Calculates the content MD5 which is base64 encoded. This should be align + * with the server calculated MD5. + * + * @param string $content the content to be calculated. + * + * @return string + */ + public static function calculateContentMD5($content) + { + Validate::notNull($content, 'content'); + Validate::canCastAsString($content, 'content'); + + return base64_encode(md5($content, true)); + } + + /** + * Return if the environment is in 64 bit PHP. + * + * @return bool + */ + public static function is64BitPHP() + { + return PHP_INT_SIZE == 8; + } +} diff --git a/src/Common/Internal/Validate.php b/src/Common/Internal/Validate.php new file mode 100644 index 0000000..f7e7362 --- /dev/null +++ b/src/Common/Internal/Validate.php @@ -0,0 +1,422 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\Exceptions\InvalidArgumentTypeException; + +/** + * Validates against a condition and throws an exception in case of failure. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Validate +{ + /** + * Throws exception if the provided variable type is not array. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws InvalidArgumentTypeException. + * + * @return void + */ + public static function isArray($var, $name) + { + if (!is_array($var)) { + throw new InvalidArgumentTypeException(gettype(array()), $name); + } + } + + /** + * Throws exception if the provided variable can not convert to a string. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws InvalidArgumentTypeException + * + * @return void + */ + public static function canCastAsString($var, $name) + { + try { + (string)$var; + } catch (\Exception $e) { + throw new InvalidArgumentTypeException(gettype(''), $name); + } + } + + /** + * Throws exception if the provided variable type is not boolean. + * + * @param mixed $var variable to check against. + * + * @throws InvalidArgumentTypeException + * + * @return void + */ + public static function isBoolean($var) + { + (bool)$var; + } + + /** + * Throws exception if the provided variable is set to null. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function notNullOrEmpty($var, $name) + { + if (is_null($var) || empty($var)) { + throw new \InvalidArgumentException( + sprintf(Resources::NULL_OR_EMPTY_MSG, $name) + ); + } + } + + /** + * Throws exception if the provided variable is not double. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function isDouble($var, $name) + { + if (!is_numeric($var)) { + throw new InvalidArgumentTypeException('double', $name); + } + } + + /** + * Throws exception if the provided variable type is not integer. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws InvalidArgumentTypeException + * + * @return void + */ + public static function isInteger($var, $name) + { + try { + (int)$var; + } catch (\Exception $e) { + throw new InvalidArgumentTypeException(gettype(123), $name); + } + } + + /** + * Returns whether the variable is an empty or null string. + * + * @param string $var value. + * + * @return boolean + */ + public static function isNullOrEmptyString($var) + { + try { + (string)$var; + } catch (\Exception $e) { + return false; + } + + return (!isset($var) || trim($var)===''); + } + + /** + * Throws exception if the provided condition is not satisfied. + * + * @param bool $isSatisfied condition result. + * @param string $failureMessage the exception message + * + * @throws \Exception + * + * @return void + */ + public static function isTrue($isSatisfied, $failureMessage) + { + if (!$isSatisfied) { + throw new \InvalidArgumentException($failureMessage); + } + } + + /** + * Throws exception if the provided $date is not of type \DateTime + * + * @param mixed $date variable to check against. + * + * @throws InvalidArgumentTypeException + * + * @return void + */ + public static function isDate($date) + { + if (gettype($date) != 'object' || get_class($date) != 'DateTime') { + throw new InvalidArgumentTypeException('DateTime'); + } + } + + /** + * Throws exception if the provided variable is set to null. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function notNull($var, $name) + { + if (is_null($var)) { + throw new \InvalidArgumentException(sprintf(Resources::NULL_MSG, $name)); + } + } + + /** + * Throws exception if the object is not of the specified class type. + * + * @param mixed $objectInstance An object that requires class type validation. + * @param mixed $classInstance The instance of the class the the + * object instance should be. + * @param string $name The name of the object. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function isInstanceOf($objectInstance, $classInstance, $name) + { + Validate::notNull($classInstance, 'classInstance'); + if (is_null($objectInstance)) { + return true; + } + + $objectType = gettype($objectInstance); + $classType = gettype($classInstance); + + if ($objectType === $classType) { + return true; + } else { + throw new \InvalidArgumentException( + sprintf( + Resources::INSTANCE_TYPE_VALIDATION_MSG, + $name, + $objectType, + $classType + ) + ); + } + } + + /** + * Creates a anonymous function that check if the given uri is valid or not. + * + * @return callable + */ + public static function getIsValidUri() + { + return function ($uri) { + return Validate::isValidUri($uri); + }; + } + + /** + * Throws exception if the string is not of a valid uri. + * + * @param string $uri String to check. + * + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function isValidUri($uri) + { + $isValid = filter_var($uri, FILTER_VALIDATE_URL); + + if ($isValid) { + return true; + } else { + throw new \RuntimeException( + sprintf(Resources::INVALID_CONFIG_URI, $uri) + ); + } + } + + /** + * Throws exception if the provided variable type is not object. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws InvalidArgumentTypeException. + * + * @return boolean + */ + public static function isObject($var, $name) + { + if (!is_object($var)) { + throw new InvalidArgumentTypeException('object', $name); + } + + return true; + } + + /** + * Throws exception if the object is not of the specified class type. + * + * @param mixed $objectInstance An object that requires class type validation. + * @param string $class The class the object instance should be. + * @param string $name The parameter name. + * + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function isA($objectInstance, $class, $name) + { + Validate::canCastAsString($class, 'class'); + Validate::notNull($objectInstance, 'objectInstance'); + Validate::isObject($objectInstance, 'objectInstance'); + + $objectType = get_class($objectInstance); + + if (is_a($objectInstance, $class)) { + return true; + } else { + throw new \InvalidArgumentException( + sprintf( + Resources::INSTANCE_TYPE_VALIDATION_MSG, + $name, + $objectType, + $class + ) + ); + } + } + + /** + * Validate if method exists in object + * + * @param object $objectInstance An object that requires method existing + * validation + * @param string $method Method name + * @param string $name The parameter name + * + * @return boolean + */ + public static function methodExists($objectInstance, $method, $name) + { + Validate::canCastAsString($method, 'method'); + Validate::notNull($objectInstance, 'objectInstance'); + Validate::isObject($objectInstance, 'objectInstance'); + + if (method_exists($objectInstance, $method)) { + return true; + } else { + throw new \InvalidArgumentException( + sprintf( + Resources::ERROR_METHOD_NOT_FOUND, + $method, + $name + ) + ); + } + } + + /** + * Validate if string is date formatted + * + * @param string $value Value to validate + * @param string $name Name of parameter to insert in erro message + * + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function isDateString($value, $name) + { + Validate::canCastAsString($value, 'value'); + + try { + new \DateTime($value); + return true; + } catch (\Exception $e) { + throw new \InvalidArgumentException( + sprintf( + Resources::ERROR_INVALID_DATE_STRING, + $name, + $value + ) + ); + } + } + + /** + * Validate if the provided array has key, throw exception otherwise. + * + * @param string $key The key to be searched. + * @param string $name The name of the array. + * @param array $array The array to be validated. + * + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function hasKey($key, $name, array $array) + { + Validate::isArray($array, $name); + + if (!array_key_exists($key, $array)) { + throw new \UnexpectedValueException( + sprintf( + Resources::INVALID_VALUE_MSG, + $name, + sprintf(Resources::ERROR_KEY_NOT_EXIST, $key) + ) + ); + } + + return true; + } +} diff --git a/src/Common/LocationMode.php b/src/Common/LocationMode.php new file mode 100644 index 0000000..0368f10 --- /dev/null +++ b/src/Common/LocationMode.php @@ -0,0 +1,53 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +/** + * Location mode for the service. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class LocationMode +{ + //Request will only be sent to primary endpoint, except for + //getServiceStats APIs. + const PRIMARY_ONLY = 'PrimaryOnly'; + + //Request will only be sent to secondary endpoint. + const SECONDARY_ONLY = 'SecondaryOnly'; + + //Request will be sent to primary endpoint first, and retry for secondary + //endpoint. + const PRIMARY_THEN_SECONDARY = 'PrimaryThenSecondary'; + + //Request will be sent to secondary endpoint first, and retry for primary + //endpoint. + const SECONDARY_THEN_PRIMARY = 'SecondaryThenPrimary'; +} diff --git a/src/Common/Logger.php b/src/Common/Logger.php new file mode 100644 index 0000000..0441ab0 --- /dev/null +++ b/src/Common/Logger.php @@ -0,0 +1,77 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Logger class for debugging purpose. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Logger +{ + /** + * @var string + */ + private static $_filePath; + + /** + * Logs $var to file. + * + * @param mixed $var The data to log. + * @param string $tip The help message. + * + * @return void + */ + public static function log($var, $tip = Resources::EMPTY_STRING) + { + if (!empty($tip)) { + error_log($tip . "\n", 3, self::$_filePath); + } + + if (is_array($var) || is_object($var)) { + error_log(print_r($var, true), 3, self::$_filePath); + } else { + error_log($var . "\n", 3, self::$_filePath); + } + } + + /** + * Sets file path to use. + * + * @param string $filePath The log file path. + * @return void + */ + public static function setLogFile($filePath) + { + self::$_filePath = $filePath; + } +} diff --git a/src/Common/MarkerContinuationTokenTrait.php b/src/Common/MarkerContinuationTokenTrait.php new file mode 100644 index 0000000..d47f186 --- /dev/null +++ b/src/Common/MarkerContinuationTokenTrait.php @@ -0,0 +1,109 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +use MicrosoftAzure\Storage\Common\Models\MarkerContinuationToken; + +/** + * Trait implementing logic for continuation tokens that has nextMarker. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait MarkerContinuationTokenTrait +{ + private $continuationToken; + + /** + * Setter for continuationToken + * + * @param MarkerContinuationToken|null $continuationToken the continuation + * token to be set. + */ + public function setContinuationToken(MarkerContinuationToken $continuationToken = null) + { + $this->continuationToken = $continuationToken; + } + + public function setMarker($marker) + { + if ($this->continuationToken == null) { + $this->continuationToken = new MarkerContinuationToken(); + }; + $this->continuationToken->setNextMarker($marker); + } + + /** + * Getter for continuationToken + * + * @return ContinuationToken + */ + public function getContinuationToken() + { + return $this->continuationToken; + } + + /** + * Gets the next marker to list/query items. + * + * @return string + */ + public function getNextMarker() + { + if ($this->continuationToken == null) { + return null; + } + return $this->continuationToken->getNextMarker(); + } + + /** + * Gets for location for previous request. + * + * @return string + */ + public function getLocation() + { + if ($this->continuationToken == null) { + return null; + } + return $this->continuationToken->getLocation(); + } + + public function getLocationMode() + { + if ($this->continuationToken == null) { + return parent::getLocationMode(); + } elseif ($this->continuationToken->getLocation() == '') { + return parent::getLocationMode(); + } else { + return $this->getLocation(); + } + } +} diff --git a/src/Common/Middlewares/HistoryMiddleware.php b/src/Common/Middlewares/HistoryMiddleware.php new file mode 100644 index 0000000..b3e4ae2 --- /dev/null +++ b/src/Common/Middlewares/HistoryMiddleware.php @@ -0,0 +1,200 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Serialization\MessageSerializer; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Promise\RejectedPromise; + +/** + * This class provides the functionality to log the requests/options/responses. + * Logging large number of entries without providing a file path may exhaust + * the memory. + * + * The middleware should be pushed into client options if the logging is + * intended to persist between different API calls. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class HistoryMiddleware extends MiddlewareBase +{ + private $history; + private $path; + private $count; + + const TITLE_LENGTH = 120; + + /** + * Gets the saved paried history. + * + * @return array + */ + public function getHistory() + { + return $this->history; + } + + /** + * Constructor + * + * @param string $path the path to save the history. If path is provided, + * no data is going to be saved to memory and the + * entries are going to be serialized and saved to given + * path. + * + */ + public function __construct($path = '') + { + $this->history = array(); + $this->path = $path; + $this->count = 0; + } + + /** + * Add an entry to history + * + * @param array $entry the entry to be added. + */ + public function addHistory(array $entry) + { + if ($this->path !== '') { + $this->appendNewEntryToPath($entry); + } else { + Validate::isTrue( + array_key_exists('request', $entry) && + array_key_exists('options', $entry) && + (array_key_exists('response', $entry) || + array_key_exists('reason', $entry)), + 'Given history entry not in correct format' + ); + $this->history[] = $entry; + } + ++$this->count; + } + + /** + * Clear the history + * + * @return void + */ + public function clearHistory() + { + $this->history = array(); + $this->count = 0; + } + + /** + * This function will be invoked after the request is sent, if + * the promise is fulfilled. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onFulfilled(RequestInterface $request, array $options) + { + $reflection = $this; + return function (ResponseInterface $response) use ( + $reflection, + $request, + $options + ) { + $reflection->addHistory([ + 'request' => $request, + 'response' => $response, + 'options' => $options + ]); + return $response; + }; + } + + /** + * This function will be executed after the request is sent, if + * the promise is rejected. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onRejected(RequestInterface $request, array $options) + { + $reflection = $this; + return function ($reason) use ( + $reflection, + $request, + $options + ) { + $reflection->addHistory([ + 'request' => $request, + 'reason' => $reason, + 'options' => $options + ]); + return new RejectedPromise($reason); + }; + } + + /** + * Append the new entry to saved file path. + * + * @param array $entry the entry to be added. + * + * @return void + */ + private function appendNewEntryToPath(array $entry) + { + $entryNoString = "Entry " . $this->count; + $delimiter = str_pad( + $entryNoString, + self::TITLE_LENGTH, + '-', + STR_PAD_BOTH + ) . PHP_EOL; + $entryString = $delimiter; + $entryString .= sprintf( + "Time: %s\n", + (new \DateTime("now", new \DateTimeZone('UTC')))->format('Y-m-d H:i:s') + ); + $entryString .= MessageSerializer::objectSerialize($entry['request']); + if (array_key_exists('reason', $entry)) { + $entryString .= MessageSerializer::objectSerialize($entry['reason']); + } elseif (array_key_exists('response', $entry)) { + $entryString .= MessageSerializer::objectSerialize($entry['response']); + } + + $entryString .= $delimiter; + + Utilities::appendToFile($this->path, $entryString); + } +} diff --git a/src/Common/Middlewares/IMiddleware.php b/src/Common/Middlewares/IMiddleware.php new file mode 100644 index 0000000..b4b3909 --- /dev/null +++ b/src/Common/Middlewares/IMiddleware.php @@ -0,0 +1,71 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +/** + * IMiddleware is called before sending the request and after receiving the + * response. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +interface IMiddleware +{ + /** + * This function will return a callable with $request and $options as + * its parameters and returns a promise. The callable can modify the + * request, fulfilled response or rejected reason when invoked with certain + * conditions. Sample middleware implementation: + * + * ``` + * return function ( + * RequestInterface $request, + * array $options + * ) use ($handler) { + * //do something prior to sending the request. + * $promise = $handler($request, $options); + * return $promise->then( + * function (ResponseInterface $response) use ($request, $options) { + * //do something + * return $response; + * }, + * function ($reason) use ($request, $options) { + * //do something + * return new GuzzleHttp\Promise\RejectedPromise($reason); + * } + * ); + * }; + * ``` + * + * @param callable $handler The next handler. + * @return callable + */ + public function __invoke(callable $handler); +} diff --git a/src/Common/Middlewares/MiddlewareBase.php b/src/Common/Middlewares/MiddlewareBase.php new file mode 100644 index 0000000..5b42080 --- /dev/null +++ b/src/Common/Middlewares/MiddlewareBase.php @@ -0,0 +1,115 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Promise\RejectedPromise; + +/** + * This class provides the base structure of middleware that can be used for + * doing customized behavior including modifying the request, response or + * other behaviors like logging, retrying and debugging. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class MiddlewareBase implements IMiddleware +{ + + /** + * Middleware augments the functionality of handlers by invoking them + * in the process of generating responses. And it returns a function + * that accepts the next handler to invoke. Refer to + * http://docs.guzzlephp.org/en/latest/handlers-and-middleware.html#middleware + * for more detailed information. + * + * @param callable The handler function. + * + * @return callable The function that accepts the next handler to invoke. + */ + public function __invoke(callable $handler) + { + $reflection = $this; + return function ($request, $options) use ($handler, $reflection) { + $request = $reflection->onRequest($request); + return $handler($request, $options)->then( + $reflection->onFulfilled($request, $options), + $reflection->onRejected($request, $options) + ); + }; + } + + /** + * This function will be executed before the request is sent. + * + * @param RequestInterface $request the request before altered. + * + * @return RequestInterface the request after altered. + */ + protected function onRequest(RequestInterface $request) + { + //do nothing + return $request; + } + + /** + * This function will be invoked after the request is sent, if + * the promise is fulfilled. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onFulfilled(RequestInterface $request, array $options) + { + return function (ResponseInterface $response) { + //do nothing + return $response; + }; + } + + /** + * This function will be executed after the request is sent, if + * the promise is rejected. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onRejected(RequestInterface $request, array $options) + { + return function ($reason) { + //do nothing + return new RejectedPromise($reason); + }; + } +} diff --git a/src/Common/Middlewares/MiddlewareStack.php b/src/Common/Middlewares/MiddlewareStack.php new file mode 100644 index 0000000..cc7fc7e --- /dev/null +++ b/src/Common/Middlewares/MiddlewareStack.php @@ -0,0 +1,70 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +/** + * This class provides the stack that handles the logic of applying each + * middlewares to the request or the response. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class MiddlewareStack +{ + private $middlewares = array(); + + /** + * Push the given middleware into the middleware stack. + * + * @param IMiddleware|callable $middleware The middleware to be pushed. + * + * @return void + */ + public function push($middleware) + { + array_unshift($this->middlewares, $middleware); + } + + /** + * Apply the middlewares to the handler. + * + * @param callable $handler the handler to which the middleware applies. + * + * @return callable + */ + public function apply(callable $handler) + { + $result = $handler; + foreach ($this->middlewares as $middleware) { + $result = $middleware($result); + } + + return $result; + } +} diff --git a/src/Common/Middlewares/RetryMiddleware.php b/src/Common/Middlewares/RetryMiddleware.php new file mode 100644 index 0000000..79c4122 --- /dev/null +++ b/src/Common/Middlewares/RetryMiddleware.php @@ -0,0 +1,181 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +use MicrosoftAzure\Storage\Common\LocationMode; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\Promise\RejectedPromise; + +/** + * This class provides the functionality of a middleware that handles all the + * retry logic for the request. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RetryMiddleware extends MiddlewareBase +{ + private $intervalCalculator; + private $decider; + + public function __construct( + callable $intervalCalculator, + callable $decider + ) { + $this->intervalCalculator = $intervalCalculator; + $this->decider = $decider; + } + + /** + * This function will be invoked after the request is sent, if + * the promise is fulfilled. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onFulfilled(RequestInterface $request, array $options) + { + return function (ResponseInterface $response) use ($request, $options) { + $isSecondary = Utilities::requestSentToSecondary($request, $options); + if (!isset($options['retries'])) { + $options['retries'] = 0; + } + if (call_user_func( + $this->decider, + $options['retries'], + $request, + $response, + null, + $isSecondary + )) { + return $this->retry($request, $options, $response); + } + //Add the header that indicates the endpoint to be used if + //continuation token is used for subsequent request. + if ($isSecondary) { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + LocationMode::SECONDARY_ONLY + ); + } else { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + LocationMode::PRIMARY_ONLY + ); + } + return $response; + }; + } + + /** + * This function will be executed after the request is sent, if + * the promise is rejected. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onRejected(RequestInterface $request, array $options) + { + return function ($reason) use ($request, $options) { + $isSecondary = Utilities::requestSentToSecondary($request, $options); + if (!isset($options['retries'])) { + $options['retries'] = 0; + } + + if (call_user_func( + $this->decider, + $options['retries'], + $request, + null, + $reason, + $isSecondary + )) { + return $this->retry($request, $options); + } + return new RejectedPromise($reason); + }; + } + + /** + * This function does the real retry job. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * @param ResponseInterface $response the response of the request + * + * @return callable + */ + private function retry( + RequestInterface $request, + array $options, + ResponseInterface $response = null + ) { + $options['delay'] = call_user_func( + $this->intervalCalculator, + ++$options['retries'] + ); + + //Change the request URI according to the location mode. + if (array_key_exists(Resources::ROS_LOCATION_MODE, $options)) { + $locationMode = $options[Resources::ROS_LOCATION_MODE]; + //If have RA-GRS enabled for the request, switch between + //primary and secondary. + if ($locationMode == LocationMode::PRIMARY_THEN_SECONDARY || + $locationMode == LocationMode::SECONDARY_THEN_PRIMARY) { + $primaryUri = $options[Resources::ROS_PRIMARY_URI]; + $secondaryUri = $options[Resources::ROS_SECONDARY_URI]; + + $target = $request->getRequestTarget(); + if (Utilities::startsWith($target, '/')) { + $target = substr($target, 1); + $primaryUri = new Uri($primaryUri . $target); + $secondaryUri = new Uri($secondaryUri . $target); + } + + //substitute the uri. + if ((string)$request->getUri() == (string)$primaryUri) { + $request = $request->withUri($secondaryUri); + } elseif ((string)$request->getUri() == (string)$secondaryUri) { + $request = $request->withUri($primaryUri); + } + } + } + $handler = $options[Resources::ROS_HANDLER]; + + return \call_user_func($handler, $request, $options); + } +} diff --git a/src/Common/Middlewares/RetryMiddlewareFactory.php b/src/Common/Middlewares/RetryMiddlewareFactory.php new file mode 100644 index 0000000..25dda38 --- /dev/null +++ b/src/Common/Middlewares/RetryMiddlewareFactory.php @@ -0,0 +1,258 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use GuzzleHttp\Exception\RequestException; + +/** + * This class provides static functions that creates retry handlers for Guzzle + * HTTP clients to handle retry policy. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RetryMiddlewareFactory +{ + //The interval will be increased linearly, the nth retry will have a + //wait time equal to n * interval. + const LINEAR_INTERVAL_ACCUMULATION = 'Linear'; + //The interval will be increased exponentially, the nth retry will have a + //wait time equal to pow(2, n) * interval. + const EXPONENTIAL_INTERVAL_ACCUMULATION = 'Exponential'; + //This is for the general type of logic that handles retry. + const GENERAL_RETRY_TYPE = 'General'; + //This is for the append blob retry only. + const APPEND_BLOB_RETRY_TYPE = 'Append Blob Retry'; + + /** + * Create the retry handler for the Guzzle client, according to the given + * attributes. + * + * @param string $type The type that controls the logic of + * the decider of the retry handler. + * Possible value can be + * self::GENERAL_RETRY_TYPE or + * self::APPEND_BLOB_RETRY_TYPE + * @param int $numberOfRetries The maximum number of retries. + * @param int $interval The minimum interval between each retry + * @param string $accumulationMethod If the interval increases linearly or + * exponentially. + * Possible value can be + * self::LINEAR_INTERVAL_ACCUMULATION or + * self::EXPONENTIAL_INTERVAL_ACCUMULATION + * @return RetryMiddleware A RetryMiddleware object that contains + * the logic of how the request should be + * handled after a response. + */ + public static function create( + $type = self::GENERAL_RETRY_TYPE, + $numberOfRetries = Resources::DEFAULT_NUMBER_OF_RETRIES, + $interval = Resources::DEAFULT_RETRY_INTERVAL, + $accumulationMethod = self::LINEAR_INTERVAL_ACCUMULATION + ) { + //Validate the input parameters + //type + Validate::isTrue( + $type == self::GENERAL_RETRY_TYPE || + $type == self::APPEND_BLOB_RETRY_TYPE, + sprintf( + Resources::INVALID_PARAM_GENERAL, + 'type' + ) + ); + //numberOfRetries + Validate::isTrue( + $numberOfRetries > 0, + sprintf( + Resources::INVALID_NEGATIVE_PARAM, + 'numberOfRetries' + ) + ); + //interval + Validate::isTrue( + $interval > 0, + sprintf( + Resources::INVALID_NEGATIVE_PARAM, + 'interval' + ) + ); + //accumulationMethod + Validate::isTrue( + $accumulationMethod == self::LINEAR_INTERVAL_ACCUMULATION || + $accumulationMethod == self::EXPONENTIAL_INTERVAL_ACCUMULATION, + sprintf( + Resources::INVALID_PARAM_GENERAL, + 'accumulationMethod' + ) + ); + + //Get the interval calculator according to the type of the + //accumulation method. + $intervalCalculator = + $accumulationMethod == self::LINEAR_INTERVAL_ACCUMULATION ? + self::createLinearDelayCalculator($interval) : + self::createExponentialDelayCalculator($interval); + + //Get the retry decider according to the type of the retry and + //the number of retries. + $retryDecider = self::createRetryDecider($type, $numberOfRetries); + + //construct the retry middle ware. + return new RetryMiddleware($intervalCalculator, $retryDecider); + } + + /** + * Create the retry decider for the retry handler. It will return a callable + * that accepts the number of retries, the request, the response and the + * exception, and return the decision for a retry. + * + * @param string $type The type of the retry handler. + * @param int $maxRetries The maximum number of retries to be done. + * + * @return callable The callable that will return if the request should + * be retried. + */ + protected static function createRetryDecider($type, $maxRetries) + { + return function ( + $retries, + $request, + $response = null, + $exception = null, + $isSecondary = false + ) use ( + $type, + $maxRetries + ) { + //Exceeds the retry limit. No retry. + if ($retries >= $maxRetries) { + return false; + } + + //Not retriable error, won't retry. + if (!$response) { + if (!$exception || !($exception instanceof RequestException)) { + return false; + } else { + $response = $exception->getResponse(); + } + } + + if ($type == self::GENERAL_RETRY_TYPE) { + return self::generalRetryDecider( + $response->getStatusCode(), + $isSecondary + ); + } else { + return self::appendBlobRetryDecider( + $response->getStatusCode(), + $isSecondary + ); + } + + return true; + }; + } + + /** + * Decide if the given status code indicate the request should be retried. + * + * @param int $statusCode status code of the previous request. + * + * @return bool true if the request should be retried. + */ + protected static function generalRetryDecider($statusCode, $isSecondary) + { + $retry = false; + if ($statusCode == 408) { + $retry = true; + } elseif ($statusCode >= 500) { + if ($statusCode != 501 && $statusCode != 505) { + $retry = true; + } + } elseif ($isSecondary && $statusCode == 404) { + $retry = true; + } + return $retry; + } + + /** + * Decide if the given status code indicate the request should be retried. + * This is for append blob. + * + * @param int $statusCode status code of the previous request. + * + * @return bool true if the request should be retried. + */ + protected static function appendBlobRetryDecider($statusCode) + { + //The retry logic is different for append blob. + //First it will need to record the former status code if it is + //server error. Then if the following request is 412 then it + //needs to be retried. Currently this is not implemented so will + //only adapt to the general retry decider. + //TODO: add logic for append blob's retry when implemented. + $retry = self::generalRetryDecider($statusCode); + return $retry; + } + + /** + * Create the delay calculator that increases the interval linearly + * according to the number of retries. + * + * @param int $interval the minimum interval of the retry. + * + * @return callable a calculator that will return the interval + * according to the number of retries. + */ + protected static function createLinearDelayCalculator($interval) + { + return function ($retries) use ($interval) { + return $retries * $interval; + }; + } + + /** + * Create the delay calculator that increases the interval exponentially + * according to the number of retries. + * + * @param int $interval the minimum interval of the retry. + * + * @return callable a calculator that will return the interval + * according to the number of retries. + */ + protected static function createExponentialDelayCalculator($interval) + { + return function ($retries) use ($interval) { + return $interval * ((int)\pow(2, $retries)); + }; + } +} diff --git a/src/Common/Models/AccessPolicy.php b/src/Common/Models/AccessPolicy.php new file mode 100644 index 0000000..31c2372 --- /dev/null +++ b/src/Common/Models/AccessPolicy.php @@ -0,0 +1,218 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Holds access policy elements + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +abstract class AccessPolicy +{ + private $start; + private $expiry; + private $permission; + private $resourceType; + + /** + * Get the valid permissions for the given resource. + * + * @return array + */ + abstract protected static function getResourceValidPermissions(); + + /** + * Constructor + * + * @param string $resourceType the resource type of this access policy. + */ + public function __construct($resourceType) + { + Validate::canCastAsString($resourceType, 'resourceType'); + Validate::isTrue( + $resourceType == Resources::RESOURCE_TYPE_BLOB || + $resourceType == Resources::RESOURCE_TYPE_CONTAINER || + $resourceType == Resources::RESOURCE_TYPE_QUEUE || + $resourceType == Resources::RESOURCE_TYPE_TABLE || + $resourceType == Resources::RESOURCE_TYPE_FILE || + $resourceType == Resources::RESOURCE_TYPE_SHARE, + Resources::ERROR_RESOURCE_TYPE_NOT_SUPPORTED + ); + + $this->resourceType = $resourceType; + } + + /** + * Gets start. + * + * @return \DateTime. + */ + public function getStart() + { + return $this->start; + } + + /** + * Sets start. + * + * @param \DateTime $start value. + * + * @return void + */ + public function setStart(\DateTime $start = null) + { + if ($start != null) { + Validate::isDate($start); + } + $this->start = $start; + } + + /** + * Gets expiry. + * + * @return \DateTime. + */ + public function getExpiry() + { + return $this->expiry; + } + + /** + * Sets expiry. + * + * @param \DateTime $expiry value. + * + * @return void + */ + public function setExpiry($expiry) + { + Validate::isDate($expiry); + $this->expiry = $expiry; + } + + /** + * Gets permission. + * + * @return string. + */ + public function getPermission() + { + return $this->permission; + } + + /** + * Sets permission. + * + * @param string $permission value. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPermission($permission) + { + $this->permission = $this->validatePermission($permission); + } + + /** + * Gets resource type. + * + * @return string. + */ + public function getResourceType() + { + return $this->resourceType; + } + + /** + * Validate the permission against its corresponding allowed permissions + * + * @param string $permission The permission to be validated. + * + * @throws \InvalidArgumentException + * + * @return string + */ + private function validatePermission($permission) + { + $validPermissions = static::getResourceValidPermissions(); + $result = ''; + foreach ($validPermissions as $validPermission) { + if (strpos($permission, $validPermission) !== false) { + //append the valid permission to result. + $result .= $validPermission; + //remove all the character that represents the permission. + $permission = str_replace( + $validPermission, + '', + $permission + ); + } + } + //After filtering all the permissions, if there is still characters + //left in the given permission, throw exception. + Validate::isTrue( + $permission == '', + sprintf( + Resources::INVALID_PERMISSION_PROVIDED, + $this->getResourceType(), + implode(', ', $validPermissions) + ) + ); + + return $result; + } + + /** + * Converts this current object to XML representation. + * + * @internal + * + * @return array + */ + public function toArray() + { + $array = array(); + + if ($this->getStart() != null) { + $array[Resources::XTAG_SIGNED_START] = + Utilities::convertToEdmDateTime($this->getStart()); + } + $array[Resources::XTAG_SIGNED_EXPIRY] = + Utilities::convertToEdmDateTime($this->getExpiry()); + $array[Resources::XTAG_SIGNED_PERMISSION] = $this->getPermission(); + + return $array; + } +} diff --git a/src/Common/Models/CORS.php b/src/Common/Models/CORS.php new file mode 100644 index 0000000..15cb228 --- /dev/null +++ b/src/Common/Models/CORS.php @@ -0,0 +1,269 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Provides functionality and data structure for Cross-Origin Resource Sharing + * rules. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CORS +{ + private $allowedOrigins; + private $allowedMethods; + private $allowedHeaders; + private $exposedHeaders; + private $maxAgeInSeconds; + + /** + * Constructor of the class. + * + * @param string[] $allowedOrigins The origin domains that are permitted + * to make request against the storage + * service via CORS. + * @param string[] $allowedMethods The methods (HTTP request verbs) that + * the origin domain may use for a CORS + * request. + * @param string[] $allowedHeaders The request headers that the origin + * domain may specify on the CORS request. + * @param string[] $exposedHeaders The response headers that may be sent in + * the response to the CORS request and + * exposed by the browser to the request + * issuer. + * @param int $maxAgeInSeconds The maximum amount of time that a + * browser should cache the preflight + * OPTIONS request. + */ + public function __construct( + array $allowedOrigins, + array $allowedMethods, + array $allowedHeaders, + array $exposedHeaders, + $maxAgeInSeconds + ) { + $this->setAllowedOrigins($allowedOrigins); + $this->setAllowedMethods($allowedMethods); + $this->setAllowedHeaders($allowedHeaders); + $this->setExposedHeaders($exposedHeaders); + $this->setMaxedAgeInSeconds($maxAgeInSeconds); + } + + /** + * Create an instance with parsed XML response with 'CORS' root. + * + * @param array $parsedResponse The response used to create an instance. + * + * @internal + * + * @return CORS + */ + public static function create(array $parsedResponse) + { + Validate::hasKey( + Resources::XTAG_ALLOWED_ORIGINS, + 'parsedResponse', + $parsedResponse + ); + Validate::hasKey( + Resources::XTAG_ALLOWED_METHODS, + 'parsedResponse', + $parsedResponse + ); + Validate::hasKey( + Resources::XTAG_ALLOWED_HEADERS, + 'parsedResponse', + $parsedResponse + ); + Validate::hasKey( + Resources::XTAG_EXPOSED_HEADERS, + 'parsedResponse', + $parsedResponse + ); + Validate::hasKey( + Resources::XTAG_MAX_AGE_IN_SECONDS, + 'parsedResponse', + $parsedResponse + ); + + // Get the values from the parsed response. + $allowedOrigins = array_filter(explode( + ',', + $parsedResponse[Resources::XTAG_ALLOWED_ORIGINS] + )); + $allowedMethods = array_filter(explode( + ',', + $parsedResponse[Resources::XTAG_ALLOWED_METHODS] + )); + $allowedHeaders = array_filter(explode( + ',', + $parsedResponse[Resources::XTAG_ALLOWED_HEADERS] + )); + $exposedHeaders = array_filter(explode( + ',', + $parsedResponse[Resources::XTAG_EXPOSED_HEADERS] + )); + $maxAgeInSeconds = intval( + $parsedResponse[Resources::XTAG_MAX_AGE_IN_SECONDS] + ); + + return new CORS( + $allowedOrigins, + $allowedMethods, + $allowedHeaders, + $exposedHeaders, + $maxAgeInSeconds + ); + } + + /** + * Converts this object to array with XML tags + * + * @return array + */ + public function toArray() + { + return array( + Resources::XTAG_ALLOWED_ORIGINS => + implode(',', $this->getAllowedOrigins()), + Resources::XTAG_ALLOWED_METHODS => + implode(',', $this->getAllowedMethods()), + Resources::XTAG_ALLOWED_HEADERS => + implode(',', $this->getAllowedHeaders()), + Resources::XTAG_EXPOSED_HEADERS => + implode(',', $this->getExposedHeaders()), + Resources::XTAG_MAX_AGE_IN_SECONDS => + $this->getMaxedAgeInSeconds() + ); + } + + /** + * Setter for allowedOrigins + * + * @param string[] $allowedOrigins the allowed origins to be set. + */ + public function setAllowedOrigins(array $allowedOrigins) + { + $this->allowedOrigins = $allowedOrigins; + } + + /** + * Getter for allowedOrigins + * + * @return string[] + */ + public function getAllowedOrigins() + { + return $this->allowedOrigins; + } + + /** + * Setter for allowedMethods + * + * @param string[] $allowedMethods the allowed methods to be set. + */ + public function setAllowedMethods(array $allowedMethods) + { + $this->allowedMethods = $allowedMethods; + } + + /** + * Getter for allowedMethods + * + * @return string[] + */ + public function getAllowedMethods() + { + return $this->allowedMethods; + } + + /** + * Setter for allowedHeaders + * + * @param string[] $allowedHeaders the allowed headers to be set. + */ + public function setAllowedHeaders(array $allowedHeaders) + { + $this->allowedHeaders = $allowedHeaders; + } + + /** + * Getter for allowedHeaders + * + * @return string[] + */ + public function getAllowedHeaders() + { + return $this->allowedHeaders; + } + + /** + * Setter for exposedHeaders + * + * @param string[] $exposedHeaders the exposed headers to be set. + */ + public function setExposedHeaders(array $exposedHeaders) + { + $this->exposedHeaders = $exposedHeaders; + } + + /** + * Getter for exposedHeaders + * + * @return string[] + */ + public function getExposedHeaders() + { + return $this->exposedHeaders; + } + + /** + * Setter for maxAgeInSeconds + * + * @param int $maxAgeInSeconds the max age in seconds to be set. + */ + public function setMaxedAgeInSeconds($maxAgeInSeconds) + { + $this->maxAgeInSeconds = $maxAgeInSeconds; + } + + /** + * Getter for maxAgeInSeconds + * + * @return int + */ + public function getMaxedAgeInSeconds() + { + return $this->maxAgeInSeconds; + } +} diff --git a/src/Common/Models/ContinuationToken.php b/src/Common/Models/ContinuationToken.php new file mode 100644 index 0000000..5956f16 --- /dev/null +++ b/src/Common/Models/ContinuationToken.php @@ -0,0 +1,82 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\LocationMode; + +/** + * Provides functionality and data structure for continuation token. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ContinuationToken +{ + private $location; + + public function __construct( + $location = '' + ) { + $this->setLocation($location); + } + + /** + * Setter for location + * + * @param string $location the location to be set. + */ + public function setLocation($location) + { + Validate::canCastAsString($location, 'location'); + Validate::isTrue( + $location == LocationMode::PRIMARY_ONLY || + $location == LocationMode::SECONDARY_ONLY || + $location == '', + sprintf( + Resources::INVALID_VALUE_MSG, + 'location', + LocationMode::PRIMARY_ONLY . ' or ' . LocationMode::SECONDARY_ONLY + ) + ); + + $this->location = $location; + } + + /** + * Getter for location + * + * @return string + */ + public function getLocation() + { + return $this->location; + } +} diff --git a/src/Common/Models/GetServicePropertiesResult.php b/src/Common/Models/GetServicePropertiesResult.php new file mode 100644 index 0000000..f8db6f5 --- /dev/null +++ b/src/Common/Models/GetServicePropertiesResult.php @@ -0,0 +1,78 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +/** + * Result from calling GetServiceProperties REST wrapper. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetServicePropertiesResult +{ + private $_serviceProperties; + + /** + * Creates object from $parsedResponse. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return \MicrosoftAzure\Storage\Common\Models\GetServicePropertiesResult + */ + public static function create(array $parsedResponse) + { + $result = new GetServicePropertiesResult(); + $result->setValue(ServiceProperties::create($parsedResponse)); + + return $result; + } + + /** + * Gets service properties object. + * + * @return \MicrosoftAzure\Storage\Common\Models\ServiceProperties + */ + public function getValue() + { + return $this->_serviceProperties; + } + + /** + * Sets service properties object. + * + * @param ServiceProperties $serviceProperties object to use. + * + * @return void + */ + protected function setValue($serviceProperties) + { + $this->_serviceProperties = clone $serviceProperties; + } +} diff --git a/src/Common/Models/GetServiceStatsResult.php b/src/Common/Models/GetServiceStatsResult.php new file mode 100644 index 0000000..aa8a73b --- /dev/null +++ b/src/Common/Models/GetServiceStatsResult.php @@ -0,0 +1,118 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Result from calling get service stats REST wrapper. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetServiceStatsResult +{ + private $status; + private $lastSyncTime; + + /** + * Creates object from $parsedResponse. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return \MicrosoftAzure\Storage\Common\Models\GetServiceStatsResult + */ + public static function create(array $parsedResponse) + { + $result = new GetServiceStatsResult(); + if (Utilities::arrayKeyExistsInsensitive( + Resources::XTAG_GEO_REPLICATION, + $parsedResponse + )) { + $geoReplication = $parsedResponse[Resources::XTAG_GEO_REPLICATION]; + if (Utilities::arrayKeyExistsInsensitive( + Resources::XTAG_STATUS, + $geoReplication + )) { + $result->setStatus($geoReplication[Resources::XTAG_STATUS]); + } + + if (Utilities::arrayKeyExistsInsensitive( + Resources::XTAG_LAST_SYNC_TIME, + $geoReplication + )) { + $lastSyncTime = $geoReplication[Resources::XTAG_LAST_SYNC_TIME]; + $result->setLastSyncTime(Utilities::convertToDateTime($lastSyncTime)); + } + } + + return $result; + } + + /** + * Gets status of the result. + * + * @return string + */ + public function getStatus() + { + return $this->status; + } + + /** + * Gets the last sync time. + * @return \DateTime + */ + public function getLastSyncTime() + { + return $this->lastSyncTime; + } + + /** + * Sets status of the result. + * + * @return void + */ + protected function setStatus($status) + { + $this->status = $status; + } + + /** + * Sets the last sync time. + * + * @return void + */ + protected function setLastSyncTime(\DateTime $lastSyncTime) + { + $this->lastSyncTime = $lastSyncTime; + } +} diff --git a/src/Common/Models/Logging.php b/src/Common/Models/Logging.php new file mode 100644 index 0000000..9ef569e --- /dev/null +++ b/src/Common/Models/Logging.php @@ -0,0 +1,198 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds elements of queue properties logging field. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Logging +{ + private $_version; + private $_delete; + private $_read; + private $_write; + private $_retentionPolicy; + + /** + * Creates object from $parsedResponse. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return Logging + */ + public static function create(array $parsedResponse) + { + $result = new Logging(); + $result->setVersion($parsedResponse['Version']); + $result->setDelete(Utilities::toBoolean($parsedResponse['Delete'])); + $result->setRead(Utilities::toBoolean($parsedResponse['Read'])); + $result->setWrite(Utilities::toBoolean($parsedResponse['Write'])); + $result->setRetentionPolicy( + RetentionPolicy::create($parsedResponse['RetentionPolicy']) + ); + + return $result; + } + + /** + * Gets the retention policy + * + * @return MicrosoftAzure\Storage\Common\Models\RetentionPolicy + * + */ + public function getRetentionPolicy() + { + return $this->_retentionPolicy; + } + + /** + * Sets retention policy + * + * @param RetentionPolicy $policy object to use + * + * @return void + */ + public function setRetentionPolicy(RetentionPolicy $policy) + { + $this->_retentionPolicy = $policy; + } + + /** + * Gets whether all write requests should be logged. + * + * @return bool. + */ + public function getWrite() + { + return $this->_write; + } + + /** + * Sets whether all write requests should be logged. + * + * @param bool $write new value. + * + * @return void + */ + public function setWrite($write) + { + $this->_write = $write; + } + + /** + * Gets whether all read requests should be logged. + * + * @return bool + */ + public function getRead() + { + return $this->_read; + } + + /** + * Sets whether all read requests should be logged. + * + * @param bool $read new value. + * + * @return void + */ + public function setRead($read) + { + $this->_read = $read; + } + + /** + * Gets whether all delete requests should be logged. + * + * @return void + */ + public function getDelete() + { + return $this->_delete; + } + + /** + * Sets whether all delete requests should be logged. + * + * @param bool $delete new value. + * + * @return void + */ + public function setDelete($delete) + { + $this->_delete = $delete; + } + + /** + * Gets the version of Storage Analytics to configure + * + * @return string + */ + public function getVersion() + { + return $this->_version; + } + + /** + * Sets the version of Storage Analytics to configure + * + * @param string $version new value. + * + * @return void + */ + public function setVersion($version) + { + $this->_version = $version; + } + + /** + * Converts this object to array with XML tags + * + * @internal + * @return array + */ + public function toArray() + { + return array( + 'Version' => $this->_version, + 'Delete' => Utilities::booleanToString($this->_delete), + 'Read' => Utilities::booleanToString($this->_read), + 'Write' => Utilities::booleanToString($this->_write), + 'RetentionPolicy' => !empty($this->_retentionPolicy) + ? $this->_retentionPolicy->toArray() + : null + ); + } +} diff --git a/src/Common/Models/MarkerContinuationToken.php b/src/Common/Models/MarkerContinuationToken.php new file mode 100644 index 0000000..e73e178 --- /dev/null +++ b/src/Common/Models/MarkerContinuationToken.php @@ -0,0 +1,72 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Provides functionality and data structure for continuation token that + * contains next marker. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class MarkerContinuationToken extends ContinuationToken +{ + private $nextMarker; + + public function __construct( + $nextMarker = '', + $location = '' + ) { + parent::__construct($location); + $this->setNextMarker($nextMarker); + } + + /** + * Setter for nextMarker + * + * @param string $nextMarker the next marker to be set. + */ + public function setNextMarker($nextMarker) + { + Validate::canCastAsString($nextMarker, 'nextMarker'); + $this->nextMarker = $nextMarker; + } + + /** + * Getter for nextMarker + * + * @return string + */ + public function getNextMarker() + { + return $this->nextMarker; + } +} diff --git a/src/Common/Models/Metrics.php b/src/Common/Models/Metrics.php new file mode 100644 index 0000000..578e4cf --- /dev/null +++ b/src/Common/Models/Metrics.php @@ -0,0 +1,181 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds elements of queue properties metrics field. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Metrics +{ + private $_version; + private $_enabled; + private $_includeAPIs; + private $_retentionPolicy; + + /** + * Creates object from $parsedResponse. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return Metrics + */ + public static function create(array $parsedResponse) + { + $result = new Metrics(); + $result->setVersion($parsedResponse['Version']); + $result->setEnabled(Utilities::toBoolean($parsedResponse['Enabled'])); + if ($result->getEnabled()) { + $result->setIncludeAPIs( + Utilities::toBoolean($parsedResponse['IncludeAPIs']) + ); + } + $result->setRetentionPolicy( + RetentionPolicy::create($parsedResponse['RetentionPolicy']) + ); + + return $result; + } + + /** + * Gets retention policy + * + * @return RetentionPolicy + * + */ + public function getRetentionPolicy() + { + return $this->_retentionPolicy; + } + + /** + * Sets retention policy + * + * @param RetentionPolicy $policy object to use + * + * @return void + */ + public function setRetentionPolicy(RetentionPolicy $policy) + { + $this->_retentionPolicy = $policy; + } + + /** + * Gets include APIs. + * + * @return bool + */ + public function getIncludeAPIs() + { + return $this->_includeAPIs; + } + + /** + * Sets include APIs. + * + * @param bool $includeAPIs value to use. + * + * @return void + */ + public function setIncludeAPIs($includeAPIs) + { + $this->_includeAPIs = $includeAPIs; + } + + /** + * Gets enabled. + * + * @return bool + */ + public function getEnabled() + { + return $this->_enabled; + } + + /** + * Sets enabled. + * + * @param bool $enabled value to use. + * + * @return void + */ + public function setEnabled($enabled) + { + $this->_enabled = $enabled; + } + + /** + * Gets version + * + * @return string + */ + public function getVersion() + { + return $this->_version; + } + + /** + * Sets version + * + * @param string $version new value. + * + * @return void + */ + public function setVersion($version) + { + $this->_version = $version; + } + + /** + * Converts this object to array with XML tags + * + * @internal + * @return array + */ + public function toArray() + { + $array = array( + 'Version' => $this->_version, + 'Enabled' => Utilities::booleanToString($this->_enabled) + ); + if ($this->_enabled) { + $array['IncludeAPIs'] = Utilities::booleanToString($this->_includeAPIs); + } + $array['RetentionPolicy'] = !empty($this->_retentionPolicy) + ? $this->_retentionPolicy->toArray() + : null; + + return $array; + } +} diff --git a/src/Common/Models/Range.php b/src/Common/Models/Range.php new file mode 100644 index 0000000..8f85cf6 --- /dev/null +++ b/src/Common/Models/Range.php @@ -0,0 +1,142 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +/** + * Holds info about resource+ range used in HTTP requests + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Range +{ + private $start; + private $end; + + /** + * Constructor + * + * @param integer $start the resource start value + * @param integer $end the resource end value + * + * @return Range + */ + public function __construct($start, $end = null) + { + $this->start = $start; + $this->end = $end; + } + + /** + * Sets resource start range + * + * @param integer $start the resource range start + * + * @return void + */ + public function setStart($start) + { + $this->start = $start; + } + + /** + * Gets resource start range + * + * @return integer + */ + public function getStart() + { + return $this->start; + } + + /** + * Sets resource end range + * + * @param integer $end the resource range end + * + * @return void + */ + public function setEnd($end) + { + $this->end = $end; + } + + /** + * Gets resource end range + * + * @return integer + */ + public function getEnd() + { + return $this->end; + } + + /** + * Gets resource range length + * + * @return integer + */ + public function getLength() + { + if ($this->end != null) { + return $this->end - $this->start + 1; + } else { + return null; + } + } + + /** + * Sets resource range length + * + * @param integer $value new resource range + * + * @return void + */ + public function setLength($value) + { + $this->end = $this->start + $value - 1; + } + + /** + * Constructs the range string according to the set start and end + * + * @return string + */ + public function getRangeString() + { + $rangeString = ''; + + $rangeString .= ('bytes=' . $this->start . '-'); + if ($this->end != null) { + $rangeString .= $this->end; + } + + return $rangeString; + } +} diff --git a/src/Common/Models/RangeDiff.php b/src/Common/Models/RangeDiff.php new file mode 100644 index 0000000..77449be --- /dev/null +++ b/src/Common/Models/RangeDiff.php @@ -0,0 +1,75 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +/** + * Holds info about page blob range diffs + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RangeDiff extends Range +{ + private $isClearedPageRange; + + /** + * Constructor + * + * @param integer $start the resource start value + * @param integer $end the resource end value + * @param bool $isClearedPageRange true if the page range is a cleared range, false otherwise. + */ + public function __construct($start, $end = null, $isClearedPageRange = false) + { + parent::__construct($start, $end); + $this->isClearedPageRange = $isClearedPageRange; + } + + /** + * True if the page range is a cleared range, false otherwise + * + * @return bool + */ + public function isClearedPageRange() + { + return $this->isClearedPageRange; + } + + /** + * Sets the isClearedPageRange property + * + * @param bool $isClearedPageRange + * + * @return bool + */ + public function setIsClearedPageRange($isClearedPageRange) + { + $this->isClearedPageRange = $isClearedPageRange; + } +} diff --git a/src/Common/Models/RetentionPolicy.php b/src/Common/Models/RetentionPolicy.php new file mode 100644 index 0000000..4094065 --- /dev/null +++ b/src/Common/Models/RetentionPolicy.php @@ -0,0 +1,124 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds elements of queue properties retention policy field. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RetentionPolicy +{ + private $_enabled; + private $_days; + + /** + * Creates object from $parsedResponse. + * + * @param array $parsedResponse XML response parsed into array. + * + * @internal + * + * @return MicrosoftAzure\Storage\Common\Models\RetentionPolicy + */ + public static function create(array $parsedResponse = null) + { + $result = new RetentionPolicy(); + $result->setEnabled(Utilities::toBoolean($parsedResponse['Enabled'])); + if ($result->getEnabled()) { + $result->setDays(intval($parsedResponse['Days'])); + } + + return $result; + } + + /** + * Gets enabled. + * + * @return bool + */ + public function getEnabled() + { + return $this->_enabled; + } + + /** + * Sets enabled. + * + * @param bool $enabled value to use. + * + * @return void + */ + public function setEnabled($enabled) + { + $this->_enabled = $enabled; + } + + /** + * Gets days field. + * + * @return int + */ + public function getDays() + { + return $this->_days; + } + + /** + * Sets days field. + * + * @param int $days value to use. + * + * @return void + */ + public function setDays($days) + { + $this->_days = $days; + } + + /** + * Converts this object to array with XML tags + * + * @internal + * + * @return array + */ + public function toArray() + { + $array = array('Enabled' => Utilities::booleanToString($this->_enabled)); + if (isset($this->_days)) { + $array['Days'] = strval($this->_days); + } + + return $array; + } +} diff --git a/src/Common/Models/ServiceOptions.php b/src/Common/Models/ServiceOptions.php new file mode 100644 index 0000000..bd83641 --- /dev/null +++ b/src/Common/Models/ServiceOptions.php @@ -0,0 +1,309 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\LocationMode; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Middlewares\MiddlewareStack; +use MicrosoftAzure\Storage\Common\Middlewares\IMiddleware; + +/** + * This class provides the base structure of service options, granting user to + * send with different options for each individual API call. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ServiceOptions +{ + /** + * The middlewares to be applied using the operation. + * @internal + */ + protected $middlewares; + + /** + * The middleware stack used for the operation. + * @internal + */ + protected $middlewareStack; + + /** + * The number of concurrency when performing concurrent requests. + * @internal + */ + protected $numberOfConcurrency; + + /** + * If streamming is used for the operation. + * @internal + */ + protected $isStreaming; + + /** + * The location mode of the operation. + * @internal + */ + protected $locationMode; + + /** + * If to decode the content of the response body. + * @internal + */ + protected $decodeContent; + + /** + * The timeout of the operation + * @internal + */ + protected $timeout; + + /** + * Initialize the properties to default value. + */ + public function __construct(ServiceOptions $options = null) + { + if ($options == null) { + $this->setNumberOfConcurrency(Resources::NUMBER_OF_CONCURRENCY); + $this->setLocationMode(LocationMode::PRIMARY_ONLY); + $this->setIsStreaming(false); + $this->setDecodeContent(false); + $this->middlewares = array(); + $this->middlewareStack = null; + } else { + $this->setNumberOfConcurrency($options->getNumberOfConcurrency()); + $this->setLocationMode($options->getLocationMode()); + $this->setIsStreaming($options->getIsStreaming()); + $this->setDecodeContent($options->getDecodeContent()); + $this->middlewares = $options->getMiddlewares(); + $this->middlewareStack = $options->getMiddlewareStack(); + } + } + + /** + * Push a middleware into the middlewares. + * @param callable|IMiddleware $middleware middleware to be pushed. + * + * @return void + */ + public function pushMiddleware($middleware) + { + self::validateIsMiddleware($middleware); + $this->middlewares[] = $middleware; + } + + /** + * Gets the middlewares. + * + * @return array + */ + public function getMiddlewares() + { + return $this->middlewares; + } + + /** + * Sets middlewares. + * + * @param array $middlewares value. + * + * @return void + */ + public function setMiddlewares(array $middlewares) + { + foreach ($middlewares as $middleware) { + self::validateIsMiddleware($middleware); + } + $this->middlewares = $middlewares; + } + + /** + * Gets the middleware stack + * + * @return MiddlewareStack + */ + public function getMiddlewareStack() + { + return $this->middlewareStack; + } + + /** + * Sets the middleware stack. + * + * @param MiddlewareStack $middlewareStack value. + * + * @return void + */ + public function setMiddlewareStack(MiddlewareStack $middlewareStack) + { + $this->middlewareStack = $middlewareStack; + } + + /** + * Gets the number of concurrency value + * + * @return int + */ + public function getNumberOfConcurrency() + { + return $this->numberOfConcurrency; + } + + /** + * Sets number of concurrency. + * + * @param int $numberOfConcurrency value. + * + * @return void + */ + public function setNumberOfConcurrency($numberOfConcurrency) + { + $this->numberOfConcurrency = $numberOfConcurrency; + } + + /** + * Gets the isStreaming value + * + * @return bool + */ + public function getIsStreaming() + { + return $this->isStreaming; + } + + /** + * Sets isStreaming. + * + * @param bool $isStreaming value. + * + * @return void + */ + public function setIsStreaming($isStreaming) + { + $this->isStreaming = $isStreaming; + } + + /** + * Gets the locationMode value + * + * @return string + */ + public function getLocationMode() + { + return $this->locationMode; + } + + /** + * Sets locationMode. + * + * @param string $locationMode value. + * + * @return void + */ + public function setLocationMode($locationMode) + { + $this->locationMode = $locationMode; + } + + /** + * Gets the decodeContent value + * + * @return bool + */ + public function getDecodeContent() + { + return $this->decodeContent; + } + + /** + * Sets decodeContent. + * + * @param bool $decodeContent value. + * + * @return void + */ + public function setDecodeContent($decodeContent) + { + $this->decodeContent = $decodeContent; + } + + /** + * Gets the timeout value + * + * @return string + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Sets timeout. + * + * @param string $timeout value. + * + * @return void + */ + public function setTimeout($timeout) + { + $this->timeout = $timeout; + } + + /** + * Generate request options using the input options and saved properties. + * + * @param array $options The options to be merged for the request options. + * + * @return array + */ + public function generateRequestOptions(array $options) + { + $result = array(); + + return $result; + } + + /** + * Validate if the given middleware is of callable or IMiddleware. + * + * @param void $middleware the middleware to be validated. + * + * @return void + */ + private static function validateIsMiddleware($middleware) + { + if (!(is_callable($middleware) || $middleware instanceof IMiddleware)) { + Validate::isTrue( + false, + Resources::INVALID_TYPE_MSG . 'callable or IMiddleware' + ); + } + } +} diff --git a/src/Common/Models/ServiceProperties.php b/src/Common/Models/ServiceProperties.php new file mode 100644 index 0000000..558fd34 --- /dev/null +++ b/src/Common/Models/ServiceProperties.php @@ -0,0 +1,279 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; + +/** + * Encapsulates service properties + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ServiceProperties +{ + private $logging; + private $hourMetrics; + private $minuteMetrics; + private $corses; + private $defaultServiceVersion; + + private static $xmlRootName = 'StorageServiceProperties'; + + /** + * Creates ServiceProperties object from parsed XML response. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return ServiceProperties. + */ + public static function create(array $parsedResponse) + { + $result = new ServiceProperties(); + + if (array_key_exists(Resources::XTAG_DEFAULT_SERVICE_VERSION, $parsedResponse) && + $parsedResponse[Resources::XTAG_DEFAULT_SERVICE_VERSION] != null) { + $result->setDefaultServiceVersion($parsedResponse[Resources::XTAG_DEFAULT_SERVICE_VERSION]); + } + + if (array_key_exists(Resources::XTAG_LOGGING, $parsedResponse)) { + $result->setLogging(Logging::create($parsedResponse[Resources::XTAG_LOGGING])); + } + $result->setHourMetrics(Metrics::create($parsedResponse[Resources::XTAG_HOUR_METRICS])); + if (array_key_exists(Resources::XTAG_MINUTE_METRICS, $parsedResponse)) { + $result->setMinuteMetrics(Metrics::create($parsedResponse[Resources::XTAG_MINUTE_METRICS])); + } + if (array_key_exists(Resources::XTAG_CORS, $parsedResponse) && + $parsedResponse[Resources::XTAG_CORS] != null) { + //There could be multiple CORS rules, so need to extract them all. + $corses = array(); + $corsArray = + $parsedResponse[Resources::XTAG_CORS][Resources::XTAG_CORS_RULE]; + if (count(array_filter(array_keys($corsArray), 'is_string')) > 0) { + //single cors rule + $corses[] = CORS::create($corsArray); + } else { + //multiple cors rule + foreach ($corsArray as $cors) { + $corses[] = CORS::create($cors); + } + } + + $result->setCorses($corses); + } else { + $result->setCorses(array()); + } + + return $result; + } + + /** + * Gets logging element. + * + * @return Logging + */ + public function getLogging() + { + return $this->logging; + } + + /** + * Sets logging element. + * + * @param Logging $logging new element. + * + * @return void + */ + public function setLogging(Logging $logging) + { + $this->logging = clone $logging; + } + + /** + * Gets hour metrics element. + * + * @return Metrics + */ + public function getHourMetrics() + { + return $this->hourMetrics; + } + + /** + * Sets hour metrics element. + * + * @param Metrics $metrics new element. + * + * @return void + */ + public function setHourMetrics(Metrics $hourMetrics) + { + $this->hourMetrics = clone $hourMetrics; + } + + /** + * Gets minute metrics element. + * + * @return Metrics + */ + public function getMinuteMetrics() + { + return $this->minuteMetrics; + } + + /** + * Sets minute metrics element. + * + * @param Metrics $metrics new element. + * + * @return void + */ + public function setMinuteMetrics(Metrics $minuteMetrics) + { + $this->minuteMetrics = clone $minuteMetrics; + } + + /** + * Gets corses element. + * + * @return CORS[] + */ + public function getCorses() + { + return $this->corses; + } + + /** + * Sets corses element. + * + * @param CORS[] $corses new elements. + * + * @return void + */ + public function setCorses(array $corses) + { + $this->corses = $corses; + } + + /** + * Gets the default service version. + * + * @return string + */ + public function getDefaultServiceVersion() + { + return $this->defaultServiceVersion; + } + + /** + * Sets the default service version. This can obly be set for the blob service. + * + * @param string $defaultServiceVersion the default service version + * + * @return void + */ + public function setDefaultServiceVersion($defaultServiceVersion) + { + $this->defaultServiceVersion = $defaultServiceVersion; + } + + /** + * Converts this object to array with XML tags + * + * @internal + * @return array + */ + public function toArray() + { + $result = array(); + + if (!empty($this->getLogging())) { + $result[Resources::XTAG_LOGGING] = + $this->getLogging()->toArray(); + } + + if (!empty($this->getHourMetrics())) { + $result[Resources::XTAG_HOUR_METRICS] = + $this->getHourMetrics()->toArray(); + } + + if (!empty($this->getMinuteMetrics())) { + $result[Resources::XTAG_MINUTE_METRICS] = + $this->getMinuteMetrics()->toArray(); + } + + $corsesArray = $this->getCorsesArray(); + if (!empty($corsesArray)) { + $result[Resources::XTAG_CORS] =$corsesArray; + } + + if ($this->defaultServiceVersion != null) { + $result[Resources::XTAG_DEFAULT_SERVICE_VERSION] = $this->defaultServiceVersion; + } + + return $result; + } + + /** + * Gets the array that contains all the CORSes. + * + * @return array + */ + private function getCorsesArray() + { + $corsesArray = array(); + if (count($this->getCorses()) == 1) { + $corsesArray = array( + Resources::XTAG_CORS_RULE => $this->getCorses()[0]->toArray() + ); + } elseif ($this->getCorses() != array()) { + foreach ($this->getCorses() as $cors) { + $corsesArray[] = [Resources::XTAG_CORS_RULE => $cors->toArray()]; + } + } + + return $corsesArray; + } + + /** + * Converts this current object to XML representation. + * + * @internal + * @param XmlSerializer $xmlSerializer The XML serializer. + * + * @return string + */ + public function toXml(XmlSerializer $xmlSerializer) + { + $properties = array(XmlSerializer::ROOT_NAME => self::$xmlRootName); + return $xmlSerializer->serialize($this->toArray(), $properties); + } +} diff --git a/src/Common/Models/SignedIdentifier.php b/src/Common/Models/SignedIdentifier.php new file mode 100644 index 0000000..94ced3c --- /dev/null +++ b/src/Common/Models/SignedIdentifier.php @@ -0,0 +1,118 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Holds signed identifiers. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SignedIdentifier +{ + private $id; + private $accessPolicy; + + /** + * Constructor + * + * @param string $id The id of this signed identifier. + * @param AccessPolicy|null $accessPolicy The access policy. + */ + public function __construct($id = '', AccessPolicy $accessPolicy = null) + { + $this->setId($id); + $this->setAccessPolicy($accessPolicy); + } + + /** + * Gets id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Sets id. + * + * @param string $id value. + * + * @return void + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * Gets accessPolicy. + * + * @return AccessPolicy + */ + public function getAccessPolicy() + { + return $this->accessPolicy; + } + + /** + * Sets accessPolicy. + * + * @param AccessPolicy|null $accessPolicy value. + * + * @return void + */ + public function setAccessPolicy(AccessPolicy $accessPolicy = null) + { + $this->accessPolicy = $accessPolicy; + } + + /** + * Converts this current object to XML representation. + * + * @internal + * + * @return array + */ + public function toArray() + { + $array = array(); + $accessPolicyArray = array(); + $accessPolicyArray[Resources::XTAG_SIGNED_ID] = $this->getId(); + $accessPolicyArray[Resources::XTAG_ACCESS_POLICY] = + $this->getAccessPolicy()->toArray(); + $array[Resources::XTAG_SIGNED_IDENTIFIER] = $accessPolicyArray; + + return $array; + } +} diff --git a/src/Common/Models/TransactionalMD5Trait.php b/src/Common/Models/TransactionalMD5Trait.php new file mode 100644 index 0000000..a015b6e --- /dev/null +++ b/src/Common/Models/TransactionalMD5Trait.php @@ -0,0 +1,64 @@ + + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +/** + * Trait implementing setting and getting useTransactionalMD5 for + * option classes which need support transactional MD5 validation + * during data transferring. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait TransactionalMD5Trait +{ + /** @var $useTransactionalMD5 boolean */ + private $useTransactionalMD5; + + /** + * Gets whether using transactional MD5 validation. + * + * @return boolean + */ + public function getUseTransactionalMD5() + { + return $this->useTransactionalMD5; + } + + /** + * Sets whether using transactional MD5 validation. + * + * @param boolean $useTransactionalMD5 whether enable transactional + * MD5 validation. + */ + public function setUseTransactionalMD5($useTransactionalMD5) + { + $this->useTransactionalMD5 = $useTransactionalMD5; + } +} diff --git a/src/Common/SharedAccessSignatureHelper.php b/src/Common/SharedAccessSignatureHelper.php new file mode 100644 index 0000000..8313472 --- /dev/null +++ b/src/Common/SharedAccessSignatureHelper.php @@ -0,0 +1,331 @@ + + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Provides methods to generate Azure Storage Shared Access Signature + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SharedAccessSignatureHelper +{ + protected $accountName; + protected $accountKey; + + /** + * Constructor. + * + * @param string $accountName the name of the storage account. + * @param string $accountKey the shared key of the storage account + * + */ + public function __construct($accountName, $accountKey) + { + Validate::canCastAsString($accountName, 'accountName'); + Validate::notNullOrEmpty($accountName, 'accountName'); + + Validate::canCastAsString($accountKey, 'accountKey'); + Validate::notNullOrEmpty($accountKey, 'accountKey'); + + $this->accountName = urldecode($accountName); + $this->accountKey = $accountKey; + } + + /** + * Generates a shared access signature at the account level. + * + * @param string $signedVersion Specifies the signed version to use. + * @param string $signedPermissions Specifies the signed permissions for + * the account SAS. + * @param string $signedService Specifies the signed services + * accessible with the account SAS. + * @param string $signedResourceType Specifies the signed resource types + * that are accessible with the account + * SAS. + * @param \Datetime|string $signedExpiry The time at which the shared access + * signature becomes invalid, in an ISO + * 8601 format. + * @param \Datetime|string $signedStart The time at which the SAS becomes + * valid, in an ISO 8601 format. + * @param string $signedIP Specifies an IP address or a range + * of IP addresses from which to accept + * requests. + * @param string $signedProtocol Specifies the protocol permitted for + * a request made with the account SAS. + * + * @see Constructing an account SAS at + * https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/constructing-an-account-sas + * + * @return string + */ + public function generateAccountSharedAccessSignatureToken( + $signedVersion, + $signedPermissions, + $signedService, + $signedResourceType, + $signedExpiry, + $signedStart = "", + $signedIP = "", + $signedProtocol = "" + ) { + // check that version is valid + Validate::canCastAsString($signedVersion, 'signedVersion'); + Validate::notNullOrEmpty($signedVersion, 'signedVersion'); + Validate::isDateString($signedVersion, 'signedVersion'); + + // validate and sanitize signed service + $signedService = $this->validateAndSanitizeSignedService($signedService); + + // validate and sanitize signed resource type + $signedResourceType = $this->validateAndSanitizeSignedResourceType($signedResourceType); + + // validate and sanitize signed permissions + $signedPermissions = $this->validateAndSanitizeSignedPermissions($signedPermissions); + + // check that expiracy is valid + if ($signedExpiry instanceof \Datetime) { + $signedExpiry = Utilities::isoDate($signedExpiry); + } + Validate::canCastAsString($signedExpiry, 'signedExpiry'); + Validate::notNullOrEmpty($signedExpiry, 'signedExpiry'); + Validate::isDateString($signedExpiry, 'signedExpiry'); + + // check that signed start is valid + if ($signedStart instanceof \Datetime) { + $signedStart = Utilities::isoDate($signedStart); + } + Validate::canCastAsString($signedStart, 'signedStart'); + if (strlen($signedStart) > 0) { + Validate::isDateString($signedStart, 'signedStart'); + } + + // check that signed IP is valid + Validate::canCastAsString($signedIP, 'signedIP'); + + // validate and sanitize signed protocol + $signedProtocol = $this->validateAndSanitizeSignedProtocol($signedProtocol); + + // construct an array with the parameters to generate the shared access signature at the account level + $parameters = array(); + $parameters[] = $this->accountName; + $parameters[] = $signedPermissions; + $parameters[] = $signedService; + $parameters[] = $signedResourceType; + $parameters[] = $signedStart; + $parameters[] = $signedExpiry; + $parameters[] = $signedIP; + $parameters[] = $signedProtocol; + $parameters[] = $signedVersion; + + // implode the parameters into a string + $stringToSign = utf8_encode(implode("\n", $parameters) . "\n"); + + // decode the account key from base64 + $decodedAccountKey = base64_decode($this->accountKey); + + // create the signature with hmac sha256 + $signature = hash_hmac("sha256", $stringToSign, $decodedAccountKey, true); + + // encode the signature as base64 and url encode. + $sig = urlencode(base64_encode($signature)); + + //adding all the components for account SAS together. + $sas = 'sv=' . $signedVersion; + $sas .= '&ss=' . $signedService; + $sas .= '&srt=' . $signedResourceType; + $sas .= '&sp=' . $signedPermissions; + $sas .= '&se=' . $signedExpiry; + $sas .= $signedStart === ''? '' : '&st=' . $signedStart; + $sas .= $signedIP === ''? '' : '&sip=' . $signedIP; + $sas .= '&spr=' . $signedProtocol; + $sas .= '&sig=' . $sig; + + // return the signature + return $sas; + } + + /** + * Validates and sanitizes the signed service parameter + * + * @param string $signedService Specifies the signed services accessible + * with the account SAS. + * + * @return string + */ + protected function validateAndSanitizeSignedService($signedService) + { + // validate signed service is not null or empty + Validate::canCastAsString($signedService, 'signedService'); + Validate::notNullOrEmpty($signedService, 'signedService'); + + // The signed service should only be a combination of the letters b(lob) q(ueue) t(able) or f(ile) + $validServices = ['b', 'q', 't', 'f']; + + return $this->validateAndSanitizeStringWithArray( + strtolower($signedService), + $validServices + ); + } + + /** + * Validates and sanitizes the signed resource type parameter + * + * @param string $signedResourceType Specifies the signed resource types + * that are accessible with the account + * SAS. + * + * @return string + */ + protected function validateAndSanitizeSignedResourceType($signedResourceType) + { + // validate signed resource type is not null or empty + Validate::canCastAsString($signedResourceType, 'signedResourceType'); + Validate::notNullOrEmpty($signedResourceType, 'signedResourceType'); + + // The signed resource type should only be a combination of the letters s(ervice) c(container) or o(bject) + $validResourceTypes = ['s', 'c', 'o']; + + return $this->validateAndSanitizeStringWithArray( + strtolower($signedResourceType), + $validResourceTypes + ); + } + + /** + * Validates and sanitizes the signed permissions parameter + * + * @param string $signedPermissions Specifies the signed permissions for the + * account SAS. + * + * @return string + */ + protected function validateAndSanitizeSignedPermissions( + $signedPermissions + ) { + // validate signed permissions are not null or empty + Validate::canCastAsString($signedPermissions, 'signedPermissions'); + Validate::notNullOrEmpty($signedPermissions, 'signedPermissions'); + + $validPermissions = ['r', 'w', 'd', 'l', 'a', 'c', 'u', 'p']; + + return $this->validateAndSanitizeStringWithArray( + strtolower($signedPermissions), + $validPermissions + ); + } + + /** + * Validates and sanitizes the signed protocol parameter + * + * @param string $signedProtocol Specifies the signed protocol for the + * account SAS. + + * @return string + */ + protected function validateAndSanitizeSignedProtocol($signedProtocol) + { + Validate::canCastAsString($signedProtocol, 'signedProtocol'); + // sanitize string + $sanitizedSignedProtocol = strtolower($signedProtocol); + if (strlen($sanitizedSignedProtocol) > 0) { + if (strcmp($sanitizedSignedProtocol, "https") != 0 && strcmp($sanitizedSignedProtocol, "https,http") != 0) { + throw new \InvalidArgumentException(Resources::SIGNED_PROTOCOL_INVALID_VALIDATION_MSG); + } + } + + return $sanitizedSignedProtocol; + } + + /** + * Removes duplicate characters from a string + * + * @param string $input The input string. + + * @return string + */ + protected function validateAndSanitizeStringWithArray($input, array $array) + { + $result = ''; + foreach ($array as $value) { + if (strpos($input, $value) !== false) { + //append the valid permission to result. + $result .= $value; + //remove all the character that represents the permission. + $input = str_replace( + $value, + '', + $input + ); + } + } + + Validate::isTrue( + strlen($input) == '', + sprintf( + Resources::STRING_NOT_WITH_GIVEN_COMBINATION, + implode(', ', $array) + ) + ); + return $result; + } + + + /** + * Generate the canonical resource using the given account name, service + * type and resource. + * + * @param string $accountName The account name of the service. + * @param string $service The service name of the service. + * @param string $resource The name of the resource. + * + * @return string + */ + protected static function generateCanonicalResource( + $accountName, + $service, + $resource + ) { + static $serviceMap = array( + Resources::RESOURCE_TYPE_BLOB => 'blob', + Resources::RESOURCE_TYPE_FILE => 'file', + Resources::RESOURCE_TYPE_QUEUE => 'queue', + Resources::RESOURCE_TYPE_TABLE => 'table', + ); + $serviceName = $serviceMap[$service]; + if (Utilities::startsWith($resource, '/')) { + $resource = substr(1, strlen($resource) - 1); + } + return sprintf('/%s/%s/%s', $serviceName, $accountName, $resource); + } +}