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