Merge pull request #407 from Torben-Brodt/undefined-variables
[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-2011 WoltLab GmbH
7 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
8 */
9 // define constants
10 define('INSTALL_SCRIPT_DIR', dirname(__FILE__).'/');
11 define('SETUP_FILE', INSTALL_SCRIPT_DIR . 'WCFSetup.tar.gz');
12 define('NO_IMPORTS', 1);
13
14 // set exception handler
15 set_exception_handler('handleException');
16 // set php error handler
17 set_error_handler('handleError', E_ALL);
18
19 // define list of needed file
20 $neededFilesPattern = array(
21 '!^setup/.*!',
22 '!^install/files/acp/images/wcfLogo.*!',
23 '!^install/files/acp/style/.*!',
24 '!^install/files/lib/data/.*!',
25 '!^install/files/icon/.*!',
26 '!^install/files/lib/system/.*!',
27 '!^install/files/lib/util/.*!',
28 '!^install/lang/.*!',
29 '!^install/packages/.*!');
30
31 // define needed functions and classes
32 /**
33 * WCF::handleException() calls the show method on exceptions that implement this interface.
34 *
35 * @package com.woltlab.wcf.system.exception
36 * @author Marcel Werk
37 */
38 interface IPrintableException {
39 public function show();
40 }
41
42 // define needed classes
43 // needed are:
44 // SystemException, PrintableException, BasicFileUtil, Tar, File, ZipFile
45 /**
46 * A SystemException is thrown when an unexpected error occurs.
47 *
48 * @package com.woltlab.wcf.system.exception
49 * @author Marcel Werk
50 */
51 class SystemException extends \Exception implements IPrintableException {
52 protected $description;
53 protected $information = '';
54 protected $functions = '';
55
56 /**
57 * Creates a new SystemException.
58 *
59 * @param message string error message
60 * @param code integer error code
61 * @param description string description of the error
62 */
63 public function __construct($message = '', $code = 0, $description = '') {
64 parent::__construct($message, $code);
65 $this->description = $description;
66 }
67
68 /**
69 * Returns the description of this exception.
70 *
71 * @return string
72 */
73 public function getDescription() {
74 return $this->description;
75 }
76
77 /**
78 * Prints this exception.
79 * This method is called by WCF::handleException().
80 */
81 public function show() {
82 ?>
83 <html>
84 <head>
85 <title>Fatal error: <?php echo htmlspecialchars($this->getMessage()); ?></title>
86
87 <style type="text/css">
88 body {
89 font-family: Verdana, Helvetica, sans-serif;
90 font-size: 0.8em;
91 }
92 div {
93 border: 1px outset lightgrey;
94 padding: 3px;
95 background-color: lightgrey;
96 }
97
98 div div {
99 border: 1px inset lightgrey;
100 padding: 4px;
101 }
102
103 h1 {
104 background-color: #154268;
105 padding: 4px;
106 color: #fff;
107 margin: 0 0 3px 0;
108 font-size: 1.15em;
109 }
110 h2 {
111 font-size: 1.1em;
112 margin-bottom: 0;
113 }
114
115 pre, p {
116 margin: 0;
117 }
118 </style>
119 </head>
120
121 <body>
122 <div>
123 <h1>Fatal error: <?php echo htmlspecialchars($this->getMessage()); ?></h1>
124
125 <div>
126 <p><?php echo $this->getDescription(); ?></p>
127 <?php if ($this->getCode()) { ?><p>You get more information about the problem in our knowledge base: <a href="http://www.woltlab.com/help/?code=<?php echo intval($this->getCode()); ?>">http://www.woltlab.com/help/?code=<?php echo intval($this->getCode()); ?></a></p><?php } ?>
128
129 <h2>Information:</h2>
130 <p>
131 <b>error message:</b> <?php echo htmlspecialchars($this->getMessage()); ?><br />
132 <b>error code:</b> <?php echo intval($this->getCode()); ?><br />
133 <?php echo $this->information; ?>
134 <b>file:</b> <?php echo htmlspecialchars($this->getFile()); ?> (<?php echo $this->getLine(); ?>)<br />
135 <b>php version:</b> <?php echo htmlspecialchars(phpversion()); ?><br />
136 <b>wcf version:</b> <?php if (defined('WCF_VERSION')) echo WCF_VERSION; ?><br />
137 <b>date:</b> <?php echo gmdate('r'); ?><br />
138 <b>request:</b> <?php if (isset($_SERVER['REQUEST_URI'])) echo htmlspecialchars($_SERVER['REQUEST_URI']); ?><br />
139 <b>referer:</b> <?php if (isset($_SERVER['HTTP_REFERER'])) echo htmlspecialchars($_SERVER['HTTP_REFERER']); ?><br />
140 </p>
141
142 <h2>Stacktrace:</h2>
143 <pre><?php echo htmlspecialchars($this->getTraceAsString()); ?></pre>
144 </div>
145
146 <?php echo $this->functions; ?>
147 </div>
148 </body>
149 </html>
150
151 <?php
152 }
153 }
154
155
156 /**
157 * Loads the required classes automatically.
158 */
159 function __autoload($className) {
160 $namespaces = explode('\\', $className);
161 if (count($namespaces) > 1) {
162 // remove 'wcf' component
163 array_shift($namespaces);
164
165 $className = implode('/', $namespaces);
166 $classPath = TMP_DIR . 'install/files/lib/' . $className . '.class.php';
167 if (file_exists($classPath)) {
168 require_once($classPath);
169 }
170 }
171 }
172
173 /**
174 * Escapes strings for execution in sql queries.
175 */
176 function escapeString($string) {
177 return \wcf\system\WCF::getDB()->escapeString($string);
178 }
179
180 /**
181 * Calls the show method on the given exception.
182 *
183 * @param Exception $e
184 */
185 function handleException(\Exception $e) {
186 if ($e instanceof IPrintableException || $e instanceof \wcf\system\exception\IPrintableException) {
187 $e->show();
188 exit;
189 }
190
191 print $e;
192 }
193
194 /**
195 * Catches php errors and throws instead a system exception.
196 *
197 * @param integer $errorNo
198 * @param string $message
199 * @param string $filename
200 * @param integer $lineNo
201 */
202 function handleError($errorNo, $message, $filename, $lineNo) {
203 if (error_reporting() != 0) {
204 $type = 'error';
205 switch ($errorNo) {
206 case 2: $type = 'warning';
207 break;
208 case 8: $type = 'notice';
209 break;
210 }
211
212 throw new SystemException('PHP '.$type.' in file '.$filename.' ('.$lineNo.'): '.$message, 0);
213 }
214 }
215
216 /**
217 * BasicFileUtil contains file-related functions.
218 *
219 * @package com.woltlab.wcf.util
220 * @author Marcel Werk
221 */
222 class BasicFileUtil {
223 /**
224 * Tries to find the temp folder.
225 *
226 * @return string
227 */
228 public static function getTempFolder() {
229 $tmpDirName = TMP_FILE_PREFIX.'/';
230
231 // use tmp folder in document root by default
232 if (!empty($_SERVER['DOCUMENT_ROOT'])) {
233 if (!@file_exists($_SERVER['DOCUMENT_ROOT'].'/tmp/'.$tmpDirName)) {
234 @mkdir($_SERVER['DOCUMENT_ROOT'].'/tmp/'.$tmpDirName, 0777, true);
235 @chmod($_SERVER['DOCUMENT_ROOT'].'/tmp/'.$tmpDirName, 0777);
236 }
237
238 if (@file_exists($_SERVER['DOCUMENT_ROOT'].'/tmp/'.$tmpDirName) && @is_writable($_SERVER['DOCUMENT_ROOT'].'/tmp/'.$tmpDirName)) {
239 return $_SERVER['DOCUMENT_ROOT'].'/tmp/'.$tmpDirName;
240 }
241 }
242
243 foreach (array('TMP', 'TEMP', 'TMPDIR') as $tmpDir) {
244 if (isset($_ENV[$tmpDir]) && @is_writable($_ENV[$tmpDir])) {
245 $dir = $_ENV[$tmpDir] . '/' . $tmpDirName;
246 @mkdir($dir, 0777);
247 @chmod($dir, 0777);
248
249 if (@file_exists($dir) && @is_writable($dir)) {
250 return $dir;
251 }
252 }
253 }
254
255 $dir = INSTALL_SCRIPT_DIR . 'tmp/' . $tmpDirName;
256 @mkdir($dir, 0777);
257 @chmod($dir, 0777);
258
259 if (!@file_exists($dir) || !@is_writable($dir)) {
260 $tmpDir = explode('/', $dir);
261 array_pop($tmpDir);
262 $dir = implode('/', $tmpDir);
263
264 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 '.$dir.' using your favorite ftp program, make it writable and then retry this installation.');
265 }
266
267 return $dir;
268 }
269 }
270
271 /**
272 * Opens tar or tar.gz archives.
273 *
274 * Usage:
275 * ------
276 * $tar = new Tar('archive.tar');
277 * $contentList = $tar->getContentList();
278 * foreach ($contentList as $key => $val) {
279 * $tar->extract($key, DESTINATION);
280 * }
281 */
282 class Tar {
283 protected $archiveName = '';
284 protected $contentList = array();
285 protected $opened = false;
286 protected $read = false;
287 protected $file = null;
288 protected $isZipped = false;
289 protected $mode = 'rb';
290
291 /**
292 * Creates a new Tar object.
293 * archiveName must be tarball or gzipped tarball
294 *
295 * @param string $archiveName
296 */
297 public function __construct($archiveName) {
298 $match = array();
299 if (!is_file($archiveName)) {
300 throw new SystemException("unable to find tar archive '".$archiveName."'");
301 }
302
303 $this->archiveName = $archiveName;
304 $this->open();
305 $this->readContent();
306 }
307
308 /**
309 * Destructor of this class, closes tar archive.
310 */
311 public function __destruct() {
312 $this->close();
313 }
314
315 /**
316 * Opens the tar archive and stores filehandle.
317 */
318 public function open() {
319 if (!$this->opened) {
320 if ($this->isZipped) $this->file = new ZipFile($this->archiveName, $this->mode);
321 else {
322 // test compression
323 $this->file = new File($this->archiveName, $this->mode);
324 if ($this->file->read(2) == "\37\213") {
325 $this->file->close();
326 $this->isZipped = true;
327 $this->file = new ZipFile($this->archiveName, $this->mode);
328 }
329 else {
330 $this->file->seek(0);
331 }
332 }
333 $this->opened = true;
334 }
335 }
336
337 /**
338 * Closes the opened file.
339 */
340 public function close() {
341 if ($this->opened) {
342 $this->file->close();
343 $this->opened = false;
344 }
345 }
346
347 /**
348 * Returns the table of contents (TOC) list for this tar archive.
349 *
350 * @return array list of content
351 */
352 public function getContentList() {
353 if (!$this->read) {
354 $this->open();
355 $this->readContent();
356 }
357 return $this->contentList;
358 }
359
360 /**
361 * Returns an associative array with information
362 * about a specific file in the archive.
363 *
364 * @param mixed $fileindex index or name of the requested file
365 * @return array $fileInfo
366 */
367 public function getFileInfo($fileIndex) {
368 if (!is_int($fileIndex)) {
369 $fileIndex = $this->getIndexByFilename($fileIndex);
370 }
371
372 if (!isset($this->contentList[$fileIndex])) {
373 throw new SystemException("Tar: could find file '$index' in archive"); //TODO: undefined variable
374 }
375 return $this->contentList[$fileIndex];
376 }
377
378 /**
379 * Searchs a file in the tar archive
380 * and returns the numeric fileindex.
381 * Returns false if not found.
382 *
383 * @param string $filename
384 * @return integer index of the requested file
385 */
386 public function getIndexByFilename($filename) {
387 foreach ($this->contentList as $index => $file) {
388 if ($file['filename'] == $filename) {
389 return $index;
390 }
391 }
392 return false;
393 }
394
395 /**
396 * Extracts a specific file and returns the content as string.
397 * Returns false if extraction failed.
398 *
399 * @param mixed $index index or name of the requested file
400 * @return string content of the requested file
401 */
402 public function extractToString($index) {
403 if (!$this->read) {
404 $this->open();
405 $this->readContent();
406 }
407 $header = $this->getFileInfo($index);
408
409 // can not extract a folder
410 if ($header['type'] != 'file') {
411 return false;
412 }
413
414 // seek to offset
415 $this->file->seek($header['offset']);
416
417 // read data
418 $content = '';
419 $n = floor($header['size'] / 512);
420 for($i = 0; $i < $n; $i++) {
421 $content .= $this->file->read(512);
422 }
423 if(($header['size'] % 512) != 0) {
424 $buffer = $this->file->read(512);
425 $content .= substr($buffer, 0, ($header['size'] % 512));
426 }
427
428 return $content;
429 }
430
431 /**
432 * Extracts a specific file and writes it's content
433 * to the file specified with $destination.
434 *
435 * @param mixed $index index or name of the requested file
436 * @param string $destination
437 * @return boolean $success
438 */
439 public function extract($index, $destination) {
440 if (!$this->read) {
441 $this->open();
442 $this->readContent();
443 }
444 $header = $this->getFileInfo($index);
445
446 // can not extract a folder
447 if ($header['type'] != 'file') {
448 return false;
449 }
450
451 // seek to offset
452 $this->file->seek($header['offset']);
453
454 $targetFile = new File($destination);
455
456 // read data
457 $n = floor($header['size'] / 512);
458 for ($i = 0; $i < $n; $i++) {
459 $content = $this->file->read(512);
460 $targetFile->write($content, 512);
461 }
462 if (($header['size'] % 512) != 0) {
463 $content = $this->file->read(512);
464 $targetFile->write($content, ($header['size'] % 512));
465 }
466
467 $targetFile->close();
468 if (function_exists('apache_get_version') || !@$targetFile->is_writable()) {
469 @$targetFile->chmod(0777);
470 }
471 else {
472 @$targetFile->chmod(0755);
473 }
474
475 if ($header['mtime']) {
476 @$targetFile->touch($header['mtime']);
477 }
478
479 // check filesize
480 if (filesize($destination) != $header['size']) {
481 throw new SystemException("Could not untar file '".$header['filename']."' to '".$destination."'. Maybe disk quota exceeded in folder '".dirname($destination)."'.");
482 }
483
484 return true;
485 }
486
487 /**
488 * Reads table of contents (TOC) from tar archive.
489 * This does not get the entire to memory but only parts of it.
490 */
491 protected function readContent() {
492 $this->contentList = array();
493 $this->read = true;
494 $i = 0;
495
496 // Read the 512 bytes header
497 while (strlen($binaryData = $this->file->read(512)) != 0) {
498 // read header
499 $header = $this->readHeader($binaryData);
500 if ($header === false) {
501 continue;
502 }
503 $this->contentList[$i] = $header;
504 $this->contentList[$i]['index'] = $i;
505 $i++;
506
507 $this->file->seek($this->file->tell() + (512 * ceil(($header['size'] / 512))));
508 }
509 }
510
511 /**
512 * Unpacks file header for one file entry.
513 *
514 * @param string $binaryData
515 * @return array $fileheader
516 */
517 protected function readHeader($binaryData) {
518 if (strlen($binaryData) != 512) {
519 return false;
520 }
521
522 $header = array();
523 $checksum = 0;
524 // First part of the header
525 for ($i = 0; $i < 148; $i++) {
526 $checksum += ord(substr($binaryData, $i, 1));
527 }
528 // Calculate the checksum
529 // Ignore the checksum value and replace it by ' ' (space)
530 for ($i = 148; $i < 156; $i++) {
531 $checksum += ord(' ');
532 }
533 // Last part of the header
534 for ($i = 156; $i < 512; $i++) {
535 $checksum += ord(substr($binaryData, $i, 1));
536 }
537
538 // Extract the values
539 //$data = unpack("a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor", $binaryData);
540 $data = unpack("a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix", $binaryData);
541
542 // Extract the properties
543 $header['checksum'] = octDec(trim($data['checksum']));
544 if ($header['checksum'] == $checksum) {
545 $header['filename'] = trim($data['filename']);
546 $header['mode'] = octDec(trim($data['mode']));
547 $header['uid'] = octDec(trim($data['uid']));
548 $header['gid'] = octDec(trim($data['gid']));
549 $header['size'] = octDec(trim($data['size']));
550 $header['mtime'] = octDec(trim($data['mtime']));
551 $header['prefix'] = trim($data['prefix']);
552 if ($header['prefix']) {
553 $header['filename'] = $header['prefix'].'/'.$header['filename'];
554 }
555 if (($header['typeflag'] = $data['typeflag']) == '5') {
556 $header['size'] = 0;
557 $header['type'] = 'folder';
558 }
559 else {
560 $header['type'] = 'file';
561 }
562 $header['offset'] = $this->file->tell();
563
564 return $header;
565 }
566 else {
567 return false;
568 }
569 }
570 }
571
572 /**
573 * The File class handles all file operations.
574 *
575 * Example:
576 * using php functions:
577 * $fp = fopen('filename', 'wb');
578 * fwrite($fp, '...');
579 * fclose($fp);
580 *
581 * using this class:
582 * $file = new File('filename');
583 * $file->write('...');
584 * $file->close();
585 *
586 * @author Marcel Werk
587 */
588 class File {
589 protected $resource = null;
590 protected $filename;
591
592 /**
593 * Opens a new file.
594 *
595 * @param string $filename
596 * @param string $mode
597 */
598 public function __construct($filename, $mode = 'wb') {
599 $this->filename = $filename;
600 $this->resource = fopen($filename, $mode);
601 if ($this->resource === false) {
602 throw new SystemException('Can not open file ' . $filename);
603 }
604 }
605
606 /**
607 * Calls the specified function on the open file.
608 * Do not call this function directly. Use $file->write('') instead.
609 *
610 * @param string $function
611 * @param array $arguments
612 */
613 public function __call($function, $arguments) {
614 if (function_exists('f' . $function)) {
615 array_unshift($arguments, $this->resource);
616 return call_user_func_array('f' . $function, $arguments);
617 }
618 else if (function_exists($function)) {
619 array_unshift($arguments, $this->filename);
620 return call_user_func_array($function, $arguments);
621 }
622 else {
623 throw new SystemException('Can not call file method ' . $function);
624 }
625 }
626 }
627
628 /**
629 * The File class handles all file operations on a zipped file.
630 *
631 * @author Marcel Werk
632 */
633 class ZipFile extends File {
634 /**
635 * Opens a new zipped file.
636 *
637 * @param string $filename
638 * @param string $mode
639 */
640 public function __construct($filename, $mode = 'wb') {
641 $this->filename = $filename;
642 if (!function_exists('gzopen')) {
643 throw new SystemException('Can not find functions of the zlib extension');
644 }
645 $this->resource = @gzopen($filename, $mode);
646 if ($this->resource === false) {
647 throw new SystemException('Can not open file ' . $filename);
648 }
649 }
650
651 /**
652 * Calls the specified function on the open file.
653 *
654 * @param string $function
655 * @param array $arguments
656 */
657 public function __call($function, $arguments) {
658 if (function_exists('gz' . $function)) {
659 array_unshift($arguments, $this->resource);
660 return call_user_func_array('gz' . $function, $arguments);
661 }
662 else if (function_exists($function)) {
663 array_unshift($arguments, $this->filename);
664 return call_user_func_array($function, $arguments);
665 }
666 else {
667 throw new SystemException('Can not call method ' . $function);
668 }
669 }
670
671 /**
672 * Returns the filesize of the unzipped file
673 */
674 public function getFileSize() {
675 $byteBlock = 1<<14;
676 $eof = $byteBlock;
677
678 // the correction is for zip files that are too small
679 // to get in the first while loop
680 $correction = 1;
681 while ($this->seek($eof) == 0) {
682 $eof += $byteBlock;
683 $correction = 0;
684 }
685
686 while ($byteBlock > 1) {
687 $byteBlock >>= 1;
688 $eof += $byteBlock * ($this->seek($eof) ? -1 : 1);
689 }
690
691 if ($this->seek($eof) == -1) $eof -= 1;
692
693 $this->rewind();
694 return $eof - $correction;
695 }
696 }
697
698 // let's go
699 // get temp file prefix
700 if (isset($_REQUEST['tmpFilePrefix'])) {
701 $prefix = preg_replace('/[^a-f0-9_]+/', '', $_REQUEST['tmpFilePrefix']);
702 }
703 else {
704 $prefix = substr(sha1(uniqid(microtime())), 0, 8);
705 }
706 define('TMP_FILE_PREFIX', $prefix);
707
708 // try to find the temp folder
709 define('TMP_DIR', BasicFileUtil::getTempFolder());
710
711 /**
712 * Reads a file resource from temp folder.
713 *
714 * @param string $key
715 * @param string $directory
716 */
717 function readFileResource($key, $directory) {
718 if (preg_match('~[\w\-]+\.(css|jpg|png|svg)~', $_GET[$key], $match)) {
719 switch ($match[1]) {
720 case 'css':
721 header('Content-Type: text/css');
722 break;
723
724 case 'jpg':
725 header('Content-Type: image/jpg');
726 break;
727
728 case 'png':
729 header('Content-Type: image/png');
730 break;
731
732 case 'svg':
733 header('Content-Type: image/svg+xml');
734 break;
735 }
736
737 readfile($directory . $_GET[$key]);
738 }
739 exit;
740 }
741
742 // show image from temp folder
743 if (isset($_GET['showImage'])) {
744 readFileResource('showImage', TMP_DIR . 'install/files/acp/images/');
745 }
746 // show icon from temp folder
747 if (isset($_GET['showIcon'])) {
748 readFileResource('showIcon', TMP_DIR . 'install/files/icon/');
749 }
750 // show css from temp folder
751 if (isset($_GET['showCSS'])) {
752 readFileResource('showCSS', TMP_DIR . 'install/files/acp/style/');
753 }
754
755 // check whether setup files are already unzipped
756 if (!file_exists(TMP_DIR . 'install/files/lib/system/WCFSetup.class.php')) {
757 // try to unzip all setup files into temp folder
758 $tar = new Tar(SETUP_FILE);
759 $contentList = $tar->getContentList();
760 if (!count($contentList)) {
761 throw new SystemException("Can not unpack 'WCFSetup.tar.gz'. File is probably broken.");
762 }
763
764 foreach ($contentList as $file) {
765 foreach ($neededFilesPattern as $pattern) {
766 if (preg_match($pattern, $file['filename'])) {
767 // create directory if not exists
768 $dir = TMP_DIR . dirname($file['filename']);
769 if (!@is_dir($dir)) {
770 @mkdir($dir, 0777, true);
771 @chmod($dir, 0777);
772 }
773
774 $tar->extract($file['index'], TMP_DIR . $file['filename']);
775 }
776 }
777 }
778 $tar->close();
779
780 // create cache folders
781 @mkdir(TMP_DIR . 'setup/lang/cache/', 0777);
782 @chmod(TMP_DIR . 'setup/lang/cache/', 0777);
783
784 @mkdir(TMP_DIR . 'setup/template/compiled/', 0777);
785 @chmod(TMP_DIR . 'setup/template/compiled/', 0777);
786 }
787
788 if (!class_exists('wcf\system\WCFSetup')) {
789 throw new SystemException("Can not find class 'WCFSetup'");
790 }
791
792 // start setup
793 new \wcf\system\WCFSetup();