1<?php
2namespace Aws;
3
4use GuzzleHttp\Client;
5use Psr\Http\Message\RequestInterface;
6use GuzzleHttp\ClientInterface;
7use GuzzleHttp\Promise\FulfilledPromise;
8
9//-----------------------------------------------------------------------------
10// Functional functions
11//-----------------------------------------------------------------------------
12
13/**
14 * Returns a function that always returns the same value;
15 *
16 * @param mixed $value Value to return.
17 *
18 * @return callable
19 */
20function constantly($value)
21{
22    return function () use ($value) { return $value; };
23}
24
25/**
26 * Filters values that do not satisfy the predicate function $pred.
27 *
28 * @param mixed    $iterable Iterable sequence of data.
29 * @param callable $pred Function that accepts a value and returns true/false
30 *
31 * @return \Generator
32 */
33function filter($iterable, callable $pred)
34{
35    foreach ($iterable as $value) {
36        if ($pred($value)) {
37            yield $value;
38        }
39    }
40}
41
42/**
43 * Applies a map function $f to each value in a collection.
44 *
45 * @param mixed    $iterable Iterable sequence of data.
46 * @param callable $f        Map function to apply.
47 *
48 * @return \Generator
49 */
50function map($iterable, callable $f)
51{
52    foreach ($iterable as $value) {
53        yield $f($value);
54    }
55}
56
57/**
58 * Creates a generator that iterates over a sequence, then iterates over each
59 * value in the sequence and yields the application of the map function to each
60 * value.
61 *
62 * @param mixed    $iterable Iterable sequence of data.
63 * @param callable $f        Map function to apply.
64 *
65 * @return \Generator
66 */
67function flatmap($iterable, callable $f)
68{
69    foreach (map($iterable, $f) as $outer) {
70        foreach ($outer as $inner) {
71            yield $inner;
72        }
73    }
74}
75
76/**
77 * Partitions the input sequence into partitions of the specified size.
78 *
79 * @param mixed    $iterable Iterable sequence of data.
80 * @param int $size Size to make each partition (except possibly the last chunk)
81 *
82 * @return \Generator
83 */
84function partition($iterable, $size)
85{
86    $buffer = [];
87    foreach ($iterable as $value) {
88        $buffer[] = $value;
89        if (count($buffer) === $size) {
90            yield $buffer;
91            $buffer = [];
92        }
93    }
94
95    if ($buffer) {
96        yield $buffer;
97    }
98}
99
100/**
101 * Returns a function that invokes the provided variadic functions one
102 * after the other until one of the functions returns a non-null value.
103 * The return function will call each passed function with any arguments it
104 * is provided.
105 *
106 *     $a = function ($x, $y) { return null; };
107 *     $b = function ($x, $y) { return $x + $y; };
108 *     $fn = \Aws\or_chain($a, $b);
109 *     echo $fn(1, 2); // 3
110 *
111 * @return callable
112 */
113function or_chain()
114{
115    $fns = func_get_args();
116    return function () use ($fns) {
117        $args = func_get_args();
118        foreach ($fns as $fn) {
119            $result = $args ? call_user_func_array($fn, $args) : $fn();
120            if ($result) {
121                return $result;
122            }
123        }
124        return null;
125    };
126}
127
128//-----------------------------------------------------------------------------
129// JSON compiler and loading functions
130//-----------------------------------------------------------------------------
131
132/**
133 * Loads a compiled JSON file from a PHP file.
134 *
135 * If the JSON file has not been cached to disk as a PHP file, it will be loaded
136 * from the JSON source file and returned.
137 *
138 * @param string $path Path to the JSON file on disk
139 *
140 * @return mixed Returns the JSON decoded data. Note that JSON objects are
141 *     decoded as associative arrays.
142 */
143function load_compiled_json($path)
144{
145    static $compiledList = [];
146
147    $compiledFilepath = "{$path}.php";
148
149    if (!isset($compiledList[$compiledFilepath])) {
150        if (is_readable($compiledFilepath)) {
151            $compiledList[$compiledFilepath] = include($compiledFilepath);
152        }
153    }
154
155    if (isset($compiledList[$compiledFilepath])) {
156        return $compiledList[$compiledFilepath];
157    }
158
159    if (!file_exists($path)) {
160        throw new \InvalidArgumentException(
161            sprintf("File not found: %s", $path)
162        );
163    }
164
165    return json_decode(file_get_contents($path), true);
166}
167
168/**
169 * No-op
170 */
171function clear_compiled_json()
172{
173    // pass
174}
175
176//-----------------------------------------------------------------------------
177// Directory iterator functions.
178//-----------------------------------------------------------------------------
179
180/**
181 * Iterates over the files in a directory and works with custom wrappers.
182 *
183 * @param string   $path Path to open (e.g., "s3://foo/bar").
184 * @param resource $context Stream wrapper context.
185 *
186 * @return \Generator Yields relative filename strings.
187 */
188function dir_iterator($path, $context = null)
189{
190    $dh = $context ? opendir($path, $context) : opendir($path);
191    if (!$dh) {
192        throw new \InvalidArgumentException('File not found: ' . $path);
193    }
194    while (($file = readdir($dh)) !== false) {
195        yield $file;
196    }
197    closedir($dh);
198}
199
200/**
201 * Returns a recursive directory iterator that yields absolute filenames.
202 *
203 * This iterator is not broken like PHP's built-in DirectoryIterator (which
204 * will read the first file from a stream wrapper, then rewind, then read
205 * it again).
206 *
207 * @param string   $path    Path to traverse (e.g., s3://bucket/key, /tmp)
208 * @param resource $context Stream context options.
209 *
210 * @return \Generator Yields absolute filenames.
211 */
212function recursive_dir_iterator($path, $context = null)
213{
214    $invalid = ['.' => true, '..' => true];
215    $pathLen = strlen($path) + 1;
216    $iterator = dir_iterator($path, $context);
217    $queue = [];
218    do {
219        while ($iterator->valid()) {
220            $file = $iterator->current();
221            $iterator->next();
222            if (isset($invalid[basename($file)])) {
223                continue;
224            }
225            $fullPath = "{$path}/{$file}";
226            yield $fullPath;
227            if (is_dir($fullPath)) {
228                $queue[] = $iterator;
229                $iterator = map(
230                    dir_iterator($fullPath, $context),
231                    function ($file) use ($fullPath, $pathLen) {
232                        return substr("{$fullPath}/{$file}", $pathLen);
233                    }
234                );
235                continue;
236            }
237        }
238        $iterator = array_pop($queue);
239    } while ($iterator);
240}
241
242//-----------------------------------------------------------------------------
243// Misc. functions.
244//-----------------------------------------------------------------------------
245
246/**
247 * Debug function used to describe the provided value type and class.
248 *
249 * @param mixed $input
250 *
251 * @return string Returns a string containing the type of the variable and
252 *                if a class is provided, the class name.
253 */
254function describe_type($input)
255{
256    switch (gettype($input)) {
257        case 'object':
258            return 'object(' . get_class($input) . ')';
259        case 'array':
260            return 'array(' . count($input) . ')';
261        default:
262            ob_start();
263            var_dump($input);
264            // normalize float vs double
265            return str_replace('double(', 'float(', rtrim(ob_get_clean()));
266    }
267}
268
269/**
270 * Creates a default HTTP handler based on the available clients.
271 *
272 * @return callable
273 */
274function default_http_handler()
275{
276    $version = guzzle_major_version();
277    // If Guzzle 6 or 7 installed
278    if ($version === 6 || $version === 7) {
279        return new \Aws\Handler\GuzzleV6\GuzzleHandler();
280    }
281
282    // If Guzzle 5 installed
283    if ($version === 5) {
284        return new \Aws\Handler\GuzzleV5\GuzzleHandler();
285    }
286
287    throw new \RuntimeException('Unknown Guzzle version: ' . $version);
288}
289
290/**
291 * Gets the default user agent string depending on the Guzzle version
292 *
293 * @return string
294 */
295function default_user_agent()
296{
297    $version = guzzle_major_version();
298    // If Guzzle 6 or 7 installed
299    if ($version === 6 || $version === 7) {
300        return \GuzzleHttp\default_user_agent();
301    }
302
303    // If Guzzle 5 installed
304    if ($version === 5) {
305        return \GuzzleHttp\Client::getDefaultUserAgent();
306    }
307
308    throw new \RuntimeException('Unknown Guzzle version: ' . $version);
309}
310
311/**
312 * Get the major version of guzzle that is installed.
313 *
314 * @internal This function is internal and should not be used outside aws/aws-sdk-php.
315 * @return int
316 * @throws \RuntimeException
317 */
318function guzzle_major_version()
319{
320    static $cache = null;
321    if (null !== $cache) {
322        return $cache;
323    }
324
325    if (defined('\GuzzleHttp\ClientInterface::VERSION')) {
326        $version = (string) ClientInterface::VERSION;
327        if ($version[0] === '6') {
328            return $cache = 6;
329        }
330        if ($version[0] === '5') {
331            return $cache = 5;
332        }
333    } elseif (defined('\GuzzleHttp\ClientInterface::MAJOR_VERSION')) {
334        return $cache = ClientInterface::MAJOR_VERSION;
335    }
336
337    throw new \RuntimeException('Unable to determine what Guzzle version is installed.');
338}
339
340/**
341 * Serialize a request for a command but do not send it.
342 *
343 * Returns a promise that is fulfilled with the serialized request.
344 *
345 * @param CommandInterface $command Command to serialize.
346 *
347 * @return RequestInterface
348 * @throws \RuntimeException
349 */
350function serialize(CommandInterface $command)
351{
352    $request = null;
353    $handlerList = $command->getHandlerList();
354
355    // Return a mock result.
356    $handlerList->setHandler(
357        function (CommandInterface $_, RequestInterface $r) use (&$request) {
358            $request = $r;
359            return new FulfilledPromise(new Result([]));
360        }
361    );
362
363    call_user_func($handlerList->resolve(), $command)->wait();
364    if (!$request instanceof RequestInterface) {
365        throw new \RuntimeException(
366            'Calling handler did not serialize request'
367        );
368    }
369
370    return $request;
371}
372
373/**
374 * Retrieves data for a service from the SDK's service manifest file.
375 *
376 * Manifest data is stored statically, so it does not need to be loaded more
377 * than once per process. The JSON data is also cached in opcache.
378 *
379 * @param string $service Case-insensitive namespace or endpoint prefix of the
380 *                        service for which you are retrieving manifest data.
381 *
382 * @return array
383 * @throws \InvalidArgumentException if the service is not supported.
384 */
385function manifest($service = null)
386{
387    // Load the manifest and create aliases for lowercased namespaces
388    static $manifest = [];
389    static $aliases = [];
390    if (empty($manifest)) {
391        $manifest = load_compiled_json(__DIR__ . '/data/manifest.json');
392        foreach ($manifest as $endpoint => $info) {
393            $alias = strtolower($info['namespace']);
394            if ($alias !== $endpoint) {
395                $aliases[$alias] = $endpoint;
396            }
397        }
398    }
399
400    // If no service specified, then return the whole manifest.
401    if ($service === null) {
402        return $manifest;
403    }
404
405    // Look up the service's info in the manifest data.
406    $service = strtolower($service);
407    if (isset($manifest[$service])) {
408        return $manifest[$service] + ['endpoint' => $service];
409    }
410
411    if (isset($aliases[$service])) {
412        return manifest($aliases[$service]);
413    }
414
415    throw new \InvalidArgumentException(
416        "The service \"{$service}\" is not provided by the AWS SDK for PHP."
417    );
418}
419
420/**
421 * Checks if supplied parameter is a valid hostname
422 *
423 * @param string $hostname
424 * @return bool
425 */
426function is_valid_hostname($hostname)
427{
428    return (
429        preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*\.?$/i", $hostname)
430        && preg_match("/^.{1,253}$/", $hostname)
431        && preg_match("/^[^\.]{1,63}(\.[^\.]{0,63})*$/", $hostname)
432    );
433}
434
435/**
436 * Checks if supplied parameter is a valid host label
437 *
438 * @param $label
439 * @return bool
440 */
441function is_valid_hostlabel($label)
442{
443    return preg_match("/^(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$/", $label);
444}
445
446/**
447 * Ignores '#' full line comments, which parse_ini_file no longer does
448 * in PHP 7+.
449 *
450 * @param $filename
451 * @param bool $process_sections
452 * @param int $scanner_mode
453 * @return array|bool
454 */
455function parse_ini_file(
456    $filename,
457    $process_sections = false,
458    $scanner_mode = INI_SCANNER_NORMAL)
459{
460    return parse_ini_string(
461        preg_replace('/^#.*\\n/m', "", file_get_contents($filename)),
462        $process_sections,
463        $scanner_mode
464    );
465}
466
467/**
468 * Outputs boolean value of input for a select range of possible values,
469 * null otherwise
470 *
471 * @param $input
472 * @return bool|null
473 */
474function boolean_value($input)
475{
476    if (is_bool($input)) {
477        return $input;
478    }
479
480    if ($input === 0) {
481        return false;
482    }
483
484    if ($input === 1) {
485        return true;
486    }
487
488    if (is_string($input)) {
489        switch (strtolower($input)) {
490            case "true":
491            case "on":
492            case "1":
493                return true;
494                break;
495
496            case "false":
497            case "off":
498            case "0":
499                return false;
500                break;
501        }
502    }
503    return null;
504}
505
506/**
507 * Checks if an input is a valid epoch time
508 *
509 * @param $input
510 * @return bool
511 */
512function is_valid_epoch($input)
513{
514    if (is_string($input) || is_numeric($input)) {
515        if (is_string($input) && !preg_match("/^-?[0-9]+\.?[0-9]*$/", $input)) {
516            return false;
517        }
518        return true;
519    }
520    return false;
521}
522