731ede0cf655487831c1cc49bf093ad0215741c7
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Utility / CommandUtility.php
1 <?php
2 namespace TYPO3\CMS\Core\Utility;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Class to handle system commands.
19 * finds executables (programs) on Unix and Windows without knowing where they are
20 *
21 * returns exec command for a program
22 * or FALSE
23 *
24 * This class is meant to be used without instance:
25 * $cmd = CommandUtility::getCommand ('awstats','perl');
26 *
27 * The data of this class is cached.
28 * That means if a program is found once it don't have to be searched again.
29 *
30 * user functions:
31 *
32 * addPaths() could be used to extend the search paths
33 * getCommand() get a command string
34 * checkCommand() returns TRUE if a command is available
35 *
36 * Search paths that are included:
37 * $TYPO3_CONF_VARS['GFX']['processor_path_lzw'] or $TYPO3_CONF_VARS['GFX']['processor_path']
38 * $TYPO3_CONF_VARS['SYS']['binPath']
39 * $GLOBALS['_SERVER']['PATH']
40 * '/usr/bin/,/usr/local/bin/' on Unix
41 *
42 * binaries can be preconfigured with
43 * $TYPO3_CONF_VARS['SYS']['binSetup']
44 */
45 class CommandUtility
46 {
47 /**
48 * Tells if object is already initialized
49 *
50 * @var bool
51 */
52 protected static $initialized = false;
53
54 /**
55 * Contains application list. This is an array with the following structure:
56 * - app => file name to the application (like 'tar' or 'bzip2')
57 * - path => full path to the application without application name (like '/usr/bin/' for '/usr/bin/tar')
58 * - valid => TRUE or FALSE
59 * Array key is identical to 'app'.
60 *
61 * @var array
62 */
63 protected static $applications = [];
64
65 /**
66 * Paths where to search for applications
67 *
68 * @var array
69 */
70 protected static $paths = null;
71
72 /**
73 * Wrapper function for php exec function
74 * Needs to be central to have better control and possible fix for issues
75 *
76 * @param string $command
77 * @param array|null $output
78 * @param int $returnValue
79 * @return string
80 */
81 public static function exec($command, &$output = null, &$returnValue = 0)
82 {
83 return exec($command, $output, $returnValue);
84 }
85
86 /**
87 * Compile the command for running ImageMagick/GraphicsMagick.
88 *
89 * @param string $command Command to be run: identify, convert or combine/composite
90 * @param string $parameters The parameters string
91 * @param string $path Override the default path (e.g. used by the install tool)
92 * @return string Compiled command that deals with ImageMagick & GraphicsMagick
93 */
94 public static function imageMagickCommand($command, $parameters, $path = '')
95 {
96 $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
97 $isExt = TYPO3_OS === 'WIN' ? '.exe' : '';
98 if (!$path) {
99 $path = $gfxConf['processor_path'];
100 }
101 $path = GeneralUtility::fixWindowsFilePath($path);
102 // This is only used internally, has no effect outside
103 if ($command === 'combine') {
104 $command = 'composite';
105 }
106 // Compile the path & command
107 if ($gfxConf['processor'] === 'GraphicsMagick') {
108 $path = self::escapeShellArgument($path . 'gm' . $isExt) . ' ' . self::escapeShellArgument($command);
109 } else {
110 $path = self::escapeShellArgument($path . $command . $isExt);
111 }
112 // strip profile information for thumbnails and reduce their size
113 if ($parameters && $command !== 'identify') {
114 // Determine whether the strip profile action has be disabled by TypoScript:
115 if ($gfxConf['processor_stripColorProfileByDefault']
116 && $gfxConf['processor_stripColorProfileCommand'] !== ''
117 && strpos($parameters, $gfxConf['processor_stripColorProfileCommand']) === false
118 && $parameters !== '-version'
119 && strpos($parameters, '###SkipStripProfile###') === false
120 ) {
121 $parameters = $gfxConf['processor_stripColorProfileCommand'] . ' ' . $parameters;
122 } else {
123 $parameters = str_replace('###SkipStripProfile###', '', $parameters);
124 }
125 }
126 // set interlace parameter for convert command
127 if ($command !== 'identify' && $gfxConf['processor_interlace']) {
128 $parameters = '-interlace ' . $gfxConf['processor_interlace'] . ' ' . $parameters;
129 }
130 $cmdLine = $path . ' ' . $parameters;
131 // It is needed to change the parameters order when a mask image has been specified
132 if ($command === 'composite') {
133 $paramsArr = GeneralUtility::unQuoteFilenames($parameters);
134 $paramsArrCount = count($paramsArr);
135 if ($paramsArrCount > 5) {
136 $tmp = $paramsArr[$paramsArrCount - 3];
137 $paramsArr[$paramsArrCount - 3] = $paramsArr[$paramsArrCount - 4];
138 $paramsArr[$paramsArrCount - 4] = $tmp;
139 }
140 $cmdLine = $path . ' ' . implode(' ', $paramsArr);
141 }
142 return $cmdLine;
143 }
144
145 /**
146 * Checks if a command is valid or not, updates global variables
147 *
148 * @param string $cmd The command that should be executed. eg: "convert"
149 * @param string $handler Executer for the command. eg: "perl"
150 * @return bool FALSE if cmd is not found, or -1 if the handler is not found
151 */
152 public static function checkCommand($cmd, $handler = '')
153 {
154 if (!self::init()) {
155 return false;
156 }
157
158 if ($handler && !self::checkCommand($handler)) {
159 return -1;
160 }
161 // Already checked and valid
162 if (self::$applications[$cmd]['valid'] ?? false) {
163 return true;
164 }
165 // Is set but was (above) not TRUE
166 if (isset(self::$applications[$cmd]['valid'])) {
167 return false;
168 }
169
170 foreach (self::$paths as $path => $validPath) {
171 // Ignore invalid (FALSE) paths
172 if ($validPath) {
173 if (TYPO3_OS === 'WIN') {
174 // Windows OS
175 // @todo Why is_executable() is not called here?
176 if (@is_file($path . $cmd)) {
177 self::$applications[$cmd]['app'] = $cmd;
178 self::$applications[$cmd]['path'] = $path;
179 self::$applications[$cmd]['valid'] = true;
180 return true;
181 }
182 if (@is_file($path . $cmd . '.exe')) {
183 self::$applications[$cmd]['app'] = $cmd . '.exe';
184 self::$applications[$cmd]['path'] = $path;
185 self::$applications[$cmd]['valid'] = true;
186 return true;
187 }
188 } else {
189 // Unix-like OS
190 $filePath = realpath($path . $cmd);
191 if ($filePath && @is_executable($filePath)) {
192 self::$applications[$cmd]['app'] = $cmd;
193 self::$applications[$cmd]['path'] = $path;
194 self::$applications[$cmd]['valid'] = true;
195 return true;
196 }
197 }
198 }
199 }
200
201 // Try to get the executable with the command 'which'.
202 // It does the same like already done, but maybe on other paths
203 if (TYPO3_OS !== 'WIN') {
204 $cmd = @self::exec('which ' . $cmd);
205 if (@is_executable($cmd)) {
206 self::$applications[$cmd]['app'] = $cmd;
207 self::$applications[$cmd]['path'] = dirname($cmd) . '/';
208 self::$applications[$cmd]['valid'] = true;
209 return true;
210 }
211 }
212
213 return false;
214 }
215
216 /**
217 * Returns a command string for exec(), system()
218 *
219 * @param string $cmd The command that should be executed. eg: "convert"
220 * @param string $handler Handler (executor) for the command. eg: "perl"
221 * @param string $handlerOpt Options for the handler, like '-w' for "perl"
222 * @return mixed Returns command string, or FALSE if cmd is not found, or -1 if the handler is not found
223 */
224 public static function getCommand($cmd, $handler = '', $handlerOpt = '')
225 {
226 if (!self::init()) {
227 return false;
228 }
229
230 // Handler
231 if ($handler) {
232 $handler = self::getCommand($handler);
233
234 if (!$handler) {
235 return -1;
236 }
237 $handler .= ' ' . $handlerOpt . ' ';
238 }
239
240 // Command
241 if (!self::checkCommand($cmd)) {
242 return false;
243 }
244 $cmd = self::$applications[$cmd]['path'] . self::$applications[$cmd]['app'] . ' ';
245
246 return trim($handler . $cmd);
247 }
248
249 /**
250 * Extend the preset paths. This way an extension can install an executable and provide the path to \TYPO3\CMS\Core\Utility\CommandUtility
251 *
252 * @param string $paths Comma separated list of extra paths where a command should be searched. Relative paths (without leading "/") are prepend with site root path (PATH_site).
253 */
254 public static function addPaths($paths)
255 {
256 self::initPaths($paths);
257 }
258
259 /**
260 * Returns an array of search paths
261 *
262 * @param bool $addInvalid If set the array contains invalid path too. Then the key is the path and the value is empty
263 * @return array Array of search paths (empty if exec is disabled)
264 */
265 public static function getPaths($addInvalid = false)
266 {
267 if (!self::init()) {
268 return [];
269 }
270
271 $paths = self::$paths;
272
273 if (!$addInvalid) {
274 foreach ($paths as $path => $validPath) {
275 if (!$validPath) {
276 unset($paths[$path]);
277 }
278 }
279 }
280 return $paths;
281 }
282
283 /**
284 * Initializes this class
285 *
286 * @return bool
287 */
288 protected static function init()
289 {
290 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']) {
291 return false;
292 }
293 if (!self::$initialized) {
294 self::initPaths();
295 self::$applications = self::getConfiguredApps();
296 self::$initialized = true;
297 }
298 return true;
299 }
300
301 /**
302 * Initializes and extends the preset paths with own
303 *
304 * @param string $paths Comma separated list of extra paths where a command should be searched. Relative paths (without leading "/") are prepend with site root path (PATH_site).
305 */
306 protected static function initPaths($paths = '')
307 {
308 $doCheck = false;
309
310 // Init global paths array if not already done
311 if (!is_array(self::$paths)) {
312 self::$paths = self::getPathsInternal();
313 $doCheck = true;
314 }
315 // Merge the submitted paths array to the global
316 if ($paths) {
317 $paths = GeneralUtility::trimExplode(',', $paths, true);
318 if (is_array($paths)) {
319 foreach ($paths as $path) {
320 // Make absolute path of relative
321 if (!preg_match('#^/#', $path)) {
322 $path = PATH_site . $path;
323 }
324 if (!isset(self::$paths[$path])) {
325 if (@is_dir($path)) {
326 self::$paths[$path] = $path;
327 } else {
328 self::$paths[$path] = false;
329 }
330 }
331 }
332 }
333 }
334 // Check if new paths are invalid
335 if ($doCheck) {
336 foreach (self::$paths as $path => $valid) {
337 // Ignore invalid (FALSE) paths
338 if ($valid && !@is_dir($path)) {
339 self::$paths[$path] = false;
340 }
341 }
342 }
343 }
344
345 /**
346 * Processes and returns the paths from $GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']
347 *
348 * @return array Array of commands and path
349 */
350 protected static function getConfiguredApps()
351 {
352 $cmdArr = [];
353
354 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']) {
355 $binSetup = str_replace(['\'.chr(10).\'', '\' . LF . \''], LF, $GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']);
356 $pathSetup = preg_split('/[\n,]+/', $binSetup);
357 foreach ($pathSetup as $val) {
358 if (trim($val) === '') {
359 continue;
360 }
361 list($cmd, $cmdPath) = GeneralUtility::trimExplode('=', $val, true, 2);
362 $cmdArr[$cmd]['app'] = basename($cmdPath);
363 $cmdArr[$cmd]['path'] = dirname($cmdPath) . '/';
364 $cmdArr[$cmd]['valid'] = true;
365 }
366 }
367
368 return $cmdArr;
369 }
370
371 /**
372 * Sets the search paths from different sources, internal
373 *
374 * @return array Array of absolute paths (keys and values are equal)
375 */
376 protected static function getPathsInternal()
377 {
378 $pathsArr = [];
379 $sysPathArr = [];
380
381 // Image magick paths first
382 // processor_path_lzw take precedence over processor_path
383 if ($imPath = $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path_lzw'] ?: $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']) {
384 $imPath = self::fixPath($imPath);
385 $pathsArr[$imPath] = $imPath;
386 }
387
388 // Add configured paths
389 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']) {
390 $sysPath = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath'], true);
391 foreach ($sysPath as $val) {
392 $val = self::fixPath($val);
393 $sysPathArr[$val] = $val;
394 }
395 }
396
397 // Add path from environment
398 if (!empty($GLOBALS['_SERVER']['PATH']) || !empty($GLOBALS['_SERVER']['Path'])) {
399 $sep = (TYPO3_OS === 'WIN' ? ';' : ':');
400 $serverPath = $GLOBALS['_SERVER']['PATH'] ?? $GLOBALS['_SERVER']['Path'];
401 $envPath = GeneralUtility::trimExplode($sep, $serverPath, true);
402 foreach ($envPath as $val) {
403 $val = self::fixPath($val);
404 $sysPathArr[$val] = $val;
405 }
406 }
407
408 // Set common paths for Unix (only)
409 if (TYPO3_OS !== 'WIN') {
410 $sysPathArr = array_merge($sysPathArr, [
411 '/usr/bin/' => '/usr/bin/',
412 '/usr/local/bin/' => '/usr/local/bin/',
413 ]);
414 }
415
416 $pathsArr = array_merge($pathsArr, $sysPathArr);
417
418 return $pathsArr;
419 }
420
421 /**
422 * Set a path to the right format
423 *
424 * @param string $path Input path
425 * @return string Output path
426 */
427 protected static function fixPath($path)
428 {
429 return str_replace('//', '/', $path . '/');
430 }
431
432 /**
433 * Escape shell arguments (for example filenames) to be used on the local system.
434 *
435 * The setting UTF8filesystem will be taken into account.
436 *
437 * @param string[] $input Input arguments to be escaped
438 * @return string[] Escaped shell arguments
439 */
440 public static function escapeShellArguments(array $input)
441 {
442 $isUTF8Filesystem = !empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']);
443 $currentLocale = false;
444 if ($isUTF8Filesystem) {
445 $currentLocale = setlocale(LC_CTYPE, 0);
446 setlocale(LC_CTYPE, $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
447 }
448
449 $output = array_map('escapeshellarg', $input);
450
451 if ($isUTF8Filesystem && $currentLocale !== false) {
452 setlocale(LC_CTYPE, $currentLocale);
453 }
454
455 return $output;
456 }
457
458 /**
459 * Escape a shell argument (for example a filename) to be used on the local system.
460 *
461 * The setting UTF8filesystem will be taken into account.
462 *
463 * @param string $input Input-argument to be escaped
464 * @return string Escaped shell argument
465 */
466 public static function escapeShellArgument($input)
467 {
468 return self::escapeShellArguments([$input])[0];
469 }
470 }