<?php

declare(strict_types=1);

namespace Buzz\Client;

use Buzz\Configuration\ParameterBag;
use Buzz\Exception\ClientException;
use Buzz\Message\ResponseBuilder;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MultiCurl extends AbstractCurl implements BatchClientInterface, BuzzClientInterface
{
    private $queue = [];
    private $curlm;

    /**
     * Raw responses that the server has pushed to us.
     *
     * @var array
     */
    private $pushedResponses = [];

    /**
     * Curl handlers with unprocessed pushed responses.
     *
     * @var array
     */
    private $pushResponseHandles = [];

    /**
     * Callbacks that decides if a pushed request should be accepted or not.
     *
     * @var array
     */
    private $pushFunctions = [];

    /**
     * @var bool
     */
    private $serverPushSupported = true;

    /**
     * To work around bugs in PHP and GC.
     *
     * @var array
     */
    private $pushCb = [];

    /**
     * {@inheritdoc}
     */
    public function __construct($responseFactory, array $options = [])
    {
        parent::__construct($responseFactory, $options);

        if (\PHP_VERSION_ID < 70215 || \PHP_VERSION_ID === 70300 || \PHP_VERSION_ID === 70301 || !(CURL_VERSION_HTTP2 & curl_version()['features'])) {
            // Dont use HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/76675
            $this->serverPushSupported = false;
        }
    }

    /**
     * Populates the supplied response with the response for the supplied request.
     *
     * If a "callback" option is supplied, its value will be called when the
     * request completes. It is ONLY in the callback you will see the response
     * or an exception.
     *
     * This is a non-blocking function call.
     *
     * The callable should have the following signature:
     *
     *     $callback = function($request, $response, $exception) {
     *         if (!$exception) {
     *             // success
     *         } else {
     *             // error ($error is one of the CURLE_* constants)
     *         }
     *     };
     */
    public function sendAsyncRequest(RequestInterface $request, array $options = []): void
    {
        $options = $this->validateOptions($options);

        $this->addToQueue($request, $options);
    }

    /**
     * This is a blocking function call.
     */
    public function sendRequest(RequestInterface $request, array $options = []): ResponseInterface
    {
        $options = $this->validateOptions($options);
        $originalCallback = $options->get('callback');
        $responseToReturn = null;
        $options = $options->add(['callback' => function (RequestInterface $request, ResponseInterface $response = null, ClientException $e = null) use (&$responseToReturn, $originalCallback) {
            $responseToReturn = $response;
            $originalCallback($request, $response, $e);

            if (null !== $e) {
                throw $e;
            }
        }]);

        $this->addToQueue($request, $options);
        $this->flush();

        return $responseToReturn;
    }

    protected function configureOptions(OptionsResolver $resolver): void
    {
        parent::configureOptions($resolver);

        $resolver->setDefault('callback', function (RequestInterface $request, ResponseInterface $response = null, ClientException $e = null) {
        });
        $resolver->setAllowedTypes('callback', 'callable');

        $resolver->setDefault('push_function_callback', function ($parent, $pushed, $headers) {
            return CURL_PUSH_OK;
        });
        $resolver->setAllowedTypes('push_function_callback', ['callable', 'null']);

        $resolver->setDefault('use_pushed_response', true);
        $resolver->setAllowedTypes('use_pushed_response', 'boolean');
    }

    public function count(): int
    {
        return \count($this->queue);
    }

    /**
     * Wait for all requests to finish.
     *
     * This is a blocking function call.
     *
     * This will not throw any exceptions. All exceptions are handled in the callback.
     */
    public function flush(): void
    {
        while (!empty($this->queue)) {
            $this->proceed();
        }
    }

    /**
     * See if any connection is ready to be processed.
     *
     * This is a non-blocking function call.
     *
     * @throws ClientException if we fail to initialized cUrl
     */
    public function proceed(): void
    {
        if (empty($this->queue)) {
            return;
        }

        if (!$this->curlm) {
            $this->initMultiCurlHandle();
        }

        $this->initQueue();
        $exception = null;
        do {
            // Start processing each handler in the stack
            $mrc = curl_multi_exec($this->curlm, $stillRunning);
        } while (CURLM_CALL_MULTI_PERFORM === $mrc);

        while ($info = curl_multi_info_read($this->curlm)) {
            // handle any completed requests
            if (CURLMSG_DONE !== $info['msg']) {
                continue;
            }

            $handled = false;

            /** @var RequestInterface $request */
            /** @var ParameterBag $options */
            /** @var ResponseBuilder $responseBuilder */
            foreach ($this->queue as $i => list($request, $options, $curl, $responseBuilder)) {
                // Try to find the correct handle from the queue.
                if ($curl !== $info['handle']) {
                    continue;
                }

                $handled = true;
                $response = null;
                try {
                    $this->parseError($request, $info['result'], $curl);
                    $response = $responseBuilder->getResponse();
                    if ($options->get('expose_curl_info', false)) {
                        $response = $response->withHeader('__curl_info', json_encode(curl_getinfo($curl)));
                    }
                } catch (\Throwable $e) {
                    if (null === $exception) {
                        $exception = $e;
                    }
                }

                // remove from queue
                curl_multi_remove_handle($this->curlm, $curl);
                $this->releaseHandle($curl);
                unset($this->queue[$i]);

                // callback
                \call_user_func($options->get('callback'), $request, $response, $exception);
                $exception = null;
            }

            if (!$handled) {
                // It must be a pushed response.
                $this->handlePushedResponse($info['handle']);
            }
        }

        $this->cleanup();
    }

    private function addPushHandle($headers, $handle)
    {
        foreach ($headers as $header) {
            if (0 === strpos($header, ':path:')) {
                $path = substr($header, 6);
                $url = (string) curl_getinfo($handle)['url'];
                $url = str_replace((string) parse_url($url, PHP_URL_PATH), $path, $url);
                $this->pushResponseHandles[$url] = $handle;
                break;
            }
        }
    }

    private function handlePushedResponse($handle)
    {
        $found = false;
        foreach ($this->pushResponseHandles as $url => $h) {
            // Weak comparison
            if ($handle == $h) {
                $found = $url;
            }
        }

        if (!$found) {
            $found = curl_getinfo($handle)['url'];
        }

        $content = curl_multi_getcontent($handle);
        // Check if we got some headers, if not, we do not bother to store it.
        if (0 !== $headerSize = curl_getinfo($handle, CURLINFO_HEADER_SIZE)) {
            $this->pushedResponses[$found] = ['content' => $content, 'headerSize' => $headerSize];
            unset($this->pushResponseHandles[$found]);
        }
    }

    private function hasPushResponse($url)
    {
        return isset($this->pushedResponses[$url]);
    }

    private function getPushedResponse($url)
    {
        $response = $this->pushedResponses[$url];
        unset($this->pushedResponses[$url]);

        return $response;
    }

    private function addToQueue(RequestInterface $request, ParameterBag $options): array
    {
        if (null !== $callback = $options->get('push_function_callback')) {
            $this->pushFunctions[] = $callback;
        }

        return $this->queue[] = [$request, $options];
    }

    /**
     * Create a multi curl handle and add some properties to it.
     */
    private function initMultiCurlHandle(): void
    {
        $this->curlm = curl_multi_init();
        if (false === $this->curlm) {
            throw new ClientException('Unable to create a new cURL multi handle');
        }

        if ($this->serverPushSupported) {
            $userCallbacks = $this->pushFunctions;

            curl_multi_setopt($this->curlm, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
            // We need to use $this->pushCb[] because of a bug in PHP
            curl_multi_setopt(
                $this->curlm,
                CURLMOPT_PUSHFUNCTION,
                $this->pushCb[] = function ($parent, $pushed, $headers) use ($userCallbacks) {
                    // If any callback say no, then do not accept.
                    foreach ($userCallbacks as $callback) {
                        if (CURL_PUSH_DENY === $callback($parent, $pushed, $headers)) {
                            return CURL_PUSH_DENY;
                        }
                    }

                    curl_setopt($pushed, CURLOPT_RETURNTRANSFER, true);
                    curl_setopt($pushed, CURLOPT_HEADER, true);
                    $this->addPushHandle($headers, $pushed);

                    return CURL_PUSH_OK;
                }
            );
        }
    }

    /**
     * Loop over the queue and make sure every item (request) is initialized (ie, got a handle).
     */
    private function initQueue(): void
    {
        foreach ($this->queue as $i => $queueItem) {
            if (2 !== \count($queueItem)) {
                // We have already prepared this curl
                continue;
            }
            // prepare curl handle
            /** @var RequestInterface $request */
            /** @var ParameterBag $options */
            list($request, $options) = $queueItem;

            // Check if we have the response in cache already.
            if ($this->serverPushSupported
                && $options->get('use_pushed_response')
                && $this->hasPushResponse($request->getUri()->__toString())
            ) {
                $data = $this->getPushedResponse($request->getUri()->__toString());
                $response = (new ResponseBuilder($this->responseFactory))->getResponseFromRawInput(
                    $data['content'],
                    $data['headerSize']
                );
                \call_user_func($options->get('callback'), $request, $response, null);
                unset($this->queue[$i]);

                continue;
            }

            $curl = $this->createHandle();
            $responseBuilder = $this->prepare($curl, $request, $options);
            $this->queue[$i][] = $curl;
            $this->queue[$i][] = $responseBuilder;
            curl_multi_add_handle($this->curlm, $curl);
        }
    }

    /**
     * If we got no requests in the queue, do a clean up to save some memory.
     */
    private function cleanup(): void
    {
        if (empty($this->queue)) {
            curl_multi_close($this->curlm);
            $this->curlm = null;
            $this->pushFunctions = [];
            $this->pushCb = [];
        }
    }
}


<?php

declare(strict_types=1);

namespace Buzz\Middleware\Cookie;

use Psr\Http\Message\RequestInterface;

class Cookie
{
    const ATTR_DOMAIN = 'domain';

    const ATTR_PATH = 'path';

    const ATTR_SECURE = 'secure';

    const ATTR_MAX_AGE = 'max-age';

    const ATTR_EXPIRES = 'expires';

    protected $name;

    protected $value;

    protected $attributes = [];

    protected $createdAt;

    /**
     * Constructor.
     */
    public function __construct()
    {
        $this->createdAt = time();
    }

    /**
     * Returns true if the current cookie matches the supplied request.
     *
     * @param RequestInterface $request A request object
     */
    public function matchesRequest(RequestInterface $request): bool
    {
        $uri = $request->getUri();
        // domain
        if (!$this->matchesDomain($uri->getHost())) {
            return false;
        }

        // path
        if (!$this->matchesPath($uri->getPath())) {
            return false;
        }

        // secure
        if ($this->hasAttribute(static::ATTR_SECURE) && 'https' !== $uri->getScheme()) {
            return false;
        }

        return true;
    }

    /**
     * Returns true of the current cookie has expired.
     *
     * Checks the max-age and expires attributes.
     *
     * @return bool Whether the current cookie has expired
     */
    public function isExpired(): bool
    {
        $maxAge = $this->getAttribute(static::ATTR_MAX_AGE);
        if ($maxAge && time() - $this->getCreatedAt() > $maxAge) {
            return true;
        }

        $expires = $this->getAttribute(static::ATTR_EXPIRES);
        if ($expires && strtotime($expires) < time()) {
            return true;
        }

        return false;
    }

    /**
     * Returns true if the current cookie matches the supplied domain.
     *
     * @param string $domain A domain hostname
     */
    public function matchesDomain(string $domain): bool
    {
        $cookieDomain = $this->getAttribute(static::ATTR_DOMAIN) ?? '';

        if (0 === strpos($cookieDomain, '.')) {
            $pattern = '/\b'.preg_quote(substr($cookieDomain, 1), '/').'$/i';

            return (bool) preg_match($pattern, $domain);
        } else {
            return 0 == strcasecmp($cookieDomain, $domain);
        }
    }

    /**
     * Returns true if the current cookie matches the supplied path.
     *
     * @param string $path A path
     */
    public function matchesPath(string $path): bool
    {
        $needle = $this->getAttribute(static::ATTR_PATH);

        return null === $needle || 0 === strpos($path, $needle);
    }

    /**
     * Populates the current cookie with data from the supplied Set-Cookie header.
     *
     * @param string $header        A Set-Cookie header
     * @param string $issuingDomain The domain that issued the header
     */
    public function fromSetCookieHeader(string $header, string $issuingDomain): void
    {
        list($this->name, $header) = explode('=', $header, 2);
        if (false === strpos($header, ';')) {
            $this->value = $header;
            $header = null;
        } else {
            list($this->value, $header) = explode(';', $header, 2);
        }

        $this->clearAttributes();
        if (null !== $header) {
            foreach (array_map('trim', explode(';', trim($header))) as $pair) {
                if (false === strpos($pair, '=')) {
                    $name = $pair;
                    $value = null;
                } else {
                    list($name, $value) = explode('=', $pair);
                }

                $this->setAttribute($name, $value);
            }
        }

        if (!$this->getAttribute(static::ATTR_DOMAIN)) {
            $this->setAttribute(static::ATTR_DOMAIN, $issuingDomain);
        }
    }

    /**
     * Formats a Cookie header for the current cookie.
     *
     * @return string An HTTP request Cookie header
     */
    public function toCookieHeader(): string
    {
        return $this->getName().'='.$this->getValue();
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setValue(string $value): void
    {
        $this->value = $value;
    }

    public function getValue(): string
    {
        return $this->value;
    }

    public function setAttributes(array $attributes): void
    {
        // attributes are case insensitive
        $this->attributes = array_change_key_case($attributes);
    }

    public function setAttribute(string $name, ?string $value): void
    {
        $this->attributes[strtolower($name)] = $value;
    }

    public function getAttributes(): array
    {
        return $this->attributes;
    }

    public function getAttribute(string $name): ?string
    {
        $name = strtolower($name);

        if (isset($this->attributes[$name])) {
            return $this->attributes[$name];
        }

        return null;
    }

    public function hasAttribute(string $name): bool
    {
        return \array_key_exists($name, $this->attributes);
    }

    public function clearAttributes(): void
    {
        $this->setAttributes([]);
    }

    public function setCreatedAt(int $createdAt): void
    {
        $this->createdAt = $createdAt;
    }

    public function getCreatedAt(): int
    {
        return $this->createdAt;
    }
}


<?php

declare(strict_types=1);

namespace Buzz\Client;

use Buzz\Configuration\ParameterBag;
use Buzz\Exception\CallbackException;
use Buzz\Exception\ClientException;
use Buzz\Exception\NetworkException;
use Buzz\Exception\RequestException;
use Buzz\Message\HeaderConverter;
use Buzz\Message\ResponseBuilder;
use Psr\Http\Message\RequestInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Base client class with helpers for working with cURL.
 */
abstract class AbstractCurl extends AbstractClient
{
    private $handles = [];

    private $maxHandles = 5;

    protected function configureOptions(OptionsResolver $resolver): void
    {
        parent::configureOptions($resolver);

        $resolver->setDefault('curl', []);
        $resolver->setAllowedTypes('curl', ['array']);
        $resolver->setDefault('expose_curl_info', false);
        $resolver->setAllowedTypes('expose_curl_info', ['boolean']);
    }

    /**
     * Creates a new cURL resource.
     *
     * @return resource A new cURL resource
     *
     * @throws ClientException If unable to create a cURL resource
     */
    protected function createHandle()
    {
        $curl = $this->handles ? array_pop($this->handles) : curl_init();
        if (false === $curl) {
            throw new ClientException('Unable to create a new cURL handle');
        }

        return $curl;
    }

    /**
     * Release a cUrl resource. This function is from Guzzle.
     *
     * @param resource $curl
     */
    protected function releaseHandle($curl): void
    {
        if (\count($this->handles) >= $this->maxHandles) {
            curl_close($curl);
        } else {
            // Remove all callback functions as they can hold onto references
            // and are not cleaned up by curl_reset. Using curl_setopt_array
            // does not work for some reason, so removing each one
            // individually.
            curl_setopt($curl, CURLOPT_HEADERFUNCTION, null);
            curl_setopt($curl, CURLOPT_READFUNCTION, null);
            curl_setopt($curl, CURLOPT_WRITEFUNCTION, null);
            curl_setopt($curl, CURLOPT_PROGRESSFUNCTION, null);
            curl_reset($curl);

            if (!\in_array($curl, $this->handles)) {
                $this->handles[] = $curl;
            }
        }
    }

    /**
     * Prepares a cURL resource to send a request.
     *
     * @param resource $curl
     */
    protected function prepare($curl, RequestInterface $request, ParameterBag $options): ResponseBuilder
    {
        if (\defined('CURLOPT_PROTOCOLS')) {
            curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
            curl_setopt($curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
        }

        curl_setopt($curl, CURLOPT_HEADER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
        curl_setopt($curl, CURLOPT_FAILONERROR, false);

        $this->setOptionsFromParameterBag($curl, $options);
        $this->setOptionsFromRequest($curl, $request);

        $responseBuilder = new ResponseBuilder($this->responseFactory);
        curl_setopt($curl, CURLOPT_HEADERFUNCTION, function ($ch, $data) use ($responseBuilder) {
            $str = trim($data);
            if ('' !== $str) {
                if (0 === strpos(strtolower($str), 'http/')) {
                    $responseBuilder->setStatus($str);
                } else {
                    $responseBuilder->addHeader($str);
                }
            }

            return \strlen($data);
        });

        curl_setopt($curl, CURLOPT_WRITEFUNCTION, function ($ch, $data) use ($responseBuilder) {
            return $responseBuilder->writeBody($data);
        });

        // apply additional options
        curl_setopt_array($curl, $options->get('curl'));

        return $responseBuilder;
    }

    /**
     * Sets options on a cURL resource based on a request.
     *
     * @param resource         $curl    A cURL resource
     * @param RequestInterface $request A request object
     */
    private function setOptionsFromRequest($curl, RequestInterface $request): void
    {
        $options = [
            CURLOPT_CUSTOMREQUEST => $request->getMethod(),
            CURLOPT_URL => $request->getUri()->__toString(),
            CURLOPT_HTTPHEADER => HeaderConverter::toBuzzHeaders($request->getHeaders()),
        ];

        if (0 !== $version = $this->getProtocolVersion($request)) {
            $options[CURLOPT_HTTP_VERSION] = $version;
        }

        if ($request->getUri()->getUserInfo()) {
            $options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
        }

        switch (strtoupper($request->getMethod())) {
            case 'HEAD':
                $options[CURLOPT_NOBODY] = true;

                break;

            case 'GET':
                $options[CURLOPT_HTTPGET] = true;

                break;

            case 'POST':
            case 'PUT':
            case 'DELETE':
            case 'PATCH':
            case 'OPTIONS':
                $body = $request->getBody();
                $bodySize = $body->getSize();
                if (0 !== $bodySize) {
                    if ($body->isSeekable()) {
                        $body->rewind();
                    }

                    // Message has non empty body.
                    if (null === $bodySize || $bodySize > 1024 * 1024) {
                        // Avoid full loading large or unknown size body into memory
                        $options[CURLOPT_UPLOAD] = true;
                        if (null !== $bodySize) {
                            $options[CURLOPT_INFILESIZE] = $bodySize;
                        }
                        $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
                            return $body->read($length);
                        };
                    } else {
                        // Small body can be loaded into memory
                        $options[CURLOPT_POSTFIELDS] = (string) $body;
                    }
                }
        }

        curl_setopt_array($curl, $options);
    }

    /**
     * @param resource $curl
     */
    private function setOptionsFromParameterBag($curl, ParameterBag $options): void
    {
        if (null !== $proxy = $options->get('proxy')) {
            curl_setopt($curl, CURLOPT_PROXY, $proxy);
        }

        $canFollow = !ini_get('safe_mode') && !ini_get('open_basedir') && $options->get('allow_redirects');
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, $canFollow);
        curl_setopt($curl, CURLOPT_MAXREDIRS, $canFollow ? $options->get('max_redirects') : 0);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $options->get('verify') ? 1 : 0);
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $options->get('verify') ? 2 : 0);
        if (0 < $options->get('timeout')) {
            curl_setopt($curl, CURLOPT_TIMEOUT, $options->get('timeout'));
        }
    }

    /**
     * @param resource $curl
     *
     * @throws NetworkException
     * @throws RequestException
     * @throws CallbackException
     */
    protected function parseError(RequestInterface $request, int $errno, $curl): void
    {
        switch ($errno) {
            case CURLE_OK:
                // All OK, create a response object
                break;
            case CURLE_COULDNT_RESOLVE_PROXY:
            case CURLE_COULDNT_RESOLVE_HOST:
            case CURLE_COULDNT_CONNECT:
            case CURLE_OPERATION_TIMEOUTED:
            case CURLE_SSL_CONNECT_ERROR:
                throw new NetworkException($request, curl_error($curl), $errno);
            case CURLE_ABORTED_BY_CALLBACK:
                throw new CallbackException($request, curl_error($curl), $errno);
            default:
                throw new RequestException($request, curl_error($curl), $errno);
        }
    }

    private function getProtocolVersion(RequestInterface $request): int
    {
        switch ($request->getProtocolVersion()) {
            case '1.0':
                return CURL_HTTP_VERSION_1_0;
            case '1.1':
                return CURL_HTTP_VERSION_1_1;
            case '2.0':
                if (\defined('CURL_HTTP_VERSION_2_0')) {
                    return CURL_HTTP_VERSION_2_0;
                }

                throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support');
            default:
                return 0;
        }
    }
}


