Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
15.62% |
10 / 64 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
CommandFactory | |
15.62% |
10 / 64 |
|
0.00% |
0 / 6 |
212.62 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
findFirejail | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
logStderr | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLocalShellboxOptions | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
create | |
45.45% |
10 / 22 |
|
0.00% |
0 / 1 |
11.84 | |||
createBoxed | |
0.00% |
0 / 16 |
|
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 | |
21 | namespace MediaWiki\Shell; |
22 | |
23 | use ExecutableFinder; |
24 | use Psr\Log\LoggerAwareTrait; |
25 | use Psr\Log\NullLogger; |
26 | use Shellbox\Command\BoxedCommand; |
27 | use Shellbox\Command\RemoteBoxedExecutor; |
28 | use Shellbox\Shellbox; |
29 | |
30 | /** |
31 | * Factory facilitating dependency injection for Command |
32 | * |
33 | * @since 1.30 |
34 | */ |
35 | class 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 | * @param ?string $service Name of Shellbox (as configured in |
176 | * $wgShellboxUrls) that should be used |
177 | * @return BoxedCommand |
178 | */ |
179 | public function createBoxed( ?string $service = null ): BoxedCommand { |
180 | if ( $this->shellboxClientFactory->isEnabled( $service ) ) { |
181 | $client = $this->shellboxClientFactory->getClient( [ |
182 | 'timeout' => $this->limits['walltime'] + 1, |
183 | 'service' => $service, |
184 | ] ); |
185 | $executor = new RemoteBoxedExecutor( $client ); |
186 | $executor->setLogger( $this->logger ); |
187 | } else { |
188 | $executor = Shellbox::createBoxedExecutor( |
189 | $this->getLocalShellboxOptions(), |
190 | $this->logger ); |
191 | } |
192 | return $executor->createCommand() |
193 | ->cpuTimeLimit( $this->limits['time'] ) |
194 | ->wallTimeLimit( $this->limits['walltime'] ) |
195 | ->memoryLimit( $this->limits['memory'] * 1024 ) |
196 | ->fileSizeLimit( $this->limits['filesize'] * 1024 ) |
197 | ->logStderr( $this->doLogStderr ); |
198 | } |
199 | } |