forked from GNUsocial/gnu-social
		
	
		
			
				
	
	
		
			312 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			312 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/**
 | 
						|
 * StatusNet, the distributed open-source microblogging tool
 | 
						|
 *
 | 
						|
 * Utilities for theme files and paths
 | 
						|
 *
 | 
						|
 * PHP version 5
 | 
						|
 *
 | 
						|
 * LICENCE: This program is free software: you can redistribute it and/or modify
 | 
						|
 * it under the terms of the GNU Affero General Public License as published by
 | 
						|
 * the Free Software Foundation, either version 3 of the License, or
 | 
						|
 * (at your option) any later version.
 | 
						|
 *
 | 
						|
 * This program is distributed in the hope that it will be useful,
 | 
						|
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						|
 * GNU Affero General Public License for more details.
 | 
						|
 *
 | 
						|
 * You should have received a copy of the GNU Affero General Public License
 | 
						|
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
						|
 *
 | 
						|
 * @category  Paths
 | 
						|
 * @package   StatusNet
 | 
						|
 * @author    Brion Vibber <brion@status.net>
 | 
						|
 * @copyright 2010 StatusNet, Inc.
 | 
						|
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 | 
						|
 * @link      http://status.net/
 | 
						|
 */
 | 
						|
 | 
						|
