Completed
Push — master ( b02ee6...031221 )
by Marcel
02:16
created

Exec::escapeWindowsArgument()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 29
rs 6.7272
cc 7
eloc 20
nc 8
nop 1
1
<?php
2
namespace nochso\Omni;
3
4
/**
5
 * Exec creates objects that help manage `\exec()` calls.
6
 *
7
 * The returned object itself is callable, which is the same as calling `run()`.
8
 *
9
 * Arguments are automatically escaped if needed.
10
 *
11
 * Methods `run()`, `create()` and `__invoke()` take any amount of arguments.
12
 * If you have an array of arguments, unpack it first: `run(...$args)`
13
 *
14
 * @see \nochso\Omni\OS::hasBinary Check if the binary/command is available before you run it.
15
 */
16
class Exec
17
{
18
    /**
19
     * @var string[]
20
     */
21
    private $prefixes;
22
    /**
23
     * @var string[]
24
     */
25
    private $output;
26
    /**
27
     * @var int
28
     */
29
    private $status;
30
    /**
31
     * @var string
32
     */
33
    private $lastCommand;
34
35
    /**
36
     * Create a new callable `Exec` object.
37
     *
38
     * @param string[] $prefixes,... Optional arguments will always be added to the beginning of the command.
0 ignored issues
show
Documentation introduced by
There is no parameter named $prefixes,.... Did you maybe mean $prefixes?

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...
39
     *
40
     * @return \nochso\Omni\Exec
41
     */
42
    public static function create(...$prefixes)
43
    {
44
        $exec = new self();
45
        $exec->prefixes = $prefixes;
46
        return $exec;
47
    }
48
49
    /**
50
     * Run a command with auto-escaped arguments.
51
     *
52
     * @param string[] $arguments,... Optional arguments will be added after the prefixes.
0 ignored issues
show
Documentation introduced by
There is no parameter named $arguments,.... Did you maybe mean $arguments?

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...
53
     *
54
     * @return $this
55
     */
56
    public function run(...$arguments)
57
    {
58
        $this->lastCommand = $this->getCommand(...$arguments);
59
        exec($this->lastCommand, $output, $status);
60
        $this->output = $output;
0 ignored issues
show
Documentation Bug introduced by
It seems like $output can be null. However, the property $output is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
61
        $this->status = $status;
62
        return $this;
63
    }
64
65
    /**
66
     * getCommand returns the string to be used by `\exec()`.
67
     *
68
     * @param string[] $arguments,...
0 ignored issues
show
Documentation introduced by
There is no parameter named $arguments,.... Did you maybe mean $arguments?

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...
69
     *
70
     * @return string
71
     */
72
    public function getCommand(...$arguments)
73
    {
74
        $command = [];
75
        $allArguments = array_merge($this->prefixes, $arguments);
76
        if (count($allArguments) === 0) {
77
            $allArguments[] = '';
78
        }
79
        foreach ($allArguments as $argument) {
80
            $command[] = $this->escapeArgument($argument);
81
        }
82
        $commandString = implode(' ', $command);
83
        return $commandString;
84
    }
85
86
    /**
87
     * getLastCommand returns the string last used by a previous call to `run()`.
88
     *
89
     * @return string|null
90
     */
91
    public function getLastCommand()
92
    {
93
        return $this->lastCommand;
94
    }
95
96
    /**
97
     * getOutput of last execution.
98
     *
99
     * @return string[]
100
     */
101
    public function getOutput()
102
    {
103
        return $this->output;
104
    }
105
106
    /**
107
     * getStatus code of last execution.
108
     *
109
     * @return int
110
     */
111
    public function getStatus()
112
    {
113
        return $this->status;
114
    }
115
116
    /**
117
     * __invoke allows using this object as a callable by calling `run()`.
118
     *
119
     * e.g. `$runner('argument');`
120
     *
121
     * @param array $arguments,...
0 ignored issues
show
Documentation introduced by
There is no parameter named $arguments,.... Did you maybe mean $arguments?

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...
122
     *
123
     * @return \nochso\Omni\Exec
124
     */
125
    public function __invoke(...$arguments)
126
    {
127
        return $this->run(...$arguments);
128
    }
129
130
    /**
131
     * @param string $argument
132
     *
133
     * @return string
134
     */
135
    private function escapeArgument($argument)
136
    {
137
        // Always escape an empty argument so it doesn't get lost.
138
        if ($argument === '') {
139
            return escapeshellarg($argument);
140
        }
141
        if (!OS::isWindows()) {
142
            return $this->escapeLinuxArgument($argument);
143
        }
144
        return $this->escapeWindowsArgument($argument);
145
    }
146
147
    /**
148
     * @param string $argument
149
     *
150
     * @return string
151
     *
152
     * @link https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
153
     */
154
    private function escapeLinuxArgument($argument)
155
    {
156
        $escapedArgument = escapeshellarg($argument);
157
        // Is escaping really needed?
158
        if (
159
            $argument !== '--'                                  // Separator must be escaped
160
            && mb_substr($escapedArgument, 1, -1) === $argument // Test if escapeshellarg actually changed anything
161
            && preg_match('/^[a-z0-9-]+$/i', $argument) === 1   // Simple arguments don't have to be escaped
162
        ) {
163
            return $argument;
164
        }
165
        return $escapedArgument;
166
    }
167
168
    /**
169
     * @param string $argument
170
     *
171
     * @return string
172
     */
173
    private function escapeWindowsArgument($argument)
174
    {
175
        // Check if there's anything to escape
176
        if (strpbrk($argument, " \t\n\v\"\\") === false) {
177
            return $argument;
178
        }
179
        $escapedArgument = '"';
180
        $strlen = mb_strlen($argument);
181
        for ($i = 0; $i < $strlen; $i++) {
182
            $backslashes = 0;
183
            while ($i < $strlen && mb_substr($argument, $i, 1) === '\\') {
184
                $i++;
185
                $backslashes++;
186
            }
187
            if ($i === $strlen) {
188
                // Escape all backslashes, but let the terminating double quote be interpreted as a meta character
189
                $escapedArgument .= str_repeat('\\', $backslashes * 2);
190
            } elseif (mb_substr($argument, $i, 1) === '"') {
191
                // Escape all backslashes and the following quotation mark
192
                $escapedArgument .= str_repeat('\\', $backslashes * 2 + 1);
193
                $escapedArgument .= '"';
194
            } else {
195
                $escapedArgument .= str_repeat('\\', $backslashes);
196
                $escapedArgument .= mb_substr($argument, $i, 1);
197
            }
198
        }
199
        $escapedArgument .= '"';
200
        return $escapedArgument;
201
    }
202
}
203