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