Drakia

Untitled

Apr 20th, 2024
44
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 38.56 KB | None | 0 0
  1. <?php
  2.  
  3. namespace wcf\system;
  4.  
  5. use Laminas\Diactoros\Response\HtmlResponse;
  6. use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
  7. use Psr\Http\Message\ResponseInterface;
  8. use wcf\data\language\LanguageEditor;
  9. use wcf\data\language\SetupLanguage;
  10. use wcf\data\package\installation\queue\PackageInstallationQueueEditor;
  11. use wcf\data\package\Package;
  12. use wcf\data\user\User;
  13. use wcf\data\user\UserAction;
  14. use wcf\system\cache\builder\LanguageCacheBuilder;
  15. use wcf\system\database\Database;
  16. use wcf\system\database\exception\DatabaseException;
  17. use wcf\system\database\MySQLDatabase;
  18. use wcf\system\database\util\SQLParser;
  19. use wcf\system\devtools\DevtoolsSetup;
  20. use wcf\system\exception\SystemException;
  21. use wcf\system\exception\UserInputException;
  22. use wcf\system\image\adapter\GDImageAdapter;
  23. use wcf\system\image\adapter\ImagickImageAdapter;
  24. use wcf\system\io\File;
  25. use wcf\system\io\Tar;
  26. use wcf\system\language\LanguageFactory;
  27. use wcf\system\package\PackageArchive;
  28. use wcf\system\request\RouteHandler;
  29. use wcf\system\session\ACPSessionFactory;
  30. use wcf\system\session\SessionHandler;
  31. use wcf\system\setup\Installer;
  32. use wcf\system\setup\SetupFileHandler;
  33. use wcf\system\template\SetupTemplateEngine;
  34. use wcf\util\FileUtil;
  35. use wcf\util\HeaderUtil;
  36. use wcf\util\StringUtil;
  37. use wcf\util\UserUtil;
  38. use wcf\util\XML;
  39.  
  40. // define
  41. \define('PACKAGE_ID', 0);
  42. \define('CACHE_SOURCE_TYPE', 'disk');
  43. \define('ENABLE_DEBUG_MODE', 1);
  44. \define('ENABLE_BENCHMARK', 0);
  45. \define('ENABLE_ENTERPRISE_MODE', 0);
  46.  
  47. /**
  48.  * Executes the installation of the basic WCF systems.
  49.  *
  50.  * @author  Marcel Werk
  51.  * @copyright   2001-2019 WoltLab GmbH
  52.  * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  53.  */
  54. final class WCFSetup extends WCF
  55. {
  56.     /**
  57.      * list of available languages
  58.      * @var string[]
  59.      */
  60.     protected static $availableLanguages = [];
  61.  
  62.     /**
  63.      * language code of selected installation language
  64.      * @var string
  65.      */
  66.     protected static $selectedLanguageCode = 'en';
  67.  
  68.     /**
  69.      * list of installed files
  70.      * @var string[]
  71.      */
  72.     protected static $installedFiles = [];
  73.  
  74.     /**
  75.      * indicates if developer mode is used to install
  76.      * @var bool
  77.      */
  78.     protected static $developerMode = 0;
  79.  
  80.     /** @noinspection PhpMissingParentConstructorInspection */
  81.  
  82.     /**
  83.      * Calls all init functions of the WCFSetup class and starts the setup process.
  84.      */
  85.     public function __construct()
  86.     {
  87.         @\set_time_limit(0);
  88.  
  89.         static::getDeveloperMode();
  90.         static::getLanguageSelection();
  91.         $this->initLanguage();
  92.         $this->initTPL();
  93.  
  94.         $emitter = new SapiEmitter();
  95.         $response = $this->dispatch();
  96.         $response = HeaderUtil::withNoCacheHeaders($response);
  97.         $response = $response->withHeader('x-frame-options', 'SAMEORIGIN');
  98.         $emitter->emit($response);
  99.     }
  100.  
  101.     /**
  102.      * Sets the status of the developer mode.
  103.      */
  104.     protected static function getDeveloperMode(): void
  105.     {
  106.         if (isset($_GET['dev'])) {
  107.             self::$developerMode = \intval($_GET['dev']);
  108.         } elseif (isset($_POST['dev'])) {
  109.             self::$developerMode = \intval($_POST['dev']);
  110.         }
  111.     }
  112.  
  113.     /**
  114.      * Sets the selected language.
  115.      */
  116.     protected static function getLanguageSelection(): void
  117.     {
  118.         self::$availableLanguages = self::getAvailableLanguages();
  119.  
  120.         if (isset($_REQUEST['languageCode']) && isset(self::$availableLanguages[$_REQUEST['languageCode']])) {
  121.             self::$selectedLanguageCode = $_REQUEST['languageCode'];
  122.         } else {
  123.             self::$selectedLanguageCode = LanguageFactory::getPreferredLanguage(
  124.                 \array_keys(self::$availableLanguages),
  125.                 self::$selectedLanguageCode
  126.             );
  127.         }
  128.     }
  129.  
  130.     /**
  131.      * Initialises the language engine.
  132.      */
  133.     protected function initLanguage(): void
  134.     {
  135.         self::$languageObj = new SetupLanguage(self::$selectedLanguageCode);
  136.     }
  137.  
  138.     /**
  139.      * Initialises the template engine.
  140.      */
  141.     protected function initTPL(): void
  142.     {
  143.         self::$tplObj = SetupTemplateEngine::getInstance();
  144.         self::getTPL()->setLanguageID((self::$selectedLanguageCode == 'en' ? 0 : 1));
  145.         self::getTPL()->setCompileDir(TMP_DIR);
  146.         self::getTPL()->addApplication('wcf', TMP_DIR);
  147.         self::getTPL()->assign([
  148.             '__wcf' => $this,
  149.             'tmpFilePrefix' => TMP_FILE_PREFIX,
  150.             'languageCode' => self::$selectedLanguageCode,
  151.             'developerMode' => self::$developerMode,
  152.  
  153.             'setupAssets' => [
  154.                 'WCFSetup.css' => \sprintf(
  155.                     'data:text/css;base64,%s',
  156.                     \base64_encode(\file_get_contents(TMP_DIR . 'install/files/acp/style/setup/WCFSetup.css'))
  157.                 ),
  158.                 'woltlabSuite.png' => \sprintf(
  159.                     'data:image/png;base64,%s',
  160.                     \base64_encode(\file_get_contents(TMP_DIR . 'install/files/acp/images/woltlabSuite.png'))
  161.                 ),
  162.             ],
  163.         ]);
  164.     }
  165.  
  166.     /**
  167.      * Returns all languages from WCFSetup.tar.gz.
  168.      *
  169.      * @return  string[]
  170.      */
  171.     protected static function getAvailableLanguages(): array
  172.     {
  173.         $languages = [];
  174.         foreach (\glob(TMP_DIR . 'setup/lang/*.xml') as $file) {
  175.             $xml = new XML();
  176.             $xml->load($file);
  177.             $languageCode = LanguageEditor::readLanguageCodeFromXML($xml);
  178.             $languageName = LanguageEditor::readLanguageNameFromXML($xml);
  179.  
  180.             $languages[$languageCode] = $languageName;
  181.         }
  182.  
  183.         // sort languages by language name
  184.         \asort($languages);
  185.  
  186.         return $languages;
  187.     }
  188.  
  189.     /**
  190.      * Calculates the current state of the progress bar.
  191.      */
  192.     protected function calcProgress(int $currentStep): void
  193.     {
  194.         $lastStep = \intval(\file_get_contents(\TMP_DIR . 'lastStep'));
  195.         if ($lastStep > $currentStep) {
  196.             throw new \Exception('Refusing to step back to a previous step.');
  197.         }
  198.         if ($lastStep !== $currentStep - 1 && $lastStep !== $currentStep) {
  199.             throw new \Exception('Refusing to skip a step.');
  200.         }
  201.  
  202.         \file_put_contents(\TMP_DIR . 'lastStep', $currentStep);
  203.  
  204.         // calculate progress
  205.         $progress = \round((100 / 22) * ++$currentStep, 0);
  206.         self::getTPL()->assign(['progress' => $progress]);
  207.     }
  208.  
  209.     /**
  210.      * Throws an exception if it appears that the 'unzipFiles' step already ran.
  211.      */
  212.     protected function assertNotUnzipped(): void
  213.     {
  214.         if (
  215.             \is_file(INSTALL_SCRIPT_DIR . 'lib/system/WCF.class.php')
  216.             || \is_file(INSTALL_SCRIPT_DIR . 'global.php')
  217.         ) {
  218.             throw new \Exception(
  219.                 'Target directory seems to be an existing installation of WoltLab Suite Core, refusing to continue.'
  220.             );
  221.         }
  222.     }
  223.  
  224.     /**
  225.      * Executes the setup steps.
  226.      */
  227.     protected function dispatch(): ResponseInterface
  228.     {
  229.         // get current step
  230.         if (isset($_POST['step'])) {
  231.             $step = $_POST['step'];
  232.         } else {
  233.             $step = 'selectSetupLanguage';
  234.         }
  235.  
  236.         \header('set-cookie: wcfsetup_cookietest=' . TMP_FILE_PREFIX . '; domain=' . \str_replace(
  237.             RouteHandler::getProtocol(),
  238.             '',
  239.             RouteHandler::getHost()
  240.         ) . (RouteHandler::secureConnection() ? '; secure' : ''));
  241.  
  242.         // execute current step
  243.         switch ($step) {
  244.             case 'selectSetupLanguage':
  245.                 $this->calcProgress(0);
  246.                 $this->assertNotUnzipped();
  247.  
  248.                 return $this->selectSetupLanguage();
  249.  
  250.             case 'showLicense':
  251.                 $this->calcProgress(1);
  252.                 $this->assertNotUnzipped();
  253.  
  254.                 return $this->showLicense();
  255.  
  256.             case 'showSystemRequirements':
  257.                 $this->calcProgress(2);
  258.                 $this->assertNotUnzipped();
  259.  
  260.                 return $this->showSystemRequirements();
  261.  
  262.             case 'configureDB':
  263.                 $this->calcProgress(3);
  264.                 $this->assertNotUnzipped();
  265.  
  266.                 return $this->configureDB();
  267.  
  268.             case 'createDB':
  269.                 $currentStep = 4;
  270.                 if (isset($_POST['offset'])) {
  271.                     $currentStep += \intval($_POST['offset']);
  272.                 }
  273.  
  274.                 $this->calcProgress($currentStep);
  275.                 $this->assertNotUnzipped();
  276.  
  277.                 return $this->createDB();
  278.  
  279.             case 'unzipFiles':
  280.                 $this->calcProgress(18);
  281.                 $this->assertNotUnzipped();
  282.  
  283.                 return $this->unzipFiles();
  284.  
  285.             case 'installLanguage':
  286.                 $this->calcProgress(19);
  287.  
  288.                 return $this->installLanguage();
  289.  
  290.             case 'createUser':
  291.                 $this->calcProgress(20);
  292.  
  293.                 return $this->createUser();
  294.  
  295.             case 'installPackages':
  296.                 $this->calcProgress(21);
  297.  
  298.                 return $this->installPackages();
  299.         }
  300.     }
  301.  
  302.     /**
  303.      * Shows the first setup page.
  304.      */
  305.     protected function selectSetupLanguage(): ResponseInterface
  306.     {
  307.         if (self::$developerMode) {
  308.             return $this->gotoNextStep('showLicense');
  309.         }
  310.  
  311.         return new HtmlResponse(
  312.             WCF::getTPL()->fetchStream(
  313.                 'stepSelectSetupLanguage',
  314.                 'wcf',
  315.                 [
  316.                     'availableLanguages' => self::$availableLanguages,
  317.                     'nextStep' => 'showLicense',
  318.                 ]
  319.             )
  320.         );
  321.     }
  322.  
  323.     /**
  324.      * Shows the license agreement.
  325.      */
  326.     protected function showLicense(): ResponseInterface
  327.     {
  328.         if (self::$developerMode) {
  329.             return $this->gotoNextStep('showSystemRequirements');
  330.         }
  331.  
  332.         $missingAcception = false;
  333.  
  334.         if (isset($_POST['send'])) {
  335.             if (isset($_POST['accepted'])) {
  336.                 return $this->gotoNextStep('showSystemRequirements');
  337.             } else {
  338.                 $missingAcception = true;
  339.             }
  340.         }
  341.  
  342.         if (\file_exists(TMP_DIR . 'setup/license/license_' . self::$selectedLanguageCode . '.txt')) {
  343.             $license = \file_get_contents(TMP_DIR . 'setup/license/license_' . self::$selectedLanguageCode . '.txt');
  344.         } else {
  345.             $license = \file_get_contents(TMP_DIR . 'setup/license/license_en.txt');
  346.         }
  347.  
  348.         return new HtmlResponse(
  349.             WCF::getTPL()->fetchStream(
  350.                 'stepShowLicense',
  351.                 'wcf',
  352.                 [
  353.                     'license' => $license,
  354.                     'missingAcception' => $missingAcception,
  355.                     'nextStep' => 'showLicense',
  356.                 ]
  357.             )
  358.         );
  359.     }
  360.  
  361.     /**
  362.      * Shows the system requirements.
  363.      */
  364.     protected function showSystemRequirements(): ResponseInterface
  365.     {
  366.         $phpVersionLowerBound = '8.1.2';
  367.         $phpVersionUpperBound = '8.3.x';
  368.         $system = [];
  369.  
  370.         // php version
  371.         $system['phpVersion']['value'] = \PHP_VERSION;
  372.         $comparePhpVersion = \preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $system['phpVersion']['value']);
  373.         $system['phpVersion']['result'] = \version_compare($comparePhpVersion, $phpVersionLowerBound, '>=')
  374.             && \version_compare($comparePhpVersion, \str_replace('x', '999', $phpVersionUpperBound), '<=');
  375.  
  376.         $system['x64']['result'] = \PHP_INT_SIZE == 8;
  377.  
  378.         // sql
  379.         $system['sql']['result'] = MySQLDatabase::isSupported();
  380.  
  381.         // upload_max_filesize
  382.         $system['uploadMaxFilesize']['value'] = \min(\ini_get('upload_max_filesize'), \ini_get('post_max_size'));
  383.         $system['uploadMaxFilesize']['result'] = (\intval($system['uploadMaxFilesize']['value']) > 0);
  384.  
  385.         // graphics library
  386.         $system['graphicsLibrary']['result'] = false;
  387.         $system['graphicsLibrary']['value'] = '';
  388.         if (
  389.             ImagickImageAdapter::isSupported()
  390.             && ImagickImageAdapter::supportsAnimatedGIFs(ImagickImageAdapter::getVersion())
  391.             && ImagickImageAdapter::supportsWebp()
  392.         ) {
  393.             $system['graphicsLibrary'] = [
  394.                 'result' => true,
  395.                 'value' => 'ImageMagick',
  396.             ];
  397.         } elseif (GDImageAdapter::isSupported()) {
  398.             $system['graphicsLibrary'] = [
  399.                 'result' => GDImageAdapter::supportsWebp(),
  400.                 'value' => 'GD Library',
  401.             ];
  402.         }
  403.  
  404.         // memory limit
  405.         $system['memoryLimit']['value'] = FileUtil::getMemoryLimit();
  406.         $system['memoryLimit']['result'] = $system['memoryLimit']['value'] === -1 || $system['memoryLimit']['value'] >= 128 * 1024 * 1024;
  407.  
  408.         // openssl extension
  409.         $system['openssl']['result'] = \extension_loaded('openssl');
  410.  
  411.         // curl
  412.         $system['curl']['result'] = \extension_loaded('curl');
  413.  
  414.         // misconfigured reverse proxy / cookies
  415.         $system['hostname']['result'] = true;
  416.         [$system['hostname']['value']] = \explode(':', $_SERVER['HTTP_HOST'], 2);
  417.         if (!empty($_SERVER['HTTP_REFERER'])) {
  418.             $refererHostname = \parse_url($_SERVER['HTTP_REFERER'], \PHP_URL_HOST);
  419.             $system['hostname']['result'] = $_SERVER['HTTP_HOST'] == $refererHostname;
  420.         }
  421.  
  422.         $system['cookie']['result'] = !empty($_COOKIE['wcfsetup_cookietest']) && $_COOKIE['wcfsetup_cookietest'] == TMP_FILE_PREFIX;
  423.  
  424.         $system['tls']['result'] = RouteHandler::secureConnection() || $system['hostname']['value'] == 'localhost';
  425.  
  426.         foreach ($system as $result) {
  427.             if (!$result['result']) {
  428.                 return new HtmlResponse(
  429.                     WCF::getTPL()->fetchStream(
  430.                         'stepShowSystemRequirements',
  431.                         'wcf',
  432.                         [
  433.                             'system' => $system,
  434.                             'nextStep' => 'configureDB',
  435.                             'phpVersionLowerBound' => $phpVersionLowerBound,
  436.                             'phpVersionUpperBound' => $phpVersionUpperBound,
  437.                         ]
  438.                     )
  439.                 );
  440.             }
  441.         }
  442.  
  443.         // If all system requirements are met, directly go to next step.
  444.         return $this->gotoNextStep('configureDB');
  445.     }
  446.  
  447.     /**
  448.      * Shows the page for configuring the database connection.
  449.      */
  450.     protected function configureDB(): ResponseInterface
  451.     {
  452.         $attemptConnection = isset($_POST['send']);
  453.  
  454.         if (self::$developerMode && isset($_ENV['WCFSETUP_DBHOST'])) {
  455.             $dbHost = $_ENV['WCFSETUP_DBHOST'];
  456.             $dbUser = $_ENV['WCFSETUP_DBUSER'];
  457.             $dbPassword = $_ENV['WCFSETUP_DBPASSWORD'];
  458.             $dbName = $_ENV['WCFSETUP_DBNAME'];
  459.  
  460.             $attemptConnection = true;
  461.         } elseif (self::$developerMode && ($config = DevtoolsSetup::getInstance()->getDatabaseConfig()) !== null) {
  462.             $dbHost = $config['host'];
  463.             $dbUser = $config['username'];
  464.             $dbPassword = $config['password'];
  465.             $dbName = $config['dbName'];
  466.  
  467.             if ($config['auto']) {
  468.                 $attemptConnection = true;
  469.             }
  470.         } else {
  471.             $dbHost = 'localhost';
  472.             $dbUser = 'root';
  473.             $dbPassword = '';
  474.             $dbName = 'wcf';
  475.         }
  476.  
  477.         if ($attemptConnection) {
  478.             if (isset($_POST['dbHost'])) {
  479.                 $dbHost = $_POST['dbHost'];
  480.             }
  481.             if (isset($_POST['dbUser'])) {
  482.                 $dbUser = $_POST['dbUser'];
  483.             }
  484.             if (isset($_POST['dbPassword'])) {
  485.                 $dbPassword = $_POST['dbPassword'];
  486.             }
  487.             if (isset($_POST['dbName'])) {
  488.                 $dbName = $_POST['dbName'];
  489.             }
  490.  
  491.             // get port
  492.             $dbHostWithoutPort = $dbHost;
  493.             $dbPort = 0;
  494.             if (\preg_match('/^(.+?):(\d+)$/', $dbHost, $match)) {
  495.                 $dbHostWithoutPort = $match[1];
  496.                 $dbPort = \intval($match[2]);
  497.             }
  498.  
  499.             // test connection
  500.             try {
  501.                 // check connection data
  502.                 /** @var \wcf\system\database\Database $db */
  503.                 try {
  504.                     $db = new MySQLDatabase(
  505.                         $dbHostWithoutPort,
  506.                         $dbUser,
  507.                         $dbPassword,
  508.                         $dbName,
  509.                         $dbPort,
  510.                         true,
  511.                         !!(self::$developerMode)
  512.                     );
  513.                 } catch (DatabaseException $e) {
  514.                     switch ($e->getPrevious()->getCode()) {
  515.                         case 1049: // try to manually create non-existing database
  516.                             try {
  517.                                 $db = new MySQLDatabase(
  518.                                     $dbHostWithoutPort,
  519.                                     $dbUser,
  520.                                     $dbPassword,
  521.                                     $dbName,
  522.                                     $dbPort,
  523.                                     true,
  524.                                     true
  525.                                 );
  526.                             } catch (DatabaseException $e) {
  527.                                 throw new SystemException("Unknown database '{$dbName}'. Please create the database manually.");
  528.                             }
  529.  
  530.                             break;
  531.  
  532.                         case 1115: // work-around for older MySQL versions that don't know utf8mb4
  533.                             throw new SystemException("Insufficient MySQL version. Version '8.0.30' or greater is needed.");
  534.                             break;
  535.  
  536.                         default:
  537.                             throw $e;
  538.                     }
  539.                 }
  540.  
  541.                 // check sql version
  542.                 $sqlVersion = $db->getVersion();
  543.                 $compareSQLVersion = \preg_replace('/^(\d+\.\d+\.\d+).*$/', '\\1', $sqlVersion);
  544.                 if (\stripos($sqlVersion, 'MariaDB')) {
  545.                     if (!(\version_compare($compareSQLVersion, '10.5.15') >= 0)) {
  546.                         throw new SystemException("Insufficient MariaDB version '" . $compareSQLVersion . "'. Version '10.5.15' or greater is needed.");
  547.                     }
  548.                 } else {
  549.                     if (!(\version_compare($compareSQLVersion, '8.0.30') >= 0)) {
  550.                         throw new SystemException("Insufficient MySQL version '" . $compareSQLVersion . "'. Version '8.0.30' or greater is needed.");
  551.                     }
  552.                 }
  553.  
  554.                 // check innodb support
  555.                 $sql = "SHOW ENGINES";
  556.                 $statement = $db->prepareStatement($sql);
  557.                 $statement->execute();
  558.                 $hasInnoDB = false;
  559.                 while ($row = $statement->fetchArray()) {
  560.                     if ($row['Engine'] == 'InnoDB' && \in_array($row['Support'], ['DEFAULT', 'YES'])) {
  561.                         $hasInnoDB = true;
  562.                         break;
  563.                     }
  564.                 }
  565.  
  566.                 if (!$hasInnoDB) {
  567.                     throw new SystemException("Support for InnoDB is missing.");
  568.                 }
  569.  
  570.                 // check for PHP's MySQL native driver
  571.                 $sql = "SELECT 1";
  572.                 $statement = $db->prepareStatement($sql);
  573.                 $statement->execute();
  574.                 // MySQL native driver understands data types, libmysqlclient does not
  575.                 if ($statement->fetchSingleColumn() !== 1) {
  576.                     throw new SystemException("MySQL Native Driver is not being used for database communication.");
  577.                 }
  578.  
  579.                 // check for table conflicts
  580.                 $conflictedTables = $this->getConflictedTables($db);
  581.  
  582.                 if (empty($conflictedTables)) {
  583.                     // connection successfully established
  584.                     // write configuration to config.inc.php
  585.                     \file_put_contents(
  586.                         WCF_DIR . 'config.inc.php',
  587.                         \sprintf(
  588.                             <<<'CONFIG'
  589.                             <?php
  590.                             $dbHost = %s;
  591.                             $dbPort = %s;
  592.                             $dbUser = %s;
  593.                             $dbPassword = %s;
  594.                             $dbName = %s;
  595.                             if (!defined('WCF_N')) define('WCF_N', 1);
  596.                             CONFIG,
  597.                             \var_export($dbHostWithoutPort, true),
  598.                             \var_export($dbPort, true),
  599.                             \var_export($dbUser, true),
  600.                             \var_export($dbPassword, true),
  601.                             \var_export($dbName, true)
  602.                         )
  603.                     );
  604.  
  605.                     return $this->gotoNextStep('createDB');
  606.                 } else {
  607.                     // show configure template again
  608.                     WCF::getTPL()->assign(['conflictedTables' => $conflictedTables]);
  609.                 }
  610.             } catch (SystemException $e) {
  611.                 WCF::getTPL()->assign(['exception' => $e]);
  612.             }
  613.         }
  614.  
  615.         return new HtmlResponse(
  616.             WCF::getTPL()->fetchStream(
  617.                 'stepConfigureDB',
  618.                 'wcf',
  619.                 [
  620.                     'dbHost' => $dbHost,
  621.                     'dbUser' => $dbUser,
  622.                     'dbPassword' => $dbPassword,
  623.                     'dbName' => $dbName,
  624.                     'nextStep' => 'configureDB',
  625.                 ]
  626.             )
  627.         );
  628.     }
  629.  
  630.     /**
  631.      * Checks if in the chosen database are tables in conflict with the wcf tables
  632.      * which will be created in the next step.
  633.      *
  634.      * @return  list<string>    list of already existing tables
  635.      */
  636.     protected function getConflictedTables(Database $db): array
  637.     {
  638.         // get content of the sql structure file
  639.         $sql = \file_get_contents(TMP_DIR . 'setup/db/install.sql');
  640.  
  641.         // get all tablenames which should be created
  642.         \preg_match_all("%CREATE\\s+TABLE\\s+(\\w+)%", $sql, $matches);
  643.  
  644.         // get all installed tables from chosen database
  645.         $existingTables = $db->getEditor()->getTableNames();
  646.  
  647.         // check if existing tables are in conflict with wcf tables
  648.         $conflictedTables = [];
  649.         foreach ($existingTables as $existingTableName) {
  650.             foreach ($matches[1] as $wcfTableName) {
  651.                 if ($existingTableName == $wcfTableName) {
  652.                     $conflictedTables[] = $wcfTableName;
  653.                 }
  654.             }
  655.         }
  656.  
  657.         return $conflictedTables;
  658.     }
  659.  
  660.     /**
  661.      * Creates the database structure of the wcf.
  662.      */
  663.     protected function createDB(): ResponseInterface
  664.     {
  665.         $this->initDB();
  666.  
  667.         // get content of the sql structure file
  668.         $sql = \file_get_contents(TMP_DIR . 'setup/db/install.sql');
  669.  
  670.         // split by offsets
  671.         $sqlData = \explode('/* SQL_PARSER_OFFSET */', $sql);
  672.         $offset = isset($_POST['offset']) ? \intval($_POST['offset']) : 0;
  673.         if (!isset($sqlData[$offset])) {
  674.             throw new SystemException("Offset for SQL parser is out of bounds, " . $offset . " was requested, but there are only " . \count($sqlData) . " sections");
  675.         }
  676.         $sql = $sqlData[$offset];
  677.  
  678.         // execute sql queries
  679.         $parser = new SQLParser($sql);
  680.         $parser->execute();
  681.  
  682.         // log sql queries
  683.         \preg_match_all("~CREATE\\s+TABLE\\s+(\\w+)~i", $sql, $matches);
  684.  
  685.         $sql = "INSERT INTO wcf1_package_installation_sql_log
  686.                            (packageID, sqlTable)
  687.                VALUES      (?, ?)";
  688.         $statement = self::getDB()->prepareStatement($sql);
  689.         foreach ($matches[1] as $tableName) {
  690.             $statement->execute([1, $tableName]);
  691.         }
  692.  
  693.         if ($offset < (\count($sqlData) - 1)) {
  694.             WCF::getTPL()->assign([
  695.                 '__additionalParameters' => [
  696.                     'offset' => $offset + 1,
  697.                 ],
  698.             ]);
  699.  
  700.             return $this->gotoNextStep('createDB');
  701.         } else {
  702.             /*
  703.              * Manually install PIPPackageInstallationPlugin since install.sql content is not escaped resulting
  704.             * in different behaviour in MySQL and MSSQL. You SHOULD NOT move this into install.sql!
  705.             */
  706.             $sql = "INSERT INTO wcf1_package_installation_plugin
  707.                                (packageID, pluginName, priority, className)
  708.                    VALUES      (?, ?, ?, ?)";
  709.             $statement = self::getDB()->prepareStatement($sql);
  710.             $statement->execute([
  711.                 1,
  712.                 'packageInstallationPlugin',
  713.                 1,
  714.                 'wcf\system\package\plugin\PIPPackageInstallationPlugin',
  715.             ]);
  716.  
  717.             return $this->gotoNextStep('unzipFiles');
  718.         }
  719.     }
  720.  
  721.     /**
  722.      * Unzips the files of the wcfsetup tar archive.
  723.      */
  724.     protected function unzipFiles(): ResponseInterface
  725.     {
  726.         $this->initDB();
  727.  
  728.         $fileHandler = new SetupFileHandler();
  729.         new Installer(WCF_DIR, SETUP_FILE, $fileHandler, 'install/files/');
  730.  
  731.         // Create initial bootstrap.php including WCF's bootstrap script.
  732.         \file_put_contents(
  733.             WCF_DIR . 'lib/bootstrap.php',
  734.             <<<'EOT'
  735.             <?php
  736.  
  737.             return (function() {
  738.                 return [
  739.                     require(__DIR__ . '/bootstrap/com.woltlab.wcf.php'),
  740.                 ];
  741.             })();
  742.             EOT
  743.         );
  744.  
  745.         return $this->gotoNextStep('installLanguage');
  746.     }
  747.  
  748.     /**
  749.      * Installs the selected languages.
  750.      */
  751.     protected function installLanguage(): ResponseInterface
  752.     {
  753.         $this->initDB();
  754.  
  755.         $languageCodes = \array_keys(self::$availableLanguages);
  756.         foreach ($languageCodes as $language) {
  757.             // get language.xml file name
  758.             $filename = TMP_DIR . 'install/lang/' . $language . '.xml';
  759.  
  760.             // check the file
  761.             if (!\file_exists($filename)) {
  762.                 throw new SystemException("unable to find language file '" . $filename . "'");
  763.             }
  764.  
  765.             // open the file
  766.             $xml = new XML();
  767.             $xml->load($filename);
  768.  
  769.             // import xml
  770.             LanguageEditor::importFromXML($xml, 1);
  771.         }
  772.  
  773.         // set default language
  774.         $language = LanguageFactory::getInstance()->getLanguageByCode(
  775.             \in_array(
  776.                 self::$selectedLanguageCode,
  777.                 $languageCodes
  778.             ) ? self::$selectedLanguageCode : $languageCodes[0]
  779.         );
  780.         LanguageFactory::getInstance()->makeDefault($language->languageID);
  781.  
  782.         // rebuild language cache
  783.         LanguageCacheBuilder::getInstance()->reset();
  784.  
  785.         return $this->gotoNextStep('createUser');
  786.     }
  787.  
  788.     /**
  789.      * Shows the page for creating the admin account.
  790.      */
  791.     protected function createUser(): ResponseInterface
  792.     {
  793.         $errorType = $errorField = $username = $email = $confirmEmail = $password = $confirmPassword = '';
  794.  
  795.         $username = '';
  796.         $email = $confirmEmail = '';
  797.         $password = $confirmPassword = '';
  798.  
  799.         if (isset($_POST['send']) || self::$developerMode) {
  800.             if (isset($_POST['send'])) {
  801.                 if (isset($_POST['username'])) {
  802.                     $username = StringUtil::trim($_POST['username']);
  803.                 }
  804.                 if (isset($_POST['email'])) {
  805.                     $email = StringUtil::trim($_POST['email']);
  806.                 }
  807.                 if (isset($_POST['confirmEmail'])) {
  808.                     $confirmEmail = StringUtil::trim($_POST['confirmEmail']);
  809.                 }
  810.                 if (isset($_POST['password'])) {
  811.                     $password = $_POST['password'];
  812.                 }
  813.                 if (isset($_POST['confirmPassword'])) {
  814.                     $confirmPassword = $_POST['confirmPassword'];
  815.                 }
  816.             } else {
  817.                 $username = 'dev';
  818.                 $password = $confirmPassword = 'root';
  819.                 $email = $confirmEmail = 'wsc-developer-mode@example.com';
  820.             }
  821.  
  822.             // error handling
  823.             try {
  824.                 // username
  825.                 if (empty($username)) {
  826.                     throw new UserInputException('username');
  827.                 }
  828.                 if (!UserUtil::isValidUsername($username)) {
  829.                     throw new UserInputException('username', 'invalid');
  830.                 }
  831.  
  832.                 // e-mail address
  833.                 if (empty($email)) {
  834.                     throw new UserInputException('email');
  835.                 }
  836.                 if (!UserUtil::isValidEmail($email)) {
  837.                     throw new UserInputException('email', 'invalid');
  838.                 }
  839.  
  840.                 // confirm e-mail address
  841.                 if ($email != $confirmEmail) {
  842.                     throw new UserInputException('confirmEmail', 'notEqual');
  843.                 }
  844.  
  845.                 // password
  846.                 if (empty($password)) {
  847.                     throw new UserInputException('password');
  848.                 }
  849.  
  850.                 // confirm e-mail address
  851.                 if ($password != $confirmPassword) {
  852.                     throw new UserInputException('confirmPassword', 'notEqual');
  853.                 }
  854.  
  855.                 // no errors
  856.                 // init database connection
  857.                 $this->initDB();
  858.  
  859.                 // get language id
  860.                 $languageID = 0;
  861.                 $sql = "SELECT  languageID
  862.                        FROM    wcf1_language
  863.                        WHERE   languageCode = ?";
  864.                 $statement = self::getDB()->prepareStatement($sql);
  865.                 $statement->execute([self::$selectedLanguageCode]);
  866.                 $row = $statement->fetchArray();
  867.                 if (isset($row['languageID'])) {
  868.                     $languageID = $row['languageID'];
  869.                 }
  870.  
  871.                 if (!$languageID) {
  872.                     $languageID = LanguageFactory::getInstance()->getDefaultLanguageID();
  873.                 }
  874.  
  875.                 // create user
  876.                 $data = [
  877.                     'data' => [
  878.                         'userID' => 1,
  879.                         'email' => $email,
  880.                         'languageID' => $languageID,
  881.                         'password' => $password,
  882.                         'username' => $username,
  883.                         'signature' => '',
  884.                         'signatureEnableHtml' => 1,
  885.                     ],
  886.                     'groups' => [
  887.                         1,
  888.                         3,
  889.                         4,
  890.                     ],
  891.                     'languages' => [
  892.                         $languageID,
  893.                     ],
  894.                 ];
  895.  
  896.                 $userAction = new UserAction([], 'create', $data);
  897.                 $userAction->executeAction();
  898.  
  899.                 return $this->gotoNextStep('installPackages');
  900.             } catch (UserInputException $e) {
  901.                 $errorField = $e->getField();
  902.                 $errorType = $e->getType();
  903.             }
  904.         }
  905.  
  906.         return new HtmlResponse(
  907.             WCF::getTPL()->fetchStream(
  908.                 'stepCreateUser',
  909.                 'wcf',
  910.                 [
  911.                     'errorField' => $errorField,
  912.                     'errorType' => $errorType,
  913.                     'username' => $username,
  914.                     'email' => $email,
  915.                     'confirmEmail' => $confirmEmail,
  916.                     'password' => $password,
  917.                     'confirmPassword' => $confirmPassword,
  918.                     'nextStep' => 'createUser',
  919.                 ]
  920.             )
  921.         );
  922.     }
  923.  
  924.     /**
  925.      * Registers with wcf setup delivered packages in the package installation queue.
  926.      */
  927.     protected function installPackages(): ResponseInterface
  928.     {
  929.         // init database connection
  930.         $this->initDB();
  931.  
  932.         // get admin account
  933.         $admin = new User(1);
  934.  
  935.         // get delivered packages
  936.         $wcfPackageFile = '';
  937.         $otherPackages = [];
  938.         $tar = new Tar(SETUP_FILE);
  939.         foreach ($tar->getContentList() as $file) {
  940.             if ($file['type'] != 'folder' && \str_starts_with($file['filename'], 'install/packages/')) {
  941.                 $packageFile = \basename($file['filename']);
  942.  
  943.                 // ignore any files which aren't an archive
  944.                 if (\preg_match('~\.(tar\.gz|tgz|tar)$~', $packageFile)) {
  945.                     $packageName = \preg_replace('!\.(tar\.gz|tgz|tar)$!', '', $packageFile);
  946.  
  947.                     if ($packageName == 'com.woltlab.wcf') {
  948.                         $wcfPackageFile = $packageFile;
  949.                     } else {
  950.                         $otherPackages[$packageName] = $packageFile;
  951.                     }
  952.                 }
  953.             }
  954.         }
  955.         $tar->close();
  956.  
  957.         // delete install files
  958.         \unlink(\INSTALL_SCRIPT);
  959.         \unlink(\SETUP_FILE);
  960.         if (\file_exists(\INSTALL_SCRIPT_DIR . 'test.php')) {
  961.             \unlink(\INSTALL_SCRIPT_DIR . 'test.php');
  962.         }
  963.  
  964.         // register packages in queue
  965.         $processNo = 1;
  966.  
  967.         if (empty($wcfPackageFile)) {
  968.             throw new SystemException('the essential package com.woltlab.wcf is missing.');
  969.         }
  970.  
  971.         $from = TMP_DIR . 'install/packages/' . $wcfPackageFile;
  972.         $to = WCF_DIR . 'tmp/' . TMP_FILE_PREFIX . '-' . $wcfPackageFile;
  973.  
  974.         \rename($from, $to);
  975.  
  976.         // register essential wcf package
  977.         $queue = PackageInstallationQueueEditor::create([
  978.             'queueID' => 1,
  979.             'processNo' => $processNo,
  980.             'userID' => $admin->userID,
  981.             'package' => 'com.woltlab.wcf',
  982.             'packageName' => 'WoltLab Suite Core',
  983.             'archive' => $to,
  984.             'isApplication' => 1,
  985.         ]);
  986.         if ($queue->queueID !== 1) {
  987.             throw new \LogicException("Failed to register queue for 'com.woltlab.wcf'.");
  988.         }
  989.  
  990.         // register all other delivered packages
  991.         \asort($otherPackages);
  992.         foreach ($otherPackages as $packageName => $packageFile) {
  993.             $from = TMP_DIR . 'install/packages/' . $packageFile;
  994.             $to = WCF_DIR . 'tmp/' . TMP_FILE_PREFIX . '-' . $packageFile;
  995.  
  996.             // extract packageName from archive's package.xml
  997.             $archive = new PackageArchive($from);
  998.             $archive->openArchive();
  999.  
  1000.             \rename($from, $to);
  1001.  
  1002.             /** @noinspection PhpUndefinedVariableInspection */
  1003.             $queue = PackageInstallationQueueEditor::create([
  1004.                 'parentQueueID' => $queue->queueID,
  1005.                 'processNo' => $processNo,
  1006.                 'userID' => $admin->userID,
  1007.                 'package' => $packageName,
  1008.                 'packageName' => $archive->getLocalizedPackageInfo('packageName'),
  1009.                 'archive' => $to,
  1010.                 'isApplication' => 1,
  1011.             ]);
  1012.         }
  1013.  
  1014.         // determine the (randomized) cookie prefix
  1015.         $useRandomCookiePrefix = true;
  1016.         if (self::$developerMode && DevtoolsSetup::getInstance()->forceStaticCookiePrefix()) {
  1017.             $useRandomCookiePrefix = false;
  1018.         }
  1019.  
  1020.         $prefix = 'wsc_';
  1021.         if ($useRandomCookiePrefix) {
  1022.             $cookieNames = \array_keys($_COOKIE);
  1023.             while (true) {
  1024.                 $prefix = 'wsc_' . \bin2hex(\random_bytes(3)) . '_';
  1025.                 $isValid = true;
  1026.                 foreach ($cookieNames as $cookieName) {
  1027.                     if (\strpos($cookieName, $prefix) === 0) {
  1028.                         $isValid = false;
  1029.                         break;
  1030.                     }
  1031.                 }
  1032.  
  1033.                 if ($isValid) {
  1034.                     break;
  1035.                 }
  1036.             }
  1037.  
  1038.             // the options have not been imported yet
  1039.             \file_put_contents(WCF_DIR . 'cookiePrefix.txt', $prefix);
  1040.         }
  1041.  
  1042.         \define('COOKIE_PREFIX', $prefix);
  1043.  
  1044.         // Generate the output. This must happen before the session updates, because the
  1045.         // language won't work correctly otherwise.
  1046.         $output = new HtmlResponse(
  1047.             WCF::getTPL()->fetchStream(
  1048.                 'stepInstallPackages',
  1049.                 'wcf',
  1050.                 [
  1051.                     'wcfAcp' => \RELATIVE_WCF_DIR . 'acp/index.php',
  1052.                 ]
  1053.             )
  1054.         );
  1055.  
  1056.         // Set up the session and login as the administrator.
  1057.         $factory = new ACPSessionFactory();
  1058.         $factory->load();
  1059.  
  1060.         SessionHandler::getInstance()->changeUser($admin);
  1061.         SessionHandler::getInstance()->register('__wcfSetup_developerMode', self::$developerMode);
  1062.         SessionHandler::getInstance()->registerReauthentication();
  1063.         SessionHandler::getInstance()->update();
  1064.  
  1065.         // Delete tmp files
  1066.         foreach (new \DirectoryIterator(\INSTALL_SCRIPT_DIR) as $fileInfo) {
  1067.             if ($fileInfo->isDot() || !$fileInfo->isDir()) {
  1068.                 continue;
  1069.             }
  1070.  
  1071.             if (!\preg_match('/^WCFSetup-[0-9a-f]{16}$/', $fileInfo->getBasename())) {
  1072.                 continue;
  1073.             }
  1074.  
  1075.             $tmpDirectory = $fileInfo->getPathname();
  1076.  
  1077.             $tmpDirectoryIterator = new \RecursiveIteratorIterator(
  1078.                 new \RecursiveDirectoryIterator(
  1079.                     $tmpDirectory,
  1080.                     \RecursiveDirectoryIterator::CURRENT_AS_FILEINFO | \RecursiveDirectoryIterator::SKIP_DOTS
  1081.                 ),
  1082.                 \RecursiveIteratorIterator::CHILD_FIRST
  1083.             );
  1084.             foreach ($tmpDirectoryIterator as $tmpFile) {
  1085.                 if ($tmpFile->isDir()) {
  1086.                     \rmdir($tmpFile);
  1087.                 } else {
  1088.                     \unlink($tmpFile);
  1089.                 }
  1090.             }
  1091.             \rmdir($tmpDirectory);
  1092.         }
  1093.  
  1094.         return $output;
  1095.     }
  1096.  
  1097.     /**
  1098.      * Goes to the next step.
  1099.      */
  1100.     protected function gotoNextStep(string $nextStep): ResponseInterface
  1101.     {
  1102.         return new HtmlResponse(
  1103.             WCF::getTPL()->fetchStream(
  1104.                 'stepNext',
  1105.                 'wcf',
  1106.                 [
  1107.                     'nextStep' => $nextStep,
  1108.                 ]
  1109.             )
  1110.         );
  1111.     }
  1112. }
  1113.  
Add Comment
Please, Sign In to add comment