<?php

declare(strict_types=1);

namespace Buzz;

use Buzz\Client\BuzzClientInterface;
use Buzz\Exception\ClientException;
use Buzz\Exception\InvalidArgumentException;
use Buzz\Exception\LogicException;
use Buzz\Middleware\MiddlewareInterface;
use Http\Message\RequestFactory;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class Browser implements BuzzClientInterface
{
    /** @var BuzzClientInterface */
    private $client;

    /** @var RequestFactoryInterface|RequestFactory */
    private $requestFactory;

    /**
     * @var MiddlewareInterface[]
     */
    private $middleware = [];

    /** @var RequestInterface */
    private $lastRequest;

    /** @var ResponseInterface */
    private $lastResponse;

    /**
     * @param RequestFactoryInterface|RequestFactory $requestFactory
     */
    public function __construct(BuzzClientInterface $client, $requestFactory)
    {
        if (!$requestFactory instanceof RequestFactoryInterface && !$requestFactory instanceof RequestFactory) {
            throw new InvalidArgumentException(sprintf('Second argument of %s must be an instance of %s or %s.', __CLASS__, RequestFactoryInterface::class, RequestFactory::class));
        }

        $this->client = $client;
        $this->requestFactory = $requestFactory;
    }

    public function get(string $url, array $headers = []): ResponseInterface
    {
        return $this->request('GET', $url, $headers);
    }

    public function post(string $url, array $headers = [], string $body = ''): ResponseInterface
    {
        return $this->request('POST', $url, $headers, $body);
    }

    public function head(string $url, array  $headers = []): ResponseInterface
    {
        return $this->request('HEAD', $url, $headers);
    }

    public function patch(string $url, array  $headers = [], string $body = ''): ResponseInterface
    {
        return $this->request('PATCH', $url, $headers, $body);
    }

    public function put(string $url, array  $headers = [], string $body = ''): ResponseInterface
    {
        return $this->request('PUT', $url, $headers, $body);
    }

    public function delete(string $url, array  $headers = [], string $body = ''): ResponseInterface
    {
        return $this->request('DELETE', $url, $headers, $body);
    }

    /**
     * Sends a request.
     *
     * @param string $method  The request method to use
     * @param string $url     The URL to call
     * @param array  $headers An array of request headers
     * @param string $body    The request content
     *
     * @return ResponseInterface The response object
     */
    public function request(string $method, string $url, array $headers = [], string $body = ''): ResponseInterface
    {
        $request = $this->createRequest($method, $url, $headers, $body);

        return $this->sendRequest($request);
    }

    /**
     * Submit a form.
     *
     * @throws ClientException
     * @throws LogicException
     * @throws InvalidArgumentException
     */
    public function submitForm(string $url, array $fields, string $method = 'POST', array $headers = []): ResponseInterface
    {
        $body = [];
        $files = '';
        $boundary = uniqid('', true);
        foreach ($fields as $name => $field) {
            if (!isset($field['path'])) {
                $body[$name] = $field;
            } else {
                // This is a file
                $fileContent = file_get_contents($field['path']);
                if (false !== $fileContent) {
                    $files .= $this->prepareMultipart($name, $fileContent, $boundary, $field);
                }
            }
        }

        if (empty($files)) {
            $headers['Content-Type'] = 'application/x-www-form-urlencoded';
            $body = http_build_query($body);
        } else {
            $headers['Content-Type'] = 'multipart/form-data; boundary="'.$boundary.'"';

            foreach ($body as $name => $value) {
                $files .= $this->prepareMultipart((string) $name, $value, $boundary);
            }
            $body = "$files--{$boundary}--\r\n";
        }

        $request = $this->createRequest($method, $url, $headers, $body);

        return $this->sendRequest($request);
    }

    /**
     * Send a PSR7 request.
     *
     * @throws ClientException
     * @throws LogicException
     * @throws InvalidArgumentException
     */
    public function sendRequest(RequestInterface $request, array $options = []): ResponseInterface
    {
        $chain = $this->createMiddlewareChain($this->middleware, function (RequestInterface $request, callable $responseChain) use ($options) {
            $response = $this->client->sendRequest($request, $options);
            $responseChain($request, $response);
        }, function (RequestInterface $request, ResponseInterface $response) {
            $this->lastRequest = $request;
            $this->lastResponse = $response;
        });

        // Call the chain
        $chain($request);

        return $this->lastResponse;
    }

    /**
     * @param MiddlewareInterface[] $middleware
     */
    private function createMiddlewareChain(array $middleware, callable $requestChainLast, callable $responseChainLast): callable
    {
        $responseChainNext = $responseChainLast;

        // Build response chain
        foreach ($middleware as $m) {
            $lastCallable = function (RequestInterface $request, ResponseInterface $response) use ($m, $responseChainNext) {
                return $m->handleResponse($request, $response, $responseChainNext);
            };

            $responseChainNext = $lastCallable;
        }

        $requestChainLast = function (RequestInterface $request) use ($requestChainLast, $responseChainNext) {
            // Send the actual request and get the response
            $requestChainLast($request, $responseChainNext);
        };

        $middleware = array_reverse($middleware);

        // Build request chain
        $requestChainNext = $requestChainLast;
        foreach ($middleware as $m) {
            $lastCallable = function (RequestInterface $request) use ($m, $requestChainNext) {
                return $m->handleRequest($request, $requestChainNext);
            };

            $requestChainNext = $lastCallable;
        }

        return $requestChainNext;
    }

    public function getLastRequest(): ?RequestInterface
    {
        return $this->lastRequest;
    }

    public function getLastResponse(): ?ResponseInterface
    {
        return $this->lastResponse;
    }

    public function getClient(): BuzzClientInterface
    {
        return $this->client;
    }

    /**
     * Add a new middleware to the stack.
     */
    public function addMiddleware(MiddlewareInterface $middleware): void
    {
        $this->middleware[] = $middleware;
    }

    private function prepareMultipart(string $name, string $content, string $boundary, array $data = []): string
    {
        $output = '';
        $fileHeaders = [];

        // Set a default content-disposition header
        $fileHeaders['Content-Disposition'] = sprintf('form-data; name="%s"', $name);
        if (isset($data['filename'])) {
            $fileHeaders['Content-Disposition'] .= sprintf('; filename="%s"', $data['filename']);
        }

        // Set a default content-length header
        if ($length = \strlen($content)) {
            $fileHeaders['Content-Length'] = (string) $length;
        }

        if (isset($data['contentType'])) {
            $fileHeaders['Content-Type'] = $data['contentType'];
        }

        // Add start
        $output .= "--$boundary\r\n";
        foreach ($fileHeaders as $key => $value) {
            $output .= sprintf("%s: %s\r\n", $key, $value);
        }
        $output .= "\r\n";
        $output .= $content;
        $output .= "\r\n";

        return $output;
    }

    protected function createRequest(string $method, string $url, array $headers, string $body): RequestInterface
    {
        $request = $this->requestFactory->createRequest($method, $url);
        $request->getBody()->write($body);
        foreach ($headers as $name => $value) {
            $request = $request->withAddedHeader($name, $value);
        }

        return $request;
    }
}


<?php

declare(strict_types=1);

namespace Buzz\Middleware\History;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class Journal implements \Countable, \IteratorAggregate
{
    private $entries = [];
    private $limit = 10;

    public function __construct(int $limit = 10)
    {
        $this->limit = $limit;
    }

    /**
     * Records an entry in the journal.
     *
     * @param RequestInterface  $request  The request
     * @param ResponseInterface $response The response
     * @param float|null        $duration The duration in seconds
     */
    public function record(RequestInterface $request, ResponseInterface $response, float $duration = null): void
    {
        $this->addEntry(new Entry($request, $response, $duration));
    }

    public function addEntry(Entry $entry): void
    {
        array_push($this->entries, $entry);
        $this->entries = \array_slice($this->entries, $this->getLimit() * -1);
        end($this->entries);
    }

    /**
     * @return Entry[]
     */
    public function getEntries(): array
    {
        return $this->entries;
    }

    public function getLast(): ?Entry
    {
        $entry = end($this->entries);

        return false === $entry ? null : $entry;
    }

    public function getLastRequest(): ?RequestInterface
    {
        $entry = $this->getLast();
        if (null === $entry) {
            return null;
        }

        return $entry->getRequest();
    }

    public function getLastResponse(): ?ResponseInterface
    {
        $entry = $this->getLast();
        if (null === $entry) {
            return null;
        }

        return $entry->getResponse();
    }

