‪TYPO3CMS  ‪main
CommandUtility.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
21 
54 {
58  protected static bool ‪$initialized = false;
59 
69  protected static array ‪$applications = [];
70 
78  protected static ?array ‪$paths = null;
79 
85  public static function ‪exec(string $command, ?array &‪$output = null, int &$returnValue = 0): string
86  {
87  return ‪exec($command, ‪$output, $returnValue);
88  }
89 
98  public static function ‪imageMagickCommand(string $command, string $parameters, string $path = ''): string
99  {
100  $gfxConf = ‪$GLOBALS['TYPO3_CONF_VARS']['GFX'];
101  $isExt = ‪Environment::isWindows() ? '.exe' : '';
102  if (!$path) {
103  $path = (string)($gfxConf['processor_path'] ?? '');
104  }
105  $path = GeneralUtility::fixWindowsFilePath($path);
106  // This is only used internally, has no effect outside
107  if ($command === 'combine') {
108  $command = 'composite';
109  }
110  // Compile the path & command
111  if ($gfxConf['processor'] === 'GraphicsMagick') {
112  $path = self::escapeShellArgument($path . 'gm' . $isExt) . ' ' . self::escapeShellArgument($command);
113  } else {
114  if (‪Environment::isWindows() && !@is_file($path . $command . $isExt)) {
115  $path = self::escapeShellArgument($path . 'magick' . $isExt) . ' ' . self::escapeShellArgument($command);
116  } else {
117  $path = self::escapeShellArgument($path . $command . $isExt);
118  }
119  }
120  // strip profile information for thumbnails and reduce their size
121  if ($parameters && $command !== 'identify') {
122  // Use legacy processor_stripColorProfileCommand setting if defined, otherwise
123  // use the preferred configuration option processor_stripColorProfileParameters
124  $stripColorProfileCommand = $gfxConf['processor_stripColorProfileCommand'] ??
125  implode(' ', array_map(CommandUtility::escapeShellArgument(...), $gfxConf['processor_stripColorProfileParameters'] ?? []));
126  // Determine whether the strip profile action has be disabled by TypoScript:
127  if ($gfxConf['processor_stripColorProfileByDefault']
128  && $stripColorProfileCommand !== ''
129  && $parameters !== '-version'
130  && !str_contains($parameters, $stripColorProfileCommand)
131  && !str_contains($parameters, '###SkipStripProfile###')
132  ) {
133  $parameters = $stripColorProfileCommand . ' ' . $parameters;
134  } else {
135  $parameters = str_replace('###SkipStripProfile###', '', $parameters);
136  }
137  }
138  // Add -auto-orient on convert so IM/GM respects the image orient
139  if ($parameters && $command === 'convert') {
140  $parameters = '-auto-orient ' . $parameters;
141  }
142  // set interlace parameter for convert command
143  if ($command !== 'identify' && $gfxConf['processor_interlace']) {
144  $parameters = '-interlace ' . CommandUtility::escapeShellArgument($gfxConf['processor_interlace']) . ' ' . $parameters;
145  }
146  $cmdLine = $path . ' ' . $parameters;
147  // It is needed to change the parameters order when a mask image has been specified
148  if ($command === 'composite') {
149  $paramsArr = self::unQuoteFilenames($parameters);
150  $paramsArrCount = count($paramsArr);
151  if ($paramsArrCount > 5) {
152  $tmp = $paramsArr[$paramsArrCount - 3];
153  $paramsArr[$paramsArrCount - 3] = $paramsArr[$paramsArrCount - 4];
154  $paramsArr[$paramsArrCount - 4] = $tmp;
155  }
156  $cmdLine = $path . ' ' . implode(' ', $paramsArr);
157  }
158  return $cmdLine;
159  }
160 
168  public static function ‪checkCommand(string $cmd, string $handler = ''): bool|int
169  {
170  if (!self::init()) {
171  return false;
172  }
173 
174  if ($handler !== '' && !self::checkCommand($handler)) {
175  return -1;
176  }
177  // Already checked and valid
178  if (self::$applications[$cmd]['valid'] ?? false) {
179  return true;
180  }
181  // Is set but was (above) not TRUE
182  if (isset(self::$applications[$cmd]['valid'])) {
183  return false;
184  }
185 
186  foreach (self::$paths as $path => $validPath) {
187  // Ignore invalid (FALSE) paths
188  if ($validPath) {
190  // Windows OS
191  // @todo Why is_executable() is not called here?
192  if (@is_file($path . $cmd)) {
193  self::$applications[$cmd]['app'] = $cmd;
194  self::$applications[$cmd]['path'] = $path;
195  self::$applications[$cmd]['valid'] = true;
196  return true;
197  }
198  if (@is_file($path . $cmd . '.exe')) {
199  self::$applications[$cmd]['app'] = $cmd . '.exe';
200  self::$applications[$cmd]['path'] = $path;
201  self::$applications[$cmd]['valid'] = true;
202  return true;
203  }
204  } else {
205  // Unix-like OS
206  $filePath = realpath($path . $cmd);
207  if ($filePath && @is_executable($filePath)) {
208  self::$applications[$cmd]['app'] = $cmd;
209  self::$applications[$cmd]['path'] = $path;
210  self::$applications[$cmd]['valid'] = true;
211  return true;
212  }
213  }
214  }
215  }
216 
217  // Try to get the executable with the command 'which'.
218  // It does the same like already done, but maybe on other paths
219  if (!‪Environment::isWindows()) {
220  ‪$output = null;
221  $returnValue = 0;
222  $cmd = @‪self::exec('which ' . self::escapeShellArgument($cmd), ‪$output, $returnValue);
223 
224  if ($returnValue === 0) {
225  self::$applications[$cmd]['app'] = $cmd;
226  self::$applications[$cmd]['path'] = ‪PathUtility::dirname($cmd) . '/';
227  self::$applications[$cmd]['valid'] = true;
228  return true;
229  }
230  }
231 
232  return false;
233  }
234 
243  public static function ‪getCommand(string $cmd, string $handler = '', string $handlerOpt = ''): string|bool|int
244  {
245  if (!self::init()) {
246  return false;
247  }
248 
249  // Handler
250  if ($handler) {
251  $handler = ‪self::getCommand($handler);
252 
253  if (!$handler) {
254  return -1;
255  }
256  $handler .= ' ' . escapeshellcmd($handlerOpt) . ' ';
257  }
258 
259  // Command
260  if (!self::checkCommand($cmd)) {
261  return false;
262  }
263  $cmd = self::$applications[$cmd]['path'] . self::$applications[$cmd]['app'] . ' ';
264 
265  return trim($handler . $cmd);
266  }
267 
273  public static function ‪addPaths(string ‪$paths): void
274  {
275  self::initPaths(‪$paths);
276  }
277 
284  public static function getPaths(bool $addInvalid = false): array
285  {
286  if (!self::init()) {
287  return [];
288  }
289 
290  return $addInvalid
292  : array_filter(self::$paths);
293  }
294 
298  protected static function init(): bool
299  {
300  if (‪$GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']) {
301  return false;
302  }
303  if (!self::$initialized) {
304  self::initPaths();
305  self::$applications = self::getConfiguredApps();
306  self::$initialized = true;
307  }
308  return true;
309  }
310 
316  protected static function initPaths(string ‪$paths = ''): void
317  {
318  $doCheck = false;
319 
320  // Init global paths array if not already done
321  if (!is_array(self::$paths)) {
322  self::$paths = self::getPathsInternal();
323  $doCheck = true;
324  }
325  // Merge the submitted paths array to the global
326  if (‪$paths) {
328  foreach (‪$paths as $path) {
329  // Make absolute path of relative
330  if (!str_starts_with($path, '/')) {
331  $path = ‪Environment::getPublicPath() . '/' . $path;
332  }
333  if (!isset(self::$paths[$path])) {
334  if (@is_dir($path)) {
335  self::$paths[$path] = $path;
336  } else {
337  self::$paths[$path] = false;
338  }
339  }
340  }
341  }
342  // Check if new paths are invalid
343  if ($doCheck) {
344  foreach (self::$paths as $path => $valid) {
345  // Ignore invalid (FALSE) paths
346  if ($valid && !@is_dir($path)) {
347  self::$paths[$path] = false;
348  }
349  }
350  }
351  }
352 
358  protected static function getConfiguredApps(): array
359  {
360  $cmdArr = [];
361 
362  if (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']) {
363  $binSetup = str_replace(['\'.chr(10).\'', '\' . LF . \''], LF, ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']);
364  $pathSetup = preg_split('/[\n,]+/', $binSetup);
365  foreach ($pathSetup as $val) {
366  if (trim($val) === '') {
367  continue;
368  }
369  [$cmd, $cmdPath] = ‪GeneralUtility::trimExplode('=', $val, true, 2);
370  $cmdArr[$cmd]['app'] = ‪PathUtility::basename($cmdPath);
371  $cmdArr[$cmd]['path'] = ‪PathUtility::dirname($cmdPath) . '/';
372  $cmdArr[$cmd]['valid'] = true;
373  }
374  }
375 
376  return $cmdArr;
377  }
378 
384  protected static function getPathsInternal(): array
385  {
386  $pathsArr = [];
387  $sysPathArr = [];
388 
389  // Image magick paths first
390  if ($imPath = ‪$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']) {
391  $imPath = self::fixPath($imPath);
392  $pathsArr[$imPath] = $imPath;
393  }
394 
395  // Add configured paths
396  if (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']) {
397  $sysPath = ‪GeneralUtility::trimExplode(',', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath'], true);
398  foreach ($sysPath as $val) {
399  $val = self::fixPath($val);
400  $sysPathArr[$val] = $val;
401  }
402  }
403 
404  // Add path from environment
405  if (!empty(‪$GLOBALS['_SERVER']['PATH']) || !empty(‪$GLOBALS['_SERVER']['Path'])) {
406  $sep = ‪Environment::isWindows() ? ';' : ':';
407  $serverPath = ‪$GLOBALS['_SERVER']['PATH'] ?? ‪$GLOBALS['_SERVER']['Path'];
408  $envPath = ‪GeneralUtility::trimExplode($sep, $serverPath, true);
409  foreach ($envPath as $val) {
410  $val = self::fixPath($val);
411  $sysPathArr[$val] = $val;
412  }
413  }
414 
415  // Set common paths for Unix (only)
416  if (!‪Environment::isWindows()) {
417  $sysPathArr = array_merge($sysPathArr, [
418  '/usr/bin/' => '/usr/bin/',
419  '/usr/local/bin/' => '/usr/local/bin/',
420  ]);
421  }
422 
423  return array_merge($pathsArr, $sysPathArr);
424  }
425 
432  protected static function fixPath(string $path): string
433  {
434  return str_replace('//', '/', $path . '/');
435  }
436 
445  public static function escapeShellArguments(array $input): array
446  {
447  $isUTF8Filesystem = !empty(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']);
448  $currentLocale = false;
449  if ($isUTF8Filesystem) {
450  if (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale'] ?? false) {
451  $currentLocale = setlocale(LC_CTYPE, '0');
452  setlocale(LC_CTYPE, ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
453  }
454  }
455 
456  ‪$output = array_map('escapeshellarg', $input);
457 
458  if ($isUTF8Filesystem && $currentLocale !== false) {
459  setlocale(LC_CTYPE, $currentLocale);
460  }
461 
462  return ‪$output;
463  }
464 
471  protected static function unQuoteFilenames(string $parameters): array
472  {
473  $paramsArr = explode(' ', trim($parameters));
474  // Whenever a quote character (") is found, $quoteActive is set to the element number inside of $params.
475  // A value of -1 means that there are not open quotes at the current position.
476  $quoteActive = -1;
477  foreach ($paramsArr as $k => $v) {
478  if ($quoteActive > -1) {
479  $paramsArr[$quoteActive] .= ' ' . $v;
480  unset($paramsArr[$k]);
481  if (substr($v, -1) === $paramsArr[$quoteActive][0]) {
482  $quoteActive = -1;
483  }
484  } elseif (!trim($v)) {
485  // Remove empty elements
486  unset($paramsArr[$k]);
487  } elseif (preg_match('/^(["\'])/', $v) && substr($v, -1) !== $v[0]) {
488  $quoteActive = $k;
489  }
490  }
491  // Return re-indexed array
492  return array_values($paramsArr);
493  }
494 
503  public static function escapeShellArgument(string $input): string
504  {
505  return self::escapeShellArguments([$input])[0];
506  }
507 }
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static getPublicPath()
Definition: Environment.php:187
‪TYPO3\CMS\Core\Utility
Definition: ArrayUtility.php:18
‪TYPO3\CMS\Core\Utility\CommandUtility\checkCommand
‪static bool int checkCommand(string $cmd, string $handler='')
Definition: CommandUtility.php:168
‪TYPO3\CMS\Core\Utility\PathUtility\basename
‪static basename(string $path)
Definition: PathUtility.php:219
‪TYPO3\CMS\Core\Utility\PathUtility\dirname
‪static dirname(string $path)
Definition: PathUtility.php:243
‪TYPO3\CMS\Core\Utility\CommandUtility\$applications
‪static array $applications
Definition: CommandUtility.php:69
‪TYPO3\CMS\Core\Utility\CommandUtility\exec
‪static exec(string $command, ?array &$output=null, int &$returnValue=0)
Definition: CommandUtility.php:85
‪$output
‪$output
Definition: annotationChecker.php:114
‪TYPO3\CMS\Core\Utility\CommandUtility\getCommand
‪static string bool int getCommand(string $cmd, string $handler='', string $handlerOpt='')
Definition: CommandUtility.php:243
‪TYPO3\CMS\Core\Utility\CommandUtility\addPaths
‪static addPaths(string $paths)
Definition: CommandUtility.php:273
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Utility\CommandUtility\imageMagickCommand
‪static string imageMagickCommand(string $command, string $parameters, string $path='')
Definition: CommandUtility.php:98
‪TYPO3\CMS\Core\Utility\CommandUtility\$initialized
‪static bool $initialized
Definition: CommandUtility.php:58
‪TYPO3\CMS\Core\Utility\CommandUtility
Definition: CommandUtility.php:54
‪TYPO3\CMS\Core\Utility\CommandUtility\$paths
‪static array $paths
Definition: CommandUtility.php:78
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Core\Core\Environment\isWindows
‪static isWindows()
Definition: Environment.php:276