‪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  // Determine whether the strip profile action has be disabled by TypoScript:
123  if ($gfxConf['processor_stripColorProfileByDefault']
124  && $gfxConf['processor_stripColorProfileCommand'] !== ''
125  && $parameters !== '-version'
126  && !str_contains($parameters, $gfxConf['processor_stripColorProfileCommand'])
127  && !str_contains($parameters, '###SkipStripProfile###')
128  ) {
129  $parameters = $gfxConf['processor_stripColorProfileCommand'] . ' ' . $parameters;
130  } else {
131  $parameters = str_replace('###SkipStripProfile###', '', $parameters);
132  }
133  }
134  // Add -auto-orient on convert so IM/GM respects the image orient
135  if ($parameters && $command === 'convert') {
136  $parameters = '-auto-orient ' . $parameters;
137  }
138  // set interlace parameter for convert command
139  if ($command !== 'identify' && $gfxConf['processor_interlace']) {
140  $parameters = '-interlace ' . $gfxConf['processor_interlace'] . ' ' . $parameters;
141  }
142  $cmdLine = $path . ' ' . $parameters;
143  // It is needed to change the parameters order when a mask image has been specified
144  if ($command === 'composite') {
145  $paramsArr = self::unQuoteFilenames($parameters);
146  $paramsArrCount = count($paramsArr);
147  if ($paramsArrCount > 5) {
148  $tmp = $paramsArr[$paramsArrCount - 3];
149  $paramsArr[$paramsArrCount - 3] = $paramsArr[$paramsArrCount - 4];
150  $paramsArr[$paramsArrCount - 4] = $tmp;
151  }
152  $cmdLine = $path . ' ' . implode(' ', $paramsArr);
153  }
154  return $cmdLine;
155  }
156 
164  public static function ‪checkCommand(string $cmd, string $handler = ''): bool|int
165  {
166  if (!self::init()) {
167  return false;
168  }
169 
170  if ($handler !== '' && !self::checkCommand($handler)) {
171  return -1;
172  }
173  // Already checked and valid
174  if (self::$applications[$cmd]['valid'] ?? false) {
175  return true;
176  }
177  // Is set but was (above) not TRUE
178  if (isset(self::$applications[$cmd]['valid'])) {
179  return false;
180  }
181 
182  foreach (self::$paths as $path => $validPath) {
183  // Ignore invalid (FALSE) paths
184  if ($validPath) {
186  // Windows OS
187  // @todo Why is_executable() is not called here?
188  if (@is_file($path . $cmd)) {
189  self::$applications[$cmd]['app'] = $cmd;
190  self::$applications[$cmd]['path'] = $path;
191  self::$applications[$cmd]['valid'] = true;
192  return true;
193  }
194  if (@is_file($path . $cmd . '.exe')) {
195  self::$applications[$cmd]['app'] = $cmd . '.exe';
196  self::$applications[$cmd]['path'] = $path;
197  self::$applications[$cmd]['valid'] = true;
198  return true;
199  }
200  } else {
201  // Unix-like OS
202  $filePath = realpath($path . $cmd);
203  if ($filePath && @is_executable($filePath)) {
204  self::$applications[$cmd]['app'] = $cmd;
205  self::$applications[$cmd]['path'] = $path;
206  self::$applications[$cmd]['valid'] = true;
207  return true;
208  }
209  }
210  }
211  }
212 
213  // Try to get the executable with the command 'which'.
214  // It does the same like already done, but maybe on other paths
215  if (!‪Environment::isWindows()) {
216  ‪$output = null;
217  $returnValue = 0;
218  $cmd = @‪self::exec('which ' . self::escapeShellArgument($cmd), ‪$output, $returnValue);
219 
220  if ($returnValue === 0) {
221  self::$applications[$cmd]['app'] = $cmd;
222  self::$applications[$cmd]['path'] = ‪PathUtility::dirname($cmd) . '/';
223  self::$applications[$cmd]['valid'] = true;
224  return true;
225  }
226  }
227 
228  return false;
229  }
230 
239  public static function ‪getCommand(string $cmd, string $handler = '', string $handlerOpt = ''): string|bool|int
240  {
241  if (!self::init()) {
242  return false;
243  }
244 
245  // Handler
246  if ($handler) {
247  $handler = ‪self::getCommand($handler);
248 
249  if (!$handler) {
250  return -1;
251  }
252  $handler .= ' ' . escapeshellcmd($handlerOpt) . ' ';
253  }
254 
255  // Command
256  if (!self::checkCommand($cmd)) {
257  return false;
258  }
259  $cmd = self::$applications[$cmd]['path'] . self::$applications[$cmd]['app'] . ' ';
260 
261  return trim($handler . $cmd);
262  }
263 
269  public static function ‪addPaths(string ‪$paths): void
270  {
271  self::initPaths(‪$paths);
272  }
273 
280  public static function getPaths(bool $addInvalid = false): array
281  {
282  if (!self::init()) {
283  return [];
284  }
285 
286  return $addInvalid
288  : array_filter(self::$paths);
289  }
290 
294  protected static function init(): bool
295  {
296  if (‪$GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']) {
297  return false;
298  }
299  if (!self::$initialized) {
300  self::initPaths();
301  self::$applications = self::getConfiguredApps();
302  self::$initialized = true;
303  }
304  return true;
305  }
306 
312  protected static function initPaths(string ‪$paths = ''): void
313  {
314  $doCheck = false;
315 
316  // Init global paths array if not already done
317  if (!is_array(self::$paths)) {
318  self::$paths = self::getPathsInternal();
319  $doCheck = true;
320  }
321  // Merge the submitted paths array to the global
322  if (‪$paths) {
323  ‪$paths = GeneralUtility::trimExplode(',', ‪$paths, true);
324  foreach (‪$paths as $path) {
325  // Make absolute path of relative
326  if (!str_starts_with($path, '/')) {
327  $path = ‪Environment::getPublicPath() . '/' . $path;
328  }
329  if (!isset(self::$paths[$path])) {
330  if (@is_dir($path)) {
331  self::$paths[$path] = $path;
332  } else {
333  self::$paths[$path] = false;
334  }
335  }
336  }
337  }
338  // Check if new paths are invalid
339  if ($doCheck) {
340  foreach (self::$paths as $path => $valid) {
341  // Ignore invalid (FALSE) paths
342  if ($valid && !@is_dir($path)) {
343  self::$paths[$path] = false;
344  }
345  }
346  }
347  }
348 
354  protected static function getConfiguredApps(): array
355  {
356  $cmdArr = [];
357 
358  if (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']) {
359  $binSetup = str_replace(['\'.chr(10).\'', '\' . LF . \''], LF, ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']);
360  $pathSetup = preg_split('/[\n,]+/', $binSetup);
361  foreach ($pathSetup as $val) {
362  if (trim($val) === '') {
363  continue;
364  }
365  [$cmd, $cmdPath] = GeneralUtility::trimExplode('=', $val, true, 2);
366  $cmdArr[$cmd]['app'] = ‪PathUtility::basename($cmdPath);
367  $cmdArr[$cmd]['path'] = ‪PathUtility::dirname($cmdPath) . '/';
368  $cmdArr[$cmd]['valid'] = true;
369  }
370  }
371 
372  return $cmdArr;
373  }
374 
380  protected static function getPathsInternal(): array
381  {
382  $pathsArr = [];
383  $sysPathArr = [];
384 
385  // Image magick paths first
386  if ($imPath = ‪$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']) {
387  $imPath = self::fixPath($imPath);
388  $pathsArr[$imPath] = $imPath;
389  }
390 
391  // Add configured paths
392  if (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']) {
393  $sysPath = GeneralUtility::trimExplode(',', ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath'], true);
394  foreach ($sysPath as $val) {
395  $val = self::fixPath($val);
396  $sysPathArr[$val] = $val;
397  }
398  }
399 
400  // Add path from environment
401  if (!empty(‪$GLOBALS['_SERVER']['PATH']) || !empty(‪$GLOBALS['_SERVER']['Path'])) {
402  $sep = ‪Environment::isWindows() ? ';' : ':';
403  $serverPath = ‪$GLOBALS['_SERVER']['PATH'] ?? ‪$GLOBALS['_SERVER']['Path'];
404  $envPath = GeneralUtility::trimExplode($sep, $serverPath, true);
405  foreach ($envPath as $val) {
406  $val = self::fixPath($val);
407  $sysPathArr[$val] = $val;
408  }
409  }
410 
411  // Set common paths for Unix (only)
412  if (!‪Environment::isWindows()) {
413  $sysPathArr = array_merge($sysPathArr, [
414  '/usr/bin/' => '/usr/bin/',
415  '/usr/local/bin/' => '/usr/local/bin/',
416  ]);
417  }
418 
419  return array_merge($pathsArr, $sysPathArr);
420  }
421 
428  protected static function fixPath(string $path): string
429  {
430  return str_replace('//', '/', $path . '/');
431  }
432 
441  public static function escapeShellArguments(array $input): array
442  {
443  $isUTF8Filesystem = !empty(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']);
444  $currentLocale = false;
445  if ($isUTF8Filesystem) {
446  $currentLocale = setlocale(LC_CTYPE, '0');
447  setlocale(LC_CTYPE, ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
448  }
449 
450  ‪$output = array_map('escapeshellarg', $input);
451 
452  if ($isUTF8Filesystem && $currentLocale !== false) {
453  setlocale(LC_CTYPE, $currentLocale);
454  }
455 
456  return ‪$output;
457  }
458 
465  protected static function unQuoteFilenames(string $parameters): array
466  {
467  $paramsArr = explode(' ', trim($parameters));
468  // Whenever a quote character (") is found, $quoteActive is set to the element number inside of $params.
469  // A value of -1 means that there are not open quotes at the current position.
470  $quoteActive = -1;
471  foreach ($paramsArr as $k => $v) {
472  if ($quoteActive > -1) {
473  $paramsArr[$quoteActive] .= ' ' . $v;
474  unset($paramsArr[$k]);
475  if (substr($v, -1) === $paramsArr[$quoteActive][0]) {
476  $quoteActive = -1;
477  }
478  } elseif (!trim($v)) {
479  // Remove empty elements
480  unset($paramsArr[$k]);
481  } elseif (preg_match('/^(["\'])/', $v) && substr($v, -1) !== $v[0]) {
482  $quoteActive = $k;
483  }
484  }
485  // Return re-indexed array
486  return array_values($paramsArr);
487  }
488 
497  public static function escapeShellArgument(string $input): string
498  {
499  return self::escapeShellArguments([$input])[0];
500  }
501 }
‪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:164
‪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:119
‪TYPO3\CMS\Core\Utility\CommandUtility\getCommand
‪static string bool int getCommand(string $cmd, string $handler='', string $handlerOpt='')
Definition: CommandUtility.php:239
‪TYPO3\CMS\Core\Utility\CommandUtility\addPaths
‪static addPaths(string $paths)
Definition: CommandUtility.php:269
‪$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\Core\Environment\isWindows
‪static isWindows()
Definition: Environment.php:276