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