ccdcea18582abebf2f11d36be04ff46278865150
[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 * @author Steffen Kamper <steffen@typo3.org>
46 * @author René Fritz <r.fritz@colorcube.de>
47 */
48 class CommandUtility {
49
50 /**
51 * Tells if object is already initialized
52 *
53 * @var bool
54 */
55 protected static $initialized = FALSE;
56
57 /**
58 * Contains application list. This is an array with the following structure:
59 * - app => file name to the application (like 'tar' or 'bzip2')
60 * - path => full path to the application without application name (like '/usr/bin/' for '/usr/bin/tar')
61 * - valid => TRUE or FALSE
62 * Array key is identical to 'app'.
63 *
64 * @var array
65 */
66 protected static $applications = array();
67
68 /**
69 * Paths where to search for applications
70 *
71 * @var array
72 */
73 protected static $paths = NULL;
74
75 /**
76 * Wrapper function for php exec function
77 * Needs to be central to have better control and possible fix for issues
78 *
79 * @static
80 * @param string $command
81 * @param NULL|array $output
82 * @param int $returnValue
83 * @return NULL|array
84 */
85 static public function exec($command, &$output = NULL, &$returnValue = 0) {
86 $lastLine = exec($command, $output, $returnValue);
87 return $lastLine;
88 }
89
90 /**
91 * Compile the command for running ImageMagick/GraphicsMagick.
92 *
93 * @param string $command Command to be run: identify, convert or combine/composite
94 * @param string $parameters The parameters string
95 * @param string $path Override the default path (e.g. used by the install tool)
96 * @return string Compiled command that deals with IM6 & GraphicsMagick
97 */
98 static public function imageMagickCommand($command, $parameters, $path = '') {
99 $gfxConf = $GLOBALS['TYPO3_CONF_VARS']['GFX'];
100 $isExt = TYPO3_OS == 'WIN' ? '.exe' : '';
101 $switchCompositeParameters = FALSE;
102 if (!$path) {
103 $path = $gfxConf['im_path'];
104 }
105 $path = GeneralUtility::fixWindowsFilePath($path);
106 $im_version = strtolower($gfxConf['im_version_5']);
107 // This is only used internally, has no effect outside
108 if ($command === 'combine') {
109 $command = 'composite';
110 }
111 // Compile the path & command
112 if ($im_version === 'gm') {
113 $switchCompositeParameters = TRUE;
114 $path = self::escapeShellArgument($path . 'gm' . $isExt) . ' ' . self::escapeShellArgument($command);
115 } else {
116 if ($im_version === 'im6') {
117 $switchCompositeParameters = TRUE;
118 }
119 $path = self::escapeShellArgument($path . ($command == 'composite' ? 'composite' : $command) . $isExt);
120 }
121 // strip profile information for thumbnails and reduce their size
122 if ($parameters && $command != 'identify' && $gfxConf['im_useStripProfileByDefault'] && $gfxConf['im_stripProfileCommand'] != '') {
123 if (strpos($parameters, $gfxConf['im_stripProfileCommand']) === FALSE) {
124 // Determine whether the strip profile action has be disabled by TypoScript:
125 if ($parameters !== '-version' && strpos($parameters, '###SkipStripProfile###') === FALSE) {
126 $parameters = $gfxConf['im_stripProfileCommand'] . ' ' . $parameters;
127 } else {
128 $parameters = str_replace('###SkipStripProfile###', '', $parameters);
129 }
130 }
131 }
132 $cmdLine = $path . ' ' . $parameters;
133 // Because of some weird incompatibilities between ImageMagick 4 and 6 (plus GraphicsMagick),
134 // it is needed to change the parameters order under some preconditions
135 if ($command == 'composite' && $switchCompositeParameters) {
136 $paramsArr = GeneralUtility::unQuoteFilenames($parameters);
137 // The mask image has been specified => swap the parameters
138 if (count($paramsArr) > 5) {
139 $tmp = $paramsArr[count($paramsArr) - 3];
140 $paramsArr[count($paramsArr) - 3] = $paramsArr[count($paramsArr) - 4];
141 $paramsArr[count($paramsArr) - 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 if (!self::init()) {
157 return FALSE;
158 }
159
160 if ($handler && !self::checkCommand($handler)) {
161 return -1;
162 }
163 // Already checked and valid
164 if (self::$applications[$cmd]['valid']) {
165 return TRUE;
166 }
167 // Is set but was (above) not TRUE
168 if (isset(self::$applications[$cmd]['valid'])) {
169 return FALSE;
170 }
171
172 foreach (self::$paths as $path => $validPath) {
173 // Ignore invalid (FALSE) paths
174 if ($validPath) {
175 if (TYPO3_OS == 'WIN') {
176 // Windows OS
177 // @todo Why is_executable() is not called here?
178 if (@is_file($path . $cmd)) {
179 self::$applications[$cmd]['app'] = $cmd;
180 self::$applications[$cmd]['path'] = $path;
181 self::$applications[$cmd]['valid'] = TRUE;
182 return TRUE;
183 }
184 if (@is_file($path . $cmd . '.exe')) {
185 self::$applications[$cmd]['app'] = $cmd . '.exe';
186 self::$applications[$cmd]['path'] = $path;
187 self::$applications[$cmd]['valid'] = TRUE;
188 return TRUE;
189 }
190 } else {
191 // Unix-like OS
192 $filePath = realpath($path . $cmd);
193 if ($filePath && @is_executable($filePath)) {
194 self::$applications[$cmd]['app'] = $cmd;
195 self::$applications[$cmd]['path'] = $path;
196 self::$applications[$cmd]['valid'] = TRUE;
197 return TRUE;
198 }
199 }
200 }
201 }
202
203 // Try to get the executable with the command 'which'.
204 // It does the same like already done, but maybe on other paths
205 if (TYPO3_OS != 'WIN') {
206 $cmd = @self::exec('which ' . $cmd);
207 if (@is_executable($cmd)) {
208 self::$applications[$cmd]['app'] = $cmd;
209 self::$applications[$cmd]['path'] = dirname($cmd) . '/';
210 self::$applications[$cmd]['valid'] = TRUE;
211 return TRUE;
212 }
213 }
214
215 return FALSE;
216 }
217
218 /**
219 * Returns a command string for exec(), system()
220 *
221 * @param string $cmd The command that should be executed. eg: "convert"
222 * @param string $handler Handler (executor) for the command. eg: "perl"
223 * @param string $handlerOpt Options for the handler, like '-w' for "perl"
224 * @return mixed Returns command string, or FALSE if cmd is not found, or -1 if the handler is not found
225 */
226 public static function getCommand($cmd, $handler = '', $handlerOpt = '') {
227 if (!self::init()) {
228 return FALSE;
229 }
230
231 // Handler
232 if ($handler) {
233 $handler = self::getCommand($handler);
234
235 if (!$handler) {
236 return -1;
237 }
238 $handler .= ' ' . $handlerOpt . ' ';
239 }
240
241 // Command
242 if (!self::checkCommand($cmd)) {
243 return FALSE;
244 }
245 $cmd = self::$applications[$cmd]['path'] . self::$applications[$cmd]['app'] . ' ';
246
247 return trim($handler . $cmd);
248 }
249
250 /**
251 * Extend the preset paths. This way an extension can install an executable and provide the path to \TYPO3\CMS\Core\Utility\CommandUtility
252 *
253 * @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).
254 * @return void
255 */
256 public static function addPaths($paths) {
257 self::initPaths($paths);
258 }
259
260 /**
261 * Returns an array of search paths
262 *
263 * @param bool $addInvalid If set the array contains invalid path too. Then the key is the path and the value is empty
264 * @return array Array of search paths (empty if exec is disabled)
265 */
266 public static function getPaths($addInvalid = FALSE) {
267 if (!self::init()) {
268 return array();
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 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['disable_exec_function']) {
290 return FALSE;
291 }
292 if (!self::$initialized) {
293 self::initPaths();
294 self::$applications = self::getConfiguredApps();
295 self::$initialized = TRUE;
296 }
297 return TRUE;
298 }
299
300 /**
301 * Initializes and extends the preset paths with own
302 *
303 * @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).
304 * @return void
305 */
306 protected static function initPaths($paths = '') {
307 $doCheck = FALSE;
308
309 // Init global paths array if not already done
310 if (!is_array(self::$paths)) {
311 self::$paths = self::getPathsInternal();
312 $doCheck = TRUE;
313 }
314 // Merge the submitted paths array to the global
315 if ($paths) {
316 $paths = GeneralUtility::trimExplode(',', $paths, TRUE);
317 if (is_array($paths)) {
318 foreach ($paths as $path) {
319 // Make absolute path of relative
320 if (!preg_match('#^/#', $path)) {
321 $path = PATH_site . $path;
322 }
323 if (!isset(self::$paths[$path])) {
324 if (@is_dir($path)) {
325 self::$paths[$path] = $path;
326 } else {
327 self::$paths[$path] = FALSE;
328 }
329 }
330 }
331 }
332 }
333 // Check if new paths are invalid
334 if ($doCheck) {
335 foreach (self::$paths as $path => $valid) {
336 // Ignore invalid (FALSE) paths
337 if ($valid AND !@is_dir($path)) {
338 self::$paths[$path] = FALSE;
339 }
340 }
341 }
342 }
343
344 /**
345 * Processes and returns the paths from $GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']
346 *
347 * @return array Array of commands and path
348 */
349 protected static function getConfiguredApps() {
350 $cmdArr = array();
351
352 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']) {
353 $pathSetup = preg_split('/[\n,]+/', $GLOBALS['TYPO3_CONF_VARS']['SYS']['binSetup']);
354 foreach ($pathSetup as $val) {
355 list($cmd, $cmdPath) = GeneralUtility::trimExplode('=', $val, TRUE);
356 $cmdArr[$cmd]['app'] = basename($cmdPath);
357 $cmdArr[$cmd]['path'] = dirname($cmdPath) . '/';
358 $cmdArr[$cmd]['valid'] = TRUE;
359 }
360 }
361
362 return $cmdArr;
363 }
364
365 /**
366 * Sets the search paths from different sources, internal
367 *
368 * @return array Array of absolute paths (keys and values are equal)
369 */
370 protected static function getPathsInternal() {
371
372 $pathsArr = array();
373 $sysPathArr = array();
374
375 // Image magick paths first
376 // im_path_lzw take precedence over im_path
377 if (($imPath = ($GLOBALS['TYPO3_CONF_VARS']['GFX']['im_path_lzw'] ?: $GLOBALS['TYPO3_CONF_VARS']['GFX']['im_path']))) {
378 $imPath = self::fixPath($imPath);
379 $pathsArr[$imPath] = $imPath;
380 }
381
382 // Add configured paths
383 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath']) {
384 $sysPath = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['SYS']['binPath'], TRUE);
385 foreach ($sysPath as $val) {
386 $val = self::fixPath($val);
387 $sysPathArr[$val] = $val;
388 }
389 }
390
391 // Add path from environment
392 // @todo how does this work for WIN
393 if ($GLOBALS['_SERVER']['PATH']) {
394 $sep = (TYPO3_OS == 'WIN' ? ';' : ':');
395 $envPath = GeneralUtility::trimExplode($sep, $GLOBALS['_SERVER']['PATH'], TRUE);
396 foreach ($envPath as $val) {
397 $val = self::fixPath($val);
398 $sysPathArr[$val] = $val;
399 }
400 }
401
402 // Set common paths for Unix (only)
403 if (TYPO3_OS !== 'WIN') {
404 $sysPathArr = array_merge($sysPathArr, array(
405 '/usr/bin/' => '/usr/bin/',
406 '/usr/local/bin/' => '/usr/local/bin/',
407 ));
408 }
409
410 $pathsArr = array_merge($pathsArr, $sysPathArr);
411
412 return $pathsArr;
413 }
414
415 /**
416 * Set a path to the right format
417 *
418 * @param string $path Input path
419 * @return string Output path
420 */
421 protected static function fixPath($path) {
422 return str_replace('//', '/', $path . '/');
423 }
424
425 /**
426 * Escape shell arguments (for example filenames) to be used on the local system.
427 *
428 * The setting UTF8filesystem will be taken into account.
429 *
430 * @param string[] $input Input arguments to be escaped
431 * @return string[] Escaped shell arguments
432 */
433 public static function escapeShellArguments(array $input) {
434 $isUTF8Filesystem = !empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']);
435 if ($isUTF8Filesystem) {
436 $currentLocale = setlocale(LC_CTYPE, 0);
437 setlocale(LC_CTYPE, $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemLocale']);
438 }
439
440 $output = array_map('escapeshellarg', $input);
441
442 if ($isUTF8Filesystem) {
443 setlocale(LC_CTYPE, $currentLocale);
444 }
445
446 return $output;
447 }
448
449 /**
450 * Escape a shell argument (for example a filename) to be used on the local system.
451 *
452 * The setting UTF8filesystem will be taken into account.
453 *
454 * @param string $input Input-argument to be escaped
455 * @return string Escaped shell argument
456 */
457 public static function escapeShellArgument($input) {
458 return self::escapeShellArguments(array($input))[0];
459 }
460 }