Skip to content

Commit

Permalink
Prepare 1.1 release
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
AnrDaemon committed Jun 21, 2017
1 parent 862115c commit d2beb47
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 61 deletions.
30 changes: 30 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -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.

------------------------------------------------------------------------
167 changes: 121 additions & 46 deletions Gallery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +20,11 @@
class Gallery
implements ArrayAccess, Countable, Iterator
{
const previewTemplate =
'<div><a href="%1$s" target="_blank"><img src="%2$s" alt="%3$s"/></a><p><a href="%1$s" target="_blank">%3$s</a></p></div>';

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
Expand All @@ -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();
Expand All @@ -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<name>[^\"]+?)\1\s+(?P<desc>.*?)\s*$/m', $f, $ta, PREG_SET_ORDER))
if(preg_match_all('/^(\"?)(?P<name>[^\"]+?)\1\s+(?P<desc>.*?)\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;
Expand All @@ -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();

Expand All @@ -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 = '<div><a href="%1$s" target="_blank"><img src="%2$s" alt="%3$s"/></a><p><a href="%1$s" target="_blank">%3$s</a></p></div>';
$template = $this->template;
}

$gp = '';
Expand All @@ -138,15 +174,15 @@ 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';
}

public function sendFile($path)
{
if(empty($this->sfPrefix))
if(!isset($this->sfPrefix))
return false;

header_register_callback(function(){
Expand All @@ -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;
}
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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;
}

Expand All @@ -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
{
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "1.1.x-dev"
}
},
"autoload": {
Expand Down
Loading

0 comments on commit d2beb47

Please sign in to comment.