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