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 =
+ '
';
+
+ 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']):