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