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