Completed
Push — master ( d1be6d...8440b6 )
by Andrey
07:58
created

ServiceRestProxy::sendContextAsync()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * LICENSE: The MIT License (the "License")
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 * https://github.com/azure/azure-storage-php/LICENSE
8
 *
9
 * Unless required by applicable law or agreed to in writing, software
10
 * distributed under the License is distributed on an "AS IS" BASIS,
11
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
 * See the License for the specific language governing permissions and
13
 * limitations under the License.
14
 *
15
 * PHP version 5
16
 *
17
 * @category  Microsoft
18
 * @package   MicrosoftAzure\Storage\Common\Internal
19
 * @author    Azure Storage PHP SDK <[email protected]>
20
 * @copyright 2016 Microsoft Corporation
21
 * @license   https://github.com/azure/azure-storage-php/LICENSE
22
 * @link      https://github.com/azure/azure-storage-php
23
 */
24
25
namespace MicrosoftAzure\Storage\Common\Internal;
26
27
use MicrosoftAzure\Storage\Common\Exceptions\ServiceException;
28
use MicrosoftAzure\Storage\Common\Internal\Resources;
29
use MicrosoftAzure\Storage\Common\Internal\Validate;
30
use MicrosoftAzure\Storage\Common\Internal\Utilities;
31
use MicrosoftAzure\Storage\Common\Internal\RetryMiddlewareFactory;
32
use MicrosoftAzure\Storage\Common\Models\ServiceOptions;
33
use MicrosoftAzure\Storage\Common\Internal\Http\HttpCallContext;
34
use MicrosoftAzure\Storage\Common\Internal\Middlewares\MiddlewareBase;
35
use MicrosoftAzure\Storage\Common\Middlewares\MiddlewareStack;
36
use MicrosoftAzure\Storage\Common\LocationMode;
37
use GuzzleHttp\Promise\PromiseInterface;
38
use GuzzleHttp\Promise\EachPromise;
39
use GuzzleHttp\Exception\RequestException;
40
use GuzzleHttp\Psr7\Response;
41
use GuzzleHttp\Psr7\Request;
42
use GuzzleHttp\Psr7\Uri;
43
use GuzzleHttp\Client;
44
use GuzzleHttp\Psr7;
45
use Psr\Http\Message\ResponseInterface;
46
47
/**
48
 * Base class for all services rest proxies.
49
 *
50
 * @ignore
51
 * @category  Microsoft
52
 * @package   MicrosoftAzure\Storage\Common\Internal
53
 * @author    Azure Storage PHP SDK <[email protected]>
54
 * @copyright 2016 Microsoft Corporation
55
 * @license   https://github.com/azure/azure-storage-php/LICENSE
56
 * @link      https://github.com/azure/azure-storage-php
57
 */
58
class ServiceRestProxy extends RestProxy
59
{
60
    private $accountName;
61
    private $psrPrimaryUri;
62
    private $psrSecondaryUri;
63
    private $options;
64
    private $client;
65
66
    /**
67
     * Initializes new ServiceRestProxy object.
68
     *
69
     * @param string                    $primaryUri     The storage account
70
     *                                                  primary uri.
71
     * @param string                    $secondaryUri   The storage account
72
     *                                                  secondary uri.
73
     * @param string                    $accountName    The name of the account.
74
     * @param Serialization\ISerializer $dataSerializer The data serializer.
75
     * @param array                     $options        Array of options for
76
     *                                                  the service
77
     */
78
    public function __construct(
79
        $primaryUri,
80
        $secondaryUri,
81
        $accountName,
82
        Serialization\ISerializer $dataSerializer,
83
        array $options = []
84
    ) {
85
        $primaryUri   = Utilities::appendDelimiter($primaryUri, '/');
86
        $secondaryUri = Utilities::appendDelimiter($secondaryUri, '/');
87
88
        parent::__construct($dataSerializer);
89
90
        $this->accountName     = $accountName;
91
        $this->psrPrimaryUri   = new Uri($primaryUri);
92
        $this->psrSecondaryUri = new Uri($secondaryUri);
93
        $this->options         = array_merge(array('http' => array()), $options);
94
        $this->client          = self::createClient($this->options['http']);
95
    }
96
97
    /**
98
     * Create a Guzzle client for future usage.
99
     *
100
     * @param  array $options Optional parameters for the client.
101
     *
102
     * @return Client
103
     */
104
    private static function createClient(array $options)
105
    {
106
        $verify = true;
107
        //Disable SSL if proxy has been set, and set the proxy in the client.
108
        $proxy = getenv('HTTP_PROXY');
109
        // For testing with Fiddler
110
        // $proxy = 'localhost:8888';
111
        // $verify = false;
112
        if (!empty($proxy)) {
113
            $options['proxy'] = $proxy;
114
        }
115
116
        return (new \GuzzleHttp\Client(
117
            array_merge(
118
                $options,
119
                array(
120
                    "defaults" => array(
121
                        "allow_redirects" => true,
122
                        "exceptions" => true,
123
                        "decode_content" => true,
124
                    ),
125
                    'cookies' => true,
126
                    'verify' => $verify,
127
                )
128
            )
129
        ));
130
    }
131
132
    /**
133
     * Gets the account name.
134
     *
135
     * @return string
136
     */
137
    public function getAccountName()
138
    {
139
        return $this->accountName;
140
    }
141
142
    /**
143
     * Create a middleware stack with given middleware.
144
     *
145
     * @param  ServiceOptions  $serviceOptions The options user passed in.
146
     *
147
     * @return MiddlewareStack
148
     */
149
    protected function createMiddlewareStack(ServiceOptions $serviceOptions)
150
    {
151
        //If handler stack is not defined by the user, create a default
152
        //middleware stack.
153
        $stack = null;
0 ignored issues
show
Unused Code introduced by
$stack is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
154
        if (array_key_exists('stack', $this->options['http'])) {
155
            $stack = $this->options['http']['stack'];
156
        } elseif ($serviceOptions->getMiddlewareStack() != null) {
157
            $stack = $serviceOptions->getMiddlewareStack();
158
        } else {
159
            $stack = new MiddlewareStack();
160
        }
161
162
        //Push all the middlewares specified in the $serviceOptions to the
163
        //handlerstack.
164
        if ($serviceOptions->getMiddlewares() != array()) {
165
            foreach ($serviceOptions->getMiddlewares() as $middleware) {
166
                $stack->push($middleware);
167
            }
168
        }
169
170
        //Push all the middlewares specified in the $options to the
171
        //handlerstack.
172
        if (array_key_exists('middlewares', $this->options)) {
173
            foreach ($this->options['middlewares'] as $middleware) {
174
                $stack->push($middleware);
175
            }
176
        }
177
178
        //Push all the middlewares specified in $this->middlewares to the
179
        //handlerstack.
180
        foreach ($this->getMiddlewares() as $middleware) {
181
            $stack->push($middleware);
182
        }
183
184
        return $stack;
185
    }
186
187
    /**
188
     * Send the requests concurrently. Number of concurrency can be modified
189
     * by inserting a new key/value pair with the key 'number_of_concurrency'
190
     * into the $requestOptions of $serviceOptions. Return only the promise.
191
     *
192
     * @param  callable $generator      the generator function to generate
193
     *                                  request upon fullfilment
194
     * @param  int      $statusCode     The expected status code for each of the
195
     *                                  request generated by generator.
196
     * @param  array    $requestOptions The service options for the concurrent
0 ignored issues
show
Documentation introduced by
There is no parameter named $requestOptions. Did you maybe mean $options?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
197
     *                                  requests.
198
     *
199
     * @return array
200
     */
201
    protected function sendConcurrentAsync(
202
        callable $generator,
203
        $statusCode,
204
        ServiceOptions $options
205
    ) {
206
        $client = $this->client;
207
        $middlewareStack = $this->createMiddlewareStack($options);
208
209
        $sendAsync = function ($request, $options) use ($client) {
210
            if ($request->getMethod() == 'HEAD') {
211
                $options['decode_content'] = false;
212
            }
213
            return $client->sendAsync($request, $options);
214
        };
215
216
        $handler = $middlewareStack->apply($sendAsync);
217
218
        $requestOptions = $this->generateRequestOptions($options, $handler);
219
220
        $promises = \call_user_func(
221
            function () use (
222
                $generator,
223
                $handler,
224
                $requestOptions
225
            ) {
226
                while (is_callable($generator) && ($request = $generator())) {
227
                    yield \call_user_func($handler, $request, $requestOptions);
228
                }
229
            }
230
        );
231
232
        $eachPromise = new EachPromise($promises, [
233
            'concurrency' => $options->getNumberOfConcurrency(),
234
            'fulfilled' => function ($response, $index) use ($statusCode) {
0 ignored issues
show
Unused Code introduced by
The parameter $index is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
235
                //the promise is fulfilled, evaluate the response
236
                self::throwIfError(
237
                    $response,
238
                    $statusCode
239
                );
240
            },
241
            'rejected' => function ($reason, $index) {
0 ignored issues
show
Unused Code introduced by
The parameter $index is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
242
                //Still rejected even if the retry logic has been applied.
243
                //Throwing exception.
244
                throw $reason;
245
            }
246
        ]);
247
        
248
        return $eachPromise->promise();
249
    }
250
251
252
    /**
253
     * Create the request to be sent.
254
     *
255
     * @param  string $method         The method of the HTTP request
256
     * @param  array  $headers        The header field of the request
257
     * @param  array  $queryParams    The query parameter of the request
258
     * @param  array  $postParameters The HTTP POST parameters
259
     * @param  string $path           URL path
260
     * @param  string $body           Request body
261
     *
262
     * @return \GuzzleHttp\Psr7\Request
263
     */
264
    protected function createRequest(
265
        $method,
266
        array $headers,
267
        array $queryParams,
268
        array $postParameters,
269
        $path,
270
        $locationMode,
271
        $body = Resources::EMPTY_STRING
272
    ) {
273
        if ($locationMode == LocationMode::SECONDARY_ONLY ||
274
            $locationMode == LocationMode::SECONDARY_THEN_PRIMARY) {
275
            $uri = $this->psrSecondaryUri;
276
        } else {
277
            $uri = $this->psrPrimaryUri;
278
        }
279
        
280
        //Append the path, not replacing it.
281
        if ($path != null) {
282
            $exPath = $uri->getPath();
283
            if ($exPath != '') {
284
                //Remove the duplicated slash in the path.
285
                if ($path != '' && $path[0] == '/') {
286
                    $path = $exPath . substr($path, 1);
287
                } else {
288
                    $path = $exPath . $path;
289
                }
290
            }
291
            $uri = $uri->withPath($path);
292
        }
293
294
        // add query parameters into headers
295
        if ($queryParams != null) {
296
            $queryString = Psr7\build_query($queryParams);
297
            $uri = $uri->withQuery($queryString);
298
        }
299
300
        // add post parameters into bodys
301
        $actualBody = null;
302
        if (empty($body)) {
303
            if (empty($headers['content-type'])) {
304
                $headers['content-type'] = 'application/x-www-form-urlencoded';
305
                $actualBody = Psr7\build_query($postParameters);
306
            }
307
        } else {
308
            $actualBody = $body;
309
        }
310
311
        $request = new Request(
312
            $method,
313
            $uri,
314
            $headers,
315
            $actualBody
316
        );
317
318
        //add content-length to header
319
        $bodySize = $request->getBody()->getSize();
320
        if ($bodySize > 0) {
321
            $request = $request->withHeader('content-length', $bodySize);
322
        }
323
        return $request;
324
    }
325
326
    /**
327
     * Create promise of sending HTTP request with the specified parameters.
328
     *
329
     * @param  string         $method         HTTP method used in the request
330
     * @param  array          $headers        HTTP headers.
331
     * @param  array          $queryParams    URL query parameters.
332
     * @param  array          $postParameters The HTTP POST parameters.
333
     * @param  string         $path           URL path
334
     * @param  array|int      $expected       Expected Status Codes.
335
     * @param  string         $body           Request body
336
     * @param  ServiceOptions $options        Service options
0 ignored issues
show
Documentation introduced by
There is no parameter named $options. Did you maybe mean $serviceOptions?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
337
     *
338
     * @return \GuzzleHttp\Promise\PromiseInterface
339
     */
340
    protected function sendAsync(
341
        $method,
342
        array $headers,
343
        array $queryParams,
344
        array $postParameters,
345
        $path,
346
        $expected = Resources::STATUS_OK,
347
        $body = Resources::EMPTY_STRING,
348
        ServiceOptions $serviceOptions = null
349
    ) {
350
        if ($serviceOptions == null) {
351
            $serviceOptions = new ServiceOptions();
352
        }
353
        $this->addOptionalQueryParam(
354
            $queryParams,
355
            Resources::QP_TIMEOUT,
356
            $serviceOptions->getTimeout()
357
        );
358
359
        $request = $this->createRequest(
360
            $method,
361
            $headers,
362
            $queryParams,
363
            $postParameters,
364
            $path,
365
            $serviceOptions->getLocationMode(),
366
            $body
367
        );
368
369
        $client = $this->client;
370
371
        $middlewareStack = $this->createMiddlewareStack($serviceOptions);
372
373
        $sendAsync = function ($request, $options) use ($client) {
374
            return $client->sendAsync($request, $options);
375
        };
376
377
        $handler = $middlewareStack->apply($sendAsync);
378
379
        $requestOptions =
380
            $this->generateRequestOptions($serviceOptions, $handler);
381
382
        if ($request->getMethod() == 'HEAD') {
383
            $requestOptions[Resources::ROS_DECODE_CONTENT] = false;
384
        }
385
386
        $promise = \call_user_func($handler, $request, $requestOptions);
387
388
        return $promise->then(
389
            function ($response) use ($expected, $requestOptions) {
390
                self::throwIfError(
391
                    $response,
392
                    $expected
393
                );
394
395
                return self::addLocationHeaderToResponse(
396
                    $response,
397
                    $requestOptions[Resources::ROS_LOCATION_MODE]
398
                );
399
            },
400
            function ($reason) use ($expected) {
401
                if (!($reason instanceof RequestException)) {
402
                    throw $reason;
403
                }
404
                $response = $reason->getResponse();
405
                if ($response != null) {
406
                    self::throwIfError(
407
                        $response,
408
                        $expected
409
                    );
410
                } else {
411
                    //if could not get response but promise rejected, throw reason.
412
                    throw $reason;
413
                }
414
                return $response;
415
            }
416
        );
417
    }
418
419
    /**
420
     * Generate the request options using the given service options and stored
421
     * information.
422
     *
423
     * @param  ServiceOptions $serviceOptions The service options used to
424
     *                                        generate request options.
425
     * @param  callable       $handler        The handler used to send the
426
     *                                        request.
427
     * @return array
428
     */
429
    protected function generateRequestOptions(
430
        ServiceOptions $serviceOptions,
431
        callable $handler
432
    ) {
433
        $result = array();
434
        $result[Resources::ROS_LOCATION_MODE]  = $serviceOptions->getLocationMode();
435
        $result[Resources::ROS_STREAM]         = $serviceOptions->getIsStreaming();
436
        $result[Resources::ROS_DECODE_CONTENT] = $serviceOptions->getDecodeContent();
437
        $result[Resources::ROS_HANDLER]        = $handler;
438
        $result[Resources::ROS_SECONDARY_URI]  = $this->getPsrSecondaryUri();
439
        $result[Resources::ROS_PRIMARY_URI]    = $this->getPsrPrimaryUri();
440
441
        return $result;
442
    }
443
444
    /**
445
     * Sends the context.
446
     *
447
     * @param  HttpCallContext $context The context of the request.
448
     * @return \GuzzleHttp\Psr7\Response
449
     */
450
    protected function sendContext(HttpCallContext $context)
451
    {
452
        return $this->sendContextAsync($context)->wait();
453
    }
454
455
    /**
456
     * Creates the promise to send the context.
457
     *
458
     * @param  HttpCallContext $context The context of the request.
459
     *
460
     * @return \GuzzleHttp\Promise\PromiseInterface
461
     */
462
    protected function sendContextAsync(HttpCallContext $context)
463
    {
464
        return $this->sendAsync(
465
            $context->getMethod(),
466
            $context->getHeaders(),
467
            $context->getQueryParameters(),
468
            $context->getPostParameters(),
469
            $context->getPath(),
470
            $context->getStatusCodes(),
471
            $context->getBody(),
472
            $context->getServiceOptions()
473
        );
474
    }
475
476
    /**
477
     * Throws ServiceException if the recieved status code is not expected.
478
     *
479
     * @param ResponseInterface $response The response received
480
     * @param array|int         $expected The expected status codes.
481
     *
482
     * @return void
483
     *
484
     * @throws ServiceException
485
     */
486
    public static function throwIfError(ResponseInterface $response, $expected)
487
    {
488
        $expectedStatusCodes = is_array($expected) ? $expected : array($expected);
489
490
        if (!in_array($response->getStatusCode(), $expectedStatusCodes)) {
491
            throw new ServiceException($response);
492
        }
493
    }
494
    
495
    /**
496
     * Adds HTTP POST parameter to the specified
497
     *
498
     * @param array  $postParameters An array of HTTP POST parameters.
499
     * @param string $key            The key of a HTTP POST parameter.
500
     * @param string $value          the value of a HTTP POST parameter.
501
     *
502
     * @return array
503
     */
504
    public function addPostParameter(
505
        array $postParameters,
506
        $key,
507
        $value
508
    ) {
509
        Validate::isArray($postParameters, 'postParameters');
510
        $postParameters[$key] = $value;
511
        return $postParameters;
512
    }
513
514
    /**
515
     * Groups set of values into one value separated with Resources::SEPARATOR
516
     *
517
     * @param array $values array of values to be grouped.
518
     *
519
     * @return string
520
     */
521
    public static function groupQueryValues(array $values)
522
    {
523
        Validate::isArray($values, 'values');
524
        $joined = Resources::EMPTY_STRING;
525
526
        sort($values);
527
528
        foreach ($values as $value) {
529
            if (!is_null($value) && !empty($value)) {
530
                $joined .= $value . Resources::SEPARATOR;
531
            }
532
        }
533
534
        return trim($joined, Resources::SEPARATOR);
535
    }
536
537
    /**
538
     * Adds metadata elements to headers array
539
     *
540
     * @param array $headers  HTTP request headers
541
     * @param array $metadata user specified metadata
542
     *
543
     * @return array
544
     */
545
    protected function addMetadataHeaders(array $headers, array $metadata = null)
546
    {
547
        Utilities::validateMetadata($metadata);
548
549
        $metadata = $this->generateMetadataHeaders($metadata);
550
        $headers  = array_merge($headers, $metadata);
551
552
        return $headers;
553
    }
554
555
    /**
556
     * Generates metadata headers by prefixing each element with 'x-ms-meta'.
557
     *
558
     * @param array $metadata user defined metadata.
559
     *
560
     * @return array
561
     */
562
    public function generateMetadataHeaders(array $metadata = null)
563
    {
564
        $metadataHeaders = array();
565
566
        if (is_array($metadata) && !is_null($metadata)) {
567
            foreach ($metadata as $key => $value) {
568
                $headerName = Resources::X_MS_META_HEADER_PREFIX;
569
                if (strpos($value, "\r") !== false
570
                    || strpos($value, "\n") !== false
571
                ) {
572
                    throw new \InvalidArgumentException(Resources::INVALID_META_MSG);
573
                }
574
575
                // Metadata name is case-presrved and case insensitive
576
                $headerName                     .= $key;
577
                $metadataHeaders[$headerName] = $value;
578
            }
579
        }
580
581
        return $metadataHeaders;
582
    }
583
584
    /**
585
     * Get the primary URI in PSR form.
586
     *
587
     * @return Uri
588
     */
589
    public function getPsrPrimaryUri()
590
    {
591
        return $this->psrPrimaryUri;
592
    }
593
594
    /**
595
     * Get the secondary URI in PSR form.
596
     *
597
     * @return Uri
598
     */
599
    public function getPsrSecondaryUri()
600
    {
601
        return $this->psrSecondaryUri;
602
    }
603
604
    /**
605
     * Adds the header that indicates the location mode to the response header.
606
     *
607
     * @return  ResponseInterface
608
     */
609
    private static function addLocationHeaderToResponse(
610
        ResponseInterface $response,
611
        $locationMode
612
    ) {
613
        //If the response already has this header, return itself.
614
        if ($response->hasHeader(Resources::X_MS_CONTINUATION_LOCATION_MODE)) {
615
            return $response;
616
        }
617
        //Otherwise, add the header that indicates the endpoint to be used if
618
        //continuation token is used for subsequent request. Notice that if the
619
        //response does not have location header set at the moment, it means
620
        //that the user have not set a retry middleware.
621
        if ($locationMode == LocationMode::PRIMARY_THEN_SECONDARY) {
622
            $response = $response->withHeader(
623
                Resources::X_MS_CONTINUATION_LOCATION_MODE,
624
                LocationMode::PRIMARY_ONLY
625
            );
626
        } elseif ($locationMode == LocationMode::SECONDARY_THEN_PRIMARY) {
627
            $response = $response->withHeader(
628
                Resources::X_MS_CONTINUATION_LOCATION_MODE,
629
                LocationMode::SECONDARY_ONLY
630
            );
631
        } elseif ($locationMode == LocationMode::SECONDARY_ONLY  ||
632
                  $locationMode == LocationMode::PRIMARY_ONLY) {
633
            $response = $response->withHeader(
634
                Resources::X_MS_CONTINUATION_LOCATION_MODE,
635
                $locationMode
636
            );
637
        }
638
        return $response;
639
    }
640
}
641