    public function clear(): void
    {
        $this->entries = [];
    }

    public function count(): int
    {
        return \count($this->entries);
    }

    public function setLimit(int $limit): void
    {
        $this->limit = $limit;
    }

    public function getLimit(): int
    {
        return $this->limit;
    }

    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator(array_reverse($this->entries));
    }
}


<?php
namespace mikehaertl\wkhtmlto;

use mikehaertl\tmp\File;

/**
 * Pdf
 *
 * This class is a slim wrapper around `wkhtmltoimage`.
 *
 * @author Michael Härtl <haertl.mike@gmail.com>
 * @license http://www.opensource.org/licenses/MIT
 */
class Image
{
    // Regular expression to detect HTML strings
    const REGEX_HTML = '/<html/i';

    // prefix for tmp files
    const TMP_PREFIX = 'tmp_wkhtmlto_pdf_';

    /**
     * @var string the name of the `wkhtmltoimage` binary. Default is
     * `wkhtmltoimage`. You can also configure a full path here.
     */
    public $binary = 'wkhtmltoimage';

    /**
     * @var string the image type. Default is 'png'. Other options are 'jpg'
     * and 'bmp'.
     */
    public $type = 'png';

    /**
     * @var array options to pass to the Command constructor. Default is none.
     */
    public $commandOptions = array();

