Amber Sprenkels
b7a37c85f6
Bug as reported by Yorick: When two Assets have the same capture date, a bug occurs in the interface where the user gets stuck in a loop when moving to the next image. This patch uses the primary key as a fallback when ordering the images by capture date. This way, the asset ordering is complete and it should resolve the bug.
663 lines
16 KiB
PHP
663 lines
16 KiB
PHP
<?php
|
|
/*****************************************************************************
|
|
* Asset.php
|
|
* Contains key class Asset.
|
|
*
|
|
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
|
*****************************************************************************/
|
|
|
|
class Asset
|
|
{
|
|
protected $id_asset;
|
|
protected $id_user_uploaded;
|
|
protected $subdir;
|
|
protected $filename;
|
|
protected $title;
|
|
protected $mimetype;
|
|
protected $image_width;
|
|
protected $image_height;
|
|
protected $date_captured;
|
|
protected $priority;
|
|
|
|
protected $meta;
|
|
protected $tags;
|
|
protected $thumbnails;
|
|
|
|
protected function __construct(array $data)
|
|
{
|
|
foreach ($data as $attribute => $value)
|
|
$this->$attribute = $value;
|
|
|
|
if (!empty($data['date_captured']) && $data['date_captured'] !== 'NULL')
|
|
$this->date_captured = new DateTime($data['date_captured']);
|
|
}
|
|
|
|
public static function fromId($id_asset, $return_format = 'object')
|
|
{
|
|
$row = Registry::get('db')->queryAssoc('
|
|
SELECT *
|
|
FROM assets
|
|
WHERE id_asset = {int:id_asset}',
|
|
[
|
|
'id_asset' => $id_asset,
|
|
]);
|
|
|
|
return empty($row) ? false : self::byRow($row, $return_format);
|
|
}
|
|
|
|
public static function fromSlug($slug, $return_format = 'object')
|
|
{
|
|
$row = Registry::get('db')->queryAssoc('
|
|
SELECT *
|
|
FROM assets
|
|
WHERE slug = {string:slug}',
|
|
[
|
|
'slug' => $slug,
|
|
]);
|
|
|
|
return empty($row) ? false : self::byRow($row, $return_format);
|
|
}
|
|
|
|
public static function byRow(array $row, $return_format = 'object')
|
|
{
|
|
$db = Registry::get('db');
|
|
|
|
// Supplement with metadata.
|
|
$row['meta'] = $db->queryPair('
|
|
SELECT variable, value
|
|
FROM assets_meta
|
|
WHERE id_asset = {int:id_asset}',
|
|
[
|
|
'id_asset' => $row['id_asset'],
|
|
]);
|
|
|
|
// And thumbnails.
|
|
$row['thumbnails'] = $db->queryPair('
|
|
SELECT
|
|
CONCAT(
|
|
width,
|
|
{string:x},
|
|
height,
|
|
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
|
) AS selector, filename
|
|
FROM assets_thumbs
|
|
WHERE id_asset = {int:id_asset}',
|
|
[
|
|
'id_asset' => $row['id_asset'],
|
|
'empty' => '',
|
|
'x' => 'x',
|
|
'_' => '_',
|
|
]);
|
|
|
|
return $return_format == 'object' ? new Asset($row) : $row;
|
|
}
|
|
|
|
public static function fromIds(array $id_assets, $return_format = 'array')
|
|
{
|
|
if (empty($id_assets))
|
|
return [];
|
|
|
|
$db = Registry::get('db');
|
|
|
|
$res = $db->query('
|
|
SELECT *
|
|
FROM assets
|
|
WHERE id_asset IN ({array_int:id_assets})
|
|
ORDER BY id_asset',
|
|
[
|
|
'id_assets' => $id_assets,
|
|
]);
|
|
|
|
$assets = [];
|
|
while ($asset = $db->fetch_assoc($res))
|
|
{
|
|
$assets[$asset['id_asset']] = $asset;
|
|
$assets[$asset['id_asset']]['meta'] = [];
|
|
$assets[$asset['id_asset']]['thumbnails'] = [];
|
|
}
|
|
|
|
$metas = $db->queryRows('
|
|
SELECT id_asset, variable, value
|
|
FROM assets_meta
|
|
WHERE id_asset IN ({array_int:id_assets})
|
|
ORDER BY id_asset',
|
|
[
|
|
'id_assets' => $id_assets,
|
|
]);
|
|
|
|
foreach ($metas as $meta)
|
|
$assets[$meta[0]]['meta'][$meta[1]] = $meta[2];
|
|
|
|
$thumbnails = $db->queryRows('
|
|
SELECT id_asset,
|
|
CONCAT(
|
|
width,
|
|
{string:x},
|
|
height,
|
|
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
|
) AS selector, filename
|
|
FROM assets_thumbs
|
|
WHERE id_asset IN ({array_int:id_assets})
|
|
ORDER BY id_asset',
|
|
[
|
|
'id_assets' => $id_assets,
|
|
'empty' => '',
|
|
'x' => 'x',
|
|
'_' => '_',
|
|
]);
|
|
|
|
foreach ($thumbnails as $thumb)
|
|
$assets[$thumb[0]]['thumbnails'][$thumb[1]] = $thumb[2];
|
|
|
|
if ($return_format == 'array')
|
|
return $assets;
|
|
else
|
|
{
|
|
$objects = [];
|
|
foreach ($assets as $id => $asset)
|
|
$objects[$id] = new Asset($asset);
|
|
return $objects;
|
|
}
|
|
}
|
|
|
|
public static function createNew(array $data, $return_format = 'object')
|
|
{
|
|
// Extract the data array.
|
|
extract($data);
|
|
|
|
// No filename? Abort!
|
|
if (!isset($filename_to_copy) || !is_file($filename_to_copy))
|
|
return false;
|
|
|
|
// No subdir? Use YYYY/MM
|
|
if (!isset($preferred_subdir))
|
|
$preferred_subdir = date('Y') . '/' . date('m');
|
|
|
|
// Does this dir exist yet? If not, create it.
|
|
if (!is_dir(ASSETSDIR . '/' . $preferred_subdir))
|
|
mkdir(ASSETSDIR . '/' . $preferred_subdir, 0755, true);
|
|
|
|
// Construct the destination filename. Make sure we don't accidentally overwrite anything.
|
|
if (!isset($preferred_filename))
|
|
$preferred_filename = basename($filename_to_copy);
|
|
|
|
$new_filename = $preferred_filename;
|
|
$destination = ASSETSDIR . '/' . $preferred_subdir . '/' . $preferred_filename;
|
|
while (file_exists($destination))
|
|
{
|
|
$filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . '_' . mt_rand(10, 99);
|
|
$extension = pathinfo($preferred_filename, PATHINFO_EXTENSION);
|
|
$new_filename = $filename . '.' . $extension;
|
|
$destination = dirname($destination) . '/' . $new_filename;
|
|
}
|
|
|
|
// Can we write to the target directory? Then copy the file.
|
|
if (is_writable(ASSETSDIR . '/' . $preferred_subdir))
|
|
copy($filename_to_copy, $destination);
|
|
else
|
|
return false;
|
|
|
|
// Figure out the mime type for the file.
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mimetype = finfo_file($finfo, $destination);
|
|
finfo_close($finfo);
|
|
|
|
// Do we have a title yet? Otherwise, use the filename.
|
|
$title = isset($data['title']) ? $data['title'] : pathinfo($preferred_filename, PATHINFO_FILENAME);
|
|
|
|
// Same with the slug.
|
|
$slug = isset($data['slug']) ? $data['slug'] : $preferred_subdir . '/' . pathinfo($preferred_filename, PATHINFO_FILENAME);
|
|
|
|
// Detected an image?
|
|
if (substr($mimetype, 0, 5) == 'image')
|
|
{
|
|
$image = new Imagick($destination);
|
|
$d = $image->getImageGeometry();
|
|
|
|
// Get image dimensions, bearing orientation in mind.
|
|
switch ($image->getImageOrientation())
|
|
{
|
|
case Imagick::ORIENTATION_LEFTBOTTOM:
|
|
case Imagick::ORIENTATION_RIGHTTOP:
|
|
$image_width = $d['height'];
|
|
$image_height = $d['width'];
|
|
break;
|
|
|
|
default:
|
|
$image_width = $d['width'];
|
|
$image_height = $d['height'];
|
|
}
|
|
|
|
unset($image);
|
|
|
|
$exif = EXIF::fromFile($destination);
|
|
$date_captured = $exif->created_timestamp > 0 ? $exif->created_timestamp : time();
|
|
}
|
|
|
|
$db = Registry::get('db');
|
|
$res = $db->query('
|
|
INSERT INTO assets
|
|
(id_user_uploaded, subdir, filename, title, slug, mimetype, image_width, image_height, date_captured, priority)
|
|
VALUES
|
|
({int:id_user_uploaded}, {string:subdir}, {string:filename}, {string:title}, {string:slug}, {string:mimetype},
|
|
{int:image_width}, {int:image_height},
|
|
IF({int:date_captured} > 0, FROM_UNIXTIME({int:date_captured}), NULL),
|
|
{int:priority})',
|
|
[
|
|
'id_user_uploaded' => isset($id_user) ? $id_user : Registry::get('user')->getUserId(),
|
|
'subdir' => $preferred_subdir,
|
|
'filename' => $new_filename,
|
|
'title' => $title,
|
|
'slug' => $slug,
|
|
'mimetype' => $mimetype,
|
|
'image_width' => isset($image_width) ? $image_width : 'NULL',
|
|
'image_height' => isset($image_height) ? $image_height : 'NULL',
|
|
'date_captured' => isset($date_captured) ? $date_captured : 'NULL',
|
|
'priority' => isset($priority) ? (int) $priority : 0,
|
|
]);
|
|
|
|
if (!$res)
|
|
{
|
|
unlink($destination);
|
|
return false;
|
|
}
|
|
|
|
$data['id_asset'] = $db->insert_id();
|
|
return $return_format == 'object' ? new self($data) : $data;
|
|
}
|
|
|
|
public function getId()
|
|
{
|
|
return $this->id_asset;
|
|
}
|
|
|
|
public function getAuthor()
|
|
{
|
|
return Member::fromId($this->id_user_uploaded);
|
|
}
|
|
|
|
public function getDateCaptured()
|
|
{
|
|
return $this->date_captured;
|
|
}
|
|
|
|
public function getFilename()
|
|
{
|
|
return $this->filename;
|
|
}
|
|
|
|
public function getLinkedPosts()
|
|
{
|
|
$posts = Registry::get('db')->queryValues('
|
|
SELECT id_post
|
|
FROM posts_assets
|
|
WHERE id_asset = {int:id_asset}',
|
|
['id_asset' => $this->id_asset]);
|
|
|
|
// TODO: fix empty post iterator.
|
|
if (empty($posts))
|
|
return [];
|
|
|
|
return PostIterator::getByOptions([
|
|
'ids' => $posts,
|
|
'type' => '',
|
|
]);
|
|
}
|
|
|
|
public function getMeta()
|
|
{
|
|
return $this->meta;
|
|
}
|
|
|
|
public function getFullPath()
|
|
{
|
|
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
|
}
|
|
|
|
public function getSlug()
|
|
{
|
|
return $this->slug;
|
|
}
|
|
|
|
public function getSubdir()
|
|
{
|
|
return $this->subdir;
|
|
}
|
|
|
|
public function getPriority()
|
|
{
|
|
return $this->priority;
|
|
}
|
|
|
|
public function getTags()
|
|
{
|
|
if (!isset($this->tags))
|
|
$this->tags = Tag::byAssetId($this->id_asset);
|
|
|
|
return $this->tags;
|
|
}
|
|
|
|
public function getTitle()
|
|
{
|
|
return $this->title;
|
|
}
|
|
|
|
public function getUrl()
|
|
{
|
|
return BASEURL . '/assets/' . $this->subdir . '/' . $this->filename;
|
|
}
|
|
|
|
public function getPageUrl()
|
|
{
|
|
return BASEURL . '/' . $this->slug . '/';
|
|
}
|
|
|
|
public function getType()
|
|
{
|
|
return substr($this->mimetype, 0, strpos($this->mimetype, '/'));
|
|
}
|
|
|
|
public function getDimensions($as_type = 'string')
|
|
{
|
|
return $as_type === 'string' ? $this->image_width . 'x' . $this->image_height : [$this->image_width, $this->image_height];
|
|
}
|
|
|
|
public function isImage()
|
|
{
|
|
return substr($this->mimetype, 0, 5) === 'image';
|
|
}
|
|
|
|
public function getImage()
|
|
{
|
|
if (!$this->isImage())
|
|
throw new Exception('Trying to upgrade an Asset to an Image while the Asset is not an image!');
|
|
|
|
return new Image(get_object_vars($this));
|
|
}
|
|
|
|
public function replaceFile($filename)
|
|
{
|
|
// No filename? Abort!
|
|
if (!isset($filename) || !is_readable($filename))
|
|
return false;
|
|
|
|
// Can we write to the target file?
|
|
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
|
if (!is_writable($destination))
|
|
return false;
|
|
|
|
copy($filename, $destination);
|
|
|
|
// Figure out the mime type for the file.
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$this->mimetype = finfo_file($finfo, $destination);
|
|
finfo_close($finfo);
|
|
|
|
// Detected an image?
|
|
if (substr($this->mimetype, 0, 5) == 'image')
|
|
{
|
|
$image = new Imagick($destination);
|
|
$d = $image->getImageGeometry();
|
|
$this->image_width = $d['width'];
|
|
$this->image_height = $d['height'];
|
|
unset($image);
|
|
|
|
$exif = EXIF::fromFile($destination);
|
|
if (!empty($exif->created_timestamp))
|
|
$this->date_captured = new DateTime(date('r', $exif->created_timestamp));
|
|
else
|
|
$this->date_captured = null;
|
|
}
|
|
else
|
|
{
|
|
$this->image_width = null;
|
|
$this->image_height = null;
|
|
$this->date_captured = null;
|
|
}
|
|
|
|
return Registry::get('db')->query('
|
|
UPDATE assets
|
|
SET
|
|
mimetype = {string:mimetype},
|
|
image_width = {int:image_width},
|
|
image_height = {int:image_height},
|
|
date_captured = {datetime:date_captured},
|
|
priority = {int:priority}
|
|
WHERE id_asset = {int:id_asset}',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
'mimetype' => $this->mimetype,
|
|
'image_width' => isset($this->image_width) ? $this->image_width : 'NULL',
|
|
'image_height' => isset($this->image_height) ? $this->image_height : 'NULL',
|
|
'date_captured' => isset($this->date_captured) ? $this->date_captured : 'NULL',
|
|
'priority' => $this->priority,
|
|
]);
|
|
}
|
|
|
|
protected function saveMetaData()
|
|
{
|
|
$this->setMetaData($this->meta);
|
|
}
|
|
|
|
public function setMetaData(array $new_meta, $mode = 'replace')
|
|
{
|
|
$db = Registry::get('db');
|
|
|
|
// If we're replacing, delete current data first.
|
|
if ($mode === 'replace')
|
|
{
|
|
$to_remove = array_diff_key($this->meta, $new_meta);
|
|
if (!empty($to_remove))
|
|
$db->query('
|
|
DELETE FROM assets_meta
|
|
WHERE id_asset = {int:id_asset} AND
|
|
variable IN({array_string:variables})',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
'variables' => array_keys($to_remove),
|
|
]);
|
|
}
|
|
|
|
// Build rows
|
|
$to_insert = [];
|
|
foreach ($new_meta as $key => $value)
|
|
$to_insert[] = [
|
|
'id_asset' => $this->id_asset,
|
|
'variable' => $key,
|
|
'value' => $value,
|
|
];
|
|
|
|
// Do the insertion
|
|
$res = Registry::get('db')->insert('replace', 'assets_meta', [
|
|
'id_asset' => 'int',
|
|
'variable' => 'string',
|
|
'value' => 'string',
|
|
], $to_insert, ['id_asset', 'variable']);
|
|
|
|
if ($res)
|
|
$this->meta = $new_meta;
|
|
}
|
|
|
|
public function delete()
|
|
{
|
|
$db = Registry::get('db');
|
|
|
|
if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename))
|
|
return false;
|
|
|
|
$db->query('
|
|
DELETE FROM assets_meta
|
|
WHERE id_asset = {int:id_asset}',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
]);
|
|
|
|
$recount_tags = $db->queryValues('
|
|
SELECT id_tag
|
|
FROM assets_tags
|
|
WHERE id_asset = {int:id_asset}',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
]);
|
|
|
|
$db->query('
|
|
DELETE FROM assets_tags
|
|
WHERE id_asset = {int:id_asset}',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
]);
|
|
|
|
Tag::recount($recount_tags);
|
|
|
|
$return = $db->query('
|
|
DELETE FROM assets
|
|
WHERE id_asset = {int:id_asset}',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
]);
|
|
|
|
$rows = $db->query('
|
|
SELECT id_tag
|
|
FROM tags
|
|
WHERE id_asset_thumb = {int:id_asset}',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
]);
|
|
|
|
if (!empty($rows))
|
|
{
|
|
foreach ($rows as $row)
|
|
{
|
|
$tag = Tag::fromId($row['id_tag']);
|
|
$tag->resetIdAsset();
|
|
}
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
public function linkTags(array $id_tags)
|
|
{
|
|
if (empty($id_tags))
|
|
return true;
|
|
|
|
$pairs = [];
|
|
foreach ($id_tags as $id_tag)
|
|
$pairs[] = ['id_asset' => $this->id_asset, 'id_tag' => $id_tag];
|
|
|
|
Registry::get('db')->insert('ignore', 'assets_tags', [
|
|
'id_asset' => 'int',
|
|
'id_tag' => 'int',
|
|
], $pairs, ['id_asset', 'id_tag']);
|
|
|
|
Tag::recount($id_tags);
|
|
}
|
|
|
|
public function unlinkTags(array $id_tags)
|
|
{
|
|
if (empty($id_tags))
|
|
return true;
|
|
|
|
Registry::get('db')->query('
|
|
DELETE FROM assets_tags
|
|
WHERE id_asset = {int:id_asset} AND id_tag IN ({array_int:id_tags})',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
'id_tags' => $id_tags,
|
|
]);
|
|
|
|
Tag::recount($id_tags);
|
|
}
|
|
|
|
public static function getCount()
|
|
{
|
|
return Registry::get('db')->queryValue('
|
|
SELECT COUNT(*)
|
|
FROM assets');
|
|
}
|
|
|
|
public function setKeyData($title, $slug, DateTime $date_captured = null, $priority)
|
|
{
|
|
$params = [
|
|
'id_asset' => $this->id_asset,
|
|
'title' => $title,
|
|
'slug' => $slug,
|
|
'priority' => $priority,
|
|
];
|
|
|
|
if (isset($date_captured))
|
|
$params['date_captured'] = $date_captured->format('Y-m-d H:i:s');
|
|
|
|
return Registry::get('db')->query('
|
|
UPDATE assets
|
|
SET title = {string:title},
|
|
slug = {string:slug},' . (isset($date_captured) ? '
|
|
date_captured = {datetime:date_captured},' : '') . '
|
|
priority = {int:priority}
|
|
WHERE id_asset = {int:id_asset}',
|
|
$params);
|
|
}
|
|
|
|
public function getUrlForPreviousInSet($id_tag = null)
|
|
{
|
|
$row = Registry::get('db')->queryAssoc('
|
|
SELECT a.*
|
|
' . (isset($id_tag) ? '
|
|
FROM assets_tags AS t
|
|
INNER JOIN assets AS a ON a.id_asset = t.id_asset
|
|
WHERE t.id_tag = {int:id_tag} AND
|
|
(a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
|
|
ORDER BY a.date_captured DESC, a.id_asset DESC'
|
|
: '
|
|
FROM assets AS a
|
|
WHERE (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
|
|
ORDER BY date_captured ASC, a.id_asset ASC')
|
|
. '
|
|
LIMIT 1',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
'id_tag' => $id_tag,
|
|
'date_captured' => $this->date_captured,
|
|
]);
|
|
|
|
if ($row)
|
|
{
|
|
$obj = self::byRow($row, 'object');
|
|
return $obj->getPageUrl() . ($id_tag ? '?in=' . $id_tag : '');
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
|
|
public function getUrlForNextInSet($id_tag = null)
|
|
{
|
|
$row = Registry::get('db')->queryAssoc('
|
|
SELECT a.*
|
|
' . (isset($id_tag) ? '
|
|
FROM assets_tags AS t
|
|
INNER JOIN assets AS a ON a.id_asset = t.id_asset
|
|
WHERE t.id_tag = {int:id_tag} AND
|
|
(a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
|
|
ORDER BY a.date_captured ASC, a.id_asset ASC'
|
|
: '
|
|
FROM assets AS a
|
|
WHERE (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
|
|
ORDER BY date_captured DESC, a.id_asset DESC')
|
|
. '
|
|
LIMIT 1',
|
|
[
|
|
'id_asset' => $this->id_asset,
|
|
'id_tag' => $id_tag,
|
|
'date_captured' => $this->date_captured,
|
|
]);
|
|
|
|
if ($row)
|
|
{
|
|
$obj = self::byRow($row, 'object');
|
|
return $obj->getPageUrl() . ($id_tag ? '?in=' . $id_tag : '');
|
|
}
|
|
else
|
|
return false;
|
|
}
|
|
}
|