$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 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)); } 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(?Tag $tag) { $row = Registry::get('db')->queryAssoc(' SELECT a.* ' . (isset($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' => isset($tag) ? $tag->id_tag : null, 'date_captured' => $this->date_captured, ]); if ($row) { $obj = self::byRow($row, 'object'); return $obj->getPageUrl() . ($tag ? '?in=' . $tag->id_tag : ''); } else return false; } public function getUrlForNextInSet(?Tag $tag) { $row = Registry::get('db')->queryAssoc(' SELECT a.* ' . (isset($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' => isset($tag) ? $tag->id_tag : null, 'date_captured' => $this->date_captured, ]); if ($row) { $obj = self::byRow($row, 'object'); return $obj->getPageUrl() . ($tag ? '?in=' . $tag->id_tag : ''); } else return false; } }