    /**
     * @var string|null the directory to use for temporary files. If `null`
     * (default) the dir is autodetected.
     */
    public $tmpDir;

    /**
     * @var bool whether to ignore any errors if some PDF file was still
     * created. Default is `false`.
     */
    public $ignoreWarnings = false;

    /**
     * @var bool whether the PDF was created
     */
    protected $_isCreated = false;

    /**
     * @var \mikehaertl\tmp\File|string the page input or a `File` instance for
     * HTML string inputs
     */
    protected $_page;

    /**
     * @var array options for `wkhtmltoimage` as `['--opt1', '--opt2' => 'val',
     * ...]`
     */
    protected $_options = array();

    /**
     * @var \mikehaertl\tmp\File the temporary image file
     */
    protected $_tmpImageFile;

    /**
     * @var Command the command instance that executes wkhtmltopdf
     */
    protected $_command;

    /**
     * @var string the detailed error message. Empty string if none.
     */
    protected $_error = '';

    /**
     * @param array|string $options global options for wkhtmltoimage, a page
     * URL, a HTML string or a filename
     */
    public function __construct($options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        } elseif (is_string($options)) {
            $this->setPage($options);
        }
    }

    /**
     * Add a page object to the output
     *
     * @param string $page either a URL, a HTML string or a filename
     * @return static the Image instance for method chaining
     */
    public function setPage($page)
    {
        $this->_page = preg_match(self::REGEX_HTML, $page) ? new File($page, '.html') : $page;
        return $this;
    }

    /**
     * Save the image to given filename (triggers image creation)
     *
     * @param string $filename to save image as
     * @return bool whether image was created successfully
     */
    public function saveAs($filename)
    {
        if (!$this->_isCreated && !$this->createImage()) {
            return false;
        }
        if (!$this->_tmpImageFile->saveAs($filename)) {
            $tmpFile = $this->_tmpImageFile->getFileName();
            $this->_error = "Could not copy image from tmp location '$tmpFile' to '$filename'";
            return false;
        }
        return true;
    }

    /**
     * Send image to client, either inline or as download (triggers image
     * creation)
     *
     * @param string|null $filename the filename to send. If empty, the PDF is
     * streamed inline. Note, that the file extension must match what you
     * configured as $type (png, jpg, ...).
     * @param bool $inline whether to force inline display of the image, even
     * if filename is present.
     * @return bool whether image was created successfully
     */
    public function send($filename = null,$inline = false)
    {
        if (!$this->_isCreated && !$this->createImage()) {
            return false;
        }
        $this->_tmpImageFile->send($filename, $this->getMimeType(), $inline);
        return true;
    }

    /**
     * Get the raw Image contents (triggers Image creation).
     * @return string|bool The Image content as a string or `false` if the
     * Image wasn't created successfully.
     */
    public function toString()
    {
        if (!$this->_isCreated && !$this->createImage()) {
            return false;
        }
        return file_get_contents($this->_tmpImageFile->getFileName());
    }

    /**
     * Set options
     *
     * @param array $options list of image options to set as name/value pairs
     * @return static the Image instance for method chaining
     */
    public function setOptions($options = array())
    {
        foreach ($options as $key => $val) {
            if (is_int($key)) {
                $this->_options[] = $val;
            } elseif ($key[0]!=='_' && property_exists($this, $key)) {
                $this->$key = $val;
            } else {
                $this->_options[$key] = $val;
            }
        }
        return $this;
    }

    /**
     * @return Command the command instance that executes wkhtmltopdf
     */
    public function getCommand()
    {
        if ($this->_command === null) {
            $options = $this->commandOptions;
            if (!isset($options['command'])) {
                $options['command'] = $this->binary;
            }
            $this->_command = new Command($options);
        }
        return $this->_command;
    }

    /**
     * @return string the detailed error message. Empty string if none.
     */
    public function getError()
    {
        return $this->_error;
    }

    /**
     * @return string the filename of the temporary image file
     */
    public function getImageFilename()
    {
        if ($this->_tmpImageFile === null) {
            $this->_tmpImageFile = new File('', '.'.$this->type, self::TMP_PREFIX);
        }
        return $this->_tmpImageFile->getFileName();
    }

    /**
     * @return string the mime type for the current image
     * @throws \Exception
     */
    public function getMimeType()
    {
        if ($this->type === 'jpg') {
            return 'image/jpeg';
        } elseif ($this->type === 'png') {
            return 'image/png';
        } elseif ($this->type === 'bmp') {
            return 'image/bmp';
        } else {
            throw new \Exception('Invalid image type');
        }
    }

    /**
     * Run the Command to create the tmp image file
     *
     * @return bool whether creation was successful
     */
    protected function createImage()
    {
        if ($this->_isCreated) {
            return false;
        }
        $command = $this->getCommand();
        $fileName = $this->getImageFilename();

        $command->addArgs($this->_options);
        // Always escape input and output filename
        $command->addArg((string) $this->_page, null, true);
        $command->addArg($fileName, null, true);
        if (!$command->execute()) {
            $this->_error = $command->getError();
            if (!(file_exists($fileName) && filesize($fileName)!==0 && $this->ignoreWarnings)) {
                return false;
            }
        }
        $this->_isCreated = true;
        return true;
    }
}


<?php
namespace mikehaertl\wkhtmlto;

use mikehaertl\tmp\File;

/**
 * Pdf
 *
 * This class is a slim wrapper around wkhtmltopdf.
 *
 * @author Michael Härtl <haertl.mike@gmail.com>
 * @license http://www.opensource.org/licenses/MIT
 */
class Pdf
{
    // Type hints for `addPage()` and `addCover()`
    const TYPE_HTML = 'html';
    const TYPE_XML = 'xml';

    // Regular expression to detect HTML strings
    const REGEX_HTML = '/<(?:!doctype )?html/i';

    // Regular expression to detect XML strings
    const REGEX_XML = '/<\??xml/i';

    // Regular expression to detect URL strings
    const REGEX_URL = '/^(https?:)?\/\//i';

    // Regular expression to detect options that expect an URL or a file name,
    // so we need to create a tmp file for the content.
    const REGEX_OPTS_TMPFILE = '/^((header|footer)-html|(xsl|user)-style-sheet)$/i';

    // Prefix for tmp files
    const TMP_PREFIX = 'tmp_wkhtmlto_pdf_';

    // Maximum length of a file path if PHP_MAXPATHLEN is not defined
    const MAX_PATHLEN = 255;

    /**
     * @var string the name of the `wkhtmltopdf` binary. Default is
     * `wkhtmltopdf`. You can also configure a full path here.
     */
    public $binary = 'wkhtmltopdf';

    /**
     * @var array options to pass to the Command constructor. Default is none.
     */
    public $commandOptions = array();

    /**
     * @var string|null the directory to use for temporary files. If null
     * (default) the dir is autodetected.
     */
    public $tmpDir;

    /**
     * @var bool whether to ignore any errors if some PDF file was still
     * created. Default is false.
     */
    public $ignoreWarnings = false;

    /**
     * @var bool whether the old version 9 of wkhtmltopdf is used (slightly
     * different syntax). Default is false.
     */
    public $version9 = false;

    /**
     * @var bool whether the PDF was created
     */
    protected $_isCreated = false;

    /**
     * @var array global options for `wkhtmltopdf` as `['--opt1', '--opt2' =>
     * 'val', ...]`
     */
    protected $_options = array();

    /**
     * @var array list of wkhtmltopdf objects as arrays
     */
    protected $_objects = array();

    /**
     * @var \mikehaertl\tmp\File the temporary PDF file
     */
    protected $_tmpPdfFile;

    /**
     * @var \mikehaertl\tmp\File[] list of tmp file objects. This is here to
     * keep a reference to `File` and thus avoid too early call of
     * [[File::__destruct]] if the file is not referenced anymore.
     */
    protected $_tmpFiles = array();

    /**
     * @var Command the command instance that executes wkhtmltopdf
     */
    protected $_command;

    /**
     * @var string the detailed error message. Empty string if none.
     */
    protected $_error = '';

    /**
     * @param array|string $options global options for wkhtmltopdf, a page URL,
     * a HTML string or a filename
     */
    public function __construct($options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        } elseif (is_string($options)) {
            $this->addPage($options);
        }
    }

    /**
     * Add a page object to the output
     *
     * @param string $input either a URL, a HTML string or a filename
     * @param array $options optional options for this page
     * @param string|null $type a type hint if the input is a string of known
     * type. This can either be `TYPE_HTML` or `TYPE_XML`. If `null` (default)
     * the type is auto detected from the string content.
     * @return static the Pdf instance for method chaining
     */
    public function addPage($input, $options = array(), $type = null)
    {
        $options['inputArg'] = $this->ensureUrlOrFile($input, $type);
        $this->_objects[] = $this->ensureUrlOrFileOptions($options);
        return $this;
    }

    /**
     * Add a cover page object to the output
     *
     * @param string $input either a URL, a HTML string or a filename
     * @param array $options optional options for the cover page
     * @param string|null $type a type hint if the input is a string of known
     * type. This can either be `TYPE_HTML` or `TYPE_XML`. If `null` (default)
     * the type is auto detected from the string content.
     * @return static the Pdf instance for method chaining
     */
    public function addCover($input, $options = array(), $type = null)
    {
        $options['input'] = ($this->version9 ? '--' : '') . 'cover';
        $options['inputArg'] = $this->ensureUrlOrFile($input, $type);
        $this->_objects[] = $this->ensureUrlOrFileOptions($options);
        return $this;
    }

    /**
     * Add a TOC object to the output
     *
     * @param array $options optional options for the table of contents
     * @return static the Pdf instance for method chaining
     */
    public function addToc($options = array())
    {
        $options['input'] = ($this->version9 ? '--' : '') . 'toc';
        $this->_objects[] = $this->ensureUrlOrFileOptions($options);
        return $this;
    }

    /**
     * Save the PDF to given filename (triggers PDF creation)
     *
     * @param string $filename to save PDF as
     * @return bool whether PDF was created successfully
     */
    public function saveAs($filename)
    {
        if (!$this->_isCreated && !$this->createPdf()) {
            return false;
        }
        if (!$this->_tmpPdfFile->saveAs($filename)) {
            $this->_error = "Could not save PDF as '$filename'";
            return false;
        }
        return true;
    }

    /**
     * Send PDF to client, either inline or as download (triggers PDF creation)
     *
     * @param string|null $filename the filename to send. If empty, the PDF is
     * streamed inline.
     * @param bool $inline whether to force inline display of the PDF, even if
     * filename is present.
     * @return bool whether PDF was created successfully
     */
    public function send($filename = null,$inline = false)
    {
        if (!$this->_isCreated && !$this->createPdf()) {
            return false;
        }
        $this->_tmpPdfFile->send($filename, 'application/pdf', $inline);
        return true;
    }

    /**
     * Get the raw PDF contents (triggers PDF creation).
     * @return string|bool The PDF content as a string or `false` if the PDF
     * wasn't created successfully.
     */
    public function toString()
    {
        if (!$this->_isCreated && !$this->createPdf()) {
            return false;
        }
        return file_get_contents($this->_tmpPdfFile->getFileName());
    }

    /**
     * Set global option(s)
     *
     * @param array $options list of global PDF options to set as name/value pairs
     * @return static the Pdf instance for method chaining
     */
    public function setOptions($options = array())
    {
        // #264 tmpDir must be set before calling ensureUrlOrFileOptions
        if (isset($options['tmpDir'])) {
            $this->tmpDir = $options['tmpDir'];
            unset($options['tmpDir']);
        }
        $options = $this->ensureUrlOrFileOptions($options);
        foreach ($options as $key => $val) {
            if (is_int($key)) {
                $this->_options[] = $val;
            } elseif ($key[0] !== '_' && property_exists($this, $key)) {
                $this->$key = $val;
            } else {
                $this->_options[$key] = $val;
            }
        }
        return $this;
    }

    /**
     * @return Command the command instance that executes wkhtmltopdf
     */
    public function getCommand()
    {
        if ($this->_command === null) {
            $options = $this->commandOptions;
            if (!isset($options['command'])) {
                $options['command'] = $this->binary;
            }
            $this->_command = new Command($options);
        }
        return $this->_command;
    }

    /**
     * @return string the detailed error message. Empty string if none.
     */
    public function getError()
    {
        return $this->_error;
    }

    /**
     * @return string the filename of the temporary PDF file
     */
    public function getPdfFilename()
    {
        if ($this->_tmpPdfFile === null) {
            $this->_tmpPdfFile = new File('', '.pdf', self::TMP_PREFIX, $this->tmpDir);
        }
        return $this->_tmpPdfFile->getFileName();
    }

    /**
     * Run the Command to create the tmp PDF file
     *
     * @return bool whether creation was successful
     */
    protected function createPdf()
    {
        if ($this->_isCreated) {
            return false;
        }
        $command = $this->getCommand();
        $fileName = $this->getPdfFilename();

        $command->addArgs($this->_options);
        foreach ($this->_objects as $object) {
            $command->addArgs($object);
        }
        $command->addArg($fileName, null, true);    // Always escape filename
        if (!$command->execute()) {
            $this->_error = $command->getError();
            if (!(file_exists($fileName) && filesize($fileName) !== 0 && $this->ignoreWarnings)) {
                return false;
            }
        }
        $this->_isCreated = true;
        return true;
    }

    /**
     * This method creates a temporary file if the passed argument is neither a
     * File instance or URL nor contains XML or HTML and is also not a valid
     * file name.
     *
     * @param string|File $input the input argument File to check
     * @param string|null $type a type hint if the input is a string of known
     * type. This can either be `TYPE_HTML` or `TYPE_XML`. If `null` (default)
     * the type is auto detected from the string content.
     * @return \mikehaertl\tmp\File|string a File object if the input is a HTML
     * or XML string. The unchanged input otherwhise.
     */
    protected function ensureUrlOrFile($input, $type = null)
    {
        if ($input instanceof File) {
            $this->_tmpFiles[] = $input;
            return $input;
        } elseif (preg_match(self::REGEX_URL, $input)) {
            return $input;
        } elseif ($type === self::TYPE_XML || $type === null && preg_match(self::REGEX_XML, $input)) {
            $ext = '.xml';
        } else {
            // First check for obvious HTML content to avoid is_file() as much
            // as possible as it can trigger open_basedir restriction warnings
            // with long strings.
            $isHtml = $type === self::TYPE_HTML || preg_match(self::REGEX_HTML, $input);
            if (!$isHtml) {
                $maxPathLen = defined('PHP_MAXPATHLEN') ?
                    constant('PHP_MAXPATHLEN') : self::MAX_PATHLEN;
                if (strlen($input) <= $maxPathLen && is_file($input)) {
                    return $input;
                }
            }
            $ext = '.html';
        }
        $file = new File($input, $ext, self::TMP_PREFIX, $this->tmpDir);
        $this->_tmpFiles[] = $file;
        return $file;
    }

    /**
     * @param array $options list of options as name/value pairs
     * @return array options with raw HTML/XML/String content converted to tmp
     * files where neccessary
     */
    protected function ensureUrlOrFileOptions($options = array())
    {
        foreach ($options as $key => $val) {
            // Some options expect a URL or a file name, so check if we need a temp file
            if (is_string($val) && preg_match(self::REGEX_OPTS_TMPFILE, $key) ) {
                $file = $this->ensureUrlOrFile($val);
                if ($file instanceof File) {
                    $options[$key] = $file;
                }
            }
        }
        return $options;
    }
}


