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