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