Merge branch 'master' into next
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / CLIWCF.class.php
1 <?php
2 declare(strict_types=1);
3 namespace wcf\system;
4 use phpline\console\ConsoleReader;
5 use phpline\internal\Log;
6 use phpline\TerminalFactory;
7 use wcf\system\cli\command\CLICommandHandler;
8 use wcf\system\cli\command\CLICommandNameCompleter;
9 use wcf\system\cli\DatabaseCLICommandHistory;
10 use wcf\system\event\EventHandler;
11 use wcf\system\exception\IllegalLinkException;
12 use wcf\system\exception\PermissionDeniedException;
13 use wcf\system\exception\UserInputException;
14 use wcf\system\language\LanguageFactory;
15 use wcf\system\package\PackageUpdateDispatcher;
16 use wcf\system\user\authentication\UserAuthenticationFactory;
17 use wcf\util\CLIUtil;
18 use wcf\util\FileUtil;
19 use wcf\util\JSON;
20 use wcf\util\StringUtil;
21 use Zend\Console\Exception\RuntimeException as ArgvException;
22 use Zend\Console\Getopt as ArgvParser;
23 use Zend\Loader\StandardAutoloader as ZendLoader;
24
25 // set exception handler
26 set_exception_handler([CLIWCF::class, 'handleCLIException']);
27
28 /**
29 * Extends WCF class with functions for CLI.
30 *
31 * @author Tim Duesterhus
32 * @copyright 2001-2018 WoltLab GmbH
33 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
34 * @package WoltLabSuite\Core\System
35 */
36 class CLIWCF extends WCF {
37 /**
38 * instance of ConsoleReader
39 * @var ConsoleReader
40 */
41 protected static $consoleReader = null;
42
43 /**
44 * instance of ArgvParser
45 * @var \Zend\Console\Getopt
46 */
47 protected static $argvParser = null;
48
49 /** @noinspection PhpMissingParentConstructorInspection */
50 /**
51 * Calls all init functions of the WCF class.
52 */
53 public function __construct() {
54 // add autoload directory
55 self::$autoloadDirectories['wcf'] = WCF_DIR . 'lib/';
56
57 // define tmp directory
58 if (!defined('TMP_DIR')) define('TMP_DIR', FileUtil::getTempFolder());
59
60 // register additional autoloaders
61 require_once(WCF_DIR.'lib/system/api/phpline/phpline.phar');
62 require_once(WCF_DIR.'lib/system/api/zend/Loader/StandardAutoloader.php');
63 $zendLoader = new ZendLoader([ZendLoader::AUTOREGISTER_ZF => true]);
64 $zendLoader->register();
65
66 $argv = new ArgvParser([
67 'packageID=i' => ''
68 ]);
69 $argv->setOption(ArgvParser::CONFIG_FREEFORM_FLAGS, true);
70 $argv->parse();
71 define('PACKAGE_ID', $argv->packageID ?: 1);
72
73 // disable benchmark
74 define('ENABLE_BENCHMARK', 0);
75
76 // start initialization
77 $this->initDB();
78 $this->loadOptions();
79 $this->initSession();
80 $this->initLanguage();
81 $this->initTPL();
82 $this->initCoreObjects();
83 $this->initApplications();
84
85 // the destructor registered in core.functions.php will only call the destructor of the parent class
86 register_shutdown_function([self::class, 'destruct']);
87
88 $this->initArgv();
89 $this->initPHPLine();
90 $this->initAuth();
91 $this->checkForUpdates();
92 $this->initCommands();
93 }
94
95 /**
96 * @inheritDoc
97 */
98 public static function destruct() {
99 if (self::getReader() !== null && self::getReader()->getHistory() instanceof DatabaseCLICommandHistory) {
100 /** @var DatabaseCLICommandHistory $history */
101 $history = self::getReader()->getHistory();
102
103 $history->save();
104 $history->autoSave = false;
105 }
106
107 self::getSession()->delete();
108 }
109
110 /**
111 * @inheritDoc
112 */
113 public static final function handleCLIException(\Exception $e) {
114 die($e->getMessage()."\n".$e->getTraceAsString());
115 }
116
117 /**
118 * Initializes parsing of command line options.
119 */
120 protected function initArgv() {
121 // initialise ArgvParser
122 self::$argvParser = new ArgvParser([
123 'language=s' => WCF::getLanguage()->get('wcf.cli.help.language'),
124 'v' => WCF::getLanguage()->get('wcf.cli.help.v'),
125 'q' => WCF::getLanguage()->get('wcf.cli.help.q'),
126 'h|help-s' => WCF::getLanguage()->get('wcf.cli.help.help'),
127 'version' => WCF::getLanguage()->get('wcf.cli.help.version'),
128 'disableUpdateCheck' => WCF::getLanguage()->get('wcf.cli.help.disableUpdateCheck'),
129 'exitOnFail' => WCF::getLanguage()->get('wcf.cli.help.exitOnFail'),
130 'packageID=i' => WCF::getLanguage()->get('wcf.cli.help.packageID')
131 ]);
132 self::getArgvParser()->setOptions([
133 ArgvParser::CONFIG_CUMULATIVE_FLAGS => true,
134 ArgvParser::CONFIG_DASHDASH => false
135 ]);
136
137 // parse arguments
138 EventHandler::getInstance()->fireAction($this, 'beforeArgumentParsing');
139 try {
140 self::getArgvParser()->parse();
141 }
142 catch (ArgvException $e) {
143 // show error message and usage
144 echo $e->getMessage().PHP_EOL;
145 echo self::getArgvParser()->getUsageMessage();
146 exit;
147 }
148 EventHandler::getInstance()->fireAction($this, 'afterArgumentParsing');
149
150 // handle arguments
151 if (self::getArgvParser()->help === true) {
152 // show usage
153 echo self::getArgvParser()->getUsageMessage();
154 exit;
155 }
156 else if (self::getArgvParser()->help) {
157 $help = WCF::getLanguage()->get('wcf.cli.help.'.self::getArgvParser()->help.'.description', true);
158 if ($help) echo $help.PHP_EOL;
159 else {
160 echo WCF::getLanguage()->getDynamicVariable('wcf.cli.help.noLongHelp', ['topic' => self::getArgvParser()->help]).PHP_EOL;
161 }
162 exit;
163 }
164 if (self::getArgvParser()->version) {
165 // show version
166 echo WCF_VERSION.PHP_EOL;
167 exit;
168 }
169 if (self::getArgvParser()->language) {
170 // set language
171 $language = LanguageFactory::getInstance()->getLanguageByCode(self::getArgvParser()->language);
172 if ($language === null) {
173 echo WCF::getLanguage()->getDynamicVariable('wcf.cli.error.language.notFound', ['languageCode' => self::getArgvParser()->language]).PHP_EOL;
174 exit;
175 }
176 self::setLanguage($language->languageID);
177 }
178 if (in_array('moo', self::getArgvParser()->getRemainingArgs())) {
179 echo '...."Have you mooed today?"...'.PHP_EOL;
180 }
181
182 define('VERBOSITY', self::getArgvParser()->v - self::getArgvParser()->q);
183 }
184
185 /**
186 * Returns the argv parser.
187 *
188 * @return \Zend\Console\Getopt
189 */
190 public static function getArgvParser() {
191 return self::$argvParser;
192 }
193
194 /**
195 * Initializes PHPLine.
196 */
197 protected function initPHPLine() {
198 $terminal = TerminalFactory::get();
199 self::$consoleReader = new ConsoleReader("WoltLab Suite", null, null, $terminal);
200
201 // don't expand events, as the username and password will follow
202 self::getReader()->setExpandEvents(false);
203
204 if (VERBOSITY >= 0) {
205 $headline = str_pad("WoltLab Suite (tm) ".WCF_VERSION, self::getTerminal()->getWidth(), " ", STR_PAD_BOTH);
206 self::getReader()->println($headline);
207 }
208 }
209
210 /**
211 * Returns ConsoleReader.
212 *
213 * @return ConsoleReader
214 */
215 public static function getReader() {
216 return self::$consoleReader;
217 }
218
219 /**
220 * Returns the terminal that is attached to ConsoleReader
221 *
222 * @return \phpline\Terminal
223 */
224 public static function getTerminal() {
225 return self::getReader()->getTerminal();
226 }
227
228 /**
229 * Does the user authentification.
230 */
231 protected function initAuth() {
232 do {
233 $line = self::getReader()->readLine(WCF::getLanguage()->get('wcf.user.username').'> ');
234 if ($line === null) exit;
235 $username = StringUtil::trim($line);
236 }
237 while ($username === '');
238
239 do {
240 $line = self::getReader()->readLine(WCF::getLanguage()->get('wcf.user.password').'> ', '*');
241 if ($line === null) exit;
242 $password = StringUtil::trim($line);
243 }
244 while ($password === '');
245
246 // check credentials and switch user
247 try {
248 $user = UserAuthenticationFactory::getInstance()->getUserAuthentication()->loginManually($username, $password);
249 WCF::getSession()->changeUser($user);
250 }
251 catch (UserInputException $e) {
252 $message = WCF::getLanguage()->getDynamicVariable('wcf.user.'.$e->getField().'.error.'.$e->getType(), ['username' => $username]);
253 self::getReader()->println($message);
254 exit(1);
255 }
256
257 // initialize history
258 $history = new DatabaseCLICommandHistory();
259 $history->load();
260 self::getReader()->setHistory($history);
261
262 // initialize language
263 if (!self::getArgvParser()->language) $this->initLanguage();
264 }
265
266 /**
267 * Initializes command handling.
268 */
269 protected function initCommands() {
270 // add command name completer
271 self::getReader()->addCompleter(new CLICommandNameCompleter());
272
273 while (true) {
274 // roll back open transactions of the previous command, as they are dangerous in a long living script
275 if (WCF::getDB()->rollBackTransaction()) {
276 Log::warn('Previous command had an open transaction.');
277 }
278 self::getReader()->setHistoryEnabled(true);
279 $line = self::getReader()->readLine('>');
280 if ($line === null) exit;
281 $line = StringUtil::trim($line);
282 try {
283 $command = CLICommandHandler::getCommand($line);
284 $command->execute(CLICommandHandler::getParameters($line));
285 }
286 catch (IllegalLinkException $e) {
287 Log::error('notFound:'.JSON::encode(['command' => $line]));
288 self::getReader()->println(WCF::getLanguage()->getDynamicVariable('wcf.cli.error.command.notFound', ['command' => $line]));
289
290 if (self::getArgvParser()->exitOnFail) {
291 exit(1);
292 }
293 continue;
294 }
295 catch (PermissionDeniedException $e) {
296 Log::error('permissionDenied');
297 self::getReader()->println(WCF::getLanguage()->getDynamicVariable('wcf.page.error.permissionDenied'));
298
299 if (self::getArgvParser()->exitOnFail) {
300 exit(1);
301 }
302 continue;
303 }
304 catch (ArgvException $e) {
305 // show error message and usage
306 if ($e->getMessage()) echo $e->getMessage().PHP_EOL;
307 echo $e->getUsageMessage();
308
309 if (self::getArgvParser()->exitOnFail) {
310 exit(1);
311 }
312 continue;
313 }
314 catch (\Exception $e) {
315 Log::error($e);
316
317 if (self::getArgvParser()->exitOnFail) {
318 exit(1);
319 }
320 continue;
321 }
322 }
323 }
324
325 /**
326 * Checks for updates.
327 *
328 * @return string
329 */
330 public function checkForUpdates() {
331 if (WCF::getSession()->getPermission('admin.configuration.package.canUpdatePackage') && VERBOSITY >= -1 && !self::getArgvParser()->disableUpdateCheck) {
332 $updates = PackageUpdateDispatcher::getInstance()->getAvailableUpdates();
333 if (!empty($updates)) {
334 self::getReader()->println(count($updates) . ' update' . (count($updates) > 1 ? 's are' : ' is') . ' available');
335
336 if (VERBOSITY >= 1) {
337 $table = [
338 [
339 WCF::getLanguage()->get('wcf.acp.package.name'),
340 WCF::getLanguage()->get('wcf.acp.package.version'),
341 WCF::getLanguage()->get('wcf.acp.package.newVersion')
342 ]
343 ];
344
345 foreach ($updates as $update) {
346 $row = [
347 WCF::getLanguage()->get($update['packageName']),
348 $update['packageVersion'],
349 $update['version']['packageVersion']
350 ];
351
352 $table[] = $row;
353 }
354
355 self::getReader()->println(CLIUtil::generateTable($table));
356 }
357 }
358 }
359 }
360 }