Escape::shellArg()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 4
nc 3
nop 2
1
<?php
2
namespace Consolidation\SiteProcess\Util;
3
4
use Consolidation\SiteAlias\SiteAliasInterface;
5
use Symfony\Component\Process\Process;
6
use Consolidation\Config\Util\Interpolator;
7
use Symfony\Component\Console\Output\OutputInterface;
8
use Consolidation\SiteProcess\Util\ShellOperatorInterface;
9
10
/**
11
 * Escape will shell-escape commandline arguments for different platforms.
12
 */
13
class Escape
14
{
15
    /**
16
     * argsForSite escapes each argument in an array for the given site.
17
     */
18
    public static function argsForSite(SiteAliasInterface $siteAlias, $args)
19
    {
20
        return array_map(
21
            function ($arg) use ($siteAlias) {
22
                return Escape::forSite($siteAlias, $arg);
23
            },
24
            $args
25
        );
26
    }
27
28
    /**
29
     * forSite escapes the provided argument for the specified alias record.
30
     */
31
    public static function forSite(SiteAliasInterface $siteAlias, $arg)
32
    {
33
        return static::shellArg($arg, $siteAlias->os());
34
    }
35
36
    /**
37
     * shellArg escapes the provided argument for the specified OS
38
     *
39
     * @param string|ShellOperatorInterface $arg The argument to escape
40
     * @param string|null $os The OS to escape for. Optional; defaults to LINUX
41
     *
42
     * @return string The escaped string
43
     */
44
    public static function shellArg($arg, $os = null)
45
    {
46
        // Short-circuit escaping for simple params (keep stuff readable);
47
        // also skip escaping for shell operators (e.g. &&), which must not
48
        // be escaped.
49
        if (($arg instanceof ShellOperatorInterface) || preg_match('|^[a-zA-Z0-9@=.:/_-]*$|', $arg)) {
50
            return (string) $arg;
51
        }
52
53
        if (static::isWindows($os)) {
54
            return static::windowsArg($arg);
55
        }
56
        return static::linuxArg($arg);
57
    }
58
59
    /**
60
     * isWindows determines whether the provided OS is Windows.
61
     *
62
     * @param string|null $os The OS to escape for.
63
     *
64
     * @return boolean
65
     */
66
    public static function isWindows($os = null)
67
    {
68
        // In most cases, $os will be NULL and PHP_OS will be returned. However,
69
        // if an OS is specified in $os, return that instead.
70
        $os = $os ?: PHP_OS;
71
        return strtoupper(substr($os, 0, 3)) === 'WIN';
72
    }
73
74
    /**
75
     * linuxArg is the Linux version of escapeshellarg().
76
     *
77
     * This is intended to work the same way that escapeshellarg() does on
78
     * Linux.  If we need to escape a string that will be used remotely on
79
     * a Linux system, then we need our own implementation of escapeshellarg,
80
     * because the Windows version behaves differently.
81
     *
82
     * Note that we behave somewhat differently than the built-in escapeshellarg()
83
     * with respect to whitespace replacement in order
84
     *
85
     * @param string $arg The argument to escape
86
     *
87
     * @return string The escaped string
88
     */
89
    public static function linuxArg($arg)
90
    {
91
        // For single quotes existing in the string, we will "exit"
92
        // single-quote mode, add a \' and then "re-enter"
93
        // single-quote mode.  The result of this is that
94
        // 'quote' becomes '\''quote'\''
95
        $arg = preg_replace('/\'/', '\'\\\'\'', $arg);
96
97
        // Replace "\t", "\n", "\r", "\0", "\x0B" with a whitespace.
98
        // Note that this replacement makes Drush's escapeshellarg work differently
99
        // than the built-in escapeshellarg in PHP on Linux, as these characters
100
        // usually are NOT replaced. However, this was done deliberately to be more
101
        // conservative when running _drush_escapeshellarg_linux on Windows
102
        // (this can happen when generating a command to run on a remote Linux server.)
103
        //
104
        // TODO: Perhaps we should only do this if the local system is Windows?
105
        // n.b. that would be a little more complicated to test.
106
        $arg = str_replace(["\t", "\n", "\r", "\0", "\x0B"], ' ', $arg);
107
108
        // Add surrounding quotes.
109
        $arg = "'" . $arg . "'";
110
111
        return $arg;
112
    }
113
114
    /**
115
     * windowsArg is the Windows version of escapeshellarg().
116
     *
117
     * @param string $arg The argument to escape
118
     *
119
     * @return string The escaped string
120
     */
121
    public static function windowsArg($arg)
122
    {
123
        if ('' === $arg || null === $arg) {
124
            return '""';
125
        }
126
        if (false !== strpos($arg, "\0")) {
127
            $arg = str_replace("\0", '?', $arg);
128
        }
129
        if (!preg_match('/[\/()%!^"<>&|\s]/', $arg)) {
130
            return $arg;
131
        }
132
        // Double up existing backslashes
133
        $arg = preg_replace('/(\\\\+)$/', '$1$1', $arg);
134
135
        // Replacing whitespace for good measure (see comment above).
136
        $arg = str_replace(["\t", "\n", "\r", "\0", "\x0B"], ' ', $arg);
137
138
        $arg = str_replace(['"', '^', '%', '!'], ['""', '"^^"', '"^%"', '"^!"'], $arg);
139
140
        // Add surrounding quotes.
141
        $arg = '"' . $arg . '"';
142
143
        return $arg;
144
    }
145
}
146