123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- <?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')) {
- // TRANS: Exception thrown when a compressed theme is uploaded while no support present in PHP configuration.
- 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'])) {
- // TRANS: Server exception thrown when uploading a theme fails.
- throw new ServerException(_('The theme file is missing or the upload failed.'));
- }
- if ($_FILES[$name]['error'] != UPLOAD_ERR_OK) {
- // TRANS: Server exception thrown when uploading a theme fails.
- 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");
- // TRANS: Server exception thrown when saving an uploaded theme after decompressing it fails.
- 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");
- // TRANS: Server exception thrown when saving an uploaded theme after decompressing it fails.
- 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;
- }
- // Is this a safe or skippable file?
- $path = pathinfo($name);
- if ($this->skippable($path['filename'], $path['extension'])) {
- // Documentation and such... booooring
- continue;
- } else {
- $this->validateFile($path['filename'], $path['extension']);
- }
- // Check the directory structure...
- $dirs = explode('/', $path['dirname']);
- $baseDir = array_shift($dirs);
- if ($commonBaseDir === false) {
- $commonBaseDir = $baseDir;
- } else {
- if ($commonBaseDir != $baseDir) {
- // TRANS: Server exception thrown when an uploaded theme has an incorrect structure.
- throw new ClientException(_('Invalid theme: Bad directory structure.'));
- }
- }
- foreach ($dirs as $dir) {
- $this->validateFileOrFolder($dir);
- }
- $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) {
- // TRANS: Client exception thrown when an uploaded theme is larger than the limit.
- // TRANS: %d is the number of bytes of the uncompressed theme.
- $msg = sprintf(_m('Uploaded theme is too large; must be less than %d byte uncompressed.',
- 'Uploaded theme is too large; must be less than %d bytes uncompressed.',
- $sizeLimit),
- $sizeLimit);
- throw new ClientException($msg);
- }
- if ($outdir) {
- $this->extractFile($zip, $data['name'], "$outdir/$localFile");
- }
- }
- if (!$hasMain) {
- // TRANS: Server exception thrown when an uploaded theme is incomplete.
- throw new ClientException(_('Invalid theme archive: ' .
- "Missing file css/display.css"));
- }
- }
- /**
- * @fixme Probably most unrecognized files should just be skipped...
- */
- protected function skippable($filename, $ext)
- {
- $skip = array('txt', 'html', 'rtf', 'doc', 'docx', 'odt', 'xcf');
- if (strtolower($filename) == 'readme') {
- return true;
- }
- if (in_array(strtolower($ext), $skip)) {
- return true;
- }
- if ($filename == '' || substr($filename, 0, 1) == '.') {
- // Skip Unix-style hidden files
- return true;
- }
- if ($filename == '__MACOSX') {
- // Skip awful metadata files Mac OS X slips in for you.
- // Thanks Apple!
- return true;
- }
- return false;
- }
- protected function validateFile($filename, $ext)
- {
- $this->validateFileOrFolder($filename);
- $this->validateExtension($filename, $ext);
- // @fixme validate content
- }
- protected function validateFileOrFolder($name)
- {
- if (!preg_match('/^[a-z0-9_\.-]+$/i', $name)) {
- common_log(LOG_ERR, "Bad theme filename: $name");
- // TRANS: Server exception thrown when an uploaded theme has an incorrect file or folder name.
- $msg = _("Theme contains invalid file or folder name. " .
- 'Stick with ASCII letters, digits, underscore, and minus sign.');
- throw new ClientException($msg);
- }
- if (preg_match('/\.(php|cgi|asp|aspx|js|vb)\w/i', $name)) {
- common_log(LOG_ERR, "Unsafe theme filename: $name");
- // TRANS: Server exception thrown when an uploaded theme contains files with unsafe file extensions.
- $msg = _('Theme contains unsafe file extension names; may be unsafe.');
- throw new ClientException($msg);
- }
- return true;
- }
- protected function validateExtension($base, $ext)
- {
- $allowed = array('css', // CSS may need validation
- 'png', 'gif', 'jpg', 'jpeg',
- 'svg', // SVG images/fonts may need validation
- 'ttf', 'eot', 'woff');
- if (!in_array(strtolower($ext), $allowed)) {
- if ($ext == 'ini' && $base == 'theme') {
- // theme.ini exception
- return true;
- }
- // TRANS: Server exception thrown when an uploaded theme contains a file type that is not allowed.
- // TRANS: %s is the file type that is not 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}");
- // TRANS: Server exception thrown when an uploaded compressed theme cannot be opened.
- 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");
- // TRANS: Server exception thrown when an uploaded theme cannot be saved during extraction.
- throw new ServerException(_('Failed saving theme.'));
- }
- } else if (!is_dir($dir)) {
- common_log(LOG_ERR, "Output directory $dir not a directory while uploading theme");
- // TRANS: Server exception thrown when an uploaded theme cannot be saved during extraction.
- 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");
- // TRANS: Server exception thrown when an uploaded theme cannot be saved during extraction.
- 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");
- // TRANS: Server exception thrown when an uploaded theme cannot be saved during extraction.
- 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);
- }
- }
|