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 2.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Console;
16:
17: use Cake\Console\Exception\MissingShellException;
18: use Cake\Console\Exception\StopException;
19: use Cake\Core\App;
20: use Cake\Core\Configure;
21: use Cake\Core\Exception\Exception;
22: use Cake\Core\Plugin;
23: use Cake\Log\Log;
24: use Cake\Shell\Task\CommandTask;
25: use Cake\Utility\Inflector;
26:
27: /**
28: * Shell dispatcher handles dispatching cli commands.
29: *
30: * Consult /bin/cake.php for how this class is used in practice.
31: */
32: class ShellDispatcher
33: {
34: /**
35: * Contains arguments parsed from the command line.
36: *
37: * @var array
38: */
39: public $args = [];
40:
41: /**
42: * List of connected aliases.
43: *
44: * @var array
45: */
46: protected static $_aliases = [];
47:
48: /**
49: * Constructor
50: *
51: * The execution of the script is stopped after dispatching the request with
52: * a status code of either 0 or 1 according to the result of the dispatch.
53: *
54: * @param array $args the argv from PHP
55: * @param bool $bootstrap Should the environment be bootstrapped.
56: */
57: public function __construct($args = [], $bootstrap = true)
58: {
59: set_time_limit(0);
60: $this->args = (array)$args;
61:
62: $this->addShortPluginAliases();
63:
64: if ($bootstrap) {
65: $this->_initEnvironment();
66: }
67: }
68:
69: /**
70: * Add an alias for a shell command.
71: *
72: * Aliases allow you to call shells by alternate names. This is most
73: * useful when dealing with plugin shells that you want to have shorter
74: * names for.
75: *
76: * If you re-use an alias the last alias set will be the one available.
77: *
78: * ### Usage
79: *
80: * Aliasing a shell named ClassName:
81: *
82: * ```
83: * $this->alias('alias', 'ClassName');
84: * ```
85: *
86: * Getting the original name for a given alias:
87: *
88: * ```
89: * $this->alias('alias');
90: * ```
91: *
92: * @param string $short The new short name for the shell.
93: * @param string|null $original The original full name for the shell.
94: * @return string|false The aliased class name, or false if the alias does not exist
95: */
96: public static function alias($short, $original = null)
97: {
98: $short = Inflector::camelize($short);
99: if ($original) {
100: static::$_aliases[$short] = $original;
101: }
102:
103: return isset(static::$_aliases[$short]) ? static::$_aliases[$short] : false;
104: }
105:
106: /**
107: * Clear any aliases that have been set.
108: *
109: * @return void
110: */
111: public static function resetAliases()
112: {
113: static::$_aliases = [];
114: }
115:
116: /**
117: * Run the dispatcher
118: *
119: * @param array $argv The argv from PHP
120: * @param array $extra Extra parameters
121: * @return int The exit code of the shell process.
122: */
123: public static function run($argv, $extra = [])
124: {
125: $dispatcher = new ShellDispatcher($argv);
126:
127: return $dispatcher->dispatch($extra);
128: }
129:
130: /**
131: * Defines current working environment.
132: *
133: * @return void
134: * @throws \Cake\Core\Exception\Exception
135: */
136: protected function _initEnvironment()
137: {
138: if (!$this->_bootstrap()) {
139: $message = "Unable to load CakePHP core.\nMake sure Cake exists in " . CAKE_CORE_INCLUDE_PATH;
140: throw new Exception($message);
141: }
142:
143: if (function_exists('ini_set')) {
144: ini_set('html_errors', '0');
145: ini_set('implicit_flush', '1');
146: ini_set('max_execution_time', '0');
147: }
148:
149: $this->shiftArgs();
150: }
151:
152: /**
153: * Initializes the environment and loads the CakePHP core.
154: *
155: * @return bool Success.
156: */
157: protected function _bootstrap()
158: {
159: if (!Configure::read('App.fullBaseUrl')) {
160: Configure::write('App.fullBaseUrl', 'http://localhost');
161: }
162:
163: return true;
164: }
165:
166: /**
167: * Dispatches a CLI request
168: *
169: * Converts a shell command result into an exit code. Null/True
170: * are treated as success. All other return values are an error.
171: *
172: * @param array $extra Extra parameters that you can manually pass to the Shell
173: * to be dispatched.
174: * Built-in extra parameter is :
175: * - `requested` : if used, will prevent the Shell welcome message to be displayed
176: * @return int The cli command exit code. 0 is success.
177: */
178: public function dispatch($extra = [])
179: {
180: try {
181: $result = $this->_dispatch($extra);
182: } catch (StopException $e) {
183: return $e->getCode();
184: }
185: if ($result === null || $result === true) {
186: return Shell::CODE_SUCCESS;
187: }
188: if (is_int($result)) {
189: return $result;
190: }
191:
192: return Shell::CODE_ERROR;
193: }
194:
195: /**
196: * Dispatch a request.
197: *
198: * @param array $extra Extra parameters that you can manually pass to the Shell
199: * to be dispatched.
200: * Built-in extra parameter is :
201: * - `requested` : if used, will prevent the Shell welcome message to be displayed
202: * @return bool|int|null
203: * @throws \Cake\Console\Exception\MissingShellMethodException
204: */
205: protected function _dispatch($extra = [])
206: {
207: $shell = $this->shiftArgs();
208:
209: if (!$shell) {
210: $this->help();
211:
212: return false;
213: }
214: if (in_array($shell, ['help', '--help', '-h'])) {
215: $this->help();
216:
217: return true;
218: }
219: if (in_array($shell, ['version', '--version'])) {
220: $this->version();
221:
222: return true;
223: }
224:
225: $Shell = $this->findShell($shell);
226:
227: $Shell->initialize();
228:
229: return $Shell->runCommand($this->args, true, $extra);
230: }
231:
232: /**
233: * For all loaded plugins, add a short alias
234: *
235: * This permits a plugin which implements a shell of the same name to be accessed
236: * Using the shell name alone
237: *
238: * @return array the resultant list of aliases
239: */
240: public function addShortPluginAliases()
241: {
242: $plugins = Plugin::loaded();
243:
244: $io = new ConsoleIo();
245: $task = new CommandTask($io);
246: $io->setLoggers(false);
247: $list = $task->getShellList() + ['app' => []];
248: $fixed = array_flip($list['app']) + array_flip($list['CORE']);
249: $aliases = $others = [];
250:
251: foreach ($plugins as $plugin) {
252: if (!isset($list[$plugin])) {
253: continue;
254: }
255:
256: foreach ($list[$plugin] as $shell) {
257: $aliases += [$shell => $plugin];
258: if (!isset($others[$shell])) {
259: $others[$shell] = [$plugin];
260: } else {
261: $others[$shell] = array_merge($others[$shell], [$plugin]);
262: }
263: }
264: }
265:
266: foreach ($aliases as $shell => $plugin) {
267: if (isset($fixed[$shell])) {
268: Log::write(
269: 'debug',
270: "command '$shell' in plugin '$plugin' was not aliased, conflicts with another shell",
271: ['shell-dispatcher']
272: );
273: continue;
274: }
275:
276: $other = static::alias($shell);
277: if ($other) {
278: $other = $aliases[$shell];
279: if ($other !== $plugin) {
280: Log::write(
281: 'debug',
282: "command '$shell' in plugin '$plugin' was not aliased, conflicts with '$other'",
283: ['shell-dispatcher']
284: );
285: }
286: continue;
287: }
288:
289: if (isset($others[$shell])) {
290: $conflicts = array_diff($others[$shell], [$plugin]);
291: if (count($conflicts) > 0) {
292: $conflictList = implode("', '", $conflicts);
293: Log::write(
294: 'debug',
295: "command '$shell' in plugin '$plugin' was not aliased, conflicts with '$conflictList'",
296: ['shell-dispatcher']
297: );
298: }
299: }
300:
301: static::alias($shell, "$plugin.$shell");
302: }
303:
304: return static::$_aliases;
305: }
306:
307: /**
308: * Get shell to use, either plugin shell or application shell
309: *
310: * All paths in the loaded shell paths are searched, handles alias
311: * dereferencing
312: *
313: * @param string $shell Optionally the name of a plugin
314: * @return \Cake\Console\Shell A shell instance.
315: * @throws \Cake\Console\Exception\MissingShellException when errors are encountered.
316: */
317: public function findShell($shell)
318: {
319: $className = $this->_shellExists($shell);
320: if (!$className) {
321: $shell = $this->_handleAlias($shell);
322: $className = $this->_shellExists($shell);
323: }
324:
325: if (!$className) {
326: throw new MissingShellException([
327: 'class' => $shell,
328: ]);
329: }
330:
331: return $this->_createShell($className, $shell);
332: }
333:
334: /**
335: * If the input matches an alias, return the aliased shell name
336: *
337: * @param string $shell Optionally the name of a plugin or alias
338: * @return string Shell name with plugin prefix
339: */
340: protected function _handleAlias($shell)
341: {
342: $aliased = static::alias($shell);
343: if ($aliased) {
344: $shell = $aliased;
345: }
346:
347: $class = array_map('Cake\Utility\Inflector::camelize', explode('.', $shell));
348:
349: return implode('.', $class);
350: }
351:
352: /**
353: * Check if a shell class exists for the given name.
354: *
355: * @param string $shell The shell name to look for.
356: * @return string|bool Either the classname or false.
357: */
358: protected function _shellExists($shell)
359: {
360: $class = App::className($shell, 'Shell', 'Shell');
361: if (class_exists($class)) {
362: return $class;
363: }
364:
365: return false;
366: }
367:
368: /**
369: * Create the given shell name, and set the plugin property
370: *
371: * @param string $className The class name to instantiate
372: * @param string $shortName The plugin-prefixed shell name
373: * @return \Cake\Console\Shell A shell instance.
374: */
375: protected function _createShell($className, $shortName)
376: {
377: list($plugin) = pluginSplit($shortName);
378: $instance = new $className();
379: $instance->plugin = trim($plugin, '.');
380:
381: return $instance;
382: }
383:
384: /**
385: * Removes first argument and shifts other arguments up
386: *
387: * @return mixed Null if there are no arguments otherwise the shifted argument
388: */
389: public function shiftArgs()
390: {
391: return array_shift($this->args);
392: }
393:
394: /**
395: * Shows console help. Performs an internal dispatch to the CommandList Shell
396: *
397: * @return void
398: */
399: public function help()
400: {
401: $this->args = array_merge(['command_list'], $this->args);
402: $this->dispatch();
403: }
404:
405: /**
406: * Prints the currently installed version of CakePHP. Performs an internal dispatch to the CommandList Shell
407: *
408: * @return void
409: */
410: public function version()
411: {
412: $this->args = array_merge(['command_list', '--version'], $this->args);
413: $this->dispatch();
414: }
415: }
416: