From d2beb47c8cb7732346fcba152179d0fb86b1ad5c Mon Sep 17 00:00:00 2001 From: AnrDaemon Date: Thu, 22 Jun 2017 00:20:59 +0300 Subject: [PATCH] Prepare 1.1 release Gallery: * Use constants to store default property values. * Normalize file handling, encapsulate charset conversions inside a class. * Use PCRE_UTF8 for regexps. * Use getimagesize() to retrieve image metadata. + Allow custom mask listing. + Allow clearing SendFile support. * Allow serving gallery from the root (empty sfPrefix). * (Windows) fix failing realpath() once again. + Allow setting index template globally. * Imagick paths use UTF-8 by default. + Caseless extension comparison. + Typehint arrays where possible. + Force use of UTF-8 for file IO under PHP 7.1. Index: - Use default extensions set for directory listing. - Check for images present in gallery, not files on disk. Gallery already skips unreadable/unsupported files. --- CHANGES | 30 +++++++++ Gallery.php | 167 ++++++++++++++++++++++++++++++++++------------- README.md | 12 ++++ composer.json | 2 +- index.sample.php | 24 +++---- 5 files changed, 174 insertions(+), 61 deletions(-) create mode 100644 CHANGES diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..250728e --- /dev/null +++ b/CHANGES @@ -0,0 +1,30 @@ +------------------------------------------------------------------------ +r668 | anrdaemon | 2017-06-22 00:12:01 +0300 (Чт, 22 июн 2017) | 2 lines + ++ Force use of UTF-8 for file IO under PHP 7.1. + +------------------------------------------------------------------------ +r667 | anrdaemon | 2017-06-21 23:19:20 +0300 (Ср, 21 июн 2017) | 21 lines + +Gallery: +* Use constants to store default property values. +* Normalize file handling, encapsulate charset conversions inside a class. +* Use PCRE_UTF8 for regexps. +* Use getimagesize() to retrieve image metadata. ++ Allow custom mask listing. ++ Allow clearing SendFile support. +* Allow serving gallery from the root (empty sfPrefix). +* (Windows) fix failing realpath() once again. ++ Allow setting index template globally. +* Imagick paths use UTF-8 by default. ++ Caseless extension comparison. ++ Typehint arrays where possible. + +Index: +- Use default extensions set for directory listing. +- Check for images present in gallery, not files on disk. + Gallery already skips unreadable/unsupported files. + ++ Bump base library version. + +------------------------------------------------------------------------ diff --git a/Gallery.php b/Gallery.php index f464e70..a1d9f02 100644 --- a/Gallery.php +++ b/Gallery.php @@ -3,7 +3,7 @@ * * A simple drop-in file-based HTML gallery. * -* $Id: Gallery.php 662 2017-06-17 12:16:41Z anrdaemon $ +* $Id: Gallery.php 668 2017-06-21 21:12:01Z anrdaemon $ */ namespace AnrDaemon\MyLittleGallery; @@ -20,6 +20,11 @@ class Gallery implements ArrayAccess, Countable, Iterator { + const previewTemplate = + '
%3$s

%3$s

'; + + const defaultTypes = 'gif|jpeg|jpg|png|tif|tiff|wbmp|webp'; + // All paths are UTF-8! (Except those from SplFileInfo) protected $path; // Gallery base path protected $prefix = array(); // Various prefixes for correct links construction @@ -35,11 +40,43 @@ class Gallery // Preview settings protected $pWidth; protected $pHeight; + protected $template; // X-SendFile settings protected $sfPrefix; protected $sfHeader = 'X-SendFile'; + protected function fromFileList(array $list) + { + $prev = null; + foreach($list as $fname) + { + if(is_dir($fname)) + continue; + + $name = iconv($this->cs, 'UTF-8', basename($fname)); + $this->isSaneName($name); + + $meta = getimagesize($fname); + if($meta === false || $meta[0] === 0 || $meta[1] === 0) + continue; + + $this->params[$name]['desc'] = $name; + $this->params[$name]['path'] = "{$this->path}/{$name}"; + $this->params[$name]['width'] = $meta[0]; + $this->params[$name]['height'] = $meta[1]; + $this->params[$name]['mime'] = $meta['mime']; + if(isset($prev)) + { + $this->params[$name]['prev'] = $prev; + $this->params[$prev]['next'] = $name; + } + $prev = $name; + } + + return $this; + } + public static function fromListfile(SplFileInfo $target, $charset = 'CP866', $fsEncoding = null) { $path = $target->getRealPath(); @@ -54,15 +91,23 @@ public static function fromListfile(SplFileInfo $target, $charset = 'CP866', $fs $f = iconv($charset, 'UTF-8', file_get_contents($path)); - if(preg_match_all('/^(\"?)(?P[^\"]+?)\1\s+(?P.*?)\s*$/m', $f, $ta, PREG_SET_ORDER)) + if(preg_match_all('/^(\"?)(?P[^\"]+?)\1\s+(?P.*?)\s*$/um', $f, $ta, PREG_SET_ORDER)) { $prev = null; foreach($ta as $a) { $name = basename(trim($a['name'])); $self->isSaneName($name); + + $meta = getimagesize(iconv('UTF-8', $self->cs, "{$self->path}/$name")); + if($meta === false || $meta[0] === 0 || $meta[1] === 0) + continue; + $self->params[$name]['desc'] = $a['desc']; $self->params[$name]['path'] = "{$self->path}/{$name}"; + $self->params[$name]['width'] = $meta[0]; + $self->params[$name]['height'] = $meta[1]; + $self->params[$name]['mime'] = $meta['mime']; if(isset($prev)) { $self->params[$name]['prev'] = $prev; @@ -75,7 +120,19 @@ public static function fromListfile(SplFileInfo $target, $charset = 'CP866', $fs return $self; } - public static function fromDirectory(SplFileInfo $target, $extensions = null, $fsEncoding = null) + public static function fromDirectory(SplFileInfo $target, array $extensions = null, $fsEncoding = null) + { + if(empty($extensions)) + { + $extensions = explode('|', static::defaultTypes); + } + + $mask = "*.{" . implode(',', $extensions) . "}"; + + return static::fromCustomMask($target, $mask, $fsEncoding); + } + + public static function fromCustomMask(SplFileInfo $target, $mask, $fsEncoding = null) { $path = $target->getRealPath(); @@ -85,39 +142,18 @@ public static function fromDirectory(SplFileInfo $target, $extensions = null, $f if(!is_dir($path)) throw new Exception('Target is not a directory', 500); - if(!is_array($extensions)) - $extensions = null; - - $self = new static($target, $extensions, $fsEncoding); + $self = new static($target, null, $fsEncoding); - $mask = iconv('UTF-8', $self->cs, "*.{" . implode(',', $self->extensions) . "}"); - - $prev = null; - foreach(glob("{$path}/{$mask}", GLOB_BRACE | GLOB_MARK) as $fname) - { - if(is_dir($fname)) - continue; - - $name = iconv($self->cs, 'UTF-8', basename($fname)); - $self->isSaneName($name); - $self->params[$name]['desc'] = $name; - $self->params[$name]['path'] = "{$self->path}/{$name}"; - if(isset($prev)) - { - $self->params[$name]['prev'] = $prev; - $self->params[$prev]['next'] = $name; - } - $prev = $name; - } - - return $self; + return $self->fromFileList(glob("{$path}/" . iconv('UTF-8', $self->cs, $mask), GLOB_BRACE | GLOB_MARK)); } - + /** + * $template($show, $preview, $description) + */ public function showIndex($template = null) { if(empty($template)) { - $template = ''; + $template = $this->template; } $gp = ''; @@ -138,7 +174,7 @@ public function setNumberFormatter($locale = 'en_US.UTF-8', $style = NumberForma return $this; } - public function allowSendFile($prefix, $header = null) + public function allowSendFile($prefix = null, $header = null) { $this->sfPrefix = $prefix; $this->sfHeader = trim($header)?: 'X-SendFile'; @@ -146,7 +182,7 @@ public function allowSendFile($prefix, $header = null) public function sendFile($path) { - if(empty($this->sfPrefix)) + if(!isset($this->sfPrefix)) return false; header_register_callback(function(){ @@ -162,7 +198,7 @@ public function sendFile($path) header_remove('Content-Type'); }); - header("{$this->sfHeader}: {$this->sfPrefix}$path"); + header("{$this->sfHeader}: {$this->sfPrefix}" . urlencode("$path")); return true; } @@ -172,6 +208,15 @@ public function imageFileSize($name, $divisor = 1) return $this->nf->format(ceil(filesize(iconv('UTF-8', $this->cs, "{$this->path}/$name")) / $divisor)); } + public function imagePreviewExists($name) + { + if(isset($this->params[$name]['preview'])) + return !empty($this->params[$name]['preview']); + + $fname = iconv('UTF-8', $this->cs, "{$this->path}/.preview/$name"); + return $this->params[$name]['preview'] = file_exists($fname); + } + public function setPreviewSize($width = null, $height = null) { if((int)$width < 0 || (int)$height < 0) @@ -193,14 +238,32 @@ public function setPrefix($name, $prefix) return $this; } + public function setTemplate($template = null) + { + $this->template = empty($template) + ? static::previewTemplate + : $template; + } + public function getPrefix($name) { return $this->prefix[$name]; } - public function getPath() + public function getPath($name = null, $local = null) { - return $this->path; + $path = $this->path; + if(isset($name)) + { + $path .= $name; + } + + if($local) + { + $path = iconv('UTF-8', $this->cs, $path); + } + + return $path; } public function thumbnailImage($name) @@ -211,14 +274,16 @@ public function thumbnailImage($name) try { - $img = new Imagick(iconv('UTF-8', $this->cs, "{$this->path}/$name")); + //$img = new Imagick(iconv('UTF-8', $this->cs, "{$this->path}/$name")); + $img = new Imagick("{$this->path}/$name"); $img->thumbnailImage($this->pWidth, $this->pHeight, true); - $img->writeImage($path); + $img->writeImage("{$this->path}/.preview/$name"); } catch(Exception $e) { if(!is_dir(dirname($path))) mkdir(dirname($path)); + return false; } @@ -230,34 +295,44 @@ public function isSaneName($fname) { $name = basename($fname); if(preg_match('/[^!#$%&\'()+,\-.;=@\[\]^_`{}~\p{L}\d\s]/uiS', $name)) - throw new Exception('Invalid character in name \'' . $name . "'.", 403); + throw new Exception("Invalid character in name '$name'.", 400); - if(!preg_match('{.+\.(' . implode('|', array_map('preg_quote', $this->extensions)) . ')$}u', $name)) - throw new Exception('Invalid filename extension.', 403); + if(!preg_match('{.+\.(' . implode('|', array_map('preg_quote', $this->extensions)) . ')$}ui', $name)) + throw new Exception('Invalid filename extension.', 400); return true; } // Magic! - protected function __construct(SplFileInfo $path, $extensions = null, $fsEncoding = null) + protected function __construct(SplFileInfo $path, array $extensions = null, $fsEncoding = null) { - $this->cs = trim($fsEncoding) ?: 'UTF-8'; - $this->path = iconv($this->cs, 'UTF-8', $path->getRealPath()); + if(version_compare(PHP_VERSION, '7.1', '<')) + { + $this->cs = trim($fsEncoding) ?: 'UTF-8'; + } + else + { + ini_set('internal_encoding', 'UTF-8'); + $this->cs = 'UTF-8'; + } + + $this->path = iconv($this->cs, 'UTF-8', realpath($path->getRealPath())); // $path is not necessarily equals $path->getRealPath() // Work off original $path - $this->prefix['index'] = iconv($this->cs, 'UTF-8', substr($path, strlen($_SERVER['DOCUMENT_ROOT']))); + $this->prefix['index'] = iconv($this->cs, 'UTF-8', substr(realpath(realpath($path)), strlen(realpath(realpath($_SERVER['DOCUMENT_ROOT']))))); $this->prefix['view'] = $this->prefix['index'] . '/?show='; $this->prefix['thumbnail'] = $this->prefix['index'] . '/?preview='; $this->prefix['image'] = $this->prefix['index'] . '/?view='; $this->setNumberFormatter(); $this->setPreviewSize(); + $this->setTemplate(); - if(!is_array($extensions)) + if(empty($extensions)) { - $this->extensions = array('gif', 'jpg', 'png'); + $this->extensions = explode('|', static::defaultTypes); } else { diff --git a/README.md b/README.md index 181f9f5..de2e7b6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ # MyLittleGallery A PHP class and templates to create a quick drop-in HTML gallery. + +## Throubleshooting the demo script + +### Unable to read files with non-ASCII names +#### PHP before 7.1 +Check that encoding of `config.php` file itself matches value of GALLERY_FS_ENCODING constant. +#### PHP 7.1 +`config.php` MUST be in `UTF-8`. +For PHP 7.1 GALLERY_FS_ENCODING and `$fsEncoding` parameter of the constructor are ignored. + +Starting from PHP 7.1, [PHP uses internal_encoding to transcode file names](https://github.com/php/php-src/blob/e33ec61f9c1baa73bfe1b03b8c48a824ab2a867e/UPGRADING#L418). +Before that, file IO under Windows (notably) done using "default" (so-called "ANSI") character set (i.e. CP1251 for Russian cyrillic). diff --git a/composer.json b/composer.json index 2610d10..96bc1d5 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { diff --git a/index.sample.php b/index.sample.php index 23caa0b..2fcd2bf 100644 --- a/index.sample.php +++ b/index.sample.php @@ -54,7 +54,7 @@ else { $gallery = AnrDaemon\MyLittleGallery\Gallery::fromDirectory(new SplFileInfo(GALLERY_BASE_DIR), - array('gif', 'jpeg', 'jpg', 'png'), GALLERY_FS_ENCODING); + null, GALLERY_FS_ENCODING); } if(defined('GALLERY_SENDFILE_HEADER')) @@ -68,38 +68,34 @@ { case isset($_REQUEST['preview']): $name = basename($_REQUEST['preview']); - if(!is_file($gallery->getPath() . "/$name")) + if(!isset($gallery[$name])) throw new Exception('No referenced image found.', 404); if(!$gallery->thumbnailImage($name)) - throw new Exception('No thumbnail image for \'' . htmlspecialchars($name) . '\'', 404); + throw new Exception("No thumbnail image for '$name'.", 404); if(isset($_REQUEST['console'])) { die("Done.\n"); } - if($gallery->sendFile("/.preview/" . rawurlencode($name))) + if($gallery->sendFile("/.preview/$name")) break; - $img = new Imagick($gallery->getPath() . "/.preview/$name"); - header('Content-type: image/' . strtolower($img->getImageFormat())); - unset($img); - readfile($gallery->getPath() . "/.preview/$name"); + header('Content-type: ' . $gallery[$name]['mime']); + readfile($gallery->getPath("/.preview/$name", true)); break; case isset($_REQUEST['view']): $name = basename($_REQUEST['view']); - if(!is_file($gallery->getPath() . "/$name")) + if(!isset($gallery[$name])) throw new Exception("Image '$name' not found.", 404); - if($gallery->sendFile("/" . rawurlencode($name))) + if($gallery->sendFile("/$name")) break; - $img = new Imagick($gallery->getPath() . "/$name"); - header('Content-type: image/' . strtolower($img->getImageFormat())); - unset($img); - readfile($gallery->getPath() . "/$name"); + header('Content-type: ' . $gallery[$name]['mime']); + readfile($gallery->getPath("/$name", true)); break; case isset($_REQUEST['show']):