<?php
namespace mikehaertl\wkhtmlto;

use mikehaertl\shellcommand\Command as BaseCommand;

/**
 * Command
 *
 * This class is an extension of mikehaertl\shellcommand\Command and adds `wk*`
 * specific features like xvfb support and proper argument handling.
 *
 * @author Michael Härtl <haertl.mike@gmail.com>
 * @license http://www.opensource.org/licenses/MIT
 */
class Command extends BaseCommand
{
    /**
     * @var bool whether to enable the built in Xvfb support (uses xvfb-run)
     */
    public $enableXvfb = false;

    /**
     * @var string the name of the xvfb-run comand. Default is `xvfb-run`.  You
     * can also configure a full path here.
     */
    public $xvfbRunBinary = 'xvfb-run';

    /**
     * @var string options to pass to the xfvb-run command. Default is
     * `--server-args="-screen 0, 1024x768x24"`.
     */
    public $xvfbRunOptions = '-a --server-args="-screen 0, 1024x768x24"';

    /**
     * @param array $args args to add to the command. These can be:
     * ```
     * [
     *   // Special argument 'input' will not get prepended with '--'.
     *   'input' => 'cover',
     *
     *   // Special argument 'inputArg' is treated like 'input' but will get escaped
     *   // Both 'input' and 'inputArg' can be used in combination
     *   'inputArg' => '/tmp/tmpFileName.html',
     *
     *   'no-outline',           // option without argument
     *   'encoding' => 'UTF-8',  // option with argument
     *
     *   // Option with 2 arguments
     *   'cookie' => array('name'=>'value'),
     *
     *   // Repeatable options with single argument
     *   'run-script' => array(
     *       'local1.js',
     *       'local2.js',
     *   ),
     *
     *   // Repeatable options with 2 arguments
     *   'replace' => array(
     *       '{page}' => $page++,
     *       '{title}' => $pageTitle,
     *   ),
     * ]
     * ```
     */
    public function addArgs($args)
    {
        if (isset($args['input'])) {
            // Typecasts TmpFile to filename
            $this->addArg((string) $args['input']);
            unset($args['input']);
        }
        if (isset($args['inputArg'])) {
            // Typecasts TmpFile to filename and escapes argument
            $this->addArg((string) $args['inputArg'], null, true);
            unset($args['inputArg']);
        }
        foreach($args as $key => $val) {
            if (is_numeric($key)) {
                $this->addArg("--$val");
            } elseif (is_array($val)) {
                foreach($val as $vkey => $vval) {
                    if (is_int($vkey)) {
                        $this->addArg("--$key", $vval);
                    } else {
                        $this->addArg("--$key", array($vkey, $vval));
                    }
                }
            } else {
                $this->addArg("--$key", $val);
            }
        }
    }

    /**
     * @return string|bool the command to execute with optional Xfvb wrapper
     * applied. Null if none set.
     */
    public function getExecCommand()
    {
        $command = parent::getExecCommand();
        if ($this->enableXvfb) {
            return $this->xvfbRunBinary.' '.$this->xvfbRunOptions.' '.$command;
        }
        return $command;
    }
}


<?php

/**
 * LICENSE: The MIT License (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * https://github.com/azure/azure-storage-php/LICENSE
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * PHP version 5
 *
 * @category  Microsoft
 * @package   MicrosoftAzure\Storage\Common\Models
 * @author    Azure Storage PHP SDK <dmsh@microsoft.com>
 * @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 <dmsh@microsoft.com>
 * @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
        );
    }
}


<?php

/**
 * LICENSE: The MIT License (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * https://github.com/azure/azure-storage-php/LICENSE
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * PHP version 5
 *
 * @category  Microsoft
 * @package   MicrosoftAzure\Storage\Common\Internal
 * @author    Azure Storage PHP SDK <dmsh@microsoft.com>
 * @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 <dmsh@microsoft.com>
 * @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;
        }

        if (!empty($options['verify'])) {
            $verify = $options['verify'];
        }

        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;
    }
}


<?php

/**
 * LICENSE: The MIT License (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * https://github.com/azure/azure-storage-php/LICENSE
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * PHP version 5
 *
 * @category  Microsoft
 * @package   MicrosoftAzure\Storage\Table\Models
 * @author    Azure Storage PHP SDK <dmsh@microsoft.com>
 * @copyright 2016 Microsoft Corporation
 * @license   https://github.com/azure/azure-storage-php/LICENSE
 * @link      https://github.com/azure/azure-storage-php
 */

namespace MicrosoftAzure\Storage\Table\Models;

use MicrosoftAzure\Storage\Common\Internal\Utilities;
use MicrosoftAzure\Storage\Common\Internal\Validate;
use MicrosoftAzure\Storage\Table\Internal\TableResources as Resources;

/**
 * Represents entity object used in tables
 *
 * @category  Microsoft
 * @package   MicrosoftAzure\Storage\Table\Models
 * @author    Azure Storage PHP SDK <dmsh@microsoft.com>
 * @copyright 2016 Microsoft Corporation
 * @license   https://github.com/azure/azure-storage-php/LICENSE
 * @link      https://github.com/azure/azure-storage-php
 */
class Entity
{
    private $_etag;
    private $_properties;

    /**
     * Validates if properties is valid or not.
     *
     * @param mixed $properties The properties array.
     *
     * @return void
     */
    private function _validateProperties($properties)
    {
        Validate::isArray($properties, 'entity properties');

        foreach ($properties as $key => $value) {
            Validate::canCastAsString($key, 'key');
            Validate::isTrue(
                $value instanceof Property,
                Resources::INVALID_PROP_MSG
            );
            Validate::isTrue(
                EdmType::validateEdmValue(
                    $value->getEdmType(),
                    $value->getValue(),
                    $condition
                ),
                sprintf(Resources::INVALID_PROP_VAL_MSG, $key, $condition)
            );
        }
    }

    /**
     * Gets property value and if the property name is not found return null.
     *
     * @param string $name The property name.
     *
     * @return mixed
     */
    public function getPropertyValue($name)
    {
        $p = Utilities::tryGetValue($this->_properties, $name);
        return is_null($p) ? null : $p->getValue();
    }

    /**
     * Sets property value.
     *
     * Note that if the property doesn't exist, it doesn't add it. Use addProperty
     * to add new properties.
     *
     * @param string $name  The property name.
     * @param mixed  $value The property value.
     *
     * @return mixed
     */
    public function setPropertyValue($name, $value)
    {
        $p = Utilities::tryGetValue($this->_properties, $name);
        if (!is_null($p)) {
            $p->setValue($value);
        }
    }

    /**
     * Gets entity etag.
     *
     * @return string
     */
    public function getETag()
    {
        return $this->_etag;
    }

    /**
     * Sets entity etag.
     *
     * @param string $etag The entity ETag value.
     *
     * @return void
     */
    public function setETag($etag)
    {
        $this->_etag = $etag;
    }

    /**
     * Gets entity PartitionKey.
     *
     * @return string
     */
    public function getPartitionKey()
    {
        return $this->getPropertyValue('PartitionKey');
    }

    /**
     * Sets entity PartitionKey.
     *
     * @param string $partitionKey The entity PartitionKey value.
     *
     * @return void
     */
    public function setPartitionKey($partitionKey)
    {
        $this->addProperty('PartitionKey', EdmType::STRING, $partitionKey);
    }

    /**
     * Gets entity RowKey.
     *
     * @return string
     */
    public function getRowKey()
    {
        return $this->getPropertyValue('RowKey');
    }

    /**
     * Sets entity RowKey.
     *
     * @param string $rowKey The entity RowKey value.
     *
     * @return void
     */
    public function setRowKey($rowKey)
    {
        $this->addProperty('RowKey', EdmType::STRING, $rowKey);
    }

    /**
     * Gets entity Timestamp.
     *
     * @return \DateTime
     */
    public function getTimestamp()
    {
        return $this->getPropertyValue('Timestamp');
    }

    /**
     * Sets entity Timestamp.
     *
     * @param \DateTime $timestamp The entity Timestamp value.
     *
     * @return void
     */
    public function setTimestamp(\DateTime $timestamp)
    {
        $this->addProperty('Timestamp', EdmType::DATETIME, $timestamp);
    }

    /**
     * Gets the entity properties array.
     *
     * @return array
     */
    public function getProperties()
    {
        return $this->_properties;
    }

    /**
     * Sets the entity properties array.
     *
     * @param array $properties The entity properties.
     *
     * @return void
     */
    public function setProperties(array $properties)
    {
        $this->_validateProperties($properties);
        $this->_properties = $properties;
    }

    /**
     * Gets property object from the entity properties.
     *
     * @param string $name The property name.
     *
     * @return Property|null
     */
    public function getProperty($name)
    {
        return Utilities::tryGetValue($this->_properties, $name);
    }

    /**
     * Sets entity property.
     *
     * @param string   $name     The property name.
     * @param Property $property The property object.
     *
     * @return void
     */
    public function setProperty($name, $property)
    {
        Validate::isTrue($property instanceof Property, Resources::INVALID_PROP_MSG);
        $this->_properties[$name] = $property;
    }

    /**
     * Creates new entity property.
     *
     * @param string $name     The property name.
     * @param string $edmType  The property edm type.
     * @param mixed  $value    The property value.
     * @param string $rawValue The raw value of the property.
     */
    public function addProperty($name, $edmType, $value, $rawValue = '')
    {
        $p = new Property();
        $p->setEdmType($edmType);
        $p->setValue($value);
        $p->setRawValue($rawValue);
        $this->setProperty($name, $p);
    }

    /**
     * Checks if the entity object is valid or not.
     * Valid means the partition and row key exists for this entity along with the
     * timestamp.
     *
     * @param string &$msg The error message.
     *
     * @internal
     *
     * @return boolean
     */
    public function isValid(&$msg = null)
    {
        try {
            $this->_validateProperties($this->_properties);
        } catch (\Exception $exc) {
            $msg = $exc->getMessage();
            return false;
        }

        if (is_null($this->getPartitionKey())
            || is_null($this->getRowKey())
        ) {
            $msg = Resources::NULL_TABLE_KEY_MSG;
            return false;
        } else {
            return true;
        }
    }
}


<?php

/**
 * LICENSE: The MIT License (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * https://github.com/azure/azure-storage-php/LICENSE
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * PHP version 5
 *
 * @category  Microsoft
 * @package   MicrosoftAzure\Storage\Table\Models
 * @author    Azure Storage PHP SDK <dmsh@microsoft.com>
 * @copyright 2016 Microsoft Corporation
 * @license   https://github.com/azure/azure-storage-php/LICENSE
 * @link      https://github.com/azure/azure-storage-php
 */

namespace MicrosoftAzure\Storage\Table\Models;

/**
 * Holds result of getTable API.
 *
 * @category  Microsoft
 * @package   MicrosoftAzure\Storage\Table\Models
 * @author    Azure Storage PHP SDK <dmsh@microsoft.com>
 * @copyright 2016 Microsoft Corporation
 * @license   https://github.com/azure/azure-storage-php/LICENSE
 * @link      https://github.com/azure/azure-storage-php
 */
class GetTableResult
{
    private $_name;

    /**
     * Creates GetTableResult from HTTP response body.
     *
     * @param string             $body            The HTTP response body.
     * @param IODataReaderWriter $odataSerializer The OData reader and writer.
     *
     * @internal
     *
     * @return GetTableResult
     */
    public static function create($body, $odataSerializer)
    {
        $result = new GetTableResult();
        $name = $odataSerializer->parseTable($body);
        $result->setName($name);

        return $result;
    }

    /**
     * Gets the name.
     *
     * @return string
     */
    public function getName()
    {
        return $this->_name;
    }

    /**
     * Sets the name.
     *
     * @param string $name The table name.
     *
     * @return void
     */
    protected function setName($name)
    {
        $this->_name = $name;
    }
}