if (!defined('STATUSNET') && !defined('LACONICA')) {
 | 
						|
    exit(1);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Encapsulation of the validation-and-save process when dealing with
 | 
						|
 * a user-uploaded StatusNet theme archive...
 | 
						|
 * 
 | 
						|
 * @todo extract theme metadata from css/display.css
 | 
						|
 * @todo allow saving multiple themes
 | 
						|
 */
 | 
						|
class ThemeUploader
 | 
						|
{
 | 
						|
    protected $sourceFile;
 | 
						|
    protected $isUpload;
 | 
						|
    private $prevErrorReporting;
 | 
						|
 | 
						|
    public function __construct($filename)
 | 
						|
    {
 | 
						|
        if (!class_exists('ZipArchive')) {
 | 
						|
            throw new Exception(_("This server cannot handle theme uploads without ZIP support."));
 | 
						|
        }
 | 
						|
        $this->sourceFile = $filename;
 | 
						|
    }
 | 
						|
 | 
						|
    public static function fromUpload($name)
 | 
						|
    {
 | 
						|
        if (!isset($_FILES[$name]['error'])) {
 | 
						|
            throw new ServerException(_("The theme file is missing or the upload failed."));
 | 
						|
        }
 | 
						|
        if ($_FILES[$name]['error'] != UPLOAD_ERR_OK) {
 | 
						|
            throw new ServerException(_("The theme file is missing or the upload failed."));
 | 
						|
        }
 | 
						|
        return new ThemeUploader($_FILES[$name]['tmp_name']);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param string $destDir
 | 
						|
     * @throws Exception on bogus files
 | 
						|
     */
 | 
						|
    public function extract($destDir)
 | 
						|
    {
 | 
						|
        $zip = $this->openArchive();
 | 
						|
 | 
						|
        // First pass: validate but don't save anything to disk.
 | 
						|
        // Any errors will trip an exception.
 | 
						|
        $this->traverseArchive($zip);
 | 
						|
 | 
						|
        // Second pass: now that we know we're good, actually extract!
 | 
						|
        $tmpDir = $destDir . '.tmp' . getmypid();
 | 
						|
        $this->traverseArchive($zip, $tmpDir);
 | 
						|
 | 
						|
        $zip->close();
 | 
						|
 | 
						|
        if (file_exists($destDir)) {
 | 
						|
            $killDir = $tmpDir . '.old';
 | 
						|
            $this->quiet();
 | 
						|
            $ok = rename($destDir, $killDir);
 | 
						|
            $this->loud();
 | 
						|
            if (!$ok) {
 | 
						|
                common_log(LOG_ERR, "Could not move old custom theme from $destDir to $killDir");
 | 
						|
                throw new ServerException(_("Failed saving theme."));
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            $killDir = false;
 | 
						|
        }
 | 
						|
 | 
						|
        $this->quiet();
 | 
						|
        $ok = rename($tmpDir, $destDir);
 | 
						|
        $this->loud();
 | 
						|
        if (!$ok) {
 | 
						|
            common_log(LOG_ERR, "Could not move saved theme from $tmpDir to $destDir");
 | 
						|
            throw new ServerException(_("Failed saving theme."));
 | 
						|
        }
 | 
						|
 | 
						|
        if ($killDir) {
 | 
						|
            $this->recursiveRmdir($killDir);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 
 | 
						|
     */
 | 
						|
    protected function traverseArchive($zip, $outdir=false)
 | 
						|
    {
 | 
						|
        $sizeLimit = 2 * 1024 * 1024; // 2 megabyte space limit?
 | 
						|
        $blockSize = 4096; // estimated; any entry probably takes this much space
 | 
						|
 | 
						|
        $totalSize = 0;
 | 
						|
        $hasMain = false;
 | 
						|
        $commonBaseDir = false;
 | 
						|
 | 
						|
        for ($i = 0; $i < $zip->numFiles; $i++) {
 | 
						|
            $data = $zip->statIndex($i);
 | 
						|
            $name = str_replace('\\', '/', $data['name']);
 | 
						|
 | 
						|
            if (substr($name, -1) == '/') {
 | 
						|
                // A raw directory... skip!
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            // Check the directory structure...
 | 
						|
            $path = pathinfo($name);
 | 
						|
            $dirs = explode('/', $path['dirname']);
 | 
						|
            $baseDir = array_shift($dirs);
 | 
						|
            if ($commonBaseDir === false) {
 | 
						|
                $commonBaseDir = $baseDir;
 | 
						|
            } else {
 | 
						|
                if ($commonBaseDir != $baseDir) {
 | 
						|
                    throw new ClientException(_("Invalid theme: bad directory structure."));
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            foreach ($dirs as $dir) {
 | 
						|
                $this->validateFileOrFolder($dir);
 | 
						|
            }
 | 
						|
 | 
						|
            // Is this a safe or skippable file?
 | 
						|
            if ($this->skippable($path['filename'], $path['extension'])) {
 | 
						|
                // Documentation and such... booooring
 | 
						|
                continue;
 | 
						|
            } else {
 | 
						|
                $this->validateFile($path['filename'], $path['extension']);
 | 
						|
            }
 | 
						|
 | 
						|
            $fullPath = $dirs;
 | 
						|
            $fullPath[] = $path['basename'];
 | 
						|
            $localFile = implode('/', $fullPath);
 | 
						|
            if ($localFile == 'css/display.css') {
 | 
						|
                $hasMain = true;
 | 
						|
            }
 | 
						|
            
 | 
						|
            $size = $data['size'];
 | 
						|
            $estSize = $blockSize * max(1, intval(ceil($size / $blockSize)));
 | 
						|
            $totalSize += $estSize;
 | 
						|
            if ($totalSize > $sizeLimit) {
 | 
						|
                $msg = sprintf(_("Uploaded theme is too large; " .
 | 
						|
                                 "must be less than %d bytes uncompressed."),
 | 
						|
                                 $sizeLimit);
 | 
						|
                throw new ClientException($msg);
 | 
						|
            }
 | 
						|
 | 
						|
            if ($outdir) {
 | 
						|
                $this->extractFile($zip, $data['name'], "$outdir/$localFile");
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (!$hasMain) {
 | 
						|
            throw new ClientException(_("Invalid theme archive: " .
 | 
						|
                                        "missing file css/display.css"));
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    protected function skippable($filename, $ext)
 | 
						|
    {
 | 
						|
        $skip = array('txt', 'rtf', 'doc', 'docx', 'odt');
 | 
						|
        if (strtolower($filename) == 'readme') {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        if (in_array(strtolower($ext), $skip)) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    protected function validateFile($filename, $ext)
 | 
						|
    {
 | 
						|
        $this->validateFileOrFolder($filename);
 | 
						|
        $this->validateExtension($ext);
 | 
						|
        // @fixme validate content
 | 
						|
    }
 | 
						|
 | 
						|
    protected function validateFileOrFolder($name)
 | 
						|
    {
 | 
						|
        if (!preg_match('/^[a-z0-9_-]+$/i', $name)) {
 | 
						|
            $msg = _("Theme contains invalid file or folder name. " .
 | 
						|
                     "Stick with ASCII letters, digits, underscore, and minus sign.");
 | 
						|
            throw new ClientException($msg);
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    protected function validateExtension($ext)
 | 
						|
    {
 | 
						|
        $allowed = array('css', 'png', 'gif', 'jpg', 'jpeg');
 | 
						|
        if (!in_array(strtolower($ext), $allowed)) {
 | 
						|
            $msg = sprintf(_("Theme contains file of type '.%s', " .
 | 
						|
                             "which is not allowed."),
 | 
						|
                           $ext);
 | 
						|
            throw new ClientException($msg);
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @return ZipArchive
 | 
						|
     */
 | 
						|
    protected function openArchive()
 | 
						|
    {
 | 
						|
        $zip = new ZipArchive;
 | 
						|
        $ok = $zip->open($this->sourceFile); 
 | 
						|
        if ($ok !== true) {
 | 
						|
            common_log(LOG_ERR, "Error opening theme zip archive: " .
 | 
						|
                                "{$this->sourceFile} code: {$ok}");
 | 
						|
            throw new Exception(_("Error opening theme archive."));
 | 
						|
        }
 | 
						|
        return $zip;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param ZipArchive $zip
 | 
						|
     * @param string $from original path inside ZIP archive
 | 
						|
     * @param string $to final destination path in filesystem
 | 
						|
     */
 | 
						|
    protected function extractFile($zip, $from, $to)
 | 
						|
    {
 | 
						|
        $dir = dirname($to);
 | 
						|
        if (!file_exists($dir)) {
 | 
						|
            $this->quiet();
 | 
						|
            $ok = mkdir($dir, 0755, true);
 | 
						|
            $this->loud();
 | 
						|
            if (!$ok) {
 | 
						|
                common_log(LOG_ERR, "Failed to mkdir $dir while uploading theme");
 | 
						|
                throw new ServerException(_("Failed saving theme."));
 | 
						|
            }
 | 
						|
        } else if (!is_dir($dir)) {
 | 
						|
            common_log(LOG_ERR, "Output directory $dir not a directory while uploading theme");
 | 
						|
            throw new ServerException(_("Failed saving theme."));
 | 
						|
        }
 | 
						|
 | 
						|
        // ZipArchive::extractTo would be easier, but won't let us alter
 | 
						|
        // the directory structure.
 | 
						|
        $in = $zip->getStream($from);
 | 
						|
        if (!$in) {
 | 
						|
            common_log(LOG_ERR, "Couldn't open archived file $from while uploading theme");
 | 
						|
            throw new ServerException(_("Failed saving theme."));
 | 
						|
        }
 | 
						|
        $this->quiet();
 | 
						|
        $out = fopen($to, "wb");
 | 
						|
        $this->loud();
 | 
						|
        if (!$out) {
 | 
						|
            common_log(LOG_ERR, "Couldn't open output file $to while uploading theme");
 | 
						|
            throw new ServerException(_("Failed saving theme."));
 | 
						|
        }
 | 
						|
        while (!feof($in)) {
 | 
						|
            $buffer = fread($in, 65536);
 | 
						|
            fwrite($out, $buffer);
 | 
						|
        }
 | 
						|
        fclose($in);
 | 
						|
        fclose($out);
 | 
						|
    }
 | 
						|
 | 
						|
    private function quiet()
 | 
						|
    {
 | 
						|
        $this->prevErrorReporting = error_reporting();
 | 
						|
        error_reporting($this->prevErrorReporting & ~E_WARNING);
 | 
						|
    }
 | 
						|
 | 
						|
    private function loud()
 | 
						|
    {
 | 
						|
        error_reporting($this->prevErrorReporting);
 | 
						|
    }
 | 
						|
 | 
						|
    private function recursiveRmdir($dir)
 | 
						|
    {
 | 
						|
        $list = dir($dir);
 | 
						|
        while (($file = $list->read()) !== false) {
 | 
						|
            if ($file == '.' || $file == '..') {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            $full = "$dir/$file";
 | 
						|
            if (is_dir($full)) {
 | 
						|
                $this->recursiveRmdir($full);
 | 
						|
            } else {
 | 
						|
                unlink($full);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        $list->close();
 | 
						|
        rmdir($dir);
 | 
						|
    }
 | 
						|
 | 
						|
}
 |