pics/models/Asset.php

781 lines
18 KiB
PHP

<?php
/*****************************************************************************
* Asset.php
* Contains key class Asset.
*
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/
class Asset
{
public $id_asset;
public $id_user_uploaded;
public $subdir;
public $filename;
public $title;
public $slug;
public $mimetype;
public $image_width;
public $image_height;
public $date_captured;
public $priority;
protected $meta;
protected $tags;
protected $thumbnails;
protected function __construct(array $data)
{
foreach ($data as $attribute => $value)
{
if (property_exists($this, $attribute))
$this->$attribute = $value;
}
if (!empty($data['date_captured']) && $data['date_captured'] !== 'NULL')
$this->date_captured = new DateTime($data['date_captured']);
}
public function canBeEditedBy(User $user)
{
return $this->isOwnedBy($user) || $user->isAdmin();
}
public static function cleanSlug($slug)
{
// Only alphanumerical chars, underscores and forward slashes are allowed
if (!preg_match_all('~([A-z0-9\/_]+)~', $slug, $allowedTokens, PREG_PATTERN_ORDER))
throw new UnexpectedValueException('Slug does not make sense.');
// Join valid substrings together with hyphens
return implode('-', $allowedTokens[1]);
}
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;
for ($i = 1; file_exists($destination); $i++)
{
$suffix = $i;
$filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . ' (' . $suffix . ')';
$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);
// We're going to need the base name a few times...
$basename = pathinfo($new_filename, PATHINFO_FILENAME);
// Do we have a title yet? Otherwise, use the filename.
$title = $data['title'] ?? $basename;
// Same with the slug.
$slug = $data['slug'] ?? self::cleanSlug(sprintf('%s/%s', $preferred_subdir, $basename));
// 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 getDeleteUrl()
{
return BASEURL . '/editasset/?id=' . $this->id_asset . '&delete';
}
public function getEditUrl()
{
return BASEURL . '/editasset/?id=' . $this->id_asset;
}
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 isset($this->mimetype) && 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 isOwnedBy(User $user)
{
return $this->id_user_uploaded == $user->getUserId();
}
public function moveToSubDir($destSubDir)
{
// Verify the original exists
$source = ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
if (!file_exists($source))
return -1;
// Ensure the intended target file doesn't exist yet
$destDir = ASSETSDIR . '/' . $destSubDir;
$destFile = $destDir . '/' . $this->filename;
if (file_exists($destFile))
return -2;
// Can we write to the target directory?
if (!is_writable($destDir))
return -3;
// Perform move
if (rename($source, $destFile))
{
$this->subdir = $destSubDir;
$this->slug = $this->subdir . '/' . $this->title;
Registry::get('db')->query('
UPDATE assets
SET subdir = {string:subdir},
slug = {string:slug}
WHERE id_asset = {int:id_asset}',
[
'id_asset' => $this->id_asset,
'subdir' => $this->subdir,
'slug' => $this->slug,
]);
return true;
}
return -4;
}
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');
// First: delete associated metadata
$db->query('
DELETE FROM assets_meta
WHERE id_asset = {int:id_asset}',
[
'id_asset' => $this->id_asset,
]);
// Second: figure out what tags to recount cardinality for
$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);
// Third: figure out what associated thumbs to delete
$thumbs_to_delete = $db->queryValues('
SELECT filename
FROM assets_thumbs
WHERE id_asset = {int:id_asset}',
[
'id_asset' => $this->id_asset,
]);
foreach ($thumbs_to_delete as $filename)
{
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename;
if (is_file($thumb_path))
unlink($thumb_path);
}
$db->query('
DELETE FROM assets_thumbs
WHERE id_asset = {int:id_asset}',
[
'id_asset' => $this->id_asset,
]);
// Reset asset ID for tags that use this asset for their thumbnail
$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();
}
}
// Finally, delete the actual asset
if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename))
return false;
$return = $db->query('
DELETE FROM assets
WHERE id_asset = {int:id_asset}',
[
'id_asset' => $this->id_asset,
]);
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 save()
{
if (empty($this->id_asset))
throw new UnexpectedValueException();
return Registry::get('db')->query('
UPDATE assets
SET id_asset = {int:id_asset},
id_user_uploaded = {int:id_user_uploaded},
subdir = {string:subdir},
filename = {string:filename},
title = {string:title},
slug = {string:slug},
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}',
get_object_vars($this));
}
protected function getUrlForAdjacentInSet($prevNext, ?Tag $tag, $activeFilter)
{
$next = $prevNext === 'next';
$previous = !$next;
$where = [];
$params = [
'id_asset' => $this->id_asset,
'date_captured' => $this->date_captured,
];
// Direction depends on whether we're browsing a tag or timeline
if (isset($tag))
{
$where[] = 't.id_tag = {int:id_tag}';
$params['id_tag'] = $tag->id_tag;
$params['where_op'] = $previous ? '<' : '>';
$params['order_dir'] = $previous ? 'DESC' : 'ASC';
}
else
{
$params['where_op'] = $previous ? '>' : '<';
$params['order_dir'] = $previous ? 'ASC' : 'DESC';
}
// Take active filter into account as well
if (!empty($activeFilter) && ($user = Member::fromSlug($activeFilter)) !== false)
{
$where[] = 'id_user_uploaded = {int:id_user_uploaded}';
$params['id_user_uploaded'] = $user->getUserId();
}
// Use complete ordering when sorting the set
$where[] = '(a.date_captured, a.id_asset) {raw:where_op} ' .
'({datetime:date_captured}, {int:id_asset})';
// Stringify conditions together
$where = '(' . implode(') AND (', $where) . ')';
// Run query, leaving out tags table if not required
$row = Registry::get('db')->queryAssoc('
SELECT a.*
FROM assets AS a
' . (isset($tag) ? '
INNER JOIN assets_tags AS t ON a.id_asset = t.id_asset' : '') . '
WHERE ' . $where . '
ORDER BY a.date_captured {raw:order_dir}, a.id_asset {raw:order_dir}
LIMIT 1',
$params);
if (!$row)
return false;
$obj = self::byRow($row, 'object');
$urlParams = [];
if (isset($tag))
$urlParams['in'] = $tag->id_tag;
if (!empty($activeFilter))
$urlParams['by'] = $activeFilter;
$queryString = !empty($urlParams) ? '?' . http_build_query($urlParams) : '';
return $obj->getPageUrl() . $queryString;
}
public function getUrlForPreviousInSet(?Tag $tag, $activeFilter)
{
return $this->getUrlForAdjacentInSet('previous', $tag, $activeFilter);
}
public function getUrlForNextInSet(?Tag $tag, $activeFilter)
{
return $this->getUrlForAdjacentInSet('next', $tag, $activeFilter);
}
}