. // }}} namespace App\Entity; use App\Core\Cache; use App\Core\DB\DB; use App\Core\Entity; use App\Core\Event; use App\Core\GSFile; use function App\Core\I18n\_m; use App\Core\Log; use App\Core\Router\Router; use App\Util\Common; use App\Util\Exception\ClientException; use App\Util\Exception\DuplicateFoundException; use App\Util\Exception\NotFoundException; use App\Util\Exception\ServerException; use DateTimeInterface; /** * Entity for uploaded files * * @category DB * @package GNUsocial * * @author Zach Copley * @copyright 2010 StatusNet Inc. * @author Mikael Nordfeldth * @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org * @author Hugo Sales * @author Diogo Peralta Cordeiro * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ class Attachment extends Entity { // {{{ Autocode // @codeCoverageIgnoreStart private int $id; private int $lives = 1; private ?string $filehash; private ?string $mimetype; private ?string $filename; private ?int $size; private ?int $width; private ?int $height; private DateTimeInterface $modified; public function setId(int $id): self { $this->id = $id; return $this; } public function getId(): int { return $this->id; } /** * @return int */ public function getLives(): int { return $this->lives; } /** * @param int $lives */ public function setLives(int $lives): void { $this->lives = $lives; } public function setFilehash(?string $filehash): self { $this->filehash = $filehash; return $this; } public function getFilehash(): ?string { return $this->filehash; } public function setMimetype(?string $mimetype): self { $this->mimetype = $mimetype; return $this; } public function getMimetype(): ?string { return $this->mimetype; } public function setFilename(?string $filename): self { $this->filename = $filename; return $this; } public function getFilename(): ?string { return $this->filename; } public function setSize(?int $size): self { $this->size = $size; return $this; } public function getSize(): ?int { return $this->size; } public function setWidth(?int $width): self { $this->width = $width; return $this; } public function getWidth(): ?int { return $this->width; } public function setHeight(?int $height): self { $this->height = $height; return $this; } public function getHeight(): ?int { return $this->height; } public function setModified(DateTimeInterface $modified): self { $this->modified = $modified; return $this; } public function getModified(): DateTimeInterface { return $this->modified; } // @codeCoverageIgnoreEnd // }}} Autocode public function getMimetypeMajor(): ?string { $mime = $this->getMimetype(); return is_null($mime) ? $mime : GSFile::mimetypeMajor($mime); } public function getMimetypeMinor(): ?string { $mime = $this->getMimetype(); return is_null($mime) ? $mime : GSFile::mimetypeMinor($mime); } /** * @return int */ public function livesIncrementAndGet(): int { ++$this->lives; return $this->lives; } /** * @return int */ public function livesDecrementAndGet(): int { --$this->lives; return $this->lives; } const FILEHASH_ALGO = 'sha256'; /** * Delete a file if safe, removes dependencies, cleanups and flushes * * @return bool */ public function kill(): bool { if ($this->livesDecrementAndGet() <= 0) { return $this->delete(); } return true; } /** * Remove the respective file from disk */ public function deleteStorage(): bool { if (!is_null($filepath = $this->getPath())) { if (file_exists($filepath)) { if (@unlink($filepath) === false) { // @codeCoverageIgnoreStart Log::error("Failed deleting file for attachment with id={$this->getId()} at {$filepath}."); return false; // @codeCoverageIgnoreEnd } else { $this->setFilename(null); $this->setSize(null); // Important not to null neither width nor height DB::persist($this); DB::flush(); } } else { // @codeCoverageIgnoreStart Log::warning("File for attachment with id={$this->getId()} at {$filepath} was already deleted when I was going to handle it."); // @codeCoverageIgnoreEnd } } return true; } /** * Attachment delete always removes dependencies, cleanups and flushes */ protected function delete(): bool { if ($this->getLives() > 0) { // @codeCoverageIgnoreStart Log::warning("Deleting file {$this->getId()} with {$this->getLives()} lives. Why are you killing it so young?"); // @codeCoverageIgnoreEnd } // Delete related files from storage $files = []; if (!is_null($filepath = $this->getPath())) { $files[] = $filepath; } foreach ($this->getThumbnails() as $at) { $files[] = $at->getPath(); $at->delete(flush: false); } DB::remove($this); foreach ($files as $f) { if (file_exists($f)) { if (@unlink($f) === false) { // @codeCoverageIgnoreStart Log::error("Failed deleting file for attachment with id={$this->getId()} at {$f}."); // @codeCoverageIgnoreEnd } } else { // @codeCoverageIgnoreStart Log::warning("File for attachment with id={$this->getId()} at {$f} was already deleted when I was going to handle it."); // @codeCoverageIgnoreEnd } } DB::flush(); return true; } /** * TODO: Maybe this isn't the best way of handling titles * * @param null|Note $note * * @throws DuplicateFoundException * @throws NotFoundException * @throws ServerException * * @return string */ public function getBestTitle(?Note $note = null): string { // If we have a note, then the best title is the title itself if (!is_null(($note))) { $title = Cache::get('attachment-title-' . $this->getId() . '-' . $note->getId(), function () use ($note) { try { $attachment_to_note = DB::findOneBy('attachment_to_note', [ 'attachment_id' => $this->getId(), 'note_id' => $note->getId(), ]); if (!is_null($attachment_to_note->getTitle())) { return $attachment_to_note->getTitle(); } } catch (NotFoundException) { $title = null; Event::handle('AttachmentGetBestTitle', [$this, $note, &$title]); return $title; } return null; }); if ($title != null) { return $title; } } // Else if (!is_null($filename = $this->getFilename())) { // A filename would do just as well return $filename; } else { // Welp return _m('Untitled attachment'); } } /** * Find all thumbnails associated with this attachment. Don't bother caching as this is not supposed to be a common operation */ public function getThumbnails() { return DB::findBy('attachment_thumbnail', ['attachment_id' => $this->id]); } public function getPath() { $filename = $this->getFilename(); return is_null($filename) ? null : Common::config('attachments', 'dir') . DIRECTORY_SEPARATOR . $filename; } public function getUrl() { return Router::url('attachment_view', ['id' => $this->getId()]); } /** * @param null|string $size * @param bool $crop * * @throws ClientException * @throws NotFoundException * @throws ServerException * * @return AttachmentThumbnail */ public function getThumbnail(?string $size = null, bool $crop = false): AttachmentThumbnail { return AttachmentThumbnail::getOrCreate(attachment: $this, size: $size, crop: $crop); } public function getThumbnailUrl(?string $size = null) { return Router::url('attachment_thumbnail', ['id' => $this->getId(), 'size' => $size ?? Common::config('thumbnail', 'default_size')]); } public static function schemaDef(): array { return [ 'name' => 'attachment', 'fields' => [ 'id' => ['type' => 'serial', 'not null' => true], 'lives' => ['type' => 'int', 'not null' => true, 'description' => 'RefCount'], 'filehash' => ['type' => 'varchar', 'length' => 64, 'description' => 'sha256 of the file contents, if the file is stored locally'], 'mimetype' => ['type' => 'varchar', 'length' => 255, 'description' => 'resource mime type 127+1+127 as per rfc6838#section-4.2'], 'filename' => ['type' => 'varchar', 'length' => 191, 'description' => 'file name of resource when available'], 'size' => ['type' => 'int', 'description' => 'size of resource when available'], 'width' => ['type' => 'int', 'description' => 'width in pixels, if it can be described as such and data is available'], 'height' => ['type' => 'int', 'description' => 'height in pixels, if it can be described as such and data is available'], 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], ], 'primary key' => ['id'], 'unique keys' => [ 'attachment_filehash_uniq' => ['filehash'], 'attachment_filename_uniq' => ['filename'], ], 'indexes' => [ 'file_filehash_idx' => ['filehash'], ], ]; } }