1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Console;
16:
17: use Cake\Console\Exception\StopException;
18: use Cake\Log\Engine\ConsoleLog;
19: use Cake\Log\Log;
20: use RuntimeException;
21: use SplFileObject;
22:
23: /**
24: * A wrapper around the various IO operations shell tasks need to do.
25: *
26: * Packages up the stdout, stderr, and stdin streams providing a simple
27: * consistent interface for shells to use. This class also makes mocking streams
28: * easy to do in unit tests.
29: */
30: class ConsoleIo
31: {
32:
33: /**
34: * The output stream
35: *
36: * @var \Cake\Console\ConsoleOutput
37: */
38: protected $_out;
39:
40: /**
41: * The error stream
42: *
43: * @var \Cake\Console\ConsoleOutput
44: */
45: protected $_err;
46:
47: /**
48: * The input stream
49: *
50: * @var \Cake\Console\ConsoleInput
51: */
52: protected $_in;
53:
54: /**
55: * The helper registry.
56: *
57: * @var \Cake\Console\HelperRegistry
58: */
59: protected $_helpers;
60:
61: /**
62: * Output constant making verbose shells.
63: *
64: * @var int
65: */
66: const VERBOSE = 2;
67:
68: /**
69: * Output constant for making normal shells.
70: *
71: * @var int
72: */
73: const NORMAL = 1;
74:
75: /**
76: * Output constants for making quiet shells.
77: *
78: * @var int
79: */
80: const QUIET = 0;
81:
82: /**
83: * The current output level.
84: *
85: * @var int
86: */
87: protected $_level = self::NORMAL;
88:
89: /**
90: * The number of bytes last written to the output stream
91: * used when overwriting the previous message.
92: *
93: * @var int
94: */
95: protected $_lastWritten = 0;
96:
97: /**
98: * Whether or not files should be overwritten
99: *
100: * @var bool
101: */
102: protected $forceOverwrite = false;
103:
104: /**
105: * Constructor
106: *
107: * @param \Cake\Console\ConsoleOutput|null $out A ConsoleOutput object for stdout.
108: * @param \Cake\Console\ConsoleOutput|null $err A ConsoleOutput object for stderr.
109: * @param \Cake\Console\ConsoleInput|null $in A ConsoleInput object for stdin.
110: * @param \Cake\Console\HelperRegistry|null $helpers A HelperRegistry instance
111: */
112: public function __construct(ConsoleOutput $out = null, ConsoleOutput $err = null, ConsoleInput $in = null, HelperRegistry $helpers = null)
113: {
114: $this->_out = $out ?: new ConsoleOutput('php://stdout');
115: $this->_err = $err ?: new ConsoleOutput('php://stderr');
116: $this->_in = $in ?: new ConsoleInput('php://stdin');
117: $this->_helpers = $helpers ?: new HelperRegistry();
118: $this->_helpers->setIo($this);
119: }
120:
121: /**
122: * Get/set the current output level.
123: *
124: * @param null|int $level The current output level.
125: * @return int The current output level.
126: */
127: public function level($level = null)
128: {
129: if ($level !== null) {
130: $this->_level = $level;
131: }
132:
133: return $this->_level;
134: }
135:
136: /**
137: * Output at the verbose level.
138: *
139: * @param string|array $message A string or an array of strings to output
140: * @param int $newlines Number of newlines to append
141: * @return int|bool The number of bytes returned from writing to stdout.
142: */
143: public function verbose($message, $newlines = 1)
144: {
145: return $this->out($message, $newlines, self::VERBOSE);
146: }
147:
148: /**
149: * Output at all levels.
150: *
151: * @param string|array $message A string or an array of strings to output
152: * @param int $newlines Number of newlines to append
153: * @return int|bool The number of bytes returned from writing to stdout.
154: */
155: public function quiet($message, $newlines = 1)
156: {
157: return $this->out($message, $newlines, self::QUIET);
158: }
159:
160: /**
161: * Outputs a single or multiple messages to stdout. If no parameters
162: * are passed outputs just a newline.
163: *
164: * ### Output levels
165: *
166: * There are 3 built-in output level. ConsoleIo::QUIET, ConsoleIo::NORMAL, ConsoleIo::VERBOSE.
167: * The verbose and quiet output levels, map to the `verbose` and `quiet` output switches
168: * present in most shells. Using ConsoleIo::QUIET for a message means it will always display.
169: * While using ConsoleIo::VERBOSE means it will only display when verbose output is toggled.
170: *
171: * @param string|array $message A string or an array of strings to output
172: * @param int $newlines Number of newlines to append
173: * @param int $level The message's output level, see above.
174: * @return int|bool The number of bytes returned from writing to stdout.
175: */
176: public function out($message = '', $newlines = 1, $level = self::NORMAL)
177: {
178: if ($level <= $this->_level) {
179: $this->_lastWritten = (int)$this->_out->write($message, $newlines);
180:
181: return $this->_lastWritten;
182: }
183:
184: return true;
185: }
186:
187: /**
188: * Convenience method for out() that wraps message between <info /> tag
189: *
190: * @param string|array|null $message A string or an array of strings to output
191: * @param int $newlines Number of newlines to append
192: * @param int $level The message's output level, see above.
193: * @return int|bool The number of bytes returned from writing to stdout.
194: * @see https://book.cakephp.org/3.0/en/console-and-shells.html#ConsoleIo::out
195: */
196: public function info($message = null, $newlines = 1, $level = self::NORMAL)
197: {
198: $messageType = 'info';
199: $message = $this->wrapMessageWithType($messageType, $message);
200:
201: return $this->out($message, $newlines, $level);
202: }
203:
204: /**
205: * Convenience method for err() that wraps message between <warning /> tag
206: *
207: * @param string|array|null $message A string or an array of strings to output
208: * @param int $newlines Number of newlines to append
209: * @return int|bool The number of bytes returned from writing to stderr.
210: * @see https://book.cakephp.org/3.0/en/console-and-shells.html#ConsoleIo::err
211: */
212: public function warning($message = null, $newlines = 1)
213: {
214: $messageType = 'warning';
215: $message = $this->wrapMessageWithType($messageType, $message);
216:
217: return $this->err($message, $newlines);
218: }
219:
220: /**
221: * Convenience method for err() that wraps message between <error /> tag
222: *
223: * @param string|array|null $message A string or an array of strings to output
224: * @param int $newlines Number of newlines to append
225: * @return int|bool The number of bytes returned from writing to stderr.
226: * @see https://book.cakephp.org/3.0/en/console-and-shells.html#ConsoleIo::err
227: */
228: public function error($message = null, $newlines = 1)
229: {
230: $messageType = 'error';
231: $message = $this->wrapMessageWithType($messageType, $message);
232:
233: return $this->err($message, $newlines);
234: }
235:
236: /**
237: * Convenience method for out() that wraps message between <success /> tag
238: *
239: * @param string|array|null $message A string or an array of strings to output
240: * @param int $newlines Number of newlines to append
241: * @param int $level The message's output level, see above.
242: * @return int|bool The number of bytes returned from writing to stdout.
243: * @see https://book.cakephp.org/3.0/en/console-and-shells.html#ConsoleIo::out
244: */
245: public function success($message = null, $newlines = 1, $level = self::NORMAL)
246: {
247: $messageType = 'success';
248: $message = $this->wrapMessageWithType($messageType, $message);
249:
250: return $this->out($message, $newlines, $level);
251: }
252:
253: /**
254: * Wraps a message with a given message type, e.g. <warning>
255: *
256: * @param string $messageType The message type, e.g. "warning".
257: * @param string|array $message The message to wrap.
258: * @return array|string The message wrapped with the given message type.
259: */
260: protected function wrapMessageWithType($messageType, $message)
261: {
262: if (is_array($message)) {
263: foreach ($message as $k => $v) {
264: $message[$k] = "<{$messageType}>{$v}</{$messageType}>";
265: }
266: } else {
267: $message = "<{$messageType}>{$message}</{$messageType}>";
268: }
269:
270: return $message;
271: }
272:
273: /**
274: * Overwrite some already output text.
275: *
276: * Useful for building progress bars, or when you want to replace
277: * text already output to the screen with new text.
278: *
279: * **Warning** You cannot overwrite text that contains newlines.
280: *
281: * @param array|string $message The message to output.
282: * @param int $newlines Number of newlines to append.
283: * @param int|null $size The number of bytes to overwrite. Defaults to the
284: * length of the last message output.
285: * @return void
286: */
287: public function overwrite($message, $newlines = 1, $size = null)
288: {
289: $size = $size ?: $this->_lastWritten;
290:
291: // Output backspaces.
292: $this->out(str_repeat("\x08", $size), 0);
293:
294: $newBytes = $this->out($message, 0);
295:
296: // Fill any remaining bytes with spaces.
297: $fill = $size - $newBytes;
298: if ($fill > 0) {
299: $this->out(str_repeat(' ', $fill), 0);
300: }
301: if ($newlines) {
302: $this->out($this->nl($newlines), 0);
303: }
304:
305: // Store length of content + fill so if the new content
306: // is shorter than the old content the next overwrite
307: // will work.
308: if ($fill > 0) {
309: $this->_lastWritten = $newBytes + $fill;
310: }
311: }
312:
313: /**
314: * Outputs a single or multiple error messages to stderr. If no parameters
315: * are passed outputs just a newline.
316: *
317: * @param string|array $message A string or an array of strings to output
318: * @param int $newlines Number of newlines to append
319: * @return int|bool The number of bytes returned from writing to stderr.
320: */
321: public function err($message = '', $newlines = 1)
322: {
323: return $this->_err->write($message, $newlines);
324: }
325:
326: /**
327: * Returns a single or multiple linefeeds sequences.
328: *
329: * @param int $multiplier Number of times the linefeed sequence should be repeated
330: * @return string
331: */
332: public function nl($multiplier = 1)
333: {
334: return str_repeat(ConsoleOutput::LF, $multiplier);
335: }
336:
337: /**
338: * Outputs a series of minus characters to the standard output, acts as a visual separator.
339: *
340: * @param int $newlines Number of newlines to pre- and append
341: * @param int $width Width of the line, defaults to 79
342: * @return void
343: */
344: public function hr($newlines = 0, $width = 79)
345: {
346: $this->out(null, $newlines);
347: $this->out(str_repeat('-', $width));
348: $this->out(null, $newlines);
349: }
350:
351: /**
352: * Prompts the user for input, and returns it.
353: *
354: * @param string $prompt Prompt text.
355: * @param string|null $default Default input value.
356: * @return mixed Either the default value, or the user-provided input.
357: */
358: public function ask($prompt, $default = null)
359: {
360: return $this->_getInput($prompt, null, $default);
361: }
362:
363: /**
364: * Change the output mode of the stdout stream
365: *
366: * @param int $mode The output mode.
367: * @return void
368: * @see \Cake\Console\ConsoleOutput::setOutputAs()
369: */
370: public function setOutputAs($mode)
371: {
372: $this->_out->setOutputAs($mode);
373: }
374:
375: /**
376: * Change the output mode of the stdout stream
377: *
378: * @deprecated 3.5.0 Use setOutputAs() instead.
379: * @param int $mode The output mode.
380: * @return void
381: * @see \Cake\Console\ConsoleOutput::outputAs()
382: */
383: public function outputAs($mode)
384: {
385: deprecationWarning('ConsoleIo::outputAs() is deprecated. Use ConsoleIo::setOutputAs() instead.');
386: $this->_out->setOutputAs($mode);
387: }
388:
389: /**
390: * Add a new output style or get defined styles.
391: *
392: * @param string|null $style The style to get or create.
393: * @param array|bool|null $definition The array definition of the style to change or create a style
394: * or false to remove a style.
395: * @return mixed If you are getting styles, the style or null will be returned. If you are creating/modifying
396: * styles true will be returned.
397: * @see \Cake\Console\ConsoleOutput::styles()
398: */
399: public function styles($style = null, $definition = null)
400: {
401: return $this->_out->styles($style, $definition);
402: }
403:
404: /**
405: * Prompts the user for input based on a list of options, and returns it.
406: *
407: * @param string $prompt Prompt text.
408: * @param string|array $options Array or string of options.
409: * @param string|null $default Default input value.
410: * @return mixed Either the default value, or the user-provided input.
411: */
412: public function askChoice($prompt, $options, $default = null)
413: {
414: if ($options && is_string($options)) {
415: if (strpos($options, ',')) {
416: $options = explode(',', $options);
417: } elseif (strpos($options, '/')) {
418: $options = explode('/', $options);
419: } else {
420: $options = [$options];
421: }
422: }
423:
424: $printOptions = '(' . implode('/', $options) . ')';
425: $options = array_merge(
426: array_map('strtolower', $options),
427: array_map('strtoupper', $options),
428: $options
429: );
430: $in = '';
431: while ($in === '' || !in_array($in, $options)) {
432: $in = $this->_getInput($prompt, $printOptions, $default);
433: }
434:
435: return $in;
436: }
437:
438: /**
439: * Prompts the user for input, and returns it.
440: *
441: * @param string $prompt Prompt text.
442: * @param string|null $options String of options. Pass null to omit.
443: * @param string|null $default Default input value. Pass null to omit.
444: * @return string Either the default value, or the user-provided input.
445: */
446: protected function _getInput($prompt, $options, $default)
447: {
448: $optionsText = '';
449: if (isset($options)) {
450: $optionsText = " $options ";
451: }
452:
453: $defaultText = '';
454: if ($default !== null) {
455: $defaultText = "[$default] ";
456: }
457: $this->_out->write('<question>' . $prompt . "</question>$optionsText\n$defaultText> ", 0);
458: $result = $this->_in->read();
459:
460: $result = trim($result);
461: if ($default !== null && ($result === '' || $result === null)) {
462: return $default;
463: }
464:
465: return $result;
466: }
467:
468: /**
469: * Connects or disconnects the loggers to the console output.
470: *
471: * Used to enable or disable logging stream output to stdout and stderr
472: * If you don't wish all log output in stdout or stderr
473: * through Cake's Log class, call this function with `$enable=false`.
474: *
475: * @param int|bool $enable Use a boolean to enable/toggle all logging. Use
476: * one of the verbosity constants (self::VERBOSE, self::QUIET, self::NORMAL)
477: * to control logging levels. VERBOSE enables debug logs, NORMAL does not include debug logs,
478: * QUIET disables notice, info and debug logs.
479: * @return void
480: */
481: public function setLoggers($enable)
482: {
483: Log::drop('stdout');
484: Log::drop('stderr');
485: if ($enable === false) {
486: return;
487: }
488: $outLevels = ['notice', 'info'];
489: if ($enable === static::VERBOSE || $enable === true) {
490: $outLevels[] = 'debug';
491: }
492: if ($enable !== static::QUIET) {
493: $stdout = new ConsoleLog([
494: 'types' => $outLevels,
495: 'stream' => $this->_out
496: ]);
497: Log::setConfig('stdout', ['engine' => $stdout]);
498: }
499: $stderr = new ConsoleLog([
500: 'types' => ['emergency', 'alert', 'critical', 'error', 'warning'],
501: 'stream' => $this->_err,
502: ]);
503: Log::setConfig('stderr', ['engine' => $stderr]);
504: }
505:
506: /**
507: * Render a Console Helper
508: *
509: * Create and render the output for a helper object. If the helper
510: * object has not already been loaded, it will be loaded and constructed.
511: *
512: * @param string $name The name of the helper to render
513: * @param array $settings Configuration data for the helper.
514: * @return \Cake\Console\Helper The created helper instance.
515: */
516: public function helper($name, array $settings = [])
517: {
518: $name = ucfirst($name);
519:
520: return $this->_helpers->load($name, $settings);
521: }
522:
523: /**
524: * Create a file at the given path.
525: *
526: * This method will prompt the user if a file will be overwritten.
527: * Setting `forceOverwrite` to true will suppress this behavior
528: * and always overwrite the file.
529: *
530: * If the user replies `a` subsequent `forceOverwrite` parameters will
531: * be coerced to true and all files will be overwritten.
532: *
533: * @param string $path The path to create the file at.
534: * @param string $contents The contents to put into the file.
535: * @param bool $forceOverwrite Whether or not the file should be overwritten.
536: * If true, no question will be asked about whether or not to overwrite existing files.
537: * @return bool Success.
538: * @throws \Cake\Console\Exception\StopException When `q` is given as an answer
539: * to whether or not a file should be overwritten.
540: */
541: public function createFile($path, $contents, $forceOverwrite = false)
542: {
543: $this->out();
544: $forceOverwrite = $forceOverwrite || $this->forceOverwrite;
545:
546: if (file_exists($path) && $forceOverwrite === false) {
547: $this->warning("File `{$path}` exists");
548: $key = $this->askChoice('Do you want to overwrite?', ['y', 'n', 'a', 'q'], 'n');
549: $key = strtolower($key);
550:
551: if ($key === 'q') {
552: $this->error('Quitting.', 2);
553: throw new StopException('Not creating file. Quitting.');
554: }
555: if ($key === 'a') {
556: $this->forceOverwrite = true;
557: $key = 'y';
558: }
559: if ($key !== 'y') {
560: $this->out("Skip `{$path}`", 2);
561:
562: return false;
563: }
564: } else {
565: $this->out("Creating file {$path}");
566: }
567:
568: try {
569: // Create the directory using the current user permissions.
570: $directory = dirname($path);
571: if (!file_exists($directory)) {
572: mkdir($directory, 0777 ^ umask(), true);
573: }
574:
575: $file = new SplFileObject($path, 'w');
576: } catch (RuntimeException $e) {
577: $this->error("Could not write to `{$path}`. Permission denied.", 2);
578:
579: return false;
580: }
581:
582: $file->rewind();
583: if ($file->fwrite($contents) > 0) {
584: $this->out("<success>Wrote</success> `{$path}`");
585:
586: return true;
587: }
588: $this->error("Could not write to `{$path}`.", 2);
589:
590: return false;
591: }
592: }
593: