Passed
Push — master ( b7d040...fc4767 )
by Alexander
03:47 queued 02:38
created

Command::escapeSpaces()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Alexkart\CurlBuilder;
4
5
use Psr\Http\Message\RequestInterface;
6
7
final class Command
8
{
9
    /**
10
     * Template part which represents command name
11
     */
12
    public const TEMPLATE_COMMAND_NAME = '{name}';
13
14
    /**
15
     * Template part which represents command line options
16
     */
17
    public const TEMPLATE_OPTIONS = '{options}';
18
19
    /**
20
     * Template part which represents url
21
     */
22
    public const TEMPLATE_URL = '{url}';
23
24
    /**
25
     * Single quote character
26
     */
27
    public const QUOTE_SINGLE = "'";
28
29
    /**
30
     * Double quote character
31
     */
32
    public const QUOTE_DOUBLE = '"';
33
34
    /**
35
     * No quote character
36
     */
37
    public const QUOTE_NONE = '';
38
39
    /**
40
     * Command name
41
     */
42
    private const COMMAND_NAME = 'curl';
43
44
    /**
45
     * Header names that can use %x2C (",") character, so multiple header fields can't be folded into a single header field
46
     */
47
    private const HEADER_EXCEPTIONS = [
48
        'set-cookie',
49
        'www-authenticate',
50
        'proxy-authenticate',
51
    ];
52
53
    /**
54
     * @var string built command
55
     */
56
    private $command = '';
57
58
    /**
59
     * @var string url
60
     */
61
    private $url = '';
62
63
    /**
64
     * @var string command template
65
     */
66
    private $template;
67
68
    /**
69
     * @var array<mixed> command line options
70
     */
71
    private $options = [];
72
73
    /**
74
     * Character used to quote arguments
75
     * @var string
76
     */
77
    private $quoteCharacter;
78
79
    /**
80
     * @var RequestInterface|null
81
     */
82
    private $request;
83
84 22
    public function __construct()
85
    {
86 22
        $this->initTemplate();
87 22
        $this->initQuoteCharacter();
88
    }
89
90
    /**
91
     * Generates command
92
     * @return string
93
     */
94 21
    public function build(): string
95
    {
96 21
        $this->setCommand($this->getTemplate());
97 21
        $this->buildName();
98 21
        $this->buildOptions();
99 21
        $this->buildUrl();
100 21
        return $this->getCommand();
101
    }
102
103
    /**
104
     * @param string $url
105
     * @return Command
106
     */
107 19
    public function setUrl(string $url): Command
108
    {
109 19
        $this->url = $url;
110 19
        return $this;
111
    }
112
113
114
    /**
115
     * @return string
116
     */
117 21
    public function getUrl(): string
118
    {
119 21
        return $this->url;
120
    }
121
122
    /**
123
     * @param string $template
124
     * @return Command
125
     */
126 22
    public function setTemplate(string $template): Command
127
    {
128 22
        $this->template = $template;
129 22
        return $this;
130
    }
131
132
    /**
133
     * @return string
134
     */
135 21
    public function getTemplate(): string
136
    {
137 21
        return $this->template;
138
    }
139
140
    /**
141
     * @param array<mixed> $options
142
     * @return Command
143
     */
144 2
    public function setOptions(array $options): Command
145
    {
146 2
        $this->options = $this->toInternalFormat($options);
147 2
        return $this;
148
    }
149
150
    /**
151
     * @return array<mixed>
152
     */
153 21
    public function getOptions(): array
154
    {
155 21
        return $this->options;
156
    }
157
158
    /**
159
     * @param string $option
160
     * @param string|null $argument
161
     * @return Command
162
     */
163 17
    public function addOption(string $option, $argument = null): Command
164
    {
165 17
        $this->options[$option][] = $argument;
166 17
        return $this;
167
    }
168
169
    /**
170
     * @param array<mixed> $options
171
     */
172 2
    public function addOptions(array $options): void
173
    {
174 2
        $options = $this->toInternalFormat($options);
175 2
        foreach ($options as $option => $arguments) {
176 2
            foreach ($arguments as $argument) {
177 2
                $this->addOption((string)$option, $argument);
178
            }
179
        }
180
    }
181
182
    /**
183
     * @return string
184
     */
185 21
    public function getCommand(): string
186
    {
187 21
        return $this->command;
188
    }
189
190
    /**
191
     * @param string $command
192
     */
193 21
    public function setCommand(string $command): void
194
    {
195 21
        $this->command = $command;
196
    }
197
198
    /**
199
     * Inits default command template
200
     */
201 22
    private function initTemplate(): void
202
    {
203 22
        $this->setTemplate(self::TEMPLATE_COMMAND_NAME . self::TEMPLATE_OPTIONS . self::TEMPLATE_URL);
204
    }
205
206
    /**
207
     * Inits default quote character
208
     */
209 22
    private function initQuoteCharacter(): void
210
    {
211 22
        $this->setQuoteCharacter(self::QUOTE_SINGLE);
212
    }
213
214
    /**
215
     * Builds command name
216
     */
217 21
    private function buildName(): void
218
    {
219 21
        $this->buildTemplatePart(self::TEMPLATE_COMMAND_NAME, self::COMMAND_NAME);
220
    }
221
222
    /**
223
     * Builds command line options
224
     */
225 21
    private function buildOptions(): void
226
    {
227 21
        $optionsString = '';
228 21
        $options = $this->getOptions();
229 21
        if (!empty($options)) {
230 19
            foreach ($options as $option => $arguments) {
231 19
                foreach ($arguments as $argument) {
232 19
                    $optionsString .= ' ' . $option;
233 19
                    if ($argument !== null) {
234 10
                        $optionsString .= ' ' . $this->quote($argument);
235
                    }
236
                }
237
            }
238
        }
239 21
        $optionsString = trim($optionsString);
240
241 21
        $this->buildTemplatePart(self::TEMPLATE_OPTIONS, $optionsString);
242
    }
243
244
    /**
245
     * Builds URL
246
     */
247 21
    private function buildUrl(): void
248
    {
249 21
        $this->buildTemplatePart(self::TEMPLATE_URL, $this->getUrl());
250
    }
251
252
    /**
253
     * Builds command part
254
     * @param string $search
255
     * @param string $replace
256
     */
257 21
    private function buildTemplatePart(string $search, string $replace): void
258
    {
259 21
        if ($replace === '') {
260
            // remove extra space
261 6
            $this->setCommand((string)preg_replace($this->getTemplatePartPattern($search), $search, $this->getCommand()));
262
        } else {
263
            // add space
264 21
            $replace = ' ' . $replace;
265
        }
266
267 21
        $this->setCommand(trim(str_replace($search, $replace, $this->getCommand())));
268
    }
269
270
    /**
271
     * Generates regular expression in order to remove extra spaces from the command template if the part is empty
272
     * @param string $search
273
     * @return string
274
     */
275 6
    private function getTemplatePartPattern(string $search): string
276
    {
277 6
        return '/ ?' . preg_quote($search, '/') . ' ?/';
278
    }
279
280
    /**
281
     * @param string $quoteCharacter
282
     * @return Command
283
     */
284 22
    public function setQuoteCharacter(string $quoteCharacter): Command
285
    {
286 22
        $this->quoteCharacter = $quoteCharacter;
287 22
        return $this;
288
    }
289
290
    /**
291
     * @return string
292
     */
293 10
    public function getQuoteCharacter(): string
294
    {
295 10
        return $this->quoteCharacter;
296
    }
297
298
    /**
299
     * Quotes argument
300
     * @param string $argument
301
     * @return string
302
     */
303 10
    private function quote(string $argument): string
304
    {
305 10
        $quoteCharacter = $this->getQuoteCharacter();
306
307 10
        if ($quoteCharacter === '') {
308 2
            return $this->escapeSpaces($argument);
309
        }
310
311 9
        if (strpos($argument, $quoteCharacter) !== false) {
312 1
            if ($quoteCharacter === self::QUOTE_SINGLE) {
313 1
                return '$' . $quoteCharacter . $this->escapeQuotes($argument) . $quoteCharacter;
314
            }
315
316 1
            return $quoteCharacter . $this->escapeQuotes($argument) . $quoteCharacter;
317
        }
318
319 9
        return $quoteCharacter . $argument . $quoteCharacter;
320
    }
321
322
    /**
323
     * Escapes quotes in the argument
324
     * @param string $argument
325
     * @return string
326
     */
327 1
    private function escapeQuotes(string $argument): string
328
    {
329 1
        return str_replace($this->getQuoteCharacter(), '\\' . $this->getQuoteCharacter(), $argument);
330
    }
331
332
    /**
333
     * Escapes spaces in the argument when no quoting is used
334
     * @param string $argument
335
     * @return string
336
     */
337 2
    private function escapeSpaces(string $argument): string
338
    {
339 2
        return str_replace(' ', '\\ ', $argument);
340
    }
341
342
    /**
343
     * Converts option from user-friendly format ot internal format
344
     * @param array<mixed> $options
345
     * @return array<mixed>
346
     */
347 4
    private function toInternalFormat(array $options): array
348
    {
349 4
        $formattedOptions = [];
350 4
        foreach ($options as $option => $arguments) {
351 4
            $option = trim((string)$option);
352
353 4
            if (strpos($option, '-') !== 0) {
354
                // ['-L', '-v']
355 4
                $option = (string)$arguments;
356 4
                $arguments = [null];
357 2
            } elseif (!is_array($arguments)) {
358
                // ['-L' => null, '-v' => null]
359 2
                $arguments = [$arguments];
360
            }
361
362 4
            foreach ($arguments as $argument) {
363 4
                $formattedOptions[$option][] = $argument;
364
            }
365
        }
366
367 4
        return $formattedOptions;
368
    }
369
370
371
    /**
372
     * Sets request. If $parse = true gets data from request
373
     * @param RequestInterface|null $request
374
     * @param bool $parse
375
     * @return Command
376
     */
377 4
    public function setRequest(?RequestInterface $request, bool $parse = true): Command
378
    {
379 4
        $this->request = $request;
380 4
        if ($parse) {
381 4
            $this->parseRequest();
382
        }
383
384 4
        return $this;
385
    }
386
387
    /**
388
     * @return RequestInterface|null
389
     */
390 4
    public function getRequest(): ?RequestInterface
391
    {
392 4
        return $this->request;
393
    }
394
395
396
    /**
397
     * Gets data from request
398
     * @return bool
399
     */
400 4
    public function parseRequest(): bool
401
    {
402 4
        $request = $this->getRequest();
403 4
        if ($request === null) {
404 1
            return false;
405
        }
406
407
        // URL
408 4
        $this->setUrl((string)$request->getUri());
409
410
        // headers
411 4
        foreach (array_keys($request->getHeaders()) as $name) {
412 4
            $name = (string)$name;
413 4
            if (strtolower($name) === 'host') {
414 4
                continue;
415
            }
416 3
            if (in_array(strtolower($name), self::HEADER_EXCEPTIONS, true)) {
417 1
                foreach ($request->getHeader($name) as $value) {
418 1
                    $this->addOption('-H', $name . ': ' . $value);
419
                }
420
            } else {
421 2
                $this->addOption('-H', $name . ': ' . $request->getHeaderLine($name));
422
            }
423
        }
424
425
        // data
426 4
        $data = (string)$request->getBody();
427 4
        if (!empty($data)) {
428 1
            $this->addOption('-d', (string)$request->getBody());
429
        }
430
431 4
        return true;
432
    }
433
}
434