Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.58% covered (warning)
73.58%
39 / 53
30.00% covered (danger)
30.00%
3 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Command
73.58% covered (warning)
73.58%
39 / 53
30.00% covered (danger)
30.00%
3 / 10
34.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 __destruct
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 setLogger
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 limits
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
9.36
 profileMethod
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 input
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 restrict
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
4.01
 whitelistPaths
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 execute
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Shell;
22
23use Exception;
24use MediaWiki\ShellDisabledError;
25use Profiler;
26use Psr\Log\LoggerInterface;
27use Psr\Log\NullLogger;
28use Shellbox\Command\UnboxedCommand;
29use Shellbox\Command\UnboxedExecutor;
30use Shellbox\Command\UnboxedResult;
31use Stringable;
32use Wikimedia\ScopedCallback;
33
34/**
35 * Class used for executing shell commands
36 *
37 * @since 1.30
38 */
39class Command extends UnboxedCommand implements Stringable {
40    private bool $everExecuted = false;
41
42    /** @var string */
43    private $method;
44
45    protected LoggerInterface $logger;
46
47    /**
48     * Don't call directly, instead use Shell::command()
49     *
50     * @param UnboxedExecutor $executor
51     * @throws ShellDisabledError
52     */
53    public function __construct( UnboxedExecutor $executor ) {
54        if ( Shell::isDisabled() ) {
55            throw new ShellDisabledError();
56        }
57        parent::__construct( $executor );
58        $this->setLogger( new NullLogger() );
59    }
60
61    /**
62     * Makes sure the programmer didn't forget to execute the command after all
63     */
64    public function __destruct() {
65        if ( !$this->everExecuted ) {
66            $context = [ 'command' => $this->getCommandString() ];
67            $message = __CLASS__ . " was instantiated, but execute() was never called.";
68            if ( $this->method ) {
69                $message .= ' Calling method: {method}.';
70                $context['method'] = $this->method;
71            }
72            $message .= ' Command: {command}';
73            $this->logger->warning( $message, $context );
74        }
75    }
76
77    /**
78     * @param LoggerInterface $logger
79     */
80    public function setLogger( LoggerInterface $logger ) {
81        $this->logger = $logger;
82        if ( $this->executor ) {
83            $this->executor->setLogger( $logger );
84        }
85    }
86
87    /**
88     * Sets execution limits
89     *
90     * @param array $limits Associative array of limits. Keys (all optional):
91     *   filesize (for ulimit -f), memory, time, walltime.
92     * @return $this
93     */
94    public function limits( array $limits ): Command {
95        if ( !isset( $limits['walltime'] ) && isset( $limits['time'] ) ) {
96            // Emulate the behavior of old wfShellExec() where walltime fell back on time
97            // if the latter was overridden and the former wasn't
98            $limits['walltime'] = $limits['time'];
99        }
100        if ( isset( $limits['filesize'] ) ) {
101            $this->fileSizeLimit( $limits['filesize'] * 1024 );
102        }
103        if ( isset( $limits['memory'] ) ) {
104            $this->memoryLimit( $limits['memory'] * 1024 );
105        }
106        if ( isset( $limits['time'] ) ) {
107            $this->cpuTimeLimit( $limits['time'] );
108        }
109        if ( isset( $limits['walltime'] ) ) {
110            $this->wallTimeLimit( $limits['walltime'] );
111        }
112
113        return $this;
114    }
115
116    /**
117     * Sets calling function for profiler. By default, the caller for execute() will be used.
118     *
119     * @param string $method
120     * @return $this
121     */
122    public function profileMethod( string $method ): Command {
123        $this->method = $method;
124
125        return $this;
126    }
127
128    /**
129     * Sends the provided input to the command. Defaults to an empty string.
130     * If you want to pass stdin through to the command instead, use
131     * passStdin().
132     *
133     * @param string $inputString
134     * @return $this
135     */
136    public function input( string $inputString ): Command {
137        return $this->stdin( $inputString );
138    }
139
140    /**
141     * Set restrictions for this request, overwriting any previously set restrictions.
142     *
143     * Add the "no network" restriction:
144     * @code
145     *     $command->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK );
146     * @endcode
147     *
148     * Allow LocalSettings.php access:
149     * @code
150     *     $command->restrict( Shell::RESTRICT_DEFAULT & ~Shell::NO_LOCALSETTINGS );
151     * @endcode
152     *
153     * Disable all restrictions:
154     * @code
155     *  $command->restrict( Shell::RESTRICT_NONE );
156     * @endcode
157     *
158     * @deprecated since 1.36 Set the options using their separate accessors
159     *
160     * @since 1.31
161     * @param int $restrictions
162     * @return $this
163     */
164    public function restrict( int $restrictions ): Command {
165        $this->privateUserNamespace( (bool)( $restrictions & Shell::NO_ROOT ) );
166        $this->firejailDefaultSeccomp( (bool)( $restrictions & Shell::SECCOMP ) );
167        $this->noNewPrivs( (bool)( $restrictions & Shell::SECCOMP ) );
168        $this->privateDev( (bool)( $restrictions & Shell::PRIVATE_DEV ) );
169        $this->disableNetwork( (bool)( $restrictions & Shell::NO_NETWORK ) );
170        if ( $restrictions & Shell::NO_EXECVE ) {
171            $this->disabledSyscalls( [ 'execve' ] );
172        } else {
173            $this->disabledSyscalls( [] );
174        }
175        if ( $restrictions & Shell::NO_LOCALSETTINGS ) {
176            $this->disallowedPaths( [ realpath( MW_CONFIG_FILE ) ] );
177        } else {
178            $this->disallowedPaths( [] );
179        }
180        if ( $restrictions === 0 ) {
181            $this->disableSandbox();
182        }
183
184        return $this;
185    }
186
187    /**
188     * If called, only the files/directories that are
189     * whitelisted will be available to the shell command.
190     *
191     * limit.sh will always be whitelisted
192     *
193     * @deprecated since 1.36 Use allowPath/disallowPath. Hard
194     *   deprecated in 1.40 and to be removed in 1.41
195     * @param string[] $paths
196     * @return $this
197     */
198    public function whitelistPaths( array $paths ): Command {
199        wfDeprecated( __METHOD__, '1.36' );
200        $this->allowedPaths( array_merge( $this->getAllowedPaths(), $paths ) );
201        return $this;
202    }
203
204    /**
205     * Executes command. Afterwards, getExitCode() and getOutput() can be used to access execution
206     * results.
207     *
208     * @return UnboxedResult
209     * @throws Exception
210     */
211    public function execute(): UnboxedResult {
212        $this->everExecuted = true;
213        $profileMethod = $this->method ?: wfGetCaller();
214        $scoped = Profiler::instance()->scopedProfileIn( __FUNCTION__ . '-' . $profileMethod );
215        $result = parent::execute();
216        ScopedCallback::consume( $scoped );
217        return $result;
218    }
219
220    /**
221     * Returns the final command line before environment/limiting, etc are applied.
222     * Use string conversion only for debugging, don't try to pass this to
223     * some other execution medium.
224     *
225     * @return string
226     */
227    public function __toString(): string {
228        return '#Command: ' . $this->getCommandString();
229    }
230}