[BUGFIX] Use correct path variable on Windows
[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']['im_path_lzw'] or $TYPO3_CONF_VARS['GFX']['im_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 * @static
77 * @param string $command
78 * @param NULL|array $output
79 * @param int $returnValue
80 * @return NULL|array
81 */
82 public static function exec($command, &$output = null, &$returnValue = 0)
83 {
84 $lastLine = exec($command, $output, $returnValue);
85 return $lastLine;
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 IM6 & GraphicsMagick
95 */
96 public static function imageMagickCommand($command, $parameters, $path = '')
97 {
98 $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
99 $isExt = TYPO3_OS == 'WIN' ? '.exe' : '';
100 $switchCompositeParameters = false;
101 if (!$path) {
102 $path = $gfxConf['im_path'];
103 }
104 $path = GeneralUtility::fixWindowsFilePath($path);
105 $im_version = strtolower($gfxConf['im_version_5']);
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 ($im_version === 'gm') {
112 $switchCompositeParameters = true;
113 $path = self::escapeShellArgument($path . 'gm' . $isExt) . ' ' . self::escapeShellArgument($command);
114 } else {
115 if ($im_version === 'im6') {
116 $switchCompositeParameters = true;
117 }
118 $path = self::escapeShellArgument($path . ($command == 'composite' ? 'composite' : $command) . $isExt);
119 }
120 // strip profile information for thumbnails and reduce their size
121 if ($parameters && $command != 'identify' && $gfxConf['im_useStripProfileByDefault'] && $gfxConf['im_stripProfileCommand'] != '') {
122 if (strpos($parameters, $gfxConf['im_stripProfileCommand']) === false) {
123 // Determine whether the strip profile action has be disabled by TypoScript:
124 if ($parameters !== '-version' && strpos($parameters, '###SkipStripProfile###') === false) {
125 $parameters = $gfxConf['im_stripProfileCommand'] . ' ' . $parameters;
126 } else {
127 $parameters = str_replace('###SkipStripProfile###', '', $parameters);
128 }
129 }
130 }
131 $cmdLine = $path . ' ' . $parameters;
132 // Because of some weird incompatibilities between ImageMagick 4 and 6 (plus GraphicsMagick),
133 // it is needed to change the parameters order under some preconditions
134 if ($command == 'composite' && $switchCompositeParameters) {
135 $paramsArr = GeneralUtility::unQuoteFilenames($parameters);
136 // The mask image has been specified => swap the parameters
137 $paramsArrCount = count($paramsArr);
138 if ($paramsArrCount > 5) {
139 $tmp = $paramsArr[$paramsArrCount - 3];
140 $paramsArr[$paramsArrCount - 3] = $paramsArr[$paramsArrCount - 4];
141 $paramsArr[$paramsArrCount - 4] = $tmp;
142 }
143 $cmdLine = $path . ' ' . implode(' ', $paramsArr);
144 }
145 return $cmdLine;
146 }
147
148 /**
149 * Checks if a command is valid or not, updates global variables
150 *
151 * @param string $cmd The command that should be executed. eg: "convert"
152 * @param string $handler Executer for the command. eg: "perl"
153 * @return bool FALSE if cmd is not found, or -1 if the handler is not found
154 */
155 public static function checkCommand($cmd, $handler = '')
156 {
157 if (!self::init()) {
158 return false;
159 }
160
161 if ($handler && !self::checkCommand($handler)) {
162 return -1;
163 }
164 // Already checked and valid
165 if (self::$applications[$cmd]['valid']) {
166 return true;
167 }
168 // Is set but was (above) not TRUE
169 if (isset(self::$applications[$cmd]['valid'])) {
170 return false;
171 }
172
173 foreach (self::$paths as $path => $validPath) {
174 // Ignore invalid (FALSE) paths
175 if ($validPath) {
176 if (TYPO3_OS == 'WIN') {
177 // Windows OS
178 // @todo Why is_executable() is not called here?
179 if (@is_file($path . $cmd)) {
180 self::$applications[$cmd]['app'] = $cmd;
181 self::$applications[$cmd]['path'] = $path;
182 self::$applications[$cmd]['valid'] = true;
183 return true;
184 }
185 if (@is_file($path . $cmd . '.exe')) {
186 self::$applications[$cmd]['app'] = $cmd . '.exe';
187 self::$applications[$cmd]['path'] = $path;
188 self::$applications[$cmd]['valid'] = true;
189 return true;
190 }
191 } else {
192 // Unix-like OS
193 $filePath = realpath($path . $cmd);
194 if ($filePath && @is_executable($filePath)) {
195 self::$applications[$cmd]['app'] = $cmd;
196 self::$applications[$cmd]['path'] = $path;
197 self::$applications[$cmd]['valid'] = true;
198 return true;
199 }
200 }
201 }
202 }
203
204 // Try to get the executable with the command 'which'.
205 // It does the same like already done, but maybe on other paths
206 if (TYPO3_OS != 'WIN') {
207 $cmd = @self::exec('which ' . $cmd);
208 if (@is_executable($cmd)) {
209 self::$applications[$cmd]['app'] = $cmd;
210 self::$applications[$cmd]['path'] = dirname($cmd) . '/';
211 self::$applications[$cmd]['valid'] = true;
212 return true;
213 }
214 }
215
216 return false;
217 }
218
219 /**
220 * Returns a command string for exec(), system()
221 *
222 * @param string $cmd The command that should be executed. eg: "convert"
223 * @param string $handler Handler (executor) for the command. eg: "perl"
224 * @param string $handlerOpt Options for the handler, like '-w' for "perl"
225 * @return mixed Returns command string, or FALSE if cmd is not found, or -1 if the handler is not found
226 */
227 public static function getCommand($cmd, $handler = '', $handlerOpt = '')
228 {
229 if (!self::init()) {
230 return false;
231 }
232
233 // Handler
234 if ($handler) {
235 $handler = self::getCommand($handler);
236
237 if (!$handler) {
238 return -1;
239 }
240 $handler .= ' ' . $handlerOpt . ' ';
241 }
242
243 // Command
244 if (!self::checkCommand($cmd)) {
245 return false;
246 }
247 $cmd = self::$applications[$cmd]['path'] . self::$applications[$cmd]['app'] . ' ';
248
249 return trim($handler . $cmd);
250 }
251
252 /**
253 * Extend the preset paths. This way an extension can install an executable and provide the path to \TYPO3\CMS\Core\Utility\CommandUtility
254 *
255 * @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).
256 * @return void
257 */
258 public static function addPaths($paths)
259 {
260 self::initPaths($paths);
261 }
262
263 /**
264 * Returns an array of search paths
265 *
266 * @param bool $addInvalid If set the array contains invalid path too. Then the key is the path and the value is empty
267 * @return array Array of search paths (empty if exec is disabled)
268 */
269 public static function getPaths($addInvalid = false)
270 {
271 if (!self::init()) {
272 return [];
273 }
274
275 $paths = self::$paths;
276
277 if (!$addInvalid) {
278 foreach ($paths as $path => $validPath) {
279 if (!$validPath) {
280 unset($paths[$path]);
281 }
282 }
283 }
284 return $paths;
285 }
286
287 /**
288 * Initializes this class
289 *
290 * @return bool
291 */
292 protected static function init()
293 {
294 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']) {
295 return false;
296 }
297 if (!self::$initialized) {
298 self::initPaths();
299 self::$applications = self::getConfiguredApps();
300 self::$initialized = true;
301 }
302 return true;
303 }
304
305 /**
306 * Initializes and extends the preset paths with own
307 *
308 * @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).
309 * @return void
310 */
311 protected static function initPaths($paths = '')
312 {
313 $doCheck = false;
314
315 // Init global paths array if not already done
316 if (!is_array(self::$paths)) {
317 self::$paths = self::getPathsInternal();
318 $doCheck = true;
319 }
320 // Merge the submitted paths array to the global
321 if ($paths) {
322 $paths = GeneralUtility::trimExplode(',', $paths, true);
323 if (is_array($paths)) {
324 foreach ($paths as $path) {
325 // Make absolute path of relative
326 if (!preg_match('#^/#', $path)) {
327 $path = PATH_site . $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 }
339 // Check if new paths are invalid
340 if ($doCheck) {
341 foreach (self::$paths as $path => $valid) {
342 // Ignore invalid (FALSE) paths
343 if ($valid and !@is_dir($path)) {
344 self::$paths[$path] = false;
345 }
346 }
347 }
348 }
349
350 /**
351 * Processes and returns the paths from $GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']
352 *
353 * @return array Array of commands and path
354 */
355 protected static function getConfiguredApps()
356 {
357 $cmdArr = [];
358
359 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']) {
360 $binSetup = str_replace(['\'.chr(10).\'', '\' . LF . \''], LF, $GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']);
361 $pathSetup = preg_split('/[\n,]+/', $binSetup);
362 foreach ($pathSetup as $val) {
363 if (trim($val) === '') {
364 continue;
365 }
366 list($cmd, $cmdPath) = GeneralUtility::trimExplode('=', $val, true, 2);
367 $cmdArr[$cmd]['app'] = basename($cmdPath);
368 $cmdArr[$cmd]['path'] = dirname($cmdPath) . '/';
369 $cmdArr[$cmd]['valid'] = true;
370 }
371 }
372
373 return $cmdArr;
374 }
375
376 /**
377 * Sets the search paths from different sources, internal
378 *
379 * @return array Array of absolute paths (keys and values are equal)
380 */
381 protected static function getPathsInternal()
382 {
383 $pathsArr = [];
384 $sysPathArr = [];
385
386 // Image magick paths first
387 // im_path_lzw take precedence over im_path
388 if (($imPath = ($GLOBALS['TYPO3_CONF_VARS']['GFX']['im_path_lzw'] ?: $GLOBALS['TYPO3_CONF_VARS']['GFX']['im_path']))) {
389 $imPath = self::fixPath($imPath);
390 $pathsArr[$imPath] = $imPath;
391 }
392
393 // Add configured paths
394 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']) {
395 $sysPath = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath'], true);
396 foreach ($sysPath as $val) {
397 $val = self::fixPath($val);
398 $sysPathArr[$val] = $val;
399 }
400 }
401
402 // Add path from environment
403 if (!empty($GLOBALS['_SERVER']['PATH']) || !empty($GLOBALS['_SERVER']['Path'])) {
404 $sep = (TYPO3_OS === 'WIN' ? ';' : ':');
405 $serverPath = !empty($GLOBALS['_SERVER']['PATH']) ? $GLOBALS['_SERVER']['PATH'] : $GLOBALS['_SERVER']['Path'];
406 $envPath = GeneralUtility::trimExplode($sep, $serverPath, true);
407 foreach ($envPath as $val) {
408 $val = self::fixPath($val);
409 $sysPathArr[$val] = $val;
410 }
411 }
412
413 // Set common paths for Unix (only)
414 if (TYPO3_OS !== 'WIN') {
415 $sysPathArr = array_merge($sysPathArr, [
416 '/usr/bin/' => '/usr/bin/',
417 '/usr/local/bin/' => '/usr/local/bin/',
418 ]);
419 }
420
421 $pathsArr = array_merge($pathsArr, $sysPathArr);
422
423 return $pathsArr;
424 }
425
426 /**
427 * Set a path to the right format
428 *
429 * @param string $path Input path
430 * @return string Output path
431 */
432 protected static function fixPath($path)
433 {
434 return str_replace('//', '/', $path . '/');
435 }
436
437 /**
438 * Escape shell arguments (for example filenames) to be used on the local system.
439 *
440 * The setting UTF8filesystem will be taken into account.
441 *
442 * @param string[] $input Input arguments to be escaped
443 * @return string[] Escaped shell arguments
444 */
445 public static function escapeShellArguments(array $input)
446 {
447 $isUTF8Filesystem = !empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']);
448 if ($isUTF8Filesystem) {
449 $currentLocale = setlocale(LC_CTYPE, 0);
450 setlocale(LC_CTYPE, $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
451 }
452
453 $output = array_map('escapeshellarg', $input);
454
455 if ($isUTF8Filesystem) {
456 setlocale(LC_CTYPE, $currentLocale);
457 }
458
459 return $output;
460 }
461
462 /**
463 * Escape a shell argument (for example a filename) to be used on the local system.
464 *
465 * The setting UTF8filesystem will be taken into account.
466 *
467 * @param string $input Input-argument to be escaped
468 * @return string Escaped shell argument
469 */
470 public static function escapeShellArgument($input)
471 {
472 return self::escapeShellArguments([$input])[0];
473 }
474 }