pics/models/Asset.php
Aaron van Geffen e28fcd8b03 Move photo deletion from ViewPhoto to EditAsset
Removes the intermediate confirmation page, instead using JavaScript for confirmation.

Fixes an XSS issue, in that the previous method was not passing or checking the session (!)
2023-11-11 15:29:32 +01:00

713 lines
17 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 $slug;
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)
{
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 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'] ?? 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 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 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;
}
}