Merge remote-tracking branch 'refs/remotes/origin/3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install.php
1 <?php
2 /**
3 * This script tries to find the temp folder and unzip all setup files into.
4 *
5 * @author Marcel Werk
6 * @copyright 2001-2018 WoltLab GmbH
7 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
8 */
9 // @codingStandardsIgnoreFile
10 // define constants
11 define('INSTALL_SCRIPT', __FILE__);
12 define('INSTALL_SCRIPT_DIR', dirname(__FILE__).'/');
13 define('SETUP_FILE', INSTALL_SCRIPT_DIR . 'WCFSetup.tar.gz');
14 define('NO_IMPORTS', 1);
15
16 // set exception handler
17 set_exception_handler('handleException');
18 // set php error handler
19 set_error_handler('handleError', E_ALL);
20
21 // define list of needed file
22 $neededFilesPattern = [
23 '!^setup/.*!',
24 '!^install/files/acp/images/woltlabSuite.*!',
25 '!^install/files/acp/style/setup/.*!',
26 '!^install/files/lib/data/.*!',
27 '!^install/files/icon/.*!',
28 '!^install/files/font/.*!',
29 '!^install/files/lib/system/.*!',
30 '!^install/files/lib/util/.*!',
31 '!^install/lang/.*!',
32 '!^install/packages/.*!'];
33
34 // define needed functions and classes
35 /** @noinspection PhpMultipleClassesDeclarationsInOneFile */
36 /**
37 * WCF::handleException() calls the show method on exceptions that implement this interface.
38 *
39 * @package com.woltlab.wcf
40 * @author Marcel Werk
41 */
42 interface IPrintableException {
43 public function show();
44 }
45
46 // define needed classes
47 // needed are:
48 // SystemException, PrintableException, BasicFileUtil, Tar, File, ZipFile
49 /** @noinspection PhpMultipleClassesDeclarationsInOneFile */
50 /**
51 * A SystemException is thrown when an unexpected error occurs.
52 *
53 * @package com.woltlab.wcf
54 * @author Marcel Werk
55 */
56 class SystemException extends \Exception implements IPrintableException {
57 protected $description;
58 protected $information = '';
59 protected $functions = '';
60
61 /**
62 * Creates a new SystemException.
63 *
64 * @param string $message error message
65 * @param integer $code error code
66 * @param string $description description of the error
67 * @param \Exception $previous repacked Exception
68 */
69 public function __construct($message = '', $code = 0, $description = '', \Exception $previous = null) {
70 parent::__construct((string) $message, (int) $code, $previous);
71 $this->description = $description;
72 }
73
74 /**
75 * Returns the description of this exception.
76 *
77 * @return string
78 */
79 public function getDescription() {
80 return $this->description;
81 }
82
83 /**
84 * Prints this exception.
85 * This method is called by WCF::handleException().
86 */
87 public function show() {
88 /*
89 * A notice on the HTML used below:
90 *
91 * It might appear a bit weird to use <p> all over the place where semantically
92 * other elements would fit in way better. The reason behind this is that we avoid
93 * inheriting unwanted styles (e.g. exception displayed in an overlay) and that
94 * the output needs to be properly readable when copied & pasted somewhere.
95 *
96 * Besides the visual appearance, the output was built to provide a maximum of
97 * compatibility and readability when pasted somewhere else, e.g. a WYSIWYG editor
98 * without the potential of messing up the formatting and thus harming the readability.
99 */
100 ?><!DOCTYPE html>
101 <html>
102 <head>
103 <title>Fatal Error: <?php echo htmlentities($this->getMessage()); ?></title>
104 <meta charset="utf-8">
105 <meta name="viewport" content="width=device-width, initial-scale=1">
106 <style>
107 .exceptionBody {
108 margin: 0;
109 padding: 0;
110 }
111
112 .exceptionContainer {
113 box-sizing: border-box;
114 font-family: 'Segoe UI', 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif;
115 font-size: 14px;
116 padding-bottom: 20px;
117 }
118
119 .exceptionContainer * {
120 box-sizing: inherit;
121 color: #000;
122 line-height: 1.5em;
123 margin: 0;
124 padding: 0;
125 }
126
127 .exceptionHeader {
128 background-color: rgb(58, 109, 156);
129 padding: 30px 0;
130 }
131
132 .exceptionTitle {
133 color: #fff;
134 font-size: 28px;
135 font-weight: 300;
136 }
137
138 .exceptionErrorCode {
139 color: #fff;
140 margin-top: .5em;
141 }
142
143 .exceptionErrorCode .exceptionInlineCode {
144 background-color: rgb(43, 79, 113);
145 border-radius: 3px;
146 color: #fff;
147 font-family: monospace;
148 padding: 3px 10px;
149 white-space: nowrap;
150 }
151
152 .exceptionSubtitle {
153 border-bottom: 1px solid rgb(238, 238, 238);
154 color: rgb(44, 62, 80);
155 font-size: 24px;
156 font-weight: 300;
157 margin-bottom: 15px;
158 padding-bottom: 10px;
159 }
160
161 .exceptionContainer > .exceptionBoundary {
162 margin-top: 30px;
163 }
164
165 .exceptionText .exceptionInlineCodeWrapper {
166 border: 1px solid rgb(169, 169, 169);
167 border-radius: 3px;
168 padding: 2px 5px;
169 }
170
171 .exceptionText .exceptionInlineCode {
172 font-family: monospace;
173 white-space: nowrap;
174 }
175
176 .exceptionFieldTitle {
177 color: rgb(59, 109, 169);
178 }
179
180 .exceptionFieldTitle .exceptionColon {
181 /* hide colon in browser, but will be visible after copy & paste */
182 opacity: 0;
183 }
184
185 .exceptionFieldValue {
186 font-size: 18px;
187 min-height: 1.5em;
188 }
189
190 .exceptionSystemInformation,
191 .exceptionErrorDetails,
192 .exceptionStacktrace {
193 list-style-type: none;
194 }
195
196 .exceptionSystemInformation > li:not(:first-child),
197 .exceptionErrorDetails > li:not(:first-child) {
198 margin-top: 10px;
199 }
200
201 .exceptionStacktrace {
202 display: block;
203 margin-top: 5px;
204 overflow: auto;
205 padding-bottom: 20px;
206 }
207
208 .exceptionStacktraceFile,
209 .exceptionStacktraceFile span,
210 .exceptionStacktraceCall,
211 .exceptionStacktraceCall span {
212 font-family: monospace !important;
213 white-space: nowrap !important;
214 }
215
216 .exceptionStacktraceCall + .exceptionStacktraceFile {
217 margin-top: 5px;
218 }
219
220 .exceptionStacktraceCall {
221 padding-left: 40px;
222 }
223
224 .exceptionStacktraceCall,
225 .exceptionStacktraceCall span {
226 color: rgb(102, 102, 102) !important;
227 font-size: 13px !important;
228 }
229
230 /* mobile */
231 @media (max-width: 767px) {
232 .exceptionBoundary {
233 min-width: 320px;
234 padding: 0 10px;
235 }
236
237 .exceptionText .exceptionInlineCodeWrapper {
238 display: inline-block;
239 overflow: auto;
240 }
241
242 .exceptionErrorCode .exceptionInlineCode {
243 font-size: 13px;
244 padding: 2px 5px;
245 }
246 }
247
248 /* desktop */
249 @media (min-width: 768px) {
250 .exceptionBoundary {
251 margin: 0 auto;
252 max-width: 1400px;
253 min-width: 1200px;
254 padding: 0 10px;
255 }
256
257 .exceptionSystemInformation {
258 display: flex;
259 flex-wrap: wrap;
260 }
261
262 .exceptionSystemInformation1,
263 .exceptionSystemInformation3,
264 .exceptionSystemInformation5 {
265 flex: 0 0 200px;
266 margin: 0 0 10px 0 !important;
267 }
268
269 .exceptionSystemInformation2,
270 .exceptionSystemInformation4,
271 .exceptionSystemInformation6 {
272 flex: 0 0 calc(100% - 210px);
273 margin: 0 0 10px 10px !important;
274 max-width: calc(100% - 210px);
275 }
276
277 .exceptionSystemInformation1 { order: 1; }
278 .exceptionSystemInformation2 { order: 2; }
279 .exceptionSystemInformation3 { order: 3; }
280 .exceptionSystemInformation4 { order: 4; }
281 .exceptionSystemInformation5 { order: 5; }
282 .exceptionSystemInformation6 { order: 6; }
283
284 .exceptionSystemInformation .exceptionFieldValue {
285 overflow: hidden;
286 text-overflow: ellipsis;
287 white-space: nowrap;
288 }
289 }
290 </style>
291 </head>
292 <body class="exceptionBody">
293 <div class="exceptionContainer">
294 <div class="exceptionHeader">
295 <div class="exceptionBoundary">
296 <p class="exceptionTitle">An error has occured</p>
297 </div>
298 </div>
299
300 <div class="exceptionBoundary">
301 <p class="exceptionSubtitle">System Information</p>
302 <ul class="exceptionSystemInformation">
303 <li class="exceptionSystemInformation1">
304 <p class="exceptionFieldTitle">PHP Version<span class="exceptionColon">:</span></p>
305 <p class="exceptionFieldValue"><?php echo htmlentities(phpversion()); ?></p>
306 </li>
307 <li class="exceptionSystemInformation3">
308 <p class="exceptionFieldTitle">WoltLab Suite Core<span class="exceptionColon">:</span></p>
309 <p class="exceptionFieldValue">3.1</p>
310 </li>
311 <li class="exceptionSystemInformation5">
312 <p class="exceptionFieldTitle">Peak Memory Usage<span class="exceptionColon">:</span></p>
313 <p class="exceptionFieldValue"><?php echo round(memory_get_peak_usage() / 1024 / 1024, 3); ?>/<?php echo ini_get('memory_limit'); ?></p>
314 </li>
315 <li class="exceptionSystemInformation2">
316 <p class="exceptionFieldTitle">Request URI<span class="exceptionColon">:</span></p>
317 <p class="exceptionFieldValue"><?php if (isset($_SERVER['REQUEST_URI'])) echo htmlentities($_SERVER['REQUEST_URI']); ?></p>
318 </li>
319 <li class="exceptionSystemInformation4">
320 <p class="exceptionFieldTitle">Referrer<span class="exceptionColon">:</span></p>
321 <p class="exceptionFieldValue"><?php if (isset($_SERVER['HTTP_REFERER'])) echo htmlentities($_SERVER['HTTP_REFERER']); ?></p>
322 </li>
323 <li class="exceptionSystemInformation6">
324 <p class="exceptionFieldTitle">User Agent<span class="exceptionColon">:</span></p>
325 <p class="exceptionFieldValue"><?php if (isset($_SERVER['HTTP_USER_AGENT'])) echo htmlentities($_SERVER['HTTP_USER_AGENT']); ?></p>
326 </li>
327 </ul>
328 </div>
329
330 <?php
331 $e = $this;
332 $first = true;
333 do {
334 $trace = $e->getTrace();
335 if (isset($trace[0]['function']) && $trace[0]['function'] === 'handleException') {
336 // ignore repacked exception
337 continue;
338 }
339
340 ?>
341 <div class="exceptionBoundary">
342 <p class="exceptionSubtitle"><?php if (!$e->getPrevious() && !$first) { echo "Original "; } else if ($e->getPrevious() && $first) { echo "Final "; } ?>Error</p>
343 <?php if ($e instanceof SystemException && $e->getDescription()) { ?>
344 <p class="exceptionText"><?php echo $e->getDescription(); ?></p>
345 <?php } ?>
346 <ul class="exceptionErrorDetails">
347 <li>
348 <p class="exceptionFieldTitle">Error Type<span class="exceptionColon">:</span></p>
349 <p class="exceptionFieldValue"><?php echo htmlentities(get_class($e)); ?></p>
350 </li>
351 <li>
352 <p class="exceptionFieldTitle">Error Message<span class="exceptionColon">:</span></p>
353 <p class="exceptionFieldValue"><?php echo htmlentities($e->getMessage()); ?></p>
354 </li>
355 <?php if ($e->getCode()) { ?>
356 <li>
357 <p class="exceptionFieldTitle">Error Code<span class="exceptionColon">:</span></p>
358 <p class="exceptionFieldValue"><?php echo intval($e->getCode()); ?></p>
359 </li>
360 <?php } ?>
361 <li>
362 <p class="exceptionFieldTitle">File<span class="exceptionColon">:</span></p>
363 <p class="exceptionFieldValue" style="word-break: break-all"><?php echo htmlentities($e->getFile()); ?> (<?php echo $e->getLine(); ?>)</p>
364 </li>
365
366 <li>
367 <p class="exceptionFieldTitle">Stack Trace<span class="exceptionColon">:</span></p>
368 <ul class="exceptionStacktrace">
369 <?php
370 $trace = $e->getTrace();
371 for ($i = 0, $max = count($trace); $i < $max; $i++) {
372 ?>
373 <li class="exceptionStacktraceFile"><?php echo '#'.$i.' '.htmlentities($trace[$i]['file']).' ('.$trace[$i]['line'].')'.':'; ?></li>
374 <li class="exceptionStacktraceCall">
375 <?php
376 echo $trace[$i]['class'].$trace[$i]['type'].$trace[$i]['function'].'(';
377 echo implode(', ', array_map(function ($item) {
378 switch (gettype($item)) {
379 case 'integer':
380 case 'double':
381 return $item;
382 case 'NULL':
383 return 'null';
384 case 'string':
385 return "'".addcslashes(htmlentities($item), "\\'")."'";
386 case 'boolean':
387 return $item ? 'true' : 'false';
388 case 'array':
389 $keys = array_keys($item);
390 if (count($keys) > 5) return "[ ".count($keys)." items ]";
391 return '[ '.implode(', ', array_map(function ($item) {
392 return $item.' => ';
393 }, $keys)).']';
394 case 'object':
395 return get_class($item);
396 }
397
398 throw new \LogicException('Unreachable');
399 }, $trace[$i]['args']));
400 echo ')</li>';
401 }
402 ?>
403 </ul>
404 </li>
405 </ul>
406 </div>
407 <?php
408 $first = false;
409 } while ($e = $e->getPrevious());
410 ?>
411 ?>
412 </div>
413 </body>
414 </html>
415
416 <?php
417 }
418 }
419
420 /**
421 * Loads the required classes automatically.
422 */
423 spl_autoload_register(function($className) {
424 $namespaces = explode('\\', $className);
425 if (count($namespaces) > 1) {
426 // remove 'wcf' component
427 array_shift($namespaces);
428
429 $className = implode('/', $namespaces);
430 $classPath = TMP_DIR . 'install/files/lib/' . $className . '.class.php';
431 if (file_exists($classPath)) {
432 require_once($classPath);
433 }
434 }
435 });
436
437 /**
438 * Escapes strings for execution in sql queries.
439 *
440 * @param string $string
441 * @return string
442 */
443 function escapeString($string) {
444 return \wcf\system\WCF::getDB()->escapeString($string);
445 }
446
447 /**
448 * Helper method to output debug data for all passed variables,
449 * uses `print_r()` for arrays and objects, `var_dump()` otherwise.
450 */
451 function wcfDebug() {
452 echo "<pre>";
453
454 $args = func_get_args();
455 $length = count($args);
456 if ($length === 0) {
457 echo "ERROR: No arguments provided.<hr>";
458 }
459 else {
460 for ($i = 0; $i < $length; $i++) {
461 $arg = $args[$i];
462
463 echo "<h2>Argument {$i} (" . gettype($arg) . ")</h2>";
464
465 if (is_array($arg) || is_object($arg)) {
466 print_r($arg);
467 }
468 else {
469 var_dump($arg);
470 }
471
472 echo "<hr>";
473 }
474 }
475
476 $backtrace = debug_backtrace();
477
478 // output call location to help finding these debug outputs again
479 echo "wcfDebug() called in {$backtrace[0]['file']} on line {$backtrace[0]['line']}";
480
481 echo "</pre>";
482
483 exit;
484 }
485
486 /**
487 * Calls the show method on the given exception.
488 *
489 * @param mixed $e
490 */
491 function handleException($e) {
492 try {
493 if (!($e instanceof \Exception)) throw $e;
494
495 if ($e instanceof IPrintableException || $e instanceof \wcf\system\exception\IPrintableException) {
496 $e->show();
497 exit;
498 }
499
500 // repacking
501 (new SystemException($e->getMessage(), $e->getCode(), '', $e))->show();
502 exit;
503 }
504 catch (\Throwable $exception) {
505 die("<pre>WCF::handleException() Unhandled exception: ".$exception->getMessage()."\n\n".$exception->getTraceAsString());
506 }
507 catch (\Exception $exception) {
508 die("<pre>WCF::handleException() Unhandled exception: ".$exception->getMessage()."\n\n".$exception->getTraceAsString());
509 }
510 }
511
512 /**
513 * Catches php errors and throws instead a system exception.
514 *
515 * @param integer $errorNo
516 * @param string $message
517 * @param string $filename
518 * @param integer $lineNo
519 * @throws SystemException
520 */
521 function handleError($errorNo, $message, $filename, $lineNo) {
522 if (error_reporting() != 0) {
523 $type = 'error';
524 switch ($errorNo) {
525 case 2: $type = 'warning';
526 break;
527 case 8: $type = 'notice';
528 break;
529 }
530
531 throw new SystemException('PHP '.$type.' in file '.$filename.' ('.$lineNo.'): '.$message, 0);
532 }
533 }
534
535 /** @noinspection PhpMultipleClassesDeclarationsInOneFile */
536 /**
537 * BasicFileUtil contains file-related functions.
538 *
539 * @package com.woltlab.wcf
540 * @author Marcel Werk
541 */
542 class BasicFileUtil {
543 /**
544 * chmod mode
545 * @var integer
546 */
547 protected static $mode = null;
548
549 /**
550 * Tries to find the temp folder.
551 *
552 * @return string
553 * @throws SystemException
554 */
555 public static function getTempFolder() {
556 // use tmp folder in document root by default
557 if (!empty($_SERVER['DOCUMENT_ROOT'])) {
558 if (strpos($_SERVER['DOCUMENT_ROOT'], 'strato') !== false) {
559 // strato bugfix
560 // create tmp folder in document root automatically
561 if (!@file_exists($_SERVER['DOCUMENT_ROOT'].'/tmp')) {
562 @mkdir($_SERVER['DOCUMENT_ROOT'].'/tmp/', 0777);
563 try {
564 self::makeWritable($_SERVER['DOCUMENT_ROOT'].'/tmp/');
565 }
566 catch (SystemException $e) {}
567 }
568 }
569 if (@file_exists($_SERVER['DOCUMENT_ROOT'].'/tmp') && @is_writable($_SERVER['DOCUMENT_ROOT'].'/tmp')) {
570 return $_SERVER['DOCUMENT_ROOT'].'/tmp/';
571 }
572 }
573
574 if (isset($_ENV['TMP']) && @is_writable($_ENV['TMP'])) {
575 return $_ENV['TMP'] . '/';
576 }
577 if (isset($_ENV['TEMP']) && @is_writable($_ENV['TEMP'])) {
578 return $_ENV['TEMP'] . '/';
579 }
580 if (isset($_ENV['TMPDIR']) && @is_writable($_ENV['TMPDIR'])) {
581 return $_ENV['TMPDIR'] . '/';
582 }
583
584 if (($path = ini_get('upload_tmp_dir')) && @is_writable($path)) {
585 return $path . '/';
586 }
587 if (@file_exists('/tmp/') && @is_writable('/tmp/')) {
588 return '/tmp/';
589 }
590 if (function_exists('session_save_path') && ($path = session_save_path()) && @is_writable($path)) {
591 return $path . '/';
592 }
593
594 $path = INSTALL_SCRIPT_DIR.'tmp/';
595 if (@file_exists($path) && @is_writable($path)) {
596 return $path;
597 }
598 else {
599 throw new SystemException('There is no access to the system temporary folder due to an unknown reason and no user specific temporary folder exists in '.INSTALL_SCRIPT_DIR.'! This is a misconfiguration of your webserver software! Please create a folder called '.$path.' using your favorite ftp program, make it writable and then retry this installation.');
600 }
601 }
602
603 /**
604 * Returns the temp folder for the installation.
605 *
606 * @return string
607 */
608 public static function getInstallTempFolder() {
609 $dir = self::getTempFolder() . TMP_FILE_PREFIX . '/';
610 @mkdir($dir);
611 self::makeWritable($dir);
612
613 return $dir;
614 }
615
616 /**
617 * Tries to make a file or directory writable. It starts of with the least
618 * permissions and goes up until 0666 for files and 0777 for directories.
619 *
620 * @param string $filename
621 * @throws SystemException
622 */
623 public static function makeWritable($filename) {
624 if (!file_exists($filename)) {
625 return;
626 }
627
628 // determine mode
629 if (self::$mode === null) {
630 // do not use PHP_OS here, as this represents the system it was built on != running on
631 // php_uname() is forbidden on some strange hosts; PHP_EOL is reliable
632 if (PHP_EOL == "\r\n") {
633 // Windows
634 self::$mode = 0777;
635 }
636 else {
637 // anything but Windows
638 clearstatcache();
639
640 self::$mode = 0666;
641
642 $tmpFilename = '__permissions_'.sha1(time()).'.txt';
643 @touch($tmpFilename);
644
645 // create a new file and check the file owner, if it is the same
646 // as this file (uploaded through FTP), we can safely grant write
647 // permissions exclusively to the owner rather than everyone
648 if (file_exists($tmpFilename)) {
649 $scriptOwner = fileowner(__FILE__);
650 $fileOwner = fileowner($tmpFilename);
651
652 if ($scriptOwner === $fileOwner) {
653 self::$mode = 0644;
654 }
655
656 @unlink($tmpFilename);
657 }
658 }
659 }
660
661 if (is_dir($filename)) {
662 if (self::$mode == 0644) {
663 @chmod($filename, 0755);
664 }
665 else {
666 @chmod($filename, 0777);
667 }
668 }
669 else {
670 @chmod($filename, self::$mode);
671 }
672
673 if (!is_writable($filename)) {
674 throw new SystemException("Unable to make '".$filename."' writable. This is a misconfiguration of your server, please contact your system administrator or hosting provider.");
675 }
676 }
677 }
678
679 /** @noinspection PhpMultipleClassesDeclarationsInOneFile */
680 /**
681 * Opens tar or tar.gz archives.
682 *
683 * Usage:
684 * ------
685 * $tar = new Tar('archive.tar');
686 * $contentList = $tar->getContentList();
687 * foreach ($contentList as $key => $val) {
688 * $tar->extract($key, DESTINATION);
689 * }
690 */
691 class Tar {
692 protected $archiveName = '';
693 protected $contentList = [];
694 protected $opened = false;
695 protected $read = false;
696 protected $file = null;
697 protected $isZipped = false;
698 protected $mode = 'rb';
699
700 /**
701 * Creates a new Tar object.
702 * archiveName must be tarball or gzipped tarball
703 *
704 * @param string $archiveName
705 * @throws SystemException
706 */
707 public function __construct($archiveName) {
708 if (!is_file($archiveName)) {
709 throw new SystemException("unable to find tar archive '".$archiveName."'");
710 }
711
712 $this->archiveName = $archiveName;
713 $this->open();
714 $this->readContent();
715 }
716
717 /**
718 * Destructor of this class, closes tar archive.
719 */
720 public function __destruct() {
721 $this->close();
722 }
723
724 /**
725 * Opens the tar archive and stores filehandle.
726 */
727 public function open() {
728 if (!$this->opened) {
729 if ($this->isZipped) $this->file = new ZipFile($this->archiveName, $this->mode);
730 else {
731 // test compression
732 $this->file = new File($this->archiveName, $this->mode);
733 if ($this->file->read(2) == "\37\213") {
734 $this->file->close();
735 $this->isZipped = true;
736 $this->file = new ZipFile($this->archiveName, $this->mode);
737 }
738 else {
739 $this->file->seek(0);
740 }
741 }
742 $this->opened = true;
743 }
744 }
745
746 /**
747 * Closes the opened file.
748 */
749 public function close() {
750 if ($this->opened) {
751 $this->file->close();
752 $this->opened = false;
753 }
754 }
755
756 /**
757 * Returns the table of contents (TOC) list for this tar archive.
758 *
759 * @return array list of content
760 */
761 public function getContentList() {
762 if (!$this->read) {
763 $this->open();
764 $this->readContent();
765 }
766 return $this->contentList;
767 }
768
769 /**
770 * Returns an associative array with information
771 * about a specific file in the archive.
772 *
773 * @param mixed $fileIndex index or name of the requested file
774 * @return array
775 * @throws SystemException
776 */
777 public function getFileInfo($fileIndex) {
778 if (!is_int($fileIndex)) {
779 $fileIndex = $this->getIndexByFilename($fileIndex);
780 }
781
782 if (!isset($this->contentList[$fileIndex])) {
783 throw new SystemException("Tar: could find file '".$fileIndex."' in archive");
784 }
785 return $this->contentList[$fileIndex];
786 }
787
788 /**
789 * Searchs a file in the tar archive
790 * and returns the numeric fileindex.
791 * Returns false if not found.
792 *
793 * @param string $filename
794 * @return integer index of the requested file
795 */
796 public function getIndexByFilename($filename) {
797 foreach ($this->contentList as $index => $file) {
798 if ($file['filename'] == $filename) {
799 return $index;
800 }
801 }
802 return false;
803 }
804
805 /**
806 * Extracts a specific file and returns the content as string.
807 * Returns false if extraction failed.
808 *
809 * @param mixed $index index or name of the requested file
810 * @return string content of the requested file
811 */
812 public function extractToString($index) {
813 if (!$this->read) {
814 $this->open();
815 $this->readContent();
816 }
817 $header = $this->getFileInfo($index);
818
819 // can not extract a folder
820 if ($header['type'] != 'file') {
821 return false;
822 }
823
824 // seek to offset
825 $this->file->seek($header['offset']);
826
827 // read data
828 $content = '';
829 $n = floor($header['size'] / 512);
830 for($i = 0; $i < $n; $i++) {
831 $content .= $this->file->read(512);
832 }
833 if(($header['size'] % 512) != 0) {
834 $buffer = $this->file->read(512);
835 $content .= substr($buffer, 0, $header['size'] % 512);
836 }
837
838 return $content;
839 }
840
841 /**
842 * Extracts a specific file and writes it's content
843 * to the file specified with $destination.
844 *
845 * @param mixed $index index or name of the requested file
846 * @param string $destination
847 * @return boolean
848 * @throws SystemException
849 */
850 public function extract($index, $destination) {
851 if (!$this->read) {
852 $this->open();
853 $this->readContent();
854 }
855 $header = $this->getFileInfo($index);
856
857 // can not extract a folder
858 if ($header['type'] != 'file') {
859 return false;
860 }
861
862 // seek to offset
863 $this->file->seek($header['offset']);
864
865 $targetFile = new File($destination);
866
867 // read data
868 $n = floor($header['size'] / 512);
869 for ($i = 0; $i < $n; $i++) {
870 $content = $this->file->read(512);
871 $targetFile->write($content, 512);
872 }
873 if (($header['size'] % 512) != 0) {
874 $content = $this->file->read(512);
875 $targetFile->write($content, $header['size'] % 512);
876 }
877
878 $targetFile->close();
879 BasicFileUtil::makeWritable($destination);
880
881 if ($header['mtime']) {
882 @$targetFile->touch($header['mtime']);
883 }
884
885 // check filesize
886 if (filesize($destination) != $header['size']) {
887 throw new SystemException("Could not untar file '".$header['filename']."' to '".$destination."'. Maybe disk quota exceeded in folder '".dirname($destination)."'.");
888 }
889
890 return true;
891 }
892
893 /**
894 * Reads table of contents (TOC) from tar archive.
895 * This does not get the entire to memory but only parts of it.
896 */
897 protected function readContent() {
898 $this->contentList = [];
899 $this->read = true;
900 $i = 0;
901
902 // Read the 512 bytes header
903 $longFilename = null;
904 while (strlen($binaryData = $this->file->read(512)) != 0) {
905 // read header
906 $header = $this->readHeader($binaryData);
907 if ($header === false) {
908 continue;
909 }
910
911 // fixes a bug that files with long names aren't correctly
912 // extracted
913 if ($longFilename !== null) {
914 $header['filename'] = $longFilename;
915 $longFilename = null;
916 }
917 if ($header['typeflag'] == 'L') {
918 $format = 'Z'.$header['size'].'filename';
919
920 $fileData = unpack($format, $this->file->read(512));
921 $longFilename = $fileData['filename'];
922 $header['size'] = 0;
923 }
924 // don't include the @LongLink file in the content list
925 else {
926 $this->contentList[$i] = $header;
927 $this->contentList[$i]['index'] = $i;
928 $i++;
929 }
930
931 $this->file->seek($this->file->tell() + (512 * ceil($header['size'] / 512)));
932 }
933 }
934
935 /**
936 * Unpacks file header for one file entry.
937 *
938 * @param string $binaryData
939 * @return array|boolean
940 */
941 protected function readHeader($binaryData) {
942 if (strlen($binaryData) != 512) {
943 return false;
944 }
945
946 $header = [];
947 $checksum = 0;
948 // First part of the header
949 for ($i = 0; $i < 148; $i++) {
950 $checksum += ord(substr($binaryData, $i, 1));
951 }
952 // Calculate the checksum
953 // Ignore the checksum value and replace it by ' ' (space)
954 for ($i = 148; $i < 156; $i++) {
955 $checksum += ord(' ');
956 }
957 // Last part of the header
958 for ($i = 156; $i < 512; $i++) {
959 $checksum += ord(substr($binaryData, $i, 1));
960 }
961
962 // extract values
963 $format = 'Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/Z8checksum/Z1typeflag/Z100link/Z6magic/Z2version/Z32uname/Z32gname/Z8devmajor/Z8devminor/Z155prefix';
964
965 $data = unpack($format, $binaryData);
966
967 // Extract the properties
968 $header['checksum'] = octdec(trim($data['checksum']));
969 if ($header['checksum'] == $checksum) {
970 $header['filename'] = trim($data['filename']);
971 $header['mode'] = octdec(trim($data['mode']));
972 $header['uid'] = octdec(trim($data['uid']));
973 $header['gid'] = octdec(trim($data['gid']));
974 $header['size'] = octdec(trim($data['size']));
975 $header['mtime'] = octdec(trim($data['mtime']));
976 $header['prefix'] = trim($data['prefix']);
977 if ($header['prefix']) {
978 $header['filename'] = $header['prefix'].'/'.$header['filename'];
979 }
980 if (($header['typeflag'] = $data['typeflag']) == '5') {
981 $header['size'] = 0;
982 $header['type'] = 'folder';
983 }
984 else {
985 $header['type'] = 'file';
986 }
987 $header['offset'] = $this->file->tell();
988
989 return $header;
990 }
991 else {
992 return false;
993 }
994 }
995 }
996
997 /** @noinspection PhpMultipleClassesDeclarationsInOneFile */
998 /**
999 * The File class handles all file operations.
1000 *
1001 * Example:
1002 * using php functions:
1003 * $fp = fopen('filename', 'wb');
1004 * fwrite($fp, '...');
1005 * fclose($fp);
1006 *
1007 * using this class:
1008 * $file = new File('filename');
1009 * $file->write('...');
1010 * $file->close();
1011 *
1012 * @author Marcel Werk
1013 */
1014 class File {
1015 protected $resource = null;
1016 protected $filename;
1017
1018 /**
1019 * Opens a new file.
1020 *
1021 * @param string $filename
1022 * @param string $mode
1023 * @throws SystemException
1024 */
1025 public function __construct($filename, $mode = 'wb') {
1026 $this->filename = $filename;
1027 $this->resource = fopen($filename, $mode);
1028 if ($this->resource === false) {
1029 throw new SystemException('Can not open file ' . $filename);
1030 }
1031 }
1032
1033 /**
1034 * Calls the specified function on the open file.
1035 * Do not call this function directly. Use $file->write('') instead.
1036 *
1037 * @param string $function
1038 * @param array $arguments
1039 * @return mixed
1040 * @throws SystemException
1041 */
1042 public function __call($function, $arguments) {
1043 if (function_exists('f' . $function)) {
1044 array_unshift($arguments, $this->resource);
1045 return call_user_func_array('f' . $function, $arguments);
1046 }
1047 else if (function_exists($function)) {
1048 array_unshift($arguments, $this->filename);
1049 return call_user_func_array($function, $arguments);
1050 }
1051 else {
1052 throw new SystemException('Can not call file method ' . $function);
1053 }
1054 }
1055 }
1056
1057 /** @noinspection PhpMultipleClassesDeclarationsInOneFile */
1058 /**
1059 * The File class handles all file operations on a zipped file.
1060 *
1061 * @author Marcel Werk
1062 */
1063 class ZipFile extends File {
1064 /**
1065 * checks if gz*64 functions are available instead of gz*
1066 * https://bugs.php.net/bug.php?id=53829
1067 * @var boolean
1068 */
1069 protected static $gzopen64 = null;
1070
1071 /** @noinspection PhpMissingParentConstructorInspection */
1072 /**
1073 * Opens a new zipped file.
1074 *
1075 * @param string $filename
1076 * @param string $mode
1077 * @throws SystemException
1078 */
1079 public function __construct($filename, $mode = 'wb') {
1080 if (self::$gzopen64 === null) {
1081 self::$gzopen64 = function_exists('gzopen64');
1082 }
1083
1084 $this->filename = $filename;
1085 if (!self::$gzopen64 && !function_exists('gzopen')) {
1086 throw new SystemException('Can not find functions of the zlib extension');
1087 }
1088 /** @noinspection PhpUndefinedFunctionInspection */
1089 $this->resource = (self::$gzopen64 ? @gzopen64($filename, $mode) : @gzopen($filename, $mode));
1090 if ($this->resource === false) {
1091 throw new SystemException('Can not open file ' . $filename);
1092 }
1093 }
1094
1095 /**
1096 * Calls the specified function on the open file.
1097 *
1098 * @param string $function
1099 * @param array $arguments
1100 * @return mixed
1101 * @throws SystemException
1102 */
1103 public function __call($function, $arguments) {
1104 if (self::$gzopen64 && function_exists('gz' . $function . '64')) {
1105 array_unshift($arguments, $this->resource);
1106 return call_user_func_array('gz' . $function . '64', $arguments);
1107 }
1108 else if (function_exists('gz' . $function)) {
1109 array_unshift($arguments, $this->resource);
1110 return call_user_func_array('gz' . $function, $arguments);
1111 }
1112 else if (function_exists($function)) {
1113 array_unshift($arguments, $this->filename);
1114 return call_user_func_array($function, $arguments);
1115 }
1116 else {
1117 throw new SystemException('Can not call method ' . $function);
1118 }
1119 }
1120
1121 /**
1122 * Returns the filesize of the unzipped file
1123 */
1124 public function getFileSize() {
1125 $byteBlock = 1<<14;
1126 $eof = $byteBlock;
1127
1128 // the correction is for zip files that are too small
1129 // to get in the first while loop
1130 $correction = 1;
1131 while ($this->seek($eof) == 0) {
1132 $eof += $byteBlock;
1133 $correction = 0;
1134 }
1135
1136 while ($byteBlock > 1) {
1137 $byteBlock >>= 1;
1138 $eof += $byteBlock * ($this->seek($eof) ? -1 : 1);
1139 }
1140
1141 if ($this->seek($eof) == -1) $eof -= 1;
1142
1143 $this->rewind();
1144 return $eof - $correction;
1145 }
1146 }
1147
1148 // let's go
1149 // get temp file prefix
1150 if (isset($_REQUEST['tmpFilePrefix'])) {
1151 $prefix = preg_replace('/[^a-f0-9_]+/', '', $_REQUEST['tmpFilePrefix']);
1152 }
1153 else {
1154 $prefix = substr(sha1(uniqid(microtime())), 0, 8);
1155 }
1156 define('TMP_FILE_PREFIX', $prefix);
1157
1158 // try to find the temp folder
1159 define('TMP_DIR', BasicFileUtil::getInstallTempFolder());
1160
1161 /**
1162 * Reads a file resource from temp folder.
1163 *
1164 * @param string $key
1165 * @param string $directory
1166 */
1167 function readFileResource($key, $directory) {
1168 if (preg_match('~[\w\-]+\.(css|jpg|png|svg|eot|woff|ttf)~', $_GET[$key], $match)) {
1169 switch ($match[1]) {
1170 case 'css':
1171 header('Content-Type: text/css');
1172 break;
1173
1174 case 'jpg':
1175 header('Content-Type: image/jpg');
1176 break;
1177
1178 case 'png':
1179 header('Content-Type: image/png');
1180 break;
1181
1182 case 'svg':
1183 header('Content-Type: image/svg+xml');
1184 break;
1185
1186 case 'eot':
1187 header('Content-Type: application/vnd.ms-fontobject');
1188 break;
1189
1190 case 'woff':
1191 header('Content-Type: application/font-woff');
1192 break;
1193
1194 case 'ttf':
1195 header('Content-Type: application/octet-stream');
1196 break;
1197 }
1198
1199 header('Expires: '.gmdate('D, d M Y H:i:s', time() + 3600).' GMT');
1200 header('Last-Modified: Mon, 26 Jul 1997 05:00:00 GMT');
1201 header('Cache-Control: public, max-age=3600');
1202
1203 readfile($directory . $_GET[$key]);
1204 }
1205 exit;
1206 }
1207
1208 // show image from temp folder
1209 if (isset($_GET['showImage'])) {
1210 readFileResource('showImage', TMP_DIR . 'install/files/acp/images/');
1211 }
1212 // show icon from temp folder
1213 if (isset($_GET['showIcon'])) {
1214 readFileResource('showIcon', TMP_DIR . 'install/files/icon/');
1215 }
1216 // show css from temp folder
1217 if (isset($_GET['showCSS'])) {
1218 readFileResource('showCSS', TMP_DIR . 'install/files/acp/style/setup/');
1219 }
1220 // show fonts from temp folder
1221 if (isset($_GET['showFont'])) {
1222 readFileResource('showFont', TMP_DIR . 'install/files/font/');
1223 }
1224
1225 // check whether setup files are already unzipped
1226 if (!file_exists(TMP_DIR . 'install/files/lib/system/WCFSetup.class.php')) {
1227 // try to unzip all setup files into temp folder
1228 $tar = new Tar(SETUP_FILE);
1229 $contentList = $tar->getContentList();
1230 if (empty($contentList)) {
1231 throw new SystemException("Cannot unpack 'WCFSetup.tar.gz'. File is probably broken.");
1232 }
1233
1234 foreach ($contentList as $file) {
1235 foreach ($neededFilesPattern as $pattern) {
1236 if (preg_match($pattern, $file['filename'])) {
1237 // create directory if not exists
1238 $dir = TMP_DIR . dirname($file['filename']);
1239 if (!@is_dir($dir)) {
1240 @mkdir($dir, 0777, true);
1241 BasicFileUtil::makeWritable($dir);
1242 }
1243
1244 $tar->extract($file['index'], TMP_DIR . $file['filename']);
1245 }
1246 }
1247 }
1248 $tar->close();
1249
1250 // create cache folders
1251 @mkdir(TMP_DIR . 'setup/lang/cache/', 0777);
1252 BasicFileUtil::makeWritable(TMP_DIR . 'setup/lang/cache/');
1253
1254 @mkdir(TMP_DIR . 'setup/template/compiled/', 0777);
1255 BasicFileUtil::makeWritable(TMP_DIR . 'setup/template/compiled/');
1256 }
1257
1258 if (!class_exists('wcf\system\WCFSetup')) {
1259 throw new SystemException("Cannot find class 'WCFSetup'");
1260 }
1261
1262 // start setup
1263 new \wcf\system\WCFSetup();