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\exception\DatabaseException
;
11 use wcf\system\database\util\SQLParser
;
12 use wcf\system\database\MySQLDatabase
;
13 use wcf\system\devtools\DevtoolsSetup
;
14 use wcf\system\exception\SystemException
;
15 use wcf\system\exception\UserInputException
;
16 use wcf\system\io\File
;
17 use wcf\system\io\Tar
;
18 use wcf\system\language\LanguageFactory
;
19 use wcf\system\package\PackageArchive
;
20 use wcf\system\session\ACPSessionFactory
;
21 use wcf\system\session\SessionHandler
;
22 use wcf\system\setup\Installer
;
23 use wcf\system\setup\SetupFileHandler
;
24 use wcf\system\template\SetupTemplateEngine
;
25 use wcf\util\DirectoryUtil
;
26 use wcf\util\FileUtil
;
27 use wcf\util\HeaderUtil
;
28 use wcf\util\StringUtil
;
29 use wcf\util\UserUtil
;
33 define('PACKAGE_ID', 0);
34 define('HTTP_ENABLE_GZIP', 0);
35 define('HTTP_GZIP_LEVEL', 0);
36 define('HTTP_SEND_X_FRAME_OPTIONS', 0);
37 define('CACHE_SOURCE_TYPE', 'disk');
38 define('MODULE_MASTER_PASSWORD', 1);
39 define('ENABLE_DEBUG_MODE', 1);
40 define('ENABLE_BENCHMARK', 0);
43 * Executes the installation of the basic WCF systems.
46 * @copyright 2001-2018 WoltLab GmbH
47 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
48 * @package WoltLabSuite\Core\System
50 class WCFSetup
extends WCF
{
52 * list of available languages
55 protected static $availableLanguages = [];
58 * installation directories
61 protected static $directories = [];
64 * language code of selected installation language
67 protected static $selectedLanguageCode = 'en';
70 * selected languages to be installed
73 protected static $selectedLanguages = [];
76 * list of installed files
79 protected static $installedFiles = [];
82 * indicates if developer mode is used to install
85 protected static $developerMode = 0;
87 /** @noinspection PhpMissingParentConstructorInspection */
89 * Calls all init functions of the WCFSetup class and starts the setup process.
91 public function __construct() {
94 static::getDeveloperMode();
95 static::getLanguageSelection();
96 static::getInstallationDirectories();
97 $this->initLanguage();
99 /** @noinspection PhpUndefinedMethodInspection */
100 self
::getLanguage()->loadLanguage();
101 static::getPackageNames();
108 * Sets the status of the developer mode.
110 protected static function getDeveloperMode() {
111 if (isset($_GET['dev'])) self
::$developerMode = intval($_GET['dev']);
112 else if (isset($_POST['dev'])) self
::$developerMode = intval($_POST['dev']);
116 * Sets the selected language.
118 protected static function getLanguageSelection() {
119 self
::$availableLanguages = self
::getAvailableLanguages();
121 if (isset($_REQUEST['languageCode']) && isset(self
::$availableLanguages[$_REQUEST['languageCode']])) {
122 self
::$selectedLanguageCode = $_REQUEST['languageCode'];
125 self
::$selectedLanguageCode = LanguageFactory
::getPreferredLanguage(array_keys(self
::$availableLanguages), self
::$selectedLanguageCode);
128 if (isset($_POST['selectedLanguages']) && is_array($_POST['selectedLanguages'])) {
129 self
::$selectedLanguages = $_POST['selectedLanguages'];
134 * Sets the selected wcf dir from request.
138 protected static function getInstallationDirectories() {
139 if (!empty($_REQUEST['directories']) && is_array($_REQUEST['directories'])) {
140 foreach ($_REQUEST['directories'] as $application => $directory) {
141 self
::$directories[$application] = $directory;
143 if ($application === 'wcf' && @file_exists
(self
::$directories['wcf'])) {
144 define('RELATIVE_WCF_DIR', FileUtil
::getRelativePath(INSTALL_SCRIPT_DIR
, self
::$directories['wcf']));
149 define('WCF_DIR', (isset(self
::$directories['wcf']) ? self
::$directories['wcf'] : ''));
153 * Initialises the language engine.
155 protected function initLanguage() {
157 mb_internal_encoding('UTF-8');
158 if (function_exists('mb_regex_encoding')) mb_regex_encoding('UTF-8');
161 // init setup language
162 self
::$languageObj = new SetupLanguage(null, ['languageCode' => self
::$selectedLanguageCode]);
166 * Initialises the template engine.
168 protected function initTPL() {
169 self
::$tplObj = SetupTemplateEngine
::getInstance();
170 self
::getTPL()->setLanguageID((self
::$selectedLanguageCode == 'en' ?
0 : 1));
171 self
::getTPL()->setCompileDir(TMP_DIR
);
172 self
::getTPL()->addApplication('wcf', TMP_DIR
);
173 self
::getTPL()->registerPrefilter(['lang']);
174 self
::getTPL()->assign([
176 'tmpFilePrefix' => TMP_FILE_PREFIX
,
177 'languageCode' => self
::$selectedLanguageCode,
178 'selectedLanguages' => self
::$selectedLanguages,
179 'directories' => self
::$directories,
180 'developerMode' => self
::$developerMode
185 * Returns all languages from WCFSetup.tar.gz.
189 protected static function getAvailableLanguages() {
190 $languages = $match = [];
191 foreach (glob(TMP_DIR
.'setup/lang/*.xml') as $file) {
194 $languageCode = LanguageEditor
::readLanguageCodeFromXML($xml);
195 $languageName = LanguageEditor
::readLanguageNameFromXML($xml);
197 $languages[$languageCode] = $languageName;
200 // sort languages by language name
207 * Calculates the current state of the progress bar.
209 * @param integer $currentStep
211 protected function calcProgress($currentStep) {
212 // calculate progress
213 $progress = round((100 / 25) * ++
$currentStep, 0);
214 self
::getTPL()->assign(['progress' => $progress]);
218 * Executes the setup steps.
220 protected function setup() {
222 if (isset($_REQUEST['step'])) $step = $_REQUEST['step'];
223 else $step = 'selectSetupLanguage';
225 // execute current step
227 /** @noinspection PhpMissingBreakStatementInspection */
228 case 'selectSetupLanguage':
229 if (!self
::$developerMode) {
230 $this->calcProgress(0);
231 $this->selectSetupLanguage();
235 /** @noinspection PhpMissingBreakStatementInspection */
237 if (!self
::$developerMode) {
238 $this->calcProgress(1);
239 $this->showLicense();
243 /** @noinspection PhpMissingBreakStatementInspection */
244 case 'showSystemRequirements':
245 if (!self
::$developerMode) {
246 $this->calcProgress(2);
247 $this->showSystemRequirements();
251 case 'configureDirectories':
252 $this->calcProgress(3);
253 $this->configureDirectories();
257 $this->calcProgress(4);
261 case 'selectLanguages':
262 $this->calcProgress(5);
263 $this->selectLanguages();
267 $this->calcProgress(6);
268 $this->configureDB();
273 if (isset($_POST['offset'])) {
274 $currentStep +
= intval($_POST['offset']);
277 $this->calcProgress($currentStep);
282 $this->calcProgress(21);
286 case 'installLanguage':
287 $this->calcProgress(22);
288 $this->installLanguage();
292 $this->calcProgress(23);
296 case 'installPackages':
297 $this->calcProgress(24);
298 $this->installPackages();
304 * Shows the first setup page.
306 protected function selectSetupLanguage() {
307 WCF
::getTPL()->assign([
308 'availableLanguages' => self
::$availableLanguages,
309 'nextStep' => 'showLicense'
311 WCF
::getTPL()->display('stepSelectSetupLanguage');
315 * Shows the license agreement.
317 protected function showLicense() {
318 if (isset($_POST['send'])) {
319 if (isset($_POST['accepted'])) {
320 $this->gotoNextStep('showSystemRequirements');
324 WCF
::getTPL()->assign(['missingAcception' => true]);
329 if (file_exists(TMP_DIR
.'setup/license/license_'.self
::$selectedLanguageCode.'.txt')) {
330 $license = file_get_contents(TMP_DIR
.'setup/license/license_'.self
::$selectedLanguageCode.'.txt');
333 $license = file_get_contents(TMP_DIR
.'setup/license/license_en.txt');
336 WCF
::getTPL()->assign([
337 'license' => $license,
338 'nextStep' => 'showLicense'
340 WCF
::getTPL()->display('stepShowLicense');
344 * Shows the system requirements.
346 protected function showSystemRequirements() {
350 $system['phpVersion']['value'] = phpversion();
351 $comparePhpVersion = preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $system['phpVersion']['value']);
352 $system['phpVersion']['result'] = (version_compare($comparePhpVersion, '5.5.4') >= 0);
355 $system['sql']['result'] = MySQLDatabase
::isSupported();
357 // upload_max_filesize
358 $system['uploadMaxFilesize']['value'] = min(ini_get('upload_max_filesize'), ini_get('post_max_size'));
359 $system['uploadMaxFilesize']['result'] = (intval($system['uploadMaxFilesize']['value']) > 0);
362 $system['gdLib']['value'] = '0.0.0';
363 if (function_exists('gd_info')) {
366 if (preg_match('!([0-9]+\.[0-9]+(?:\.[0-9]+)?)!', $temp['GD Version'], $match)) {
367 if (preg_match('/^[0-9]+\.[0-9]+$/', $match[1])) $match[1] .= '.0';
368 $system['gdLib']['value'] = $match[1];
371 $system['gdLib']['result'] = (version_compare($system['gdLib']['value'], '2.0.0') >= 0);
374 $system['memoryLimit']['value'] = ini_get('memory_limit');
375 $system['memoryLimit']['result'] = $this->compareMemoryLimit();
378 $system['openssl']['result'] = @extension_loaded
('openssl');
380 WCF
::getTPL()->assign([
382 'nextStep' => 'configureDirectories'
384 WCF
::getTPL()->display('stepShowSystemRequirements');
388 * Returns true if memory_limit is set to at least 128 MB
392 protected function compareMemoryLimit() {
393 $memoryLimit = ini_get('memory_limit');
396 if ($memoryLimit == -1) {
400 // completely numeric, PHP assumes byte
401 if (is_numeric($memoryLimit)) {
402 $memoryLimit = $memoryLimit / 1024 / 1024;
403 return ($memoryLimit >= 128);
406 // PHP supports 'K', 'M' and 'G' shorthand notation
407 if (preg_match('~^(\d+)([KMG])$~', $memoryLimit, $matches)) {
408 switch ($matches[2]) {
410 $memoryLimit = $matches[1] * 1024;
411 return ($memoryLimit >= 128);
415 return ($matches[1] >= 128);
419 return ($matches[1] >= 1);
428 * Searches the wcf dir.
432 protected function configureDirectories() {
433 // get available packages
435 foreach (glob(TMP_DIR
. 'install/packages/*') as $file) {
436 $filename = basename($file);
437 if (preg_match('~\.(?:tar|tar\.gz|tgz)$~', $filename)) {
438 $package = new PackageArchive($file);
439 $package->openArchive();
441 $application = Package
::getAbbreviation($package->getPackageInfo('name'));
443 $packages[$application] = [
444 'directory' => $package->getPackageInfo('applicationDirectory') ?
: $application,
445 'packageDescription' => $package->getLocalizedPackageInfo('packageDescription'),
446 'packageName' => $package->getLocalizedPackageInfo('packageName')
451 uasort($packages, function($a, $b) {
452 return strcmp($a['packageName'], $b['packageName']);
455 // force cms being shown first
456 $showOrder = ['wcf'];
457 foreach (array_keys($packages) as $application) {
458 if ($application !== 'wcf') $showOrder[] = $application;
461 $documentRoot = FileUtil
::unifyDirSeparator(realpath($_SERVER['DOCUMENT_ROOT']));
462 if (self
::$developerMode && (isset($_ENV['WCFSETUP_USEDEFAULTWCFDIR']) || DevtoolsSetup
::getInstance()->useDefaultInstallPath())) {
463 // resolve path relative to document root
464 $relativePath = FileUtil
::getRelativePath($documentRoot, INSTALL_SCRIPT_DIR
);
465 foreach ($packages as $application => $packageData) {
466 self
::$directories[$application] = $relativePath . ($application === 'wcf' ?
'' : $packageData['directory'] . '/');
471 if (!empty(self
::$directories)) {
472 $applicationPaths = $knownPaths = [];
474 // use $showOrder to ensure that the error message for duplicate directories
475 // will trigger in display order rather than the random sort order returned
477 foreach ($showOrder as $application) {
478 $path = FileUtil
::getRealPath($documentRoot . '/' . FileUtil
::addTrailingSlash(FileUtil
::removeLeadingSlash(self
::$directories[$application])));
479 if (!empty($documentRoot) && strpos($path, $documentRoot) !== 0) {
480 // verify that given path is still within the current document root
481 $errors[$application] = 'outsideDocumentRoot';
483 else if (in_array($path, $knownPaths)) {
484 // prevent the same path for two or more applications
485 $errors[$application] = 'duplicate';
487 else if (@is_file
($path . 'global.php')) {
488 // check if directory is empty (dotfiles are okay)
489 $errors[$application] = 'notEmpty';
492 // try to create directory if it does not exist
493 if (!is_dir($path) && !FileUtil
::makePath($path)) {
494 $errors[$application] = 'makePath';
498 FileUtil
::makeWritable($path);
500 catch (SystemException
$e) {
501 $errors[$application] = 'makeWritable';
505 $applicationPaths[$application] = $path;
506 $knownPaths[] = $path;
509 if (empty($errors)) {
510 // copy over the actual paths
511 self
::$directories = array_merge(self
::$directories, $applicationPaths);
512 WCF
::getTPL()->assign(['directories' => self
::$directories]);
519 // resolve path relative to document root
520 $relativePath = FileUtil
::getRelativePath($documentRoot, INSTALL_SCRIPT_DIR
);
521 foreach ($packages as $application => $packageData) {
522 $dir = $relativePath . ($application === 'wcf' ?
'' : $packageData['directory'] . '/');
523 if (mb_strpos($dir, './') === 0) $dir = mb_substr($dir, 1);
525 self
::$directories[$application] = $dir;
529 WCF
::getTPL()->assign([
530 'directories' => self
::$directories,
531 'documentRoot' => $documentRoot,
533 'installScriptDir' => FileUtil
::unifyDirSeparator(INSTALL_SCRIPT_DIR
),
534 'nextStep' => 'configureDirectories', // call this step again to validate paths
535 'packages' => $packages,
536 'showOrder' => $showOrder
539 WCF
::getTPL()->display('stepConfigureDirectories');
543 * Unzips the files of the wcfsetup tar archive.
545 protected function unzipFiles() {
546 // WCF seems to be installed, abort
547 if (@is_file
(self
::$directories['wcf'].'lib/system/WCF.class.php')) {
548 throw new SystemException('Target directory seems to be an existing installation of WCF, unable to continue.');
550 // WCF not yet installed, install files first
552 static::installFiles();
554 $this->gotoNextStep('selectLanguages');
559 * Shows the page for choosing the installed languages.
561 protected function selectLanguages() {
562 $errorField = $errorType = '';
564 // skip step in developer mode
565 // select all available languages automatically
566 if (self
::$developerMode) {
567 self
::$selectedLanguages = [];
568 foreach (self
::$availableLanguages as $languageCode => $language) {
569 self
::$selectedLanguages[] = $languageCode;
572 self
::getTPL()->assign(['selectedLanguages' => self
::$selectedLanguages]);
573 $this->gotoNextStep('configureDB');
577 // start error handling
578 if (isset($_POST['send'])) {
580 // no languages selected
581 if (empty(self
::$selectedLanguages)) {
582 throw new UserInputException('selectedLanguages');
586 foreach (self
::$selectedLanguages as $language) {
587 if (!isset(self
::$availableLanguages[$language])) {
588 throw new UserInputException('selectedLanguages');
594 $this->gotoNextStep('configureDB');
597 catch (UserInputException
$e) {
598 $errorField = $e->getField();
599 $errorType = $e->getType();
603 self
::$selectedLanguages[] = self
::$selectedLanguageCode;
604 WCF
::getTPL()->assign(['selectedLanguages' => self
::$selectedLanguages]);
607 WCF
::getTPL()->assign([
608 'errorField' => $errorField,
609 'errorType' => $errorType,
610 'availableLanguages' => self
::$availableLanguages,
611 'nextStep' => 'selectLanguages'
613 WCF
::getTPL()->display('stepSelectLanguages');
617 * Shows the page for configuring the database connection.
619 protected function configureDB() {
620 $attemptConnection = isset($_POST['send']);
622 if (self
::$developerMode && isset($_ENV['WCFSETUP_DBHOST'])) {
623 $dbHost = $_ENV['WCFSETUP_DBHOST'];
624 $dbUser = $_ENV['WCFSETUP_DBUSER'];
625 $dbPassword = $_ENV['WCFSETUP_DBPASSWORD'];
626 $dbName = $_ENV['WCFSETUP_DBNAME'];
629 $attemptConnection = true;
631 else if (self
::$developerMode && ($config = DevtoolsSetup
::getInstance()->getDatabaseConfig()) !== null) {
632 $dbHost = $config['host'];
633 $dbUser = $config['username'];
634 $dbPassword = $config['password'];
635 $dbName = $config['dbName'];
636 $dbNumber = $config['dbNumber'];
638 if ($config['auto']) $attemptConnection = true;
641 $dbHost = 'localhost';
648 if ($attemptConnection) {
649 if (isset($_POST['dbHost'])) $dbHost = $_POST['dbHost'];
650 if (isset($_POST['dbUser'])) $dbUser = $_POST['dbUser'];
651 if (isset($_POST['dbPassword'])) $dbPassword = $_POST['dbPassword'];
652 if (isset($_POST['dbName'])) $dbName = $_POST['dbName'];
654 // ensure that $dbNumber is zero or a positive integer
655 if (isset($_POST['dbNumber'])) $dbNumber = max(0, intval($_POST['dbNumber']));
658 $dbHostWithoutPort = $dbHost;
660 if (preg_match('/^(.+?):(\d+)$/', $dbHost, $match)) {
661 $dbHostWithoutPort = $match[1];
662 $dbPort = intval($match[2]);
667 // check connection data
668 /** @var \wcf\system\database\Database $db */
670 $db = new MySQLDatabase($dbHostWithoutPort, $dbUser, $dbPassword, $dbName, $dbPort, true, !!(self
::$developerMode));
672 catch (DatabaseException
$e) {
673 // work-around for older MySQL versions that don't know utf8mb4
674 if ($e->getPrevious()->getCode() == 1115) {
675 throw new SystemException("Insufficient MySQL version. Version '5.5.35' or greater is needed.");
682 $sqlVersion = $db->getVersion();
683 $compareSQLVersion = preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $sqlVersion);
684 if (stripos($sqlVersion, 'MariaDB')) {
685 // MariaDB 5.5.47+ or 10.0.22+ are required
686 // https://jira.mariadb.org/browse/MDEV-8756
687 if ($compareSQLVersion[0] === '5') {
689 if (!(version_compare($compareSQLVersion, '5.5.47') >= 0)) {
690 throw new SystemException("Insufficient MariaDB version '".$compareSQLVersion."'. Version '5.5.47' or greater, or version '10.0.22' or greater is needed.");
693 else if (!(version_compare($compareSQLVersion, '10.0.22') >= 0)) {
695 throw new SystemException("Insufficient MariaDB version '".$compareSQLVersion."'. Version '5.5.47' or greater, or version '10.0.22' or greater is needed.");
700 if (!(version_compare($compareSQLVersion, '5.5.35') >= 0)) {
701 throw new SystemException("Insufficient MySQL version '".$compareSQLVersion."'. Version '5.5.35' or greater is needed.");
705 // check innodb support
706 $sql = "SHOW ENGINES";
707 $statement = $db->prepareStatement($sql);
708 $statement->execute();
710 while ($row = $statement->fetchArray()) {
711 if ($row['Engine'] == 'InnoDB' && in_array($row['Support'], ['DEFAULT', 'YES'])) {
718 throw new SystemException("Support for InnoDB is missing.");
721 // check for PHP's MySQL native driver
724 $statement = $db->prepareStatement($sql);
725 $statement->execute();
726 // MySQL native driver understands data types, libmysqlclient does not
727 if ($statement->fetchSingleColumn() !== 1) {
728 throw new SystemException("MySQLnd is not being used for database communication.");
732 // check for table conflicts
733 $conflictedTables = $this->getConflictedTables($db, $dbNumber);
736 if (empty($conflictedTables)) {
737 // connection successfully established
738 // write configuration to config.inc.php
739 $file = new File(WCF_DIR
.'config.inc.php');
740 $file->write("<?php\n");
741 $file->write("\$dbHost = '".str_replace("'", "\\'", $dbHostWithoutPort)."';\n");
742 $file->write("\$dbPort = ".$dbPort.";\n");
743 $file->write("\$dbUser = '".str_replace("'", "\\'", $dbUser)."';\n");
744 $file->write("\$dbPassword = '".str_replace("'", "\\'", $dbPassword)."';\n");
745 $file->write("\$dbName = '".str_replace("'", "\\'", $dbName)."';\n");
746 $file->write("if (!defined('WCF_N')) define('WCF_N', $dbNumber);\n");
750 $this->gotoNextStep('createDB');
753 // show configure template again
755 WCF
::getTPL()->assign(['conflictedTables' => $conflictedTables]);
758 catch (SystemException
$e) {
759 WCF
::getTPL()->assign(['exception' => $e]);
762 WCF
::getTPL()->assign([
765 'dbPassword' => $dbPassword,
767 'dbNumber' => $dbNumber,
768 'nextStep' => 'configureDB'
770 WCF
::getTPL()->display('stepConfigureDB');
774 * Checks if in the chosen database are tables in conflict with the wcf tables
775 * which will be created in the next step.
777 * @param \wcf\system\database\Database $db
778 * @param integer $dbNumber
779 * @return string[] list of already existing tables
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');
785 // installation number value 'n' (WCF_N) must be reflected in the executed sql queries
786 $sql = str_replace('wcf1_', 'wcf'.$dbNumber.'_', $sql);
788 // get all tablenames which should be created
789 preg_match_all("%CREATE\s+TABLE\s+(\w+)%", $sql, $matches);
791 // get all installed tables from chosen database
792 $existingTables = $db->getEditor()->getTableNames();
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;
803 return $conflictedTables;
807 * Creates the database structure of the wcf.
809 protected function createDB() {
812 // get content of the sql structure file
813 $sql = file_get_contents(TMP_DIR
.'setup/db/install.sql');
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");
821 $sql = $sqlData[$offset];
823 // installation number value 'n' (WCF_N) must be reflected in the executed sql queries
824 $sql = str_replace('wcf1_', 'wcf'.WCF_N
.'_', $sql);
826 // execute sql queries
827 $parser = new SQLParser($sql);
831 preg_match_all("~CREATE\s+TABLE\s+(\w+)~i", $sql, $matches);
833 if (!empty($matches[1])) {
834 $sql = "INSERT INTO wcf".WCF_N
."_package_installation_sql_log
837 $statement = self
::getDB()->prepareStatement($sql);
838 foreach ($matches[1] as $tableName) {
839 $statement->execute([$tableName]);
843 if ($offset < (count($sqlData) - 1)) {
844 WCF
::getTPL()->assign([
845 '__additionalParameters' => [
846 'offset' => $offset +
1
850 $this->gotoNextStep('createDB');
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!
857 $sql = "INSERT INTO wcf".WCF_N
."_package_installation_plugin
858 (pluginName, priority, className)
860 $statement = self
::getDB()->prepareStatement($sql);
861 $statement->execute([
862 'packageInstallationPlugin',
864 'wcf\system\package\plugin\PIPPackageInstallationPlugin'
867 $this->gotoNextStep('logFiles');
872 * Logs the unzipped files.
874 protected function logFiles() {
877 $this->getInstalledFiles(WCF_DIR
);
878 $acpTemplateInserts = $fileInserts = [];
879 foreach (self
::$installedFiles as $file) {
881 if (preg_match('~^acp/templates/([^/]+)\.tpl$~', $file, $match)) {
883 $acpTemplateInserts[] = $match[1];
887 $fileInserts[] = $file;
891 // save acp template log
892 if (!empty($acpTemplateInserts)) {
893 $sql = "INSERT INTO wcf".WCF_N
."_acp_template
894 (templateName, application)
896 $statement = self
::getDB()->prepareStatement($sql);
898 self
::getDB()->beginTransaction();
899 foreach ($acpTemplateInserts as $acpTemplate) {
900 $statement->execute([$acpTemplate, 'wcf']);
902 self
::getDB()->commitTransaction();
906 if (!empty($fileInserts)) {
907 $sql = "INSERT INTO wcf".WCF_N
."_package_installation_file_log
908 (filename, application)
910 $statement = self
::getDB()->prepareStatement($sql);
912 self
::getDB()->beginTransaction();
913 foreach ($fileInserts as $file) {
914 $statement->execute([$file, 'wcf']);
916 self
::getDB()->commitTransaction();
919 $this->gotoNextStep('installLanguage');
923 * Scans the given dir for installed files.
926 * @throws SystemException
928 protected function getInstalledFiles($dir) {
929 $logFile = $dir . 'files.log';
930 if (!file_exists($logFile)) {
931 throw new SystemException("Expected a valid file log at '".$logFile."'.");
934 self
::$installedFiles = explode("\n", file_get_contents($logFile));
940 * Installs the selected languages.
942 protected function installLanguage() {
945 foreach (self
::$selectedLanguages as $language) {
946 // get language.xml file name
947 $filename = TMP_DIR
.'install/lang/'.$language.'.xml';
950 if (!file_exists($filename)) {
951 throw new SystemException("unable to find language file '".$filename."'");
956 $xml->load($filename);
959 LanguageEditor
::importFromXML($xml, 0);
962 // set default language
963 $language = LanguageFactory
::getInstance()->getLanguageByCode(in_array(self
::$selectedLanguageCode, self
::$selectedLanguages) ? self
::$selectedLanguageCode : self
::$selectedLanguages[0]);
964 LanguageFactory
::getInstance()->makeDefault($language->languageID
);
966 // rebuild language cache
967 LanguageCacheBuilder
::getInstance()->reset();
970 $this->gotoNextStep('createUser');
974 * Shows the page for creating the admin account.
976 protected function createUser() {
977 $errorType = $errorField = $username = $email = $confirmEmail = $password = $confirmPassword = '';
980 $email = $confirmEmail = '';
981 $password = $confirmPassword = '';
983 if (isset($_POST['send']) || self
::$developerMode) {
984 if (isset($_POST['send'])) {
985 if (isset($_POST['username'])) $username = StringUtil
::trim($_POST['username']);
986 if (isset($_POST['email'])) $email = StringUtil
::trim($_POST['email']);
987 if (isset($_POST['confirmEmail'])) $confirmEmail = StringUtil
::trim($_POST['confirmEmail']);
988 if (isset($_POST['password'])) $password = $_POST['password'];
989 if (isset($_POST['confirmPassword'])) $confirmPassword = $_POST['confirmPassword'];
992 $username = $password = $confirmPassword = 'root';
993 $email = $confirmEmail = 'wsc-developer-mode@example.com';
999 if (empty($username)) {
1000 throw new UserInputException('username');
1002 if (!UserUtil
::isValidUsername($username)) {
1003 throw new UserInputException('username', 'invalid');
1007 if (empty($email)) {
1008 throw new UserInputException('email');
1010 if (!UserUtil
::isValidEmail($email)) {
1011 throw new UserInputException('email', 'invalid');
1014 // confirm e-mail address
1015 if ($email != $confirmEmail) {
1016 throw new UserInputException('confirmEmail', 'notEqual');
1020 if (empty($password)) {
1021 throw new UserInputException('password');
1024 // confirm e-mail address
1025 if ($password != $confirmPassword) {
1026 throw new UserInputException('confirmPassword', 'notEqual');
1030 // init database connection
1035 $sql = "SELECT languageID
1036 FROM wcf".WCF_N
."_language
1037 WHERE languageCode = ?";
1038 $statement = self
::getDB()->prepareStatement($sql);
1039 $statement->execute([self
::$selectedLanguageCode]);
1040 $row = $statement->fetchArray();
1041 if (isset($row['languageID'])) $languageID = $row['languageID'];
1044 $languageID = LanguageFactory
::getInstance()->getDefaultLanguageID();
1051 'languageID' => $languageID,
1052 'password' => $password,
1053 'username' => $username,
1054 'signatureEnableHtml' => 1
1066 $userAction = new UserAction([], 'create', $data);
1067 $userAction->executeAction();
1070 $this->gotoNextStep('installPackages');
1073 catch (UserInputException
$e) {
1074 $errorField = $e->getField();
1075 $errorType = $e->getType();
1079 WCF
::getTPL()->assign([
1080 'errorField' => $errorField,
1081 'errorType' => $errorType,
1082 'username' => $username,
1084 'confirmEmail' => $confirmEmail,
1085 'password' => $password,
1086 'confirmPassword' => $confirmPassword,
1087 'nextStep' => 'createUser'
1089 WCF
::getTPL()->display('stepCreateUser');
1093 * Registers with wcf setup delivered packages in the package installation queue.
1095 protected function installPackages() {
1096 // init database connection
1099 // get admin account
1100 $admin = new User(1);
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']);
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);
1114 if ($packageName == 'com.woltlab.wcf') {
1115 $wcfPackageFile = $packageFile;
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';
1127 $otherPackages[$packageName] = $packageFile;
1134 // delete install files
1135 $installPhpDeleted = @unlink
('./install.php');
1136 @unlink
('./test.php');
1137 $wcfSetupTarDeleted = @unlink
('./WCFSetup.tar.gz');
1140 WCF
::getTPL()->assign([
1141 'installPhpDeleted' => $installPhpDeleted,
1142 'wcfSetupTarDeleted' => $wcfSetupTarDeleted
1144 $output = WCF
::getTPL()->fetch('stepInstallPackages');
1146 // register packages in queue
1147 // get new process id
1148 $sql = "SELECT MAX(processNo) AS processNo
1149 FROM wcf".WCF_N
."_package_installation_queue";
1150 $statement = self
::getDB()->prepareStatement($sql);
1151 $statement->execute();
1152 $result = $statement->fetchArray();
1153 $processNo = intval($result['processNo']) +
1;
1155 // search existing wcf package
1156 $sql = "SELECT COUNT(*) AS count
1157 FROM wcf".WCF_N
."_package
1158 WHERE package = 'com.woltlab.wcf'";
1159 $statement = self
::getDB()->prepareStatement($sql);
1160 $statement->execute();
1161 if (!$statement->fetchSingleColumn()) {
1162 if (empty($wcfPackageFile)) {
1163 throw new SystemException('the essential package com.woltlab.wcf is missing.');
1166 // register essential wcf package
1167 $queue = PackageInstallationQueueEditor
::create([
1168 'processNo' => $processNo,
1169 'userID' => $admin->userID
,
1170 'package' => 'com.woltlab.wcf',
1171 'packageName' => 'WoltLab Suite Core',
1172 'archive' => TMP_DIR
.'install/packages/'.$wcfPackageFile,
1173 'isApplication' => 1
1177 // register all other delivered packages
1178 asort($otherPackages);
1179 foreach ($otherPackages as $packageName => $packageFile) {
1180 // extract packageName from archive's package.xml
1181 $archive = new PackageArchive(TMP_DIR
.'install/packages/'.$packageFile);
1183 $archive->openArchive();
1185 catch (\Exception
$e) {
1186 // we've encountered a broken archive, revert everything and then fail
1187 $sql = "SELECT queueID, parentQueueID
1188 FROM wcf".WCF_N
."_package_installation_queue";
1189 $statement = WCF
::getDB()->prepareStatement($sql);
1190 $statement->execute();
1191 $queues = $statement->fetchMap('queueID', 'parentQueueID');
1194 /** @noinspection PhpUndefinedVariableInspection */
1195 $queueID = $queue->queueID
;
1197 $queueIDs[] = $queueID;
1199 $queueID = isset($queues[$queueID]) ?
$queues[$queueID] : 0;
1202 // remove previously created queues
1203 if (!empty($queueIDs)) {
1204 $sql = "DELETE FROM wcf".WCF_N
."_package_installation_queue
1206 $statement = WCF
::getDB()->prepareStatement($sql);
1207 WCF
::getDB()->beginTransaction();
1208 foreach ($queueIDs as $queueID) {
1209 $statement->execute([$queueID]);
1211 WCF
::getDB()->commitTransaction();
1214 // remove package files
1215 @unlink
(TMP_DIR
.'install/packages/'.$wcfPackageFile);
1216 foreach ($otherPackages as $otherPackageFile) {
1217 @unlink
(TMP_DIR
.'install/packages/'.$otherPackageFile);
1220 // throw exception again
1221 throw new SystemException('', 0, '', $e);
1224 /** @noinspection PhpUndefinedVariableInspection */
1225 $queue = PackageInstallationQueueEditor
::create([
1226 'parentQueueID' => $queue->queueID
,
1227 'processNo' => $processNo,
1228 'userID' => $admin->userID
,
1229 'package' => $packageName,
1230 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
1231 'archive' => TMP_DIR
.'install/packages/'.$packageFile,
1232 'isApplication' => 1
1236 // determine the (randomized) cookie prefix
1237 $useRandomCookiePrefix = true;
1238 if (self
::$developerMode && DevtoolsSetup
::getInstance()->forceStaticCookiePrefix()) {
1239 $useRandomCookiePrefix = false;
1243 if ($useRandomCookiePrefix) {
1244 $cookieNames = array_keys($_COOKIE);
1246 $prefix = 'wsc_' . substr(sha1(mt_rand()), 0, 6) . '_';
1248 foreach ($cookieNames as $cookieName) {
1249 if (strpos($cookieName, $prefix) === 0) {
1260 // the options have not been imported yet
1261 file_put_contents(WCF_DIR
. 'cookiePrefix.txt', $prefix);
1265 define('COOKIE_PREFIX', $prefix);
1267 $factory = new ACPSessionFactory();
1270 SessionHandler
::getInstance()->changeUser($admin);
1271 SessionHandler
::getInstance()->register('masterPassword', 1);
1272 SessionHandler
::getInstance()->register('__wcfSetup_developerMode', self
::$developerMode);
1273 SessionHandler
::getInstance()->register('__wcfSetup_directories', self
::$directories);
1274 SessionHandler
::getInstance()->unregister('__changeSessionID');
1275 SessionHandler
::getInstance()->update();
1278 HeaderUtil
::sendHeaders();
1282 $directory = TMP_DIR
.'/';
1283 DirectoryUtil
::getInstance($directory)->removePattern(new Regex('\.tar(\.gz)?$'), true);
1287 * Goes to the next step.
1289 * @param string $nextStep
1291 protected function gotoNextStep($nextStep) {
1292 WCF
::getTPL()->assign(['nextStep' => $nextStep]);
1293 WCF
::getTPL()->display('stepNext');
1297 * Installs the files of the tar archive.
1299 protected static function installFiles() {
1300 $fileHandler = new SetupFileHandler();
1301 new Installer(self
::$directories['wcf'], SETUP_FILE
, $fileHandler, 'install/files/');
1302 $fileHandler->dumpToFile(self
::$directories['wcf'] . 'files.log');
1306 * Reads the package names of the bundled applications in WCFSetup.tar.gz.
1308 protected static function getPackageNames() {
1311 $tar = new Tar(SETUP_FILE
);
1312 foreach ($tar->getContentList() as $file) {
1313 if ($file['type'] != 'folder' && mb_strpos($file['filename'], 'install/packages/') === 0) {
1314 $packageFile = basename($file['filename']);
1317 $archive = new PackageArchive(TMP_DIR
.'install/packages/'.$packageFile);
1318 $archive->openArchive();
1319 $packageNames[] = $archive->getLocalizedPackageInfo('packageName');
1320 $archive->getTar()->close();
1322 catch (SystemException
$e) {}
1327 sort($packageNames);
1329 // assign package name
1330 WCF
::getTPL()->assign(['setupPackageNames' => $packageNames]);