Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.62% covered (warning)
84.62%
22 / 26
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Shell
84.62% covered (warning)
84.62%
22 / 26
75.00% covered (warning)
75.00%
3 / 4
8.23
0.00% covered (danger)
0.00%
0 / 1
 command
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 isDisabled
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 escape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeScriptCommand
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Class used for executing shell commands
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 */
22
23namespace MediaWiki\Shell;
24
25use MediaWiki\HookContainer\HookRunner;
26use MediaWiki\MainConfigNames;
27use MediaWiki\MediaWikiServices;
28use Shellbox\Shellbox;
29
30/**
31 * Executes shell commands
32 *
33 * @since 1.30
34 *
35 * Use call chaining with this class for expressiveness:
36 *  $result = Shell::command( 'some command' )
37 *       ->input( 'foo' )
38 *       ->environment( [ 'ENVIRONMENT_VARIABLE' => 'VALUE' ] )
39 *       ->limits( [ 'time' => 300 ] )
40 *       ->execute();
41 *
42 *  ... = $result->getExitCode();
43 *  ... = $result->getStdout();
44 *  ... = $result->getStderr();
45 */
46class Shell {
47
48    /**
49     * Disallow any root access. Any setuid binaries
50     * will be run without elevated access.
51     *
52     * @since 1.31
53     */
54    public const NO_ROOT = 1;
55
56    /**
57     * Use seccomp to block dangerous syscalls
58     * @see <https://en.wikipedia.org/wiki/seccomp>
59     *
60     * @since 1.31
61     */
62    public const SECCOMP = 2;
63
64    /**
65     * Create a private /dev
66     *
67     * @since 1.31
68     */
69    public const PRIVATE_DEV = 4;
70
71    /**
72     * Restrict the request to have no
73     * network access
74     *
75     * @since 1.31
76     */
77    public const NO_NETWORK = 8;
78
79    /**
80     * Deny execve syscall with seccomp
81     * @see <https://en.wikipedia.org/wiki/exec_(system_call)>
82     *
83     * @since 1.31
84     */
85    public const NO_EXECVE = 16;
86
87    /**
88     * Deny access to LocalSettings.php (MW_CONFIG_FILE)
89     *
90     * @since 1.31
91     */
92    public const NO_LOCALSETTINGS = 32;
93
94    /**
95     * Apply a default set of restrictions for improved
96     * security out of the box.
97     *
98     * @note This value will change over time to provide increased security
99     *       by default, and is not guaranteed to be backwards-compatible.
100     * @since 1.31
101     */
102    public const RESTRICT_DEFAULT = self::NO_ROOT | self::SECCOMP | self::PRIVATE_DEV |
103                                    self::NO_LOCALSETTINGS;
104
105    /**
106     * Don't apply any restrictions
107     *
108     * @since 1.31
109     */
110    public const RESTRICT_NONE = 0;
111
112    /**
113     * Returns a new instance of Command class
114     *
115     * @note You should check Shell::isDisabled() before calling this
116     * @param string|string[] ...$commands String or array of strings representing the command to
117     * be executed, each value will be escaped.
118     *   Example:   [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'"
119     * @return Command
120     */
121    public static function command( ...$commands ): Command {
122        if ( count( $commands ) === 1 && is_array( reset( $commands ) ) ) {
123            // If only one argument has been passed, and that argument is an array,
124            // treat it as a list of arguments
125            $commands = reset( $commands );
126        }
127        $command = MediaWikiServices::getInstance()
128            ->getShellCommandFactory()
129            ->create();
130
131        return $command->params( $commands );
132    }
133
134    /**
135     * Check if this class is effectively disabled via php.ini config
136     *
137     * @return bool
138     */
139    public static function isDisabled(): bool {
140        static $disabled = null;
141
142        if ( $disabled === null ) {
143            if ( !function_exists( 'proc_open' ) ) {
144                wfDebug( "proc_open() is disabled" );
145                $disabled = true;
146            } else {
147                $disabled = false;
148            }
149        }
150
151        return $disabled;
152    }
153
154    /**
155     * Locale-independent version of escapeshellarg()
156     *
157     * Originally, this fixed the incorrect use of single quotes on Windows
158     * (https://bugs.php.net/bug.php?id=26285) and the locale problems on Linux in
159     * PHP 5.2.6+ (https://bugs.php.net/bug.php?id=54391). The second bug is still
160     * open as of 2021.
161     *
162     * @param string|string[] ...$args strings to escape and glue together, or a single
163     *     array of strings parameter. Null values are ignored.
164     * @return string
165     */
166    public static function escape( ...$args ): string {
167        return Shellbox::escape( ...$args );
168    }
169
170    /**
171     * Generate a Command object to run a MediaWiki maintenance script.
172     * Note that $parameters should be a flat array and an option with an argument
173     * should consist of two consecutive items in the array (do not use "--option value").
174     *
175     * @note You should check Shell::isDisabled() before calling this
176     * @param string $script MediaWiki CLI script in a form accepted by run.php, e.g.
177     *        an absolute path, a class name, or the plain name of a script in the
178     *        maintenance directory.
179     * @param string[] $parameters Arguments and options to the script
180     * @param array $options Associative array of options:
181     *     'php': The path to the php executable
182     *     'wrapper': Path to a wrapper to run the maintenance script
183     * @phan-param array{php?:string,wrapper?:string} $options
184     * @return Command
185     */
186    public static function makeScriptCommand(
187        string $script, array $parameters, array $options = []
188    ): Command {
189        $services = MediaWikiServices::getInstance();
190        $phpCli = $services->getMainConfig()->get( MainConfigNames::PhpCli );
191        // Give site config file a chance to run the script in a wrapper.
192        // The caller may likely want to call wfBasename() on $script.
193        ( new HookRunner( $services->getHookContainer() ) )->onWfShellWikiCmd( $script, $parameters, $options );
194        $cmd = [];
195        $cmd[] = $options['php'] ?? $phpCli;
196        $cmd[] = $options['wrapper'] ?? ( MW_INSTALL_PATH . '/maintenance/run.php' );
197        $cmd[] = $script;
198
199        return self::command( $cmd )
200            ->params( $parameters )
201            // Not much point in trying to sandbox maintenance scripts when they run unsandboxed
202            // most of the time, and doing so can cause permission problems.
203            ->disableSandbox();
204    }
205
206}