Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.62% covered (danger)
15.62%
10 / 64
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommandFactory
15.62% covered (danger)
15.62%
10 / 64
0.00% covered (danger)
0.00%
0 / 6
212.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 findFirejail
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 logStderr
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLocalShellboxOptions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 create
45.45% covered (danger)
45.45%
10 / 22
0.00% covered (danger)
0.00%
0 / 1
11.84
 createBoxed
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
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 ExecutableFinder;
24use Psr\Log\LoggerAwareTrait;
25use Psr\Log\NullLogger;
26use Shellbox\Command\BoxedCommand;
27use Shellbox\Command\RemoteBoxedExecutor;
28use Shellbox\Shellbox;
29
30/**
31 * Factory facilitating dependency injection for Command
32 *
33 * @since 1.30
34 */
35class CommandFactory {
36    use LoggerAwareTrait;
37
38    /** @var array */
39    private $limits;
40
41    /** @var string|bool */
42    private $cgroup;
43
44    /** @var bool */
45    private $doLogStderr = false;
46
47    /**
48     * @var string|bool
49     */
50    private $restrictionMethod;
51
52    /**
53     * @var string|bool|null
54     */
55    private $firejail;
56
57    /** @var bool */
58    private $useAllUsers;
59
60    /** @var ShellboxClientFactory */
61    private $shellboxClientFactory;
62
63    /**
64     * @param ShellboxClientFactory $shellboxClientFactory
65     * @param array $limits See {@see Command::limits()}
66     * @param string|bool $cgroup
67     * @param string|bool $restrictionMethod
68     */
69    public function __construct( ShellboxClientFactory $shellboxClientFactory,
70        array $limits, $cgroup, $restrictionMethod
71    ) {
72        $this->shellboxClientFactory = $shellboxClientFactory;
73        $this->limits = $limits;
74        $this->cgroup = $cgroup;
75        if ( $restrictionMethod === 'autodetect' ) {
76            // On Linux systems check for firejail
77            if ( PHP_OS === 'Linux' && $this->findFirejail() ) {
78                $this->restrictionMethod = 'firejail';
79            } else {
80                $this->restrictionMethod = false;
81            }
82        } else {
83            $this->restrictionMethod = $restrictionMethod;
84        }
85        $this->setLogger( new NullLogger() );
86    }
87
88    /**
89     * @return bool|string
90     */
91    protected function findFirejail() {
92        if ( $this->firejail === null ) {
93            $this->firejail = ExecutableFinder::findInDefaultPaths( 'firejail' );
94        }
95
96        return $this->firejail;
97    }
98
99    /**
100     * When enabled, text sent to stderr will be logged with a level of 'error'.
101     *
102     * @param bool $yesno
103     * @see Command::logStderr
104     */
105    public function logStderr( bool $yesno = true ): void {
106        $this->doLogStderr = $yesno;
107    }
108
109    /**
110     * Get the options which will be used for local unboxed execution.
111     * Shellbox should be configured to act in an approximately backwards
112     * compatible way, equivalent to the pre-Shellbox MediaWiki shell classes.
113     *
114     * @return array
115     */
116    private function getLocalShellboxOptions() {
117        $options = [
118            'tempDir' => wfTempDir(),
119            'useBashWrapper' => file_exists( '/bin/bash' ),
120            'cgroup' => $this->cgroup
121        ];
122        if ( $this->restrictionMethod === 'firejail' ) {
123            $firejailPath = $this->findFirejail();
124            if ( !$firejailPath ) {
125                throw new \RuntimeException( 'firejail is enabled, but cannot be found' );
126            }
127            $options['useFirejail'] = true;
128            $options['firejailPath'] = $firejailPath;
129            $options['firejailProfile'] = __DIR__ . '/firejail.profile';
130        }
131        return $options;
132    }
133
134    /**
135     * Instantiates a new Command
136     *
137     * @return Command
138     */
139    public function create(): Command {
140        $allUsers = false;
141        if ( $this->restrictionMethod === 'firejail' ) {
142            if ( $this->useAllUsers === null ) {
143                global $IP;
144                // In case people are doing funny things with symlinks
145                // or relative paths, resolve them all.
146                $realIP = realpath( $IP );
147                $currentUser = posix_getpwuid( posix_geteuid() );
148                $this->useAllUsers = str_starts_with( $realIP, '/home/' )
149                    && !str_starts_with( $realIP, $currentUser['dir'] );
150                if ( $this->useAllUsers ) {
151                    $this->logger->warning( 'firejail: MediaWiki is located ' .
152                        'in a home directory that does not belong to the ' .
153                        'current user, so allowing access to all home ' .
154                        'directories (--allusers)' );
155                }
156            }
157            $allUsers = $this->useAllUsers;
158        }
159        $executor = Shellbox::createUnboxedExecutor(
160            $this->getLocalShellboxOptions(), $this->logger );
161
162        $command = new Command( $executor );
163        $command->setLogger( $this->logger );
164        if ( $allUsers ) {
165            $command->allowPath( '/home' );
166        }
167        return $command
168            ->limits( $this->limits )
169            ->logStderr( $this->doLogStderr );
170    }
171
172    /**
173     * Instantiates a new BoxedCommand.
174     *
175     * @since 1.36
176     * @param ?string $service Name of Shellbox (as configured in
177     *                         $wgShellboxUrls) that should be used
178     * @return BoxedCommand
179     */
180    public function createBoxed( ?string $service = null ): BoxedCommand {
181        if ( $this->shellboxClientFactory->isEnabled( $service ) ) {
182            $client = $this->shellboxClientFactory->getClient( [
183                'timeout' => $this->limits['walltime'] + 1,
184                'service' => $service,
185            ] );
186            $executor = new RemoteBoxedExecutor( $client );
187            $executor->setLogger( $this->logger );
188        } else {
189            $executor = Shellbox::createBoxedExecutor(
190                $this->getLocalShellboxOptions(),
191                $this->logger );
192        }
193        return $executor->createCommand()
194            ->cpuTimeLimit( $this->limits['time'] )
195            ->wallTimeLimit( $this->limits['walltime'] )
196            ->memoryLimit( $this->limits['memory'] * 1024 )
197            ->fileSizeLimit( $this->limits['filesize'] * 1024 )
198            ->logStderr( $this->doLogStderr );
199    }
200}