Added new methods _input and _inputYesNo to get input
[TYPO3CMS/Extensions/t3build.git] / provider / class.abstract.php
1 <?php
2 /**
3 * Base class for providers - which extracts the CLI help
4 * from the docBlocks of the class and the class vars which
5 * have an @arg tag.
6 *
7 * If you label an argument with @required it will be
8 * required and checked upfront - if it's missing, the
9 * execution will stop with an error.
10 *
11 * If you need wildcard arguments (eg. to pass them to
12 * another provider) you can label them with @mask:
13 * @mask --clean-*
14 *
15 * The type (@var) of the arguments will be considered and
16 * CLI args will be casted accordingly before execution.
17 *
18 * When there are setter methods for the arguments
19 * (setArgument) they will be called instead of directly
20 * setting the class vars.
21 *
22 * @package t3build
23 * @author Christian Opitz <co@netzelf.de>
24 */
25 abstract class tx_t3build_provider_abstract
26 {
27 /**
28 * Missing arguments
29 * @var array
30 */
31 private $_missing = array();
32
33 /**
34 * Argument information array
35 * @var array
36 */
37 private $_infos = array();
38
39 /**
40 * Required arguments
41 * @var array
42 */
43 private $_requireds = array();
44
45 /**
46 * Reflection of $this class
47 * @var ReflectionClass
48 */
49 private $_class;
50
51 /**
52 * Override this if you want the default action
53 * to be another than that with the class name
54 * + 'Action' as method name.
55 * @var string
56 */
57 protected $defaultActionName;
58
59 /**
60 * Print debug information
61 * @arg
62 * @var boolean
63 */
64 protected $debug = false;
65
66 /**
67 * Print help information
68 * @arg
69 * @var boolean
70 */
71 protected $help = false;
72
73 /**
74 * Non interactive mode: Exit on required input or
75 * use default when available
76 * @arg
77 * @var boolean
78 */
79 protected $nonInteractive = false;
80
81 /**
82 * Yes to all: Answer all yes/no questions with yes
83 * @arg
84 * @var boolean
85 */
86 protected $yesToAll = false;
87
88 /**
89 * The raw cli args as passed from TYPO3
90 * @var array
91 */
92 protected $cliArgs = array();
93
94 /**
95 * The stdIn resource
96 * @var resource
97 */
98 private $stdIn;
99
100 /**
101 * Initialization: Retrieve the information about
102 * the arguments and set the corresponding class
103 * vars accordingly or fail the execution when
104 * @required arguments are missing.
105 *
106 * @param array $args
107 */
108 public function init($args)
109 {
110 if (!TYPO3_cliMode) {
111 $this->_debug('Not in CLI mode - entering non-interactive mode');
112 $this->nonInteractive = true;
113 }
114
115 $this->cliArgs = $args;
116 $this->_class = new ReflectionClass($this);
117 $masks = array();
118 $modifiers = array();
119
120 foreach ($this->_class->getProperties() as $i => $property) {
121 if (preg_match_all('/^\s+\*\s+@([^\s]+)(.*)$/m', $property->getDocComment(), $matches)) {
122 if (!in_array('arg', $matches[1])) {
123 continue;
124 }
125 $filteredName = ltrim($property->getName(), '_');
126 $name = ucfirst($filteredName);
127 preg_match_all('/[A-Z][a-z]*/', $name, $words);
128 $shorthand = '';
129 $switch = strtolower(implode('-', $words[0]));
130 $shorthand = !array_key_exists('-'.$filteredName[0], $modifiers) ? $filteredName[0] : null;
131 $info = array(
132 'setter' => method_exists($this, 'set'.$name) ? 'set'.$name : null,
133 'property' => $property->getName(),
134 'switch' => $switch,
135 'shorthand' => $shorthand,
136 'comment' => $property->getDocComment(),
137 'type' => null,
138 'mask' => null
139 );
140
141 $maskKey = array_search('mask', $matches[1]);
142 if ($maskKey !== false && $matches[2][$maskKey]) {
143 $info['type'] = 'mask';
144 $info['mask'] = ltrim(trim($matches[2][$maskKey]), '-');
145 $info['shorthand'] = $shorthand = null;
146 $info['switch'] = $switch = null;
147 $masks[$i] = trim($info['mask'], '*');
148 } else {
149 $varKey = array_search('var', $matches[1]);
150 if ($varKey !== false) {
151 $info['type'] = trim($matches[2][$varKey]);
152 }
153 }
154 $this->_infos[$i] = $info;
155 $this->_requireds[$i] = in_array('required', $matches[1]);
156 if ($shorthand) {
157 $modifiers['-'.$shorthand] = $i;
158 }
159 if ($switch) {
160 $modifiers['--'.$switch] = $i;
161 }
162 }
163 }
164 $values = array();
165 foreach ($args as $argument => $value) {
166 if (!preg_match('/^(-{1,2})(.+)/', $argument, $parts)) {
167 continue;
168 }
169 $realArgs = array($parts[2]);
170 $argsCount = 1;
171 for ($n = 0; $n < $argsCount; $n++) {
172 $modifier = $parts[1].$realArgs[$n];
173 if (!array_key_exists($modifier, $modifiers)) {
174 if ($argsCount == 1) {
175 foreach ($masks as $i => $mask) {
176 if (substr($parts[2], 0, $l = strlen($mask)) == $mask) {
177 if (!isset($values[$i])) {
178 $values[$i] = (array) $this->{$this->_infos[$i]['property']};
179 }
180 $values[$i][$parts[1].substr($parts[2], $l)] = $value;
181 break 2;
182 }
183 }
184 if ($parts[1] == '-') {
185 $realArgs = str_split('0'.$parts[2]);
186 $argsCount = count($realArgs);
187 continue;
188 }
189 }
190 $this->_die('Unknown modifier "%s"', $modifier);
191 }
192 $i = $modifiers[$modifier];
193 switch ($this->_infos[$i]['type']) {
194 case 'boolean':
195 case 'bool':
196 $value = !count($value) || !in_array($value[0], array('false', '0'), true) ? true : false;
197 break;
198 case 'string':
199 $value = implode(',', $value);
200 break;
201 case 'int':
202 case 'integer':
203 $value = (int) $value[0];
204 break;
205 case 'float':
206 $value = (float) $value[0];
207 break;
208 case 'array':
209 break;
210 default:
211 $value = $value[0];
212 }
213 if ($this->_infos[$i]['property'] == 'debug') {
214 $this->debug = $value;
215 }
216 $values[$i] = $value;
217 }
218 }
219 foreach ($values as $i => $value) {
220 if ($this->_infos[$i]['setter']) {
221 $this->_debug('Calling setter '.$this->_infos[$i]['setter'].' with ', $value);
222 $this->{$this->_infos[$i]['setter']}($value);
223 } else {
224 $this->_debug('Setting property '.$this->_infos[$i]['property'].' to ', $value);
225 $this->{$this->_infos[$i]['property']} = $value;
226 }
227 }
228 foreach ($this->_requireds as $i => $required) {
229 if ($required && !array_key_exists($i, $values)) {
230 $this->_missing[] = '"'.$this->_infos[$i]['switch'].'"';
231 }
232 }
233 }
234
235 /**
236 * Render the help from the argument information
237 * @return string
238 */
239 protected function renderHelp()
240 {
241 preg_match_all('/^\s+\* ([^@\/].*)$/m', $this->_class->getDocComment(), $lines);
242 $help = implode("\n", $lines[1])."\n\n";
243 $help .= 'php '.$_SERVER['PHP_SELF'];
244 foreach ($this->_requireds as $i => $required) {
245 if ($required) {
246 $help .= ' -'.$this->_infos[$i]['shorthand'].' "'.$this->_infos[$i]['switch'].'"';
247 }
248 }
249
250 $longest = 0;
251 $order = array();
252 foreach ($this->_infos as $i => $info) {
253 // Help stuff
254 preg_match_all('/^\s+\* ([^@\/].*)$/m', $info['comment'], $lines);
255 $this->_infos[$i]['desc'] = $lines[1];
256 $this->_infos[$i]['default'] = $this->{$info['property']};
257 if ($this->_infos[$i]['mask']) {
258 $this->_infos[$i]['switchDesc'] = '-'.$this->_infos[$i]['mask'].', --'.$this->_infos[$i]['mask'];
259 } else {
260 $this->_infos[$i]['switchDesc'] = '--'.$info['switch'];
261 if ($info['shorthand']) {
262 $this->_infos[$i]['switchDesc'] = '-'.$info['shorthand'].' ['.$this->_infos[$i]['switchDesc'].']';
263 }
264 }
265 $length = strlen($this->_infos[$i]['switchDesc']);
266 if ($length > $longest) {
267 $longest = $length;
268 }
269 $order[$i] = $info['switch'];
270 }
271
272 asort($order);
273
274 $help .= PHP_EOL.PHP_EOL;
275 $pre = str_repeat(' ', $longest+1);
276 foreach (array_keys($order) as $i) {
277 $info = $this->_infos[$i];
278 $length = strlen($info['switchDesc']);
279 $default = $info['default'];
280 if ($default !== '' && $default !== null) {
281 if ($default === true) {
282 $default = 'true';
283 } elseif ($default === false) {
284 $default = 'false';
285 } elseif ($info['type'] == 'array') {
286 $default = implode(', ', (array) $default);
287 }
288 $info['desc'][] .= '(defaults to "'.$default.'")';
289 }
290 $help .= $info['switchDesc'].str_repeat(' ', $longest - $length + 1).':'.' ';
291 $help .= implode(PHP_EOL.str_repeat(' ', $longest+3), $info['desc']);
292 $help .= PHP_EOL;
293 }
294
295 return $help;
296 }
297
298 /**
299 * Output help
300 */
301 public function helpAction()
302 {
303 $this->_echo($this->renderHelp());
304 }
305
306 /**
307 * Run the provider
308 *
309 * @param string|null $action
310 * @return mixed|void
311 */
312 public function run($action = null)
313 {
314 if ($this->help) {
315 $action = 'help';
316 }
317 if (!$action) {
318 if ($this->defaultActionName) {
319 $action = $this->defaultActionName;
320 } else {
321 $methods = $this->_class->getMethods();
322 $actions = array();
323 foreach ($methods as $method) {
324 /* @var $method ReflectionMethod */
325 if ($method->name != 'helpAction' && substr($method->name, -6) == 'Action') {
326 $actions[$name = substr($method->name, 0, -6)] = $name;
327 }
328 }
329 if (count($actions) == 1) {
330 $action = array_shift($actions);
331 }
332 }
333 }
334 if (!$action) {
335 $this->_echo('No action provided');
336 $action = 'help';
337 }
338 if (!is_callable(array($this, $action.'Action'))) {
339 $this->_echo('Invalid action "'.$action.'"');
340 $action = 'help';
341 }
342 if (count($this->_missing) && $action != 'help') {
343 $this->_echo('Missing argument'.(count($this->_missing) > 1 ? 's' : '').' %s', $this->_missing);
344 $action = 'help';
345 }
346 return call_user_func(array($this, $action.'Action'));
347 }
348
349 /**
350 * Optionally ask $question and ask user for an answer
351 * (if in non-interactive mode, it will use the default
352 * value if given or fail otherwise)
353 * When you pass $validResults the user input will be
354 * validated to match one of them (you can allow short
355 * answers by providing non numeric keys in this array)
356 *
357 * @param string|array|null $questionOrValidResults
358 * @param array|null $validResults
359 * @param string|null $default
360 */
361 protected function _input($questionOrValidResults = null, $validResults = null, $default = null)
362 {
363 if (is_array($questionOrValidResults)) {
364 $validResults = $questionOrValidResults;
365 $questionOrValidResults = null;
366 }
367 if ($questionOrValidResults) {
368 if ($default !== null) {
369 $questionOrValidResults .= ' (leave empty for '.$default.')';
370 }
371 $this->_echo($questionOrValidResults);
372 }
373
374 if ($this->nonInteractive) {
375 if ($default !== null) {
376 $this->_echo('Non-interactive mode - answering with "'.$default.'"');
377 return $default;
378 }
379 $this->_die('Non-interactive mode - aborting');
380 } else {
381 if (!$this->stdIn) {
382 $this->stdIn = fopen('php://stdin', 'r');
383 }
384 while (FALSE == ($line = fgets($this->stdIn, 1000))) {
385 }
386 }
387
388 $line = trim($line);
389 if ($line === '' && $default !== null) {
390 $this->_echo($default);
391 return $default;
392 }
393 if (is_array($validResults) && !in_array($line, $validResults)) {
394 $validResultsTemp = $validResults;
395 foreach ($validResults as $key => $value) {
396 if (!is_numeric($key)) {
397 if ($line == $key) {
398 return $value;
399 }
400 $validResults[$key] .= '/'.$key;
401 }
402 }
403 $last = array_pop($validResults);
404 $valid = count($validResults) ? implode(', ', $validResults). ' or '.$last : $last;
405 $this->_echo('Please type '.$valid.': ');
406 return $this->_input($validResultsTemp, $default);
407 }
408
409 return $line;
410 }
411
412 /**
413 * Optionally ask a $question and ask the user for an answer
414 * (yes/y, no/n) - automatically answer with yes when --yes-to-all
415 * is set
416 *
417 * @param string $question
418 * @param boolean $default
419 * @return boolean
420 */
421 protected function _inputYesNo($question = null, $default = null)
422 {
423 if ($this->yesToAll) {
424 if ($question) {
425 $this->_echo($question);
426 }
427 $this->_echo('yes');
428 return true;
429 }
430 if ($default !== null) {
431 $default = $default ? 'yes' : 'no';
432 }
433 return $this->_input($question, array('y' => 'yes', 'n' => 'no')) == 'yes';
434 }
435
436 /**
437 * Echo vsprintfed string
438 *
439 * @param string $msg (can contain sprintf format)
440 * @param mixed $arg
441 * @param ...
442 */
443 protected function _echo($msg)
444 {
445 $args = func_get_args();
446 array_shift($args);
447 foreach ($args as $i => $arg) {
448 if (is_array($arg)) {
449 $and = is_numeric($i) ? 'and' : $i;
450 $last = array_pop($arg);
451 $args[$i] = count($arg) ? implode(', ', $arg).' '.$and.' '.$last : $last;
452 }
453 }
454 echo vsprintf((string) $msg, $args)."\n";
455 }
456
457 /**
458 * Echo vsprintfed string and exit with error
459 *
460 * @param string $msg (can contain sprintf format)
461 * @param mixed $arg
462 * @param ...
463 */
464 protected function _die($msg)
465 {
466 $args = func_get_args();
467 call_user_func_array(array($this, '_echo'), $args);
468 exit(1);
469 }
470
471 /**
472 * Dump vars only if --debug is on
473 *
474 * @param string $msg
475 * @param mixed $var
476 * @param ...
477 */
478 protected function _debug($msg)
479 {
480 if (!$this->debug) {
481 return;
482 }
483 $args = func_get_args();
484 echo '[Debug] '.trim(array_shift($args));
485 if (count($args)) {
486 echo ' ';
487 call_user_func_array('var_dump', $args);
488 } else {
489 echo PHP_EOL;
490 }
491 }
492
493 /**
494 * Write config to extConf
495 *
496 * @param string $extKey
497 * @param array $update
498 */
499 protected function writeExtConf($extKey, array $update)
500 {
501 global $TYPO3_CONF_VARS;
502
503 $absPath = t3lib_extMgm::extPath($extKey);
504 $relPath = t3lib_extMgm::extRelPath($extKey);
505
506 /* @var $tsStyleConfig t3lib_tsStyleConfig */
507 $tsStyleConfig = t3lib_div::makeInstance('t3lib_tsStyleConfig');
508 $theConstants = $tsStyleConfig->ext_initTSstyleConfig(
509 t3lib_div::getUrl($absPath . 'ext_conf_template.txt'),
510 $absPath,
511 $relPath,
512 ''
513 );
514
515 $arr = @unserialize($TYPO3_CONF_VARS['EXT']['extConf'][$extKey]);
516 $arr = is_array($arr) ? $arr : array();
517
518 // Call processing function for constants config and data before write and form rendering:
519 if (is_array($TYPO3_CONF_VARS['SC_OPTIONS']['typo3/mod/tools/em/index.php']['tsStyleConfigForm'])) {
520 $_params = array('fields' => &$theConstants, 'data' => &$arr, 'extKey' => $extKey);
521 foreach ($TYPO3_CONF_VARS['SC_OPTIONS']['typo3/mod/tools/em/index.php']['tsStyleConfigForm'] as $_funcRef) {
522 t3lib_div::callUserFunction($_funcRef, $_params, $this);
523 }
524 unset($_params);
525 }
526
527
528 $arr = t3lib_div::array_merge_recursive_overrule($arr, $update);
529
530 /* @var $instObj t3lib_install */
531 $instObj = t3lib_div::makeInstance('t3lib_install');
532 $instObj->allowUpdateLocalConf = 1;
533 $instObj->updateIdentity = 'TYPO3 Extension Manager';
534
535 // Get lines from localconf file
536 $lines = $instObj->writeToLocalconf_control();
537 $instObj->setValueInLocalconfFile($lines, '$TYPO3_CONF_VARS[\'EXT\'][\'extConf\'][\'' . $extKey . '\']', serialize($arr)); // This will be saved only if there are no linebreaks in it !
538 $instObj->writeToLocalconf_control($lines);
539
540 t3lib_extMgm::removeCacheFiles();
541 }
542
543 /**
544 * Parses $vars into a path mask and makes it FS-safe
545 *
546 * @param string $mask
547 * @param array $vars
548 * @param string $renameMode
549 * @param boolean $absolute
550 * @return string
551 */
552 protected function getPath($mask, $vars, $renameMode = 'camelCase', $absolute = false)
553 {
554 $replace = array();
555 foreach ($vars as $key => $value) {
556 $replace[] = '${'.$key.'}';
557 }
558 $path = str_replace($replace, $vars, $mask);
559 if (preg_match('/\$\{([^\}]*)\}/', $path, $res)) {
560 $this->_die('Unknown var "'.$res[1].'" in path mask');
561 }
562
563 $pre = '';
564 if ($absolute) {
565 $parts = preg_split('#\s*[\\/]+\s*#', $path);
566 $rest = array();
567 while (count($parts)) {
568 $file = implode('/', $parts);
569 if (file_exists($file)) {
570 if (!count($rest) && is_file($file)) {
571 return $file;
572 }
573 $pre = $file.'/';
574 $path = implode('/', $rest);
575 break;
576 }
577 array_unshift($rest, array_pop($parts));
578 }
579 }
580
581 $path = strtolower($path);
582 $path = str_replace(':', '-', $path);
583 $path = preg_replace('#[^A-Za-z0-9/\-_\.]+#', ' ', $path);
584 $path = preg_replace('#\s*/+\s*#', '/', $path);
585 $parts = explode(' ', $path);
586 if ($renameMode == 'underscore') {
587 $path = implode('_', $parts);
588 } else {
589 $path = '';
590 $uc = false;
591 foreach ($parts as $part) {
592 $ucPart = ucfirst($part);
593 $path .= ($uc || $renameMode === 'CamelCase') ? $ucPart : $part;
594 $uc = $ucPart != $part;
595 }
596 }
597 return $pre.$path;
598 }
599 }