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 Wikimedia\ScopedCallback;
32
33/**
34 * Class used for executing shell commands
35 *
36 * @since 1.30
37 */
38class Command extends UnboxedCommand {
39    private bool $everExecuted = false;
40
41    /** @var string */
42    private $method;
43
44    protected LoggerInterface $logger;
45
46    /**
47     * Don't call directly, instead use Shell::command()
48     *
49     * @param UnboxedExecutor $executor
50     * @throws ShellDisabledError
51     */
52    public function __construct( UnboxedExecutor $executor ) {
53        if ( Shell::isDisabled() ) {
54            throw new ShellDisabledError();
55        }
56        parent::__construct( $executor );
57        $this->setLogger( new NullLogger() );
58    }
59
60    /**
61     * Makes sure the programmer didn't forget to execute the command after all
62     */
63    public function __destruct() {
64        if ( !$this->everExecuted ) {
65            $context = [ 'command' => $this->getCommandString() ];
66            $message = __CLASS__ . " was instantiated, but execute() was never called.";
67            if ( $this->method ) {
68                $message .= ' Calling method: {method}.';
69                $context['method'] = $this->method;
70            }
71            $message .= ' Command: {command}';
72            $this->logger->warning( $message, $context );
73        }
74    }
75
76    /**
77     * @param LoggerInterface $logger
78     */
79    public function setLogger( LoggerInterface $logger ) {
80        $this->logger = $logger;
81        if ( $this->executor ) {
82            $this->executor->setLogger( $logger );
83        }
84    }
85
86    /**
87     * Sets execution limits
88     *
89     * @param array $limits Associative array of limits. Keys (all optional):
90     *   filesize (for ulimit -f), memory, time, walltime.
91     * @return $this
92     */
93    public function limits( array $limits ): Command {
94        if ( !isset( $limits['walltime'] ) && isset( $limits['time'] ) ) {
95            // Emulate the behavior of old wfShellExec() where walltime fell back on time
96            // if the latter was overridden and the former wasn't
97            $limits['walltime'] = $limits['time'];
98        }
99        if ( isset( $limits['filesize'] ) ) {
100            $this->fileSizeLimit( $limits['filesize'] * 1024 );
101        }
102        if ( isset( $limits['memory'] ) ) {
103            $this->memoryLimit( $limits['memory'] * 1024 );
104        }
105        if ( isset( $limits['time'] ) ) {
106            $this->cpuTimeLimit( $limits['time'] );
107        }
108        if ( isset( $limits['walltime'] ) ) {
109            $this->wallTimeLimit( $limits['walltime'] );
110        }
111
112        return $this;
113    }
114
115    /**
116     * Sets calling function for profiler. By default, the caller for execute() will be used.
117     *
118     * @param string $method
119     * @return $this
120     */
121    public function profileMethod( string $method ): Command {
122        $this->method = $method;
123
124        return $this;
125    }
126
127    /**
128     * Sends the provided input to the command. Defaults to an empty string.
129     * If you want to pass stdin through to the command instead, use
130     * passStdin().
131     *
132     * @param string $inputString
133     * @return $this
134     */
135    public function input( string $inputString ): Command {
136        return $this->stdin( $inputString );
137    }
138
139    /**
140     * Set restrictions for this request, overwriting any previously set restrictions.
141     *
142     * Add the "no network" restriction:
143     * @code
144     *     $command->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK );
145     * @endcode
146     *
147     * Allow LocalSettings.php access:
148     * @code
149     *     $command->restrict( Shell::RESTRICT_DEFAULT & ~Shell::NO_LOCALSETTINGS );
150     * @endcode
151     *
152     * Disable all restrictions:
153     * @code
154     *  $command->restrict( Shell::RESTRICT_NONE );
155     * @endcode
156     *
157     * @deprecated since 1.36 Set the options using their separate accessors
158     *
159     * @since 1.31
160     * @param int $restrictions
161     * @return $this
162     */
163    public function restrict( int $restrictions ): Command {
164        $this->privateUserNamespace( (bool)( $restrictions & Shell::NO_ROOT ) );
165        $this->firejailDefaultSeccomp( (bool)( $restrictions & Shell::SECCOMP ) );
166        $this->noNewPrivs( (bool)( $restrictions & Shell::SECCOMP ) );
167        $this->privateDev( (bool)( $restrictions & Shell::PRIVATE_DEV ) );
168        $this->disableNetwork( (bool)( $restrictions & Shell::NO_NETWORK ) );
169        if ( $restrictions & Shell::NO_EXECVE ) {
170            $this->disabledSyscalls( [ 'execve' ] );
171        } else {
172            $this->disabledSyscalls( [] );
173        }
174        if ( $restrictions & Shell::NO_LOCALSETTINGS ) {
175            $this->disallowedPaths( [ realpath( MW_CONFIG_FILE ) ] );
176        } else {
177            $this->disallowedPaths( [] );
178        }
179        if ( $restrictions === 0 ) {
180            $this->disableSandbox();
181        }
182
183        return $this;
184    }
185
186    /**
187     * If called, only the files/directories that are
188     * whitelisted will be available to the shell command.
189     *
190     * limit.sh will always be whitelisted
191     *
192     * @deprecated since 1.36 Use allowPath/disallowPath. Hard
193     *   deprecated in 1.40 and to be removed in 1.41
194     * @param string[] $paths
195     * @return $this
196     */
197    public function whitelistPaths( array $paths ): Command {
198        wfDeprecated( __METHOD__, '1.36' );
199        $this->allowedPaths( array_merge( $this->getAllowedPaths(), $paths ) );
200        return $this;
201    }
202
203    /**
204     * Executes command. Afterwards, getExitCode() and getOutput() can be used to access execution
205     * results.
206     *
207     * @return UnboxedResult
208     * @throws Exception
209     */
210    public function execute(): UnboxedResult {
211        $this->everExecuted = true;
212        $profileMethod = $this->method ?: wfGetCaller();
213        $scoped = Profiler::instance()->scopedProfileIn( __FUNCTION__ . '-' . $profileMethod );
214        $result = parent::execute();
215        ScopedCallback::consume( $scoped );
216        return $result;
217    }
218
219    /**
220     * Returns the final command line before environment/limiting, etc are applied.
221     * Use string conversion only for debugging, don't try to pass this to
222     * some other execution medium.
223     *
224     * @return string
225     */
226    public function __toString(): string {
227        return '#Command: ' . $this->getCommandString();
228    }
229}