Fixed missing language variables in last step of wcfsetup
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / WCFSetup.class.php
1 <?php
2 namespace wcf\system;
3 use wcf\data\language\LanguageEditor;
4 use wcf\data\language\SetupLanguage;
5 use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
6 use wcf\data\package\Package;
7 use wcf\data\user\User;
8 use wcf\data\user\UserAction;
9 use wcf\system\cache\builder\LanguageCacheBuilder;
10 use wcf\system\database\util\SQLParser;
11 use wcf\system\database\MySQLDatabase;
12 use wcf\system\exception\SystemException;
13 use wcf\system\exception\UserInputException;
14 use wcf\system\io\File;
15 use wcf\system\io\Tar;
16 use wcf\system\language\LanguageFactory;
17 use wcf\system\package\PackageArchive;
18 use wcf\system\session\ACPSessionFactory;
19 use wcf\system\session\SessionHandler;
20 use wcf\system\setup\Installer;
21 use wcf\system\template\SetupTemplateEngine;
22 use wcf\util\DirectoryUtil;
23 use wcf\util\FileUtil;
24 use wcf\util\HeaderUtil;
25 use wcf\util\StringUtil;
26 use wcf\util\UserUtil;
27 use wcf\util\XML;
28
29 // define
30 define('PACKAGE_ID', 0);
31 define('HTTP_ENABLE_NO_CACHE_HEADERS', 0);
32 define('HTTP_ENABLE_GZIP', 0);
33 define('HTTP_GZIP_LEVEL', 0);
34 define('HTTP_SEND_X_FRAME_OPTIONS', 0);
35 define('CACHE_SOURCE_TYPE', 'disk');
36 define('MODULE_MASTER_PASSWORD', 1);
37 define('ENABLE_DEBUG_MODE', 1);
38 define('ENABLE_BENCHMARK', 0);
39
40 /**
41 * Executes the installation of the basic WCF systems.
42 *
43 * @author Marcel Werk
44 * @copyright 2001-2016 WoltLab GmbH
45 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
46 * @package WoltLabSuite\Core\System
47 */
48 class WCFSetup extends WCF {
49 /**
50 * list of available languages
51 * @var string[]
52 */
53 protected static $availableLanguages = [];
54
55 /**
56 * installation directories
57 * @var string[]
58 */
59 protected static $directories = [];
60
61 /**
62 * language code of selected installation language
63 * @var string
64 */
65 protected static $selectedLanguageCode = 'en';
66
67 /**
68 * selected languages to be installed
69 * @var string[]
70 */
71 protected static $selectedLanguages = [];
72
73 /**
74 * list of installed files
75 * @var string[]
76 */
77 protected static $installedFiles = [];
78
79 /**
80 * indicates if developer mode is used to install
81 * @var boolean
82 */
83 protected static $developerMode = 0;
84
85 /** @noinspection PhpMissingParentConstructorInspection */
86 /**
87 * Calls all init functions of the WCFSetup class and starts the setup process.
88 */
89 public function __construct() {
90 @set_time_limit(0);
91
92 $this->getDeveloperMode();
93 $this->getLanguageSelection();
94 $this->getInstallationDirectories();
95 $this->initLanguage();
96 $this->initTPL();
97 /** @noinspection PhpUndefinedMethodInspection */
98 self::getLanguage()->loadLanguage();
99 $this->getPackageNames();
100
101 // start setup
102 $this->setup();
103 }
104
105 /**
106 * Gets the status of the developer mode.
107 */
108 protected static function getDeveloperMode() {
109 if (isset($_GET['dev'])) self::$developerMode = intval($_GET['dev']);
110 else if (isset($_POST['dev'])) self::$developerMode = intval($_POST['dev']);
111 }
112
113 /**
114 * Gets the selected language.
115 */
116 protected static function getLanguageSelection() {
117 self::$availableLanguages = self::getAvailableLanguages();
118
119 if (isset($_REQUEST['languageCode']) && isset(self::$availableLanguages[$_REQUEST['languageCode']])) {
120 self::$selectedLanguageCode = $_REQUEST['languageCode'];
121 }
122 else {
123 self::$selectedLanguageCode = LanguageFactory::getPreferredLanguage(array_keys(self::$availableLanguages), self::$selectedLanguageCode);
124 }
125
126 if (isset($_POST['selectedLanguages']) && is_array($_POST['selectedLanguages'])) {
127 self::$selectedLanguages = $_POST['selectedLanguages'];
128 }
129 }
130
131 /**
132 * Gets the selected wcf dir from request.
133 *
134 * @since 3.0
135 */
136 protected static function getInstallationDirectories() {
137 if (self::$developerMode && isset($_ENV['WCFSETUP_USEDEFAULTWCFDIR'])) {
138 if (!isset($_REQUEST['directories']) || !is_array($_REQUEST['directories'])) $_REQUEST['directories'] = [];
139 $_REQUEST['directories']['wcf'] = FileUtil::unifyDirSeparator(INSTALL_SCRIPT_DIR).'wcf/';
140 }
141
142 if (!empty($_REQUEST['directories']) && is_array($_REQUEST['directories'])) {
143 foreach ($_REQUEST['directories'] as $application => $directory) {
144 self::$directories[$application] = $directory;
145
146 if ($application === 'wcf' && @file_exists(self::$directories['wcf'])) {
147 define('RELATIVE_WCF_DIR', FileUtil::getRelativePath(INSTALL_SCRIPT_DIR, self::$directories['wcf']));
148 }
149 }
150 }
151
152 define('WCF_DIR', (isset(self::$directories['wcf']) ? self::$directories['wcf'] : ''));
153 }
154
155 /**
156 * Initialises the language engine.
157 */
158 protected function initLanguage() {
159 // set mb settings
160 mb_internal_encoding('UTF-8');
161 if (function_exists('mb_regex_encoding')) mb_regex_encoding('UTF-8');
162 mb_language('uni');
163
164 // init setup language
165 self::$languageObj = new SetupLanguage(null, ['languageCode' => self::$selectedLanguageCode]);
166 }
167
168 /**
169 * Initialises the template engine.
170 */
171 protected function initTPL() {
172 self::$tplObj = SetupTemplateEngine::getInstance();
173 self::getTPL()->setLanguageID((self::$selectedLanguageCode == 'en' ? 0 : 1));
174 self::getTPL()->setCompileDir(TMP_DIR);
175 self::getTPL()->addApplication('wcf', TMP_DIR);
176 self::getTPL()->registerPrefilter(['lang']);
177 self::getTPL()->assign([
178 '__wcf' => $this,
179 'tmpFilePrefix' => TMP_FILE_PREFIX,
180 'languageCode' => self::$selectedLanguageCode,
181 'selectedLanguages' => self::$selectedLanguages,
182 'directories' => self::$directories,
183 'developerMode' => self::$developerMode
184 ]);
185 }
186
187 /**
188 * Returns all languages from WCFSetup.tar.gz.
189 *
190 * @return string[]
191 */
192 protected static function getAvailableLanguages() {
193 $languages = $match = [];
194 foreach (glob(TMP_DIR.'setup/lang/*.xml') as $file) {
195 $xml = new XML();
196 $xml->load($file);
197 $languageCode = LanguageEditor::readLanguageCodeFromXML($xml);
198 $languageName = LanguageEditor::readLanguageNameFromXML($xml);
199
200 $languages[$languageCode] = $languageName;
201 }
202
203 // sort languages by language name
204 asort($languages);
205
206 return $languages;
207 }
208
209 /**
210 * Calculates the current state of the progress bar.
211 *
212 * @param integer $currentStep
213 */
214 protected function calcProgress($currentStep) {
215 // calculate progress
216 $progress = round((100 / 18) * ++$currentStep, 0);
217 self::getTPL()->assign(['progress' => $progress]);
218 }
219
220 /**
221 * Executes the setup steps.
222 */
223 protected function setup() {
224 // get current step
225 if (isset($_REQUEST['step'])) $step = $_REQUEST['step'];
226 else $step = 'selectSetupLanguage';
227
228 // execute current step
229 switch ($step) {
230 /** @noinspection PhpMissingBreakStatementInspection */
231 case 'selectSetupLanguage':
232 if (!self::$developerMode) {
233 $this->calcProgress(0);
234 $this->selectSetupLanguage();
235 break;
236 }
237
238 /** @noinspection PhpMissingBreakStatementInspection */
239 case 'showLicense':
240 if (!self::$developerMode) {
241 $this->calcProgress(1);
242 $this->showLicense();
243 break;
244 }
245
246 /** @noinspection PhpMissingBreakStatementInspection */
247 case 'showSystemRequirements':
248 if (!self::$developerMode) {
249 $this->calcProgress(2);
250 $this->showSystemRequirements();
251 break;
252 }
253
254 /** @noinspection PhpMissingBreakStatementInspection */
255 case 'configureDirectories':
256 if (!self::$developerMode || !isset($_ENV['WCFSETUP_USEDEFAULTWCFDIR'])) {
257 $this->calcProgress(3);
258 $this->configureDirectories();
259 break;
260 }
261
262 case 'unzipFiles':
263 $this->calcProgress(4);
264 $this->unzipFiles();
265 break;
266
267 case 'selectLanguages':
268 $this->calcProgress(5);
269 $this->selectLanguages();
270 break;
271
272 case 'configureDB':
273 $this->calcProgress(6);
274 $this->configureDB();
275 break;
276
277 case 'createDB':
278 $currentStep = 7;
279 if (isset($_POST['offset'])) {
280 $currentStep += intval($_POST['offset']);
281 }
282
283 $this->calcProgress($currentStep);
284 $this->createDB();
285 break;
286
287 case 'logFiles':
288 $this->calcProgress(14);
289 $this->logFiles();
290 break;
291
292 case 'installLanguage':
293 $this->calcProgress(15);
294 $this->installLanguage();
295 break;
296
297 case 'createUser':
298 $this->calcProgress(16);
299 $this->createUser();
300 break;
301
302 case 'installPackages':
303 $this->calcProgress(17);
304 $this->installPackages();
305 break;
306 }
307 }
308
309 /**
310 * Shows the first setup page.
311 */
312 protected function selectSetupLanguage() {
313 WCF::getTPL()->assign([
314 'availableLanguages' => self::$availableLanguages,
315 'nextStep' => 'showLicense'
316 ]);
317 WCF::getTPL()->display('stepSelectSetupLanguage');
318 }
319
320 /**
321 * Shows the license agreement.
322 */
323 protected function showLicense() {
324 if (isset($_POST['send'])) {
325 if (isset($_POST['accepted'])) {
326 $this->gotoNextStep('showSystemRequirements');
327 exit;
328 }
329 else {
330 WCF::getTPL()->assign(['missingAcception' => true]);
331 }
332
333 }
334
335 if (file_exists(TMP_DIR.'setup/license/license_'.self::$selectedLanguageCode.'.txt')) {
336 $license = file_get_contents(TMP_DIR.'setup/license/license_'.self::$selectedLanguageCode.'.txt');
337 }
338 else {
339 $license = file_get_contents(TMP_DIR.'setup/license/license_en.txt');
340 }
341
342 WCF::getTPL()->assign([
343 'license' => $license,
344 'nextStep' => 'showLicense'
345 ]);
346 WCF::getTPL()->display('stepShowLicense');
347 }
348
349 /**
350 * Shows the system requirements.
351 */
352 protected function showSystemRequirements() {
353 $system = [];
354
355 // php version
356 $system['phpVersion']['value'] = phpversion();
357 $comparePhpVersion = preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $system['phpVersion']['value']);
358 $system['phpVersion']['result'] = (version_compare($comparePhpVersion, '5.5.4') >= 0);
359
360 // sql
361 $system['sql']['result'] = MySQLDatabase::isSupported();
362
363 // upload_max_filesize
364 $system['uploadMaxFilesize']['value'] = min(ini_get('upload_max_filesize'), ini_get('post_max_size'));
365 $system['uploadMaxFilesize']['result'] = (intval($system['uploadMaxFilesize']['value']) > 0);
366
367 // gdlib version
368 $system['gdLib']['value'] = '0.0.0';
369 if (function_exists('gd_info')) {
370 $temp = gd_info();
371 $match = [];
372 if (preg_match('!([0-9]+\.[0-9]+(?:\.[0-9]+)?)!', $temp['GD Version'], $match)) {
373 if (preg_match('/^[0-9]+\.[0-9]+$/', $match[1])) $match[1] .= '.0';
374 $system['gdLib']['value'] = $match[1];
375 }
376 }
377 $system['gdLib']['result'] = (version_compare($system['gdLib']['value'], '2.0.0') >= 0);
378
379 // memory limit
380 $system['memoryLimit']['value'] = ini_get('memory_limit');
381 $system['memoryLimit']['result'] = $this->compareMemoryLimit();
382
383 WCF::getTPL()->assign([
384 'system' => $system,
385 'nextStep' => 'configureDirectories'
386 ]);
387 WCF::getTPL()->display('stepShowSystemRequirements');
388 }
389
390 /**
391 * Returns true if memory_limit is set to at least 128 MB
392 *
393 * @return boolean
394 */
395 protected function compareMemoryLimit() {
396 $memoryLimit = ini_get('memory_limit');
397
398 // no limit
399 if ($memoryLimit == -1) {
400 return true;
401 }
402
403 // completely numeric, PHP assumes byte
404 if (is_numeric($memoryLimit)) {
405 $memoryLimit = $memoryLimit / 1024 / 1024;
406 return ($memoryLimit >= 128);
407 }
408
409 // PHP supports 'K', 'M' and 'G' shorthand notation
410 if (preg_match('~^(\d+)([KMG])$~', $memoryLimit, $matches)) {
411 switch ($matches[2]) {
412 case 'K':
413 $memoryLimit = $matches[1] * 1024;
414 return ($memoryLimit >= 128);
415 break;
416
417 case 'M':
418 return ($matches[1] >= 128);
419 break;
420
421 case 'G':
422 return ($matches[1] >= 1);
423 break;
424 }
425 }
426
427 return false;
428 }
429
430 /**
431 * Searches the wcf dir.
432 *
433 * @since 3.0
434 */
435 protected function configureDirectories() {
436 // get available packages
437 $applications = $packages = [];
438 foreach (glob(TMP_DIR . 'install/packages/*') as $file) {
439 $filename = basename($file);
440 if (preg_match('~\.(?:tar|tar\.gz|tgz)$~', $filename)) {
441 $package = new PackageArchive($file);
442 $package->openArchive();
443
444 $application = Package::getAbbreviation($package->getPackageInfo('name'));
445
446 $applications[] = $application;
447 $packages[$application] = [
448 'directory' => ($package->getPackageInfo('applicationDirectory') ?: $application),
449 'packageDescription' => $package->getLocalizedPackageInfo('packageDescription'),
450 'packageName' => $package->getLocalizedPackageInfo('packageName')
451 ];
452
453 }
454 }
455
456 uasort($packages, function($a, $b) {
457 return strcmp($a['packageName'], $b['packageName']);
458 });
459
460 // force cms being shown first
461 $showOrder = ['wcf'];
462 foreach (array_keys($packages) as $application) {
463 if ($application !== 'wcf') $showOrder[] = $application;
464 }
465
466 $documentRoot = FileUtil::unifyDirSeparator($_SERVER['DOCUMENT_ROOT']);
467 $errors = [];
468 if (!empty(self::$directories)) {
469 $applicationPaths = $knownPaths = [];
470
471 // use $showOrder instead of $applications to ensure that the error message for
472 // duplicate directories will trigger in display order rather than the random
473 // sort order returned by glob() above
474 foreach ($showOrder as $application) {
475 $path = FileUtil::getRealPath($documentRoot . '/' . FileUtil::addTrailingSlash(FileUtil::removeLeadingSlash(self::$directories[$application])));
476 if (strpos($path, $documentRoot) !== 0) {
477 // verify that given path is still within the current document root
478 $errors[$application] = 'outsideDocumentRoot';
479 }
480 else if (in_array($path, $knownPaths)) {
481 // prevent the same path for two or more applications
482 $errors[$application] = 'duplicate';
483 }
484 else if (@is_file($path . 'global.php')) {
485 // check if directory is empty (dotfiles are okay)
486 $errors[$application] = 'notEmpty';
487 }
488 else {
489 // try to create directory if it does not exist
490 if (!is_dir($path) && !FileUtil::makePath($path)) {
491 $errors[$application] = 'makePath';
492 }
493
494 try {
495 FileUtil::makeWritable($path);
496 }
497 catch (SystemException $e) {
498 $errors[$application] = 'makeWritable';
499 }
500 }
501
502 $applicationPaths[$application] = $path;
503 $knownPaths[] = $path;
504 }
505
506 if (empty($errors)) {
507 // copy over the actual paths
508 self::$directories = array_merge(self::$directories, $applicationPaths);
509 WCF::getTPL()->assign(['directories' => self::$directories]);
510
511 $this->unzipFiles();
512 return;
513 }
514 }
515 else {
516 // resolve path relative to document root
517 $relativePath = str_replace(FileUtil::unifyDirSeparator($_SERVER['DOCUMENT_ROOT']), '', FileUtil::unifyDirSeparator(INSTALL_SCRIPT_DIR));
518 foreach ($packages as $application => $packageData) {
519 self::$directories[$application] = $relativePath . ($application === 'wcf' ? '' : $packageData['directory'] . '/');
520 }
521 }
522
523 WCF::getTPL()->assign([
524 'directories' => self::$directories,
525 'documentRoot' => $documentRoot,
526 'errors' => $errors,
527 'installScriptDir' => FileUtil::unifyDirSeparator(INSTALL_SCRIPT_DIR),
528 'nextStep' => 'configureDirectories', // call this step again to validate paths
529 'packages' => $packages,
530 'showOrder' => $showOrder
531 ]);
532
533 WCF::getTPL()->display('stepConfigureDirectories');
534 }
535
536 /**
537 * Unzips the files of the wcfsetup tar archive.
538 */
539 protected function unzipFiles() {
540 // WCF seems to be installed, abort
541 if (@is_file(self::$directories['wcf'].'lib/system/WCF.class.php')) {
542 throw new SystemException('Target directory seems to be an existing installation of WCF, unable to continue.');
543 }
544 // WCF not yet installed, install files first
545 else {
546 $this->installFiles();
547
548 $this->gotoNextStep('selectLanguages');
549 }
550 }
551
552 /**
553 * Shows the page for choosing the installed languages.
554 */
555 protected function selectLanguages() {
556 $errorField = $errorType = '';
557
558 // skip step in developer mode
559 // select all available languages automatically
560 if (self::$developerMode) {
561 self::$selectedLanguages = [];
562 foreach (self::$availableLanguages as $languageCode => $language) {
563 self::$selectedLanguages[] = $languageCode;
564 }
565
566 self::getTPL()->assign(['selectedLanguages' => self::$selectedLanguages]);
567 $this->gotoNextStep('configureDB');
568 exit;
569 }
570
571 // start error handling
572 if (isset($_POST['send'])) {
573 try {
574 // no languages selected
575 if (empty(self::$selectedLanguages)) {
576 throw new UserInputException('selectedLanguages');
577 }
578
579 // illegal selection
580 foreach (self::$selectedLanguages as $language) {
581 if (!isset(self::$availableLanguages[$language])) {
582 throw new UserInputException('selectedLanguages');
583 }
584 }
585
586 // no errors
587 // go to next step
588 $this->gotoNextStep('configureDB');
589 exit;
590 }
591 catch (UserInputException $e) {
592 $errorField = $e->getField();
593 $errorType = $e->getType();
594 }
595 }
596 else {
597 self::$selectedLanguages[] = self::$selectedLanguageCode;
598 WCF::getTPL()->assign(['selectedLanguages' => self::$selectedLanguages]);
599 }
600
601 WCF::getTPL()->assign([
602 'errorField' => $errorField,
603 'errorType' => $errorType,
604 'availableLanguages' => self::$availableLanguages,
605 'nextStep' => 'selectLanguages'
606 ]);
607 WCF::getTPL()->display('stepSelectLanguages');
608 }
609
610 /**
611 * Shows the page for configuring the database connection.
612 */
613 protected function configureDB() {
614 if (self::$developerMode && isset($_ENV['WCFSETUP_DBHOST'])) {
615 $dbHost = $_ENV['WCFSETUP_DBHOST'];
616 $dbUser = $_ENV['WCFSETUP_DBUSER'];
617 $dbPassword = $_ENV['WCFSETUP_DBPASSWORD'];
618 $dbName = $_ENV['WCFSETUP_DBNAME'];
619 $dbNumber = 1;
620 }
621 else {
622 $dbHost = 'localhost';
623 $dbUser = 'root';
624 $dbPassword = '';
625 $dbName = 'wcf';
626 $dbNumber = 1;
627 }
628
629 if (isset($_POST['send']) || (self::$developerMode && isset($_ENV['WCFSETUP_DBHOST']))) {
630 if (isset($_POST['dbHost'])) $dbHost = $_POST['dbHost'];
631 if (isset($_POST['dbUser'])) $dbUser = $_POST['dbUser'];
632 if (isset($_POST['dbPassword'])) $dbPassword = $_POST['dbPassword'];
633 if (isset($_POST['dbName'])) $dbName = $_POST['dbName'];
634
635 // ensure that $dbNumber is zero or a positive integer
636 if (isset($_POST['dbNumber'])) $dbNumber = max(0, intval($_POST['dbNumber']));
637
638 // get port
639 $dbPort = 0;
640 if (preg_match('/^(.+?):(\d+)$/', $dbHost, $match)) {
641 $dbHost = $match[1];
642 $dbPort = intval($match[2]);
643 }
644
645 // test connection
646 try {
647 // check connection data
648 /** @var \wcf\system\database\Database $db */
649 $db = new MySQLDatabase($dbHost, $dbUser, $dbPassword, $dbName, $dbPort, true);
650 $db->connect();
651
652 // check sql version
653 $sqlVersion = $db->getVersion();
654 $compareSQLVersion = preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $sqlVersion);
655 if (stripos($sqlVersion, 'MariaDB')) {
656 // MariaDB 10.0.22+
657 if (!(version_compare($compareSQLVersion, '10.0.22') >= 0)) {
658 throw new SystemException("Insufficient MariaDB version '".$compareSQLVersion."'. Version '10.0.22' or greater is needed.");
659 }
660 }
661 else {
662 // MySQL 5.5.35+
663 if (!(version_compare($compareSQLVersion, '5.5.35') >= 0)) {
664 throw new SystemException("Insufficient MySQL version '".$compareSQLVersion."'. Version '5.5.35' or greater is needed.");
665 }
666 }
667
668 // check innodb support
669 $sql = "SHOW ENGINES";
670 $statement = $db->prepareStatement($sql);
671 $statement->execute();
672 $hasInnoDB = false;
673 while ($row = $statement->fetchArray()) {
674 if ($row['Engine'] == 'InnoDB' && in_array($row['Support'], ['DEFAULT', 'YES'])) {
675 $hasInnoDB = true;
676 break;
677 }
678 }
679
680 if (!$hasInnoDB) {
681 throw new SystemException("Support for InnoDB is missing.");
682 }
683
684 // check for table conflicts
685 $conflictedTables = $this->getConflictedTables($db, $dbNumber);
686
687 // write config.inc
688 if (empty($conflictedTables)) {
689 // connection successfully established
690 // write configuration to config.inc.php
691 $file = new File(WCF_DIR.'config.inc.php');
692 $file->write("<?php\n");
693 $file->write("\$dbHost = '".str_replace("'", "\\'", $dbHost)."';\n");
694 $file->write("\$dbPort = ".$dbPort.";\n");
695 $file->write("\$dbUser = '".str_replace("'", "\\'", $dbUser)."';\n");
696 $file->write("\$dbPassword = '".str_replace("'", "\\'", $dbPassword)."';\n");
697 $file->write("\$dbName = '".str_replace("'", "\\'", $dbName)."';\n");
698 $file->write("if (!defined('WCF_N')) define('WCF_N', $dbNumber);\n");
699 $file->close();
700
701 // go to next step
702 $this->gotoNextStep('createDB');
703 exit;
704 }
705 // show configure template again
706 else {
707 WCF::getTPL()->assign(['conflictedTables' => $conflictedTables]);
708 }
709 }
710 catch (SystemException $e) {
711 WCF::getTPL()->assign(['exception' => $e]);
712 }
713 }
714 WCF::getTPL()->assign([
715 'dbHost' => $dbHost,
716 'dbUser' => $dbUser,
717 'dbPassword' => $dbPassword,
718 'dbName' => $dbName,
719 'dbNumber' => $dbNumber,
720 'nextStep' => 'configureDB'
721 ]);
722 WCF::getTPL()->display('stepConfigureDB');
723 }
724
725 /**
726 * Checks if in the chosen database are tables in conflict with the wcf tables
727 * which will be created in the next step.
728 *
729 * @param \wcf\system\database\Database $db
730 * @param integer $dbNumber
731 * @return string[] list of already existing tables
732 */
733 protected function getConflictedTables($db, $dbNumber) {
734 // get content of the sql structure file
735 $sql = file_get_contents(TMP_DIR.'setup/db/install.sql');
736
737 // installation number value 'n' (WCF_N) must be reflected in the executed sql queries
738 $sql = str_replace('wcf1_', 'wcf'.$dbNumber.'_', $sql);
739
740 // get all tablenames which should be created
741 preg_match_all("%CREATE\s+TABLE\s+(\w+)%", $sql, $matches);
742
743 // get all installed tables from chosen database
744 $existingTables = $db->getEditor()->getTableNames();
745
746 // check if existing tables are in conflict with wcf tables
747 $conflictedTables = [];
748 foreach ($existingTables as $existingTableName) {
749 foreach ($matches[1] as $wcfTableName) {
750 if ($existingTableName == $wcfTableName) {
751 $conflictedTables[] = $wcfTableName;
752 }
753 }
754 }
755 return $conflictedTables;
756 }
757
758 /**
759 * Creates the database structure of the wcf.
760 */
761 protected function createDB() {
762 $this->initDB();
763
764 // get content of the sql structure file
765 $sql = file_get_contents(TMP_DIR.'setup/db/install.sql');
766
767 // split by offsets
768 $sqlData = explode('/* SQL_PARSER_OFFSET */', $sql);
769 $offset = (isset($_POST['offset'])) ? intval($_POST['offset']) : 0;
770 if (!isset($sqlData[$offset])) {
771 throw new SystemException("Offset for SQL parser is out of bounds, ".$offset." was requested, but there are only ".count($sqlData)." sections");
772 }
773 $sql = $sqlData[$offset];
774
775 // installation number value 'n' (WCF_N) must be reflected in the executed sql queries
776 $sql = str_replace('wcf1_', 'wcf'.WCF_N.'_', $sql);
777
778 // execute sql queries
779 $parser = new SQLParser($sql);
780 $parser->execute();
781
782 // log sql queries
783 preg_match_all("~CREATE\s+TABLE\s+(\w+)~i", $sql, $matches);
784
785 if (!empty($matches[1])) {
786 $sql = "INSERT INTO wcf".WCF_N."_package_installation_sql_log
787 (sqlTable)
788 VALUES (?)";
789 $statement = self::getDB()->prepareStatement($sql);
790 foreach ($matches[1] as $tableName) {
791 $statement->execute([$tableName]);
792 }
793 }
794
795 if ($offset < (count($sqlData) - 1)) {
796 WCF::getTPL()->assign([
797 '__additionalParameters' => [
798 'offset' => $offset + 1
799 ]
800 ]);
801
802 $this->gotoNextStep('createDB');
803 }
804 else {
805 /*
806 * Manually install PIPPackageInstallationPlugin since install.sql content is not escaped resulting
807 * in different behaviour in MySQL and MSSQL. You SHOULD NOT move this into install.sql!
808 */
809 $sql = "INSERT INTO wcf".WCF_N."_package_installation_plugin
810 (pluginName, priority, className)
811 VALUES (?, ?, ?)";
812 $statement = self::getDB()->prepareStatement($sql);
813 $statement->execute([
814 'packageInstallationPlugin',
815 1,
816 'wcf\system\package\plugin\PIPPackageInstallationPlugin'
817 ]);
818
819 $this->gotoNextStep('logFiles');
820 }
821 }
822
823 /**
824 * Logs the unzipped files.
825 */
826 protected function logFiles() {
827 $this->initDB();
828
829 $this->getInstalledFiles(WCF_DIR);
830 $acpTemplateInserts = $fileInserts = [];
831 foreach (self::$installedFiles as $file) {
832 $match = [];
833 if (preg_match('!/acp/templates/([^/]+)\.tpl$!', $file, $match)) {
834 // acp template
835 $acpTemplateInserts[] = $match[1];
836 }
837 else {
838 // regular file
839 $fileInserts[] = str_replace(WCF_DIR, '', $file);
840 }
841 }
842
843 // save acp template log
844 if (!empty($acpTemplateInserts)) {
845 $sql = "INSERT INTO wcf".WCF_N."_acp_template
846 (templateName, application)
847 VALUES (?, ?)";
848 $statement = self::getDB()->prepareStatement($sql);
849
850 self::getDB()->beginTransaction();
851 foreach ($acpTemplateInserts as $acpTemplate) {
852 $statement->execute([$acpTemplate, 'wcf']);
853 }
854 self::getDB()->commitTransaction();
855 }
856
857 // save file log
858 if (!empty($fileInserts)) {
859 $sql = "INSERT INTO wcf".WCF_N."_package_installation_file_log
860 (filename, application)
861 VALUES (?, ?)";
862 $statement = self::getDB()->prepareStatement($sql);
863
864 self::getDB()->beginTransaction();
865 foreach ($fileInserts as $file) {
866 $statement->execute([$file, 'wcf']);
867 }
868 self::getDB()->commitTransaction();
869 }
870
871 $this->gotoNextStep('installLanguage');
872 }
873
874 /**
875 * Scans the given dir for installed files.
876 *
877 * @param string $dir
878 */
879 protected function getInstalledFiles($dir) {
880 if ($files = glob($dir.'*')) {
881 foreach ($files as $file) {
882 if (is_dir($file)) {
883 $this->getInstalledFiles(FileUtil::addTrailingSlash($file));
884 }
885 else {
886 self::$installedFiles[] = FileUtil::unifyDirSeparator($file);
887 }
888 }
889 }
890 }
891
892 /**
893 * Installs the selected languages.
894 */
895 protected function installLanguage() {
896 $this->initDB();
897
898 foreach (self::$selectedLanguages as $language) {
899 // get language.xml file name
900 $filename = TMP_DIR.'install/lang/'.$language.'.xml';
901
902 // check the file
903 if (!file_exists($filename)) {
904 throw new SystemException("unable to find language file '".$filename."'");
905 }
906
907 // open the file
908 $xml = new XML();
909 $xml->load($filename);
910
911 // import xml
912 LanguageEditor::importFromXML($xml, 0);
913 }
914
915 // set default language
916 $language = LanguageFactory::getInstance()->getLanguageByCode(in_array(self::$selectedLanguageCode, self::$selectedLanguages) ? self::$selectedLanguageCode : self::$selectedLanguages[0]);
917 LanguageFactory::getInstance()->makeDefault($language->languageID);
918
919 // rebuild language cache
920 LanguageCacheBuilder::getInstance()->reset();
921
922 // go to next step
923 $this->gotoNextStep('createUser');
924 }
925
926 /**
927 * Shows the page for creating the admin account.
928 */
929 protected function createUser() {
930 $errorType = $errorField = $username = $email = $confirmEmail = $password = $confirmPassword = '';
931
932 $username = '';
933 $email = $confirmEmail = '';
934 $password = $confirmPassword = '';
935
936 if (isset($_POST['send']) || self::$developerMode) {
937 if (isset($_POST['send'])) {
938 if (isset($_POST['username'])) $username = StringUtil::trim($_POST['username']);
939 if (isset($_POST['email'])) $email = StringUtil::trim($_POST['email']);
940 if (isset($_POST['confirmEmail'])) $confirmEmail = StringUtil::trim($_POST['confirmEmail']);
941 if (isset($_POST['password'])) $password = $_POST['password'];
942 if (isset($_POST['confirmPassword'])) $confirmPassword = $_POST['confirmPassword'];
943 }
944 else {
945 $username = $password = $confirmPassword = 'root';
946 $email = $confirmEmail = 'wsc-developer-mode@example.com';
947 }
948
949 // error handling
950 try {
951 // username
952 if (empty($username)) {
953 throw new UserInputException('username');
954 }
955 if (!UserUtil::isValidUsername($username)) {
956 throw new UserInputException('username', 'notValid');
957 }
958
959 // e-mail address
960 if (empty($email)) {
961 throw new UserInputException('email');
962 }
963 if (!UserUtil::isValidEmail($email)) {
964 throw new UserInputException('email', 'notValid');
965 }
966
967 // confirm e-mail address
968 if ($email != $confirmEmail) {
969 throw new UserInputException('confirmEmail', 'notEqual');
970 }
971
972 // password
973 if (empty($password)) {
974 throw new UserInputException('password');
975 }
976
977 // confirm e-mail address
978 if ($password != $confirmPassword) {
979 throw new UserInputException('confirmPassword', 'notEqual');
980 }
981
982 // no errors
983 // init database connection
984 $this->initDB();
985
986 // get language id
987 $languageID = 0;
988 $sql = "SELECT languageID
989 FROM wcf".WCF_N."_language
990 WHERE languageCode = ?";
991 $statement = self::getDB()->prepareStatement($sql);
992 $statement->execute([self::$selectedLanguageCode]);
993 $row = $statement->fetchArray();
994 if (isset($row['languageID'])) $languageID = $row['languageID'];
995
996 if (!$languageID) {
997 $languageID = LanguageFactory::getInstance()->getDefaultLanguageID();
998 }
999
1000 // create user
1001 $data = [
1002 'data' => [
1003 'email' => $email,
1004 'languageID' => $languageID,
1005 'password' => $password,
1006 'username' => $username
1007 ],
1008 'groups' => [
1009 1,
1010 3,
1011 4
1012 ],
1013 'languages' => [
1014 $languageID
1015 ]
1016 ];
1017
1018 $userAction = new UserAction([], 'create', $data);
1019 $userAction->executeAction();
1020
1021 // go to next step
1022 $this->gotoNextStep('installPackages');
1023 exit;
1024 }
1025 catch (UserInputException $e) {
1026 $errorField = $e->getField();
1027 $errorType = $e->getType();
1028 }
1029 }
1030
1031 WCF::getTPL()->assign([
1032 'errorField' => $errorField,
1033 'errorType' => $errorType,
1034 'username' => $username,
1035 'email' => $email,
1036 'confirmEmail' => $confirmEmail,
1037 'password' => $password,
1038 'confirmPassword' => $confirmPassword,
1039 'nextStep' => 'createUser'
1040 ]);
1041 WCF::getTPL()->display('stepCreateUser');
1042 }
1043
1044 /**
1045 * Registers with wcf setup delivered packages in the package installation queue.
1046 */
1047 protected function installPackages() {
1048 // init database connection
1049 $this->initDB();
1050
1051 // get admin account
1052 $admin = new User(1);
1053
1054 // get delivered packages
1055 $wcfPackageFile = '';
1056 $otherPackages = [];
1057 $tar = new Tar(SETUP_FILE);
1058 foreach ($tar->getContentList() as $file) {
1059 if ($file['type'] != 'folder' && mb_strpos($file['filename'], 'install/packages/') === 0) {
1060 $packageFile = basename($file['filename']);
1061
1062 // ignore any files which aren't an archive
1063 if (preg_match('~\.(tar\.gz|tgz|tar)$~', $packageFile)) {
1064 $packageName = preg_replace('!\.(tar\.gz|tgz|tar)$!', '', $packageFile);
1065
1066 if ($packageName == 'com.woltlab.wcf') {
1067 $wcfPackageFile = $packageFile;
1068 }
1069 else {
1070 $isStrato = (!empty($_SERVER['DOCUMENT_ROOT']) && (strpos($_SERVER['DOCUMENT_ROOT'], 'strato') !== false));
1071 if (!$isStrato && preg_match('!\.(tar\.gz|tgz)$!', $packageFile)) {
1072 // try to unzip zipped package files
1073 if (FileUtil::uncompressFile(TMP_DIR.'install/packages/'.$packageFile, TMP_DIR.'install/packages/'.$packageName.'.tar')) {
1074 @unlink(TMP_DIR.'install/packages/'.$packageFile);
1075 $packageFile = $packageName.'.tar';
1076 }
1077 }
1078
1079 $otherPackages[$packageName] = $packageFile;
1080 }
1081 }
1082 }
1083 }
1084 $tar->close();
1085
1086 // delete install files
1087 $installPhpDeleted = @unlink('./install.php');
1088 @unlink('./test.php');
1089 $wcfSetupTarDeleted = @unlink('./WCFSetup.tar.gz');
1090
1091 // render page
1092 WCF::getTPL()->assign([
1093 'installPhpDeleted' => $installPhpDeleted,
1094 'wcfSetupTarDeleted' => $wcfSetupTarDeleted
1095 ]);
1096 $output = WCF::getTPL()->fetch('stepInstallPackages');
1097
1098 // register packages in queue
1099 // get new process id
1100 $sql = "SELECT MAX(processNo) AS processNo
1101 FROM wcf".WCF_N."_package_installation_queue";
1102 $statement = self::getDB()->prepareStatement($sql);
1103 $statement->execute();
1104 $result = $statement->fetchArray();
1105 $processNo = intval($result['processNo']) + 1;
1106
1107 // search existing wcf package
1108 $sql = "SELECT COUNT(*) AS count
1109 FROM wcf".WCF_N."_package
1110 WHERE package = 'com.woltlab.wcf'";
1111 $statement = self::getDB()->prepareStatement($sql);
1112 $statement->execute();
1113 if (!$statement->fetchSingleColumn()) {
1114 if (empty($wcfPackageFile)) {
1115 throw new SystemException('the essential package com.woltlab.wcf is missing.');
1116 }
1117
1118 // register essential wcf package
1119 $queue = PackageInstallationQueueEditor::create([
1120 'processNo' => $processNo,
1121 'userID' => $admin->userID,
1122 'package' => 'com.woltlab.wcf',
1123 'packageName' => 'WoltLab Suite Core',
1124 'archive' => TMP_DIR.'install/packages/'.$wcfPackageFile,
1125 'isApplication' => 1
1126 ]);
1127 }
1128
1129 // register all other delivered packages
1130 asort($otherPackages);
1131 foreach ($otherPackages as $packageName => $packageFile) {
1132 // extract packageName from archive's package.xml
1133 $archive = new PackageArchive(TMP_DIR.'install/packages/'.$packageFile);
1134 try {
1135 $archive->openArchive();
1136 }
1137 catch (\Exception $e) {
1138 // we've encountered a broken archive, revert everything and then fail
1139 $sql = "SELECT queueID, parentQueueID
1140 FROM wcf".WCF_N."_package_installation_queue";
1141 $statement = WCF::getDB()->prepareStatement($sql);
1142 $statement->execute();
1143 $queues = $statement->fetchMap('queueID', 'parentQueueID');
1144
1145 $queueIDs = [];
1146 /** @noinspection PhpUndefinedVariableInspection */
1147 $queueID = $queue->queueID;
1148 while ($queueID) {
1149 $queueIDs[] = $queueID;
1150
1151 $queueID = (isset($queues[$queueID])) ? $queues[$queueID] : 0;
1152 }
1153
1154 // remove previously created queues
1155 if (!empty($queueIDs)) {
1156 $sql = "DELETE FROM wcf".WCF_N."_package_installation_queue
1157 WHERE queueID = ?";
1158 $statement = WCF::getDB()->prepareStatement($sql);
1159 WCF::getDB()->beginTransaction();
1160 foreach ($queueIDs as $queueID) {
1161 $statement->execute([$queueID]);
1162 }
1163 WCF::getDB()->commitTransaction();
1164 }
1165
1166 // remove package files
1167 @unlink(TMP_DIR.'install/packages/'.$wcfPackageFile);
1168 foreach ($otherPackages as $otherPackageFile) {
1169 @unlink(TMP_DIR.'install/packages/'.$otherPackageFile);
1170 }
1171
1172 // throw exception again
1173 throw new SystemException('', 0, '', $e);
1174 }
1175
1176 /** @noinspection PhpUndefinedVariableInspection */
1177 $queue = PackageInstallationQueueEditor::create([
1178 'parentQueueID' => $queue->queueID,
1179 'processNo' => $processNo,
1180 'userID' => $admin->userID,
1181 'package' => $packageName,
1182 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
1183 'archive' => TMP_DIR.'install/packages/'.$packageFile,
1184 'isApplication' => 1
1185 ]);
1186 }
1187
1188 // login as admin
1189 define('COOKIE_PREFIX', 'wcf22_');
1190
1191 $factory = new ACPSessionFactory();
1192 $factory->load();
1193
1194 SessionHandler::getInstance()->changeUser($admin);
1195 SessionHandler::getInstance()->register('masterPassword', 1);
1196 SessionHandler::getInstance()->register('__wcfSetup_developerMode', self::$developerMode);
1197 SessionHandler::getInstance()->register('__wcfSetup_directories', self::$directories);
1198 SessionHandler::getInstance()->update();
1199
1200 // print page
1201 HeaderUtil::sendHeaders();
1202 echo $output;
1203
1204 // delete tmp files
1205 $directory = TMP_DIR.'/';
1206 DirectoryUtil::getInstance($directory)->removePattern(new Regex('\.tar(\.gz)?$'), true);
1207 }
1208
1209 /**
1210 * Goes to the next step.
1211 *
1212 * @param string $nextStep
1213 */
1214 protected function gotoNextStep($nextStep) {
1215 WCF::getTPL()->assign(['nextStep' => $nextStep]);
1216 WCF::getTPL()->display('stepNext');
1217 }
1218
1219 /**
1220 * Installs the files of the tar archive.
1221 */
1222 protected static function installFiles() {
1223 new Installer(self::$directories['wcf'], SETUP_FILE, null, 'install/files/');
1224 }
1225
1226 /**
1227 * Gets the package names of the bundled applications in WCFSetup.tar.gz.
1228 */
1229 protected static function getPackageNames() {
1230 // get package name
1231 $packageNames = [];
1232 $tar = new Tar(SETUP_FILE);
1233 foreach ($tar->getContentList() as $file) {
1234 if ($file['type'] != 'folder' && mb_strpos($file['filename'], 'install/packages/') === 0) {
1235 $packageFile = basename($file['filename']);
1236
1237 try {
1238 $archive = new PackageArchive(TMP_DIR.'install/packages/'.$packageFile);
1239 $archive->openArchive();
1240 $packageNames[] = $archive->getLocalizedPackageInfo('packageName');
1241 $archive->getTar()->close();
1242 }
1243 catch (SystemException $e) {}
1244 }
1245 }
1246 $tar->close();
1247
1248 sort($packageNames);
1249
1250 // assign package name
1251 WCF::getTPL()->assign(['setupPackageNames' => $packageNames]);
1252 }
1253 }