[ENTITY] Split Attachment in various new entities
Remove Attachment Scope Fixed some minor bugs Scope will be implemented later in v3. It doesn't make sense to have the scope handling being per attachment. Different actors can post the same attachment with different scopes. The attachment controller will assume the highest level of scope applied to the attachment and the rest will be handled at the note level. Motivation: * Remove title from attachment, as it's part of the relation between attachment and note. * Remove actor from attachment, many actors may publish the same attachment. * Remove is_local from attachment, as it's part of the relation between attachment and note. * Remove remote_url from attachment, different urls can return the same attachment. Addition: * Attachment now has a lives attribute, it's a reference counter with a nicer name * GSActorToAttachment * GSActorToRemoteURL * RemoteURL * RemoteURLToNote * RemoteURLToAttachment * AttachmentToNote now has a title attribute
This commit is contained in:
parent
a7c8da0534
commit
3f61537140
@ -75,12 +75,6 @@ class Avatar extends Component
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onAttachmentRefCount(int $attachment_id, int &$dependencies): bool
|
||||
{
|
||||
$dependencies += DB::count('avatar', ['attachment_id' => $attachment_id]);
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
// UTILS ----------------------------------
|
||||
|
||||
/**
|
||||
@ -126,7 +120,7 @@ class Avatar extends Component
|
||||
{
|
||||
$res = Cache::get("avatar-file-info-{$gsactor_id}",
|
||||
function () use ($gsactor_id) {
|
||||
return DB::dql('select f.id, f.filename, f.mimetype, f.title ' .
|
||||
return DB::dql('select f.id, f.filename, f.mimetype ' .
|
||||
'from App\Entity\Attachment f ' .
|
||||
'join Component\Avatar\Entity\Avatar a with f.id = a.attachment_id ' .
|
||||
'where a.gsactor_id = :gsactor_id',
|
||||
|
@ -29,7 +29,6 @@ use App\Core\GSFile;
|
||||
use App\Core\GSFile as M;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Security;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
@ -106,12 +105,9 @@ class Avatar extends Controller
|
||||
} else {
|
||||
throw new ClientException('Invalid form');
|
||||
}
|
||||
$attachment = GSFile::validateAndStoreFileAsAttachment(
|
||||
$attachment = GSFile::sanitizeAndStoreFileAsAttachment(
|
||||
$file,
|
||||
dest_dir: Common::config('attachments', 'dir'),
|
||||
actor_id: $gsactor_id,
|
||||
title: Security::sanitize($file->getClientOriginalName()),
|
||||
is_local: true
|
||||
dest_dir: Common::config('attachments', 'dir')
|
||||
);
|
||||
// Delete current avatar if there's one
|
||||
$avatar = DB::find('avatar', ['gsactor_id' => $gsactor_id]);
|
||||
|
@ -122,24 +122,17 @@ class Avatar extends Entity
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this avatar and, if safe, the corresponding file and thumbnails, which this owns
|
||||
* Delete this avatar and kill corresponding attachment
|
||||
*
|
||||
* @param bool $cascade
|
||||
* @param bool $flush
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(bool $cascade = true, bool $flush = true): void
|
||||
public function delete(): bool
|
||||
{
|
||||
DB::remove($this);
|
||||
if ($cascade) {
|
||||
$attachment = $this->getAttachment();
|
||||
// We can't use $attachment->isSafeDelete() because underlying findBy doesn't respect remove persistence
|
||||
if ($attachment->refCount() - 1 === 0) {
|
||||
$attachment->delete(cascade: true, flush: false);
|
||||
}
|
||||
}
|
||||
if ($flush) {
|
||||
DB::flush();
|
||||
}
|
||||
$attachment = $this->getAttachment();
|
||||
$attachment->kill();
|
||||
DB::flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function schemaDef(): array
|
||||
|
@ -28,13 +28,17 @@ use App\Core\Form;
|
||||
use App\Core\GSFile;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Security;
|
||||
use App\Entity\Attachment;
|
||||
use App\Entity\AttachmentToNote;
|
||||
use App\Entity\GSActorToAttachment;
|
||||
use App\Entity\GSActorToRemoteURL;
|
||||
use App\Entity\Note;
|
||||
use App\Entity\RemoteURL;
|
||||
use App\Entity\RemoteURLToNote;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\InvalidFormException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
@ -115,31 +119,42 @@ END;
|
||||
|
||||
$processed_attachments = [];
|
||||
foreach ($attachments as $f) { // where $f is a Symfony\Component\HttpFoundation\File\UploadedFile
|
||||
$processed_attachments[] = GSFile::validateAndStoreFileAsAttachment(
|
||||
$filesize = $f->getSize();
|
||||
Event::handle('EnforceQuota', [$actor_id, $filesize]);
|
||||
$processed_attachments[] = GSFile::sanitizeAndStoreFileAsAttachment(
|
||||
$f,
|
||||
dest_dir: Common::config('attachments', 'dir'),
|
||||
actor_id: $actor_id,
|
||||
title: Security::sanitize($f->getClientOriginalName()),
|
||||
is_local: true
|
||||
dest_dir: Common::config('attachments', 'dir')
|
||||
);
|
||||
}
|
||||
|
||||
$matched_urls = [];
|
||||
preg_match_all(self::URL_REGEX, $content, $matched_urls, PREG_SET_ORDER);
|
||||
foreach ($matched_urls as $match) {
|
||||
$processed_attachments[] = GSFile::validateAndStoreURLAsAttachment($match[0]);
|
||||
}
|
||||
|
||||
DB::persist($note);
|
||||
|
||||
// Need file and note ids for the next step
|
||||
DB::flush();
|
||||
if ($processed_attachments != []) {
|
||||
foreach ($processed_attachments as $a) {
|
||||
DB::persist(GSActorToAttachment::create(['attachment_id' => $a->getId(), 'gsactor_id' => $actor_id]));
|
||||
DB::persist(AttachmentToNote::create(['attachment_id' => $a->getId(), 'note_id' => $note->getId()]));
|
||||
}
|
||||
DB::flush();
|
||||
}
|
||||
|
||||
$matched_urls = [];
|
||||
$processed_urls = false;
|
||||
preg_match_all(self::URL_REGEX, $content, $matched_urls, PREG_SET_ORDER);
|
||||
foreach ($matched_urls as $match) {
|
||||
try {
|
||||
$remoteurl_id = RemoteURL::getOrCreate($match[0])->getId();
|
||||
DB::persist(GSActorToRemoteURL::create(['remoteurl_id' => $remoteurl_id, 'gsactor_id' => $actor_id]));
|
||||
DB::persist(RemoteURLToNote::create(['remoteurl_id' => $a->getId(), 'note_id' => $note->getId()]));
|
||||
$processed_urls = true;
|
||||
} catch (InvalidArgumentException) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ($processed_urls) {
|
||||
DB::flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,7 +71,7 @@ class ImageEncoder extends Plugin
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onAttachmentSanitization(SplFileInfo &$file, ?string &$mimetype, ?string &$title, ?int &$width, ?int &$height): bool
|
||||
public function onAttachmentSanitization(SplFileInfo &$file, ?string &$mimetype, ?int &$width, ?int &$height): bool
|
||||
{
|
||||
$original_mimetype = $mimetype;
|
||||
if (GSFile::mimetypeMajor($original_mimetype) != 'image') {
|
||||
@ -81,8 +81,8 @@ class ImageEncoder extends Plugin
|
||||
|
||||
// Try to maintain original mimetype extension, otherwise default to preferred.
|
||||
$extension = image_type_to_extension($this->preferredType(), include_dot: true);
|
||||
GSFile::titleToFilename(
|
||||
title: $title,
|
||||
GSFile::ensureFilenameWithProperExtension(
|
||||
title: $file->getFilename(),
|
||||
mimetype: $original_mimetype,
|
||||
ext: $extension,
|
||||
force: false
|
||||
|
@ -1,7 +1,7 @@
|
||||
<figure>
|
||||
<img class="u-photo" src="{{ path('attachment_thumbnail', thumbnail_parameters) }}"
|
||||
alt="{{ attachment.getTitle() }}">
|
||||
alt="{{ attachment.getFilename() }}">
|
||||
<figcaption><a
|
||||
href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getTitle() }}</a>
|
||||
href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getFilename() }}</a>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<video class="u-video" src="{{ path('attachment_view', {'id': attachment.getId()}) }}" controls poster="{{ path('attachment_thumbnail', thumbnail_parameters) }}">
|
||||
</video>
|
||||
<figcaption><a
|
||||
href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getTitle() }}</a>
|
||||
href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getFilename() }}</a>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
@ -75,7 +75,6 @@ class Attachment extends Controller
|
||||
return $this->attachment($id, function ($res) use ($id, $attachment) {
|
||||
return [
|
||||
'_template' => 'attachments/show.html.twig',
|
||||
'title' => $res['title'],
|
||||
'download' => Router::url('attachment_download', ['id' => $id]),
|
||||
'attachment' => $attachment,
|
||||
'right_panel_vars' => ['attachment_id' => $id],
|
||||
@ -91,12 +90,12 @@ class Attachment extends Controller
|
||||
*/
|
||||
public function attachment_view(Request $request, int $id)
|
||||
{
|
||||
return $this->attachment($id, fn (array $res) => GSFile::sendFile($res['filepath'], $res['mimetype'], GSFile::titleToFilename($res['title'], $res['mimetype']), HeaderUtils::DISPOSITION_INLINE));
|
||||
return $this->attachment($id, fn (array $res) => GSFile::sendFile($res['filepath'], $res['mimetype'], GSFile::ensureFilenameWithProperExtension($res['filename'], $res['mimetype']) ?? $res['filename'], HeaderUtils::DISPOSITION_INLINE));
|
||||
}
|
||||
|
||||
public function attachment_download(Request $request, int $id)
|
||||
{
|
||||
return $this->attachment($id, fn (array $res) => GSFile::sendFile($res['filepath'], $res['mimetype'], GSFile::titleToFilename($res['title'], $res['mimetype']), HeaderUtils::DISPOSITION_ATTACHMENT));
|
||||
return $this->attachment($id, fn (array $res) => GSFile::sendFile($res['filepath'], $res['mimetype'], GSFile::ensureFilenameWithProperExtension($res['filename'], $res['mimetype']) ?? $res['filename'], HeaderUtils::DISPOSITION_ATTACHMENT));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,14 +115,6 @@ class Attachment extends Controller
|
||||
{
|
||||
$attachment = DB::findOneBy('attachment', ['id' => $id]);
|
||||
|
||||
if (!is_null($attachment->getScope())) {
|
||||
// @codeCoverageIgnoreStart
|
||||
// && ($attachment->scope | VisibilityScope::PUBLIC) != 0
|
||||
// $user = Common::ensureLoggedIn();
|
||||
assert(false, 'Attachment scope not implemented');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$default_width = Common::config('thumbnail', 'width');
|
||||
$default_height = Common::config('thumbnail', 'height');
|
||||
$default_crop = Common::config('thumbnail', 'smart_crop');
|
||||
@ -140,7 +131,7 @@ class Attachment extends Controller
|
||||
|
||||
$filename = $thumbnail->getFilename();
|
||||
$path = $thumbnail->getPath();
|
||||
$mimetype = $attachment->getMimetype();
|
||||
$mimetype = $thumbnail->getMimetype();
|
||||
|
||||
return GSFile::sendFile(filepath: $path, mimetype: $mimetype, output_filename: $filename . '.' . MimeTypes::getDefault()->getExtensions($mimetype)[0], disposition: HeaderUtils::DISPOSITION_INLINE);
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ use App\Util\Exception\NoSuchFileException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\Formatting;
|
||||
use InvalidArgumentException;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\HeaderUtils;
|
||||
@ -51,7 +50,7 @@ use Symfony\Component\Mime\MimeTypes;
|
||||
class GSFile
|
||||
{
|
||||
/**
|
||||
* Perform file validation (checks and normalization) and store the given file
|
||||
* Perform file validation (checks and normalization), store the given file if needed and increment lives
|
||||
*
|
||||
* @param SplFileInfo $file
|
||||
* @param string $dest_dir
|
||||
@ -63,11 +62,8 @@ class GSFile
|
||||
*
|
||||
* @return Attachment
|
||||
*/
|
||||
public static function validateAndStoreFileAsAttachment(SplFileInfo $file,
|
||||
string $dest_dir,
|
||||
int $actor_id,
|
||||
?string $title = null,
|
||||
bool $is_local = true): Attachment
|
||||
public static function sanitizeAndStoreFileAsAttachment(SplFileInfo $file,
|
||||
string $dest_dir): Attachment
|
||||
{
|
||||
if (!Formatting::startsWith($dest_dir, Common::config('storage', 'dir'))) {
|
||||
throw new \InvalidArgumentException("Attempted to store a file in a directory outside the GNU social files location: {$dest_dir}");
|
||||
@ -76,65 +72,27 @@ class GSFile
|
||||
$hash = null;
|
||||
Event::handle('HashFile', [$file->getPathname(), &$hash]);
|
||||
try {
|
||||
return DB::findOneBy('attachment', ['file_hash' => $hash]);
|
||||
$attachment = DB::findOneBy('attachment', ['filehash' => $hash]);
|
||||
$attachment->livesIncrementAndGet();
|
||||
} catch (NotFoundException) {
|
||||
// The following properly gets the mimetype with `file` or other
|
||||
// available methods, so should be safe
|
||||
$mimetype = $file->getMimeType();
|
||||
$width = $height = null;
|
||||
Event::handle('AttachmentSanitization', [&$file, &$mimetype, &$title, &$width, &$height]);
|
||||
if ($is_local) {
|
||||
$filesize = $file->getSize();
|
||||
Event::handle('EnforceQuota', [$actor_id, $filesize]);
|
||||
}
|
||||
Event::handle('AttachmentSanitization', [&$file, &$mimetype, &$width, &$height]);
|
||||
$attachment = Attachment::create([
|
||||
'file_hash' => $hash,
|
||||
'gsactor_id' => $actor_id,
|
||||
'mimetype' => $mimetype,
|
||||
'title' => $title,
|
||||
'filename' => Formatting::removePrefix($dest_dir, Common::config('attachments', 'dir')) . $hash,
|
||||
'is_local' => $is_local,
|
||||
'size' => $file->getSize(),
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'filehash' => $hash,
|
||||
'mimetype' => $mimetype,
|
||||
'filename' => Formatting::removePrefix($dest_dir, Common::config('attachments', 'dir')) . $hash,
|
||||
'size' => $file->getSize(),
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
]);
|
||||
$file->move($dest_dir, $hash);
|
||||
DB::persist($attachment);
|
||||
Event::handle('AttachmentStoreNew', [&$attachment]);
|
||||
return $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an attachment for the given URL, fetching the mimetype
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function validateAndStoreURLAsAttachment(string $url): Attachment
|
||||
{
|
||||
if (Common::isValidHttpUrl($url)) {
|
||||
$head = HTTPClient::head($url);
|
||||
// This must come before getInfo given that Symfony HTTPClient is lazy (thus forcing curl exec)
|
||||
$headers = $head->getHeaders();
|
||||
$url = $head->getInfo('url'); // The last effective url (after getHeaders so it follows redirects)
|
||||
$url_hash = hash(Attachment::URLHASH_ALGO, $url);
|
||||
try {
|
||||
return DB::findOneBy('attachment', ['remote_url_hash' => $url_hash]);
|
||||
} catch (NotFoundException) {
|
||||
$headers = array_change_key_case($headers, CASE_LOWER);
|
||||
$attachment = Attachment::create([
|
||||
'remote_url' => $url,
|
||||
'remote_url_hash' => $url_hash,
|
||||
'mimetype' => $headers['content-type'][0],
|
||||
'is_local' => false,
|
||||
]);
|
||||
DB::persist($attachment);
|
||||
Event::handle('AttachmentStoreNew', [&$attachment]);
|
||||
return $attachment;
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -203,7 +161,7 @@ class GSFile
|
||||
$id,
|
||||
Cache::get("file-info-{$id}",
|
||||
function () use ($id) {
|
||||
return DB::dql('select at.filename, at.mimetype, at.title ' .
|
||||
return DB::dql('select at.filename, at.mimetype ' .
|
||||
'from App\\Entity\\Attachment at ' .
|
||||
'where at.id = :id',
|
||||
['id' => $id]);
|
||||
@ -255,16 +213,16 @@ class GSFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an attachment title and mimetype allows to generate the most appropriate filename.
|
||||
* Given an attachment filename and mimetype allows to generate the most appropriate filename.
|
||||
*
|
||||
* @param string $title
|
||||
* @param string $mimetype
|
||||
* @param null|string $ext
|
||||
* @param bool $force
|
||||
* @param string $title Original filename with or without extension
|
||||
* @param string $mimetype Original mimetype of the file
|
||||
* @param null|string $ext Extension we believe to be best
|
||||
* @param bool $force Should we force the extension we believe to be best? Defaults to false
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public static function titleToFilename(string $title, string $mimetype, ?string &$ext = null, bool $force = false): string | null
|
||||
public static function ensureFilenameWithProperExtension(string $title, string $mimetype, ?string &$ext = null, bool $force = false): string | null
|
||||
{
|
||||
$valid_extensions = MimeTypes::getDefault()->getExtensions($mimetype);
|
||||
|
||||
@ -286,6 +244,9 @@ class GSFile
|
||||
if (!empty($valid_extensions)) {
|
||||
return "{$title}.{$valid_extensions[0]}";
|
||||
} else {
|
||||
if (!is_null($ext)) {
|
||||
return ($title_without_extension ?? $title) . ".{$ext}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class CoreFixtures extends Fixture
|
||||
$copy_filepath = $filepath . '.copy';
|
||||
copy($filepath, $copy_filepath);
|
||||
$file = new File($copy_filepath, checkPath: true);
|
||||
GSFile::validateAndStoreFileAsAttachment($file, dest_dir: Common::config('attachments', 'dir') . 'test/', title: '1x1 JPEG image title', actor_id: $actors['taken_user']->getId());
|
||||
GSFile::sanitizeAndStoreFileAsAttachment($file, dest_dir: Common::config('attachments', 'dir') . 'test/');
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ namespace App\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use App\Core\Log;
|
||||
use App\Util\Common;
|
||||
@ -40,6 +39,7 @@ use DateTimeInterface;
|
||||
* @author Mikael Nordfeldth <mmn@hethane.se>
|
||||
* @copyright 2009-2014 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @author Hugo Sales <hugo@hsal.es>
|
||||
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
||||
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
@ -48,16 +48,10 @@ class Attachment extends Entity
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $id;
|
||||
private ?string $remote_url;
|
||||
private ?string $remote_url_hash;
|
||||
private ?string $file_hash;
|
||||
private ?int $gsactor_id;
|
||||
private int $lives = 1;
|
||||
private ?string $filehash;
|
||||
private ?string $mimetype;
|
||||
private ?string $title;
|
||||
private ?string $filename;
|
||||
private ?bool $is_local;
|
||||
private ?int $source;
|
||||
private ?int $scope;
|
||||
private ?int $size;
|
||||
private ?int $width;
|
||||
private ?int $height;
|
||||
@ -74,48 +68,32 @@ class Attachment extends Entity
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setRemoteUrl(?string $remote_url): self
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
|
||||
public function getLives(): int
|
||||
{
|
||||
$this->remote_url = $remote_url;
|
||||
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 getRemoteUrl(): ?string
|
||||
public function getFilehash(): ?string
|
||||
{
|
||||
return $this->remote_url;
|
||||
}
|
||||
|
||||
public function setRemoteUrlHash(?string $remote_url_hash): self
|
||||
{
|
||||
$this->remote_url_hash = $remote_url_hash;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRemoteUrlHash(): ?string
|
||||
{
|
||||
return $this->remote_url_hash;
|
||||
}
|
||||
|
||||
public function setFileHash(?string $file_hash): self
|
||||
{
|
||||
$this->file_hash = $file_hash;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFileHash(): ?string
|
||||
{
|
||||
return $this->file_hash;
|
||||
}
|
||||
|
||||
public function setGSActorId(?int $gsactor_id): self
|
||||
{
|
||||
$this->gsactor_id = $gsactor_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGSActorId(): ?int
|
||||
{
|
||||
return $this->gsactor_id;
|
||||
return $this->filehash;
|
||||
}
|
||||
|
||||
public function setMimetype(?string $mimetype): self
|
||||
@ -141,17 +119,6 @@ class Attachment extends Entity
|
||||
return is_null($mime) ? $mime : GSFile::mimetypeMinor($mime);
|
||||
}
|
||||
|
||||
public function setTitle(?string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setFilename(?string $filename): self
|
||||
{
|
||||
$this->filename = $filename;
|
||||
@ -163,39 +130,6 @@ class Attachment extends Entity
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
public function setIsLocal(?bool $is_local): self
|
||||
{
|
||||
$this->is_local = $is_local;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsLocal(): ?bool
|
||||
{
|
||||
return $this->is_local;
|
||||
}
|
||||
|
||||
public function setSource(?int $source): self
|
||||
{
|
||||
$this->source = $source;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSource(): ?int
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function setScope(?int $scope): self
|
||||
{
|
||||
$this->scope = $scope;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getScope(): ?int
|
||||
{
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function setSize(?int $size): self
|
||||
{
|
||||
$this->size = $size;
|
||||
@ -243,51 +177,63 @@ class Attachment extends Entity
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
const URLHASH_ALGO = 'sha256';
|
||||
/**
|
||||
* @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';
|
||||
|
||||
public function refCount(): int
|
||||
public function kill(): bool
|
||||
{
|
||||
$attachment_id = $this->getId();
|
||||
$dependencies = DB::count('attachment_to_note', ['attachment_id' => $attachment_id]);
|
||||
Event::handle('AttachmentRefCount', [$attachment_id, &$dependencies]);
|
||||
return $dependencies;
|
||||
if ($this->livesDecrementAndGet() <= 0) {
|
||||
return $this->delete();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends Attachment->refCount()
|
||||
*
|
||||
* @return bool
|
||||
* Attachment delete always removes dependencies, cleanups and flushes
|
||||
*/
|
||||
public function isSafeDelete(): bool
|
||||
{
|
||||
return $this->refCount() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this attachment and optionally all the associated entities (avatar and/or thumbnails, which this owns)
|
||||
*/
|
||||
public function delete(bool $cascade = true, bool $flush = true): void
|
||||
public function delete(): bool
|
||||
{
|
||||
if ($this->getLives() > 0) {
|
||||
Log::warning("Deleting file {$this->getId()} with {$this->getLives()} lives. Why are you killing it so young?");
|
||||
}
|
||||
// Delete related files from storage
|
||||
$files = [];
|
||||
if ($cascade) {
|
||||
foreach ($this->getThumbnails() as $at) {
|
||||
$files[] = $at->getPath();
|
||||
$at->delete(flush: false);
|
||||
}
|
||||
if (!is_null($filepath = $this->getPath())) {
|
||||
$files[] = $filepath;
|
||||
}
|
||||
foreach ($this->getThumbnails() as $at) {
|
||||
$files[] = $at->getPath();
|
||||
$at->delete(flush: false);
|
||||
}
|
||||
$files[] = $this->getPath();
|
||||
DB::remove($this);
|
||||
if ($flush) {
|
||||
DB::flush();
|
||||
}
|
||||
foreach ($files as $f) {
|
||||
if (file_exists($f)) {
|
||||
if (@unlink($f) === false) {
|
||||
Log::warning("Failed deleting file for attachment with id={$this->id} at {$f}");
|
||||
Log::error("Failed deleting file for attachment with id={$this->getId()} at {$f}.");
|
||||
}
|
||||
} else {
|
||||
Log::warning("File for attachment with id={$this->getId()} at {$f} was already deleted when I was going to handle it.");
|
||||
}
|
||||
}
|
||||
DB::flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -300,7 +246,8 @@ class Attachment extends Entity
|
||||
|
||||
public function getPath()
|
||||
{
|
||||
return Common::config('attachments', 'dir') . $this->getFilename();
|
||||
$filename = $this->getFilename();
|
||||
return is_null($filename) ? null : Common::config('attachments', 'dir') . $filename;
|
||||
}
|
||||
|
||||
public function getAttachmentUrl()
|
||||
@ -313,28 +260,23 @@ class Attachment extends Entity
|
||||
return [
|
||||
'name' => 'attachment',
|
||||
'fields' => [
|
||||
'id' => ['type' => 'serial', 'not null' => true],
|
||||
'remote_url' => ['type' => 'text', 'description' => 'URL after following possible redirections'],
|
||||
'remote_url_hash' => ['type' => 'varchar', 'length' => 64, 'description' => 'sha256 of destination URL (url field)'],
|
||||
'file_hash' => ['type' => 'varchar', 'length' => 64, 'description' => 'sha256 of the file contents, if the file is stored locally'],
|
||||
'gsactor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'GSActor.id', 'multiplicity' => 'one to one', 'description' => 'If set, used so each actor can have a version of this file (for avatars, for instance)'],
|
||||
'mimetype' => ['type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'],
|
||||
'title' => ['type' => 'text', 'description' => 'title of resource when available'],
|
||||
'filename' => ['type' => 'varchar', 'length' => 191, 'description' => 'file name of resource when available'],
|
||||
'is_local' => ['type' => 'bool', 'description' => 'whether the file is stored locally'],
|
||||
'source' => ['type' => 'int', 'default' => null, 'description' => 'Source of the Attachment (upload, TFN, embed)'],
|
||||
'scope' => ['type' => 'int', 'default' => null, 'description' => 'visibility scope for this attachment'],
|
||||
'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'],
|
||||
'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' => 50, 'description' => 'mime type of resource'],
|
||||
'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_file_hash_uniq' => ['file_hash'],
|
||||
'attachment_filehash_uniq' => ['filehash'],
|
||||
'attachment_filename_uniq' => ['filename'],
|
||||
],
|
||||
'indexes' => [
|
||||
'file_filehash_idx' => ['file_hash'],
|
||||
'file_filehash_idx' => ['filehash'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ class AttachmentThumbnail extends Entity
|
||||
$thumbnail->setWidth($predicted_width);
|
||||
$thumbnail->setHeight($predicted_height);
|
||||
$ext = '.' . MimeTypes::getDefault()->getExtensions($temp->getMimeType())[0];
|
||||
$filename = "{$predicted_width}x{$predicted_height}{$ext}-" . $attachment->getFileHash();
|
||||
$filename = "{$predicted_width}x{$predicted_height}{$ext}-" . $attachment->getFilehash();
|
||||
$thumbnail->setFilename($filename);
|
||||
$thumbnail->setMimetype($mimetype);
|
||||
DB::persist($thumbnail);
|
||||
|
@ -42,6 +42,7 @@ class AttachmentToNote extends Entity
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $attachment_id;
|
||||
private int $note_id;
|
||||
private ?string $title;
|
||||
private \DateTimeInterface $modified;
|
||||
|
||||
public function setAttachmentId(int $attachment_id): self
|
||||
@ -66,6 +67,17 @@ class AttachmentToNote extends Entity
|
||||
return $this->note_id;
|
||||
}
|
||||
|
||||
public function setTitle(?string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
@ -87,6 +99,7 @@ class AttachmentToNote extends Entity
|
||||
'fields' => [
|
||||
'attachment_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Attachment.id', 'multiplicity' => 'one to one', 'name' => 'attachment_to_note_attachment_id_fkey', 'not null' => true, 'description' => 'id of attachment'],
|
||||
'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'name' => 'attachment_to_note_note_id_fkey', 'not null' => true, 'description' => 'id of the note it belongs to'],
|
||||
'title' => ['type' => 'text', 'description' => 'title of resource when available'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['attachment_id', 'note_id'],
|
||||
|
95
src/Entity/GSActorToAttachment.php
Normal file
95
src/Entity/GSActorToAttachment.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Entity for relating an actor to an attachment
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
||||
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class GSActorToAttachment extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $attachment_id;
|
||||
private int $gsactor_id;
|
||||
private \DateTimeInterface $modified;
|
||||
|
||||
public function setAttachmentId(int $attachment_id): self
|
||||
{
|
||||
$this->attachment_id = $attachment_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAttachmentId(): int
|
||||
{
|
||||
return $this->attachment_id;
|
||||
}
|
||||
|
||||
public function setGSActorId(int $gsactor_id): self
|
||||
{
|
||||
$this->gsactor_id = $gsactor_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGSActorId(): int
|
||||
{
|
||||
return $this->gsactor_id;
|
||||
}
|
||||
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'gsactor_to_attachment',
|
||||
'fields' => [
|
||||
'attachment_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Attachment.id', 'multiplicity' => 'one to one', 'name' => 'attachment_to_note_attachment_id_fkey', 'not null' => true, 'description' => 'id of attachment'],
|
||||
'gsactor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'GSActor.id', 'multiplicity' => 'one to one', 'name' => 'attachment_to_note_note_id_fkey', 'not null' => true, 'description' => 'id of the note it belongs to'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['attachment_id', 'gsactor_id'],
|
||||
'indexes' => [
|
||||
'attachment_id_idx' => ['attachment_id'],
|
||||
'gsactor_id_idx' => ['gsactor_id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
95
src/Entity/GSActorToRemoteURL.php
Normal file
95
src/Entity/GSActorToRemoteURL.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Entity for relating an actor to a remote url
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
||||
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class GSActorToRemoteURL extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $remoteurl_id;
|
||||
private int $gsactor_id;
|
||||
private \DateTimeInterface $modified;
|
||||
|
||||
public function setRemoteURLId(int $remoteurl_id): self
|
||||
{
|
||||
$this->remoteurl_id = $remoteurl_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRemoteURLId(): int
|
||||
{
|
||||
return $this->remoteurl_id;
|
||||
}
|
||||
|
||||
public function setGSActorId(int $gsactor_id): self
|
||||
{
|
||||
$this->gsactor_id = $gsactor_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGSActorId(): int
|
||||
{
|
||||
return $this->gsactor_id;
|
||||
}
|
||||
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'gsactor_to_remoteurl',
|
||||
'fields' => [
|
||||
'remoteurl_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'RemoteURL.id', 'multiplicity' => 'one to one', 'name' => 'attachment_to_note_attachment_id_fkey', 'not null' => true, 'description' => 'id of attachment'],
|
||||
'gsactor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'GSActor.id', 'multiplicity' => 'one to one', 'name' => 'attachment_to_note_note_id_fkey', 'not null' => true, 'description' => 'id of the note it belongs to'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['remoteurl_id', 'gsactor_id'],
|
||||
'indexes' => [
|
||||
'remoteurl_id_idx' => ['remoteurl_id'],
|
||||
'gsactor_id_idx' => ['gsactor_id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
177
src/Entity/RemoteURL.php
Normal file
177
src/Entity/RemoteURL.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Core\DB\DB;
|
||||
use App\Core\Entity;
|
||||
use App\Core\Event;
|
||||
use App\Core\HTTPClient;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\DuplicateFoundException;
|
||||
use App\Util\Exception\NotFoundException;
|
||||
use DateTimeInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Entity for representing a RemoteURL
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
||||
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class RemoteURL extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $id;
|
||||
private ?string $remote_url;
|
||||
private ?string $remote_url_hash;
|
||||
private ?string $mimetype;
|
||||
private DateTimeInterface $modified;
|
||||
|
||||
public function setId(int $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getRemoteUrl(): ?string
|
||||
{
|
||||
return $this->remote_url;
|
||||
}
|
||||
|
||||
public function setRemoteUrl(?string $remote_url): self
|
||||
{
|
||||
$this->remote_url = $remote_url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRemoteUrlHash(?string $remote_url_hash): self
|
||||
{
|
||||
$this->remote_url_hash = $remote_url_hash;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRemoteUrlHash(): ?string
|
||||
{
|
||||
return $this->remote_url_hash;
|
||||
}
|
||||
|
||||
public function setMimetype(?string $mimetype): self
|
||||
{
|
||||
$this->mimetype = $mimetype;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMimetype(): ?string
|
||||
{
|
||||
return $this->mimetype;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
const URLHASH_ALGO = 'sha256';
|
||||
|
||||
/**
|
||||
* Create an attachment for the given URL, fetching the mimetype
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @throws DuplicateFoundException
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return RemoteURL
|
||||
*/
|
||||
public static function getOrCreate(string $url): self
|
||||
{
|
||||
if (Common::isValidHttpUrl($url)) {
|
||||
$head = HTTPClient::head($url);
|
||||
// This must come before getInfo given that Symfony HTTPClient is lazy (thus forcing curl exec)
|
||||
$headers = $head->getHeaders();
|
||||
$url = $head->getInfo('url'); // The last effective url (after getHeaders so it follows redirects)
|
||||
$url_hash = hash(self::URLHASH_ALGO, $url);
|
||||
try {
|
||||
return DB::findOneBy('remoteurl', ['remote_url_hash' => $url_hash]);
|
||||
} catch (NotFoundException) {
|
||||
$headers = array_change_key_case($headers, CASE_LOWER);
|
||||
$remoteurl = self::create([
|
||||
'remote_url' => $url,
|
||||
'remote_url_hash' => $url_hash,
|
||||
'mimetype' => $headers['content-type'][0],
|
||||
]);
|
||||
DB::persist($remoteurl);
|
||||
Event::handle('RemoteURLStoreNew', [&$remoteurl]);
|
||||
return $remoteurl;
|
||||
}
|
||||
} else {
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'remoteurl',
|
||||
'fields' => [
|
||||
'id' => ['type' => 'serial', 'not null' => true],
|
||||
'remote_url' => ['type' => 'text', 'description' => 'URL after following possible redirections'],
|
||||
'remote_url_hash' => ['type' => 'varchar', 'length' => 64, 'description' => 'sha256 of destination URL (url field)'],
|
||||
'mimetype' => ['type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['id'],
|
||||
'indexes' => [
|
||||
'gsactor_remote_url_hash_idx' => ['remote_url_hash'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
95
src/Entity/RemoteURLToAttachment.php
Normal file
95
src/Entity/RemoteURLToAttachment.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Entity for relating a remote url to an attachment
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
||||
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class RemoteURLToAttachment extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $attachment_id;
|
||||
private int $remoteurl_id;
|
||||
private \DateTimeInterface $modified;
|
||||
|
||||
public function setAttachmentId(int $attachment_id): self
|
||||
{
|
||||
$this->attachment_id = $attachment_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAttachmentId(): int
|
||||
{
|
||||
return $this->attachment_id;
|
||||
}
|
||||
|
||||
public function setRemoteURLId(int $remoteurl_id): self
|
||||
{
|
||||
$this->remoteurl_id = $remoteurl_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRemoteURLId(): int
|
||||
{
|
||||
return $this->remoteurl_id;
|
||||
}
|
||||
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'remoteurl_to_attachment',
|
||||
'fields' => [
|
||||
'remoteurl_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'RemoteURL.id', 'multiplicity' => 'one to one', 'name' => 'attachment_to_note_note_id_fkey', 'not null' => true, 'description' => 'id of the note it belongs to'],
|
||||
'attachment_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Attachment.id', 'multiplicity' => 'one to one', 'name' => 'attachment_to_note_attachment_id_fkey', 'not null' => true, 'description' => 'id of attachment'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['remoteurl_id', 'attachment_id'],
|
||||
'indexes' => [
|
||||
'remoteurl_id_idx' => ['remoteurl_id'],
|
||||
'attachment_id_idx' => ['attachment_id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
95
src/Entity/RemoteURLToNote.php
Normal file
95
src/Entity/RemoteURLToNote.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
// {{{ License
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
// }}}
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Core\Entity;
|
||||
use DateTimeInterface;
|
||||
|
||||
/**
|
||||
* Entity for relating a RemoteURL to a post
|
||||
*
|
||||
* @category DB
|
||||
* @package GNUsocial
|
||||
*
|
||||
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
||||
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class RemoteURLToNote extends Entity
|
||||
{
|
||||
// {{{ Autocode
|
||||
// @codeCoverageIgnoreStart
|
||||
private int $remoteurl_id;
|
||||
private int $note_id;
|
||||
private \DateTimeInterface $modified;
|
||||
|
||||
public function setRemoteURLId(int $remoteurl_id): self
|
||||
{
|
||||
$this->remoteurl_id = $remoteurl_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRemoteURLId(): int
|
||||
{
|
||||
return $this->remoteurl_id;
|
||||
}
|
||||
|
||||
public function setNoteId(int $note_id): self
|
||||
{
|
||||
$this->note_id = $note_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNoteId(): int
|
||||
{
|
||||
return $this->note_id;
|
||||
}
|
||||
|
||||
public function setModified(DateTimeInterface $modified): self
|
||||
{
|
||||
$this->modified = $modified;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModified(): DateTimeInterface
|
||||
{
|
||||
return $this->modified;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function schemaDef(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'remoteurl_to_note',
|
||||
'fields' => [
|
||||
'remoteurl_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'remoteurl.id', 'multiplicity' => 'one to one', 'name' => 'remoteurl_to_note_remoteurl_id_fkey', 'not null' => true, 'description' => 'id of remoteurl'],
|
||||
'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'name' => 'remoteurl_to_note_note_id_fkey', 'not null' => true, 'description' => 'id of the note it belongs to'],
|
||||
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
|
||||
],
|
||||
'primary key' => ['remoteurl_id', 'note_id'],
|
||||
'indexes' => [
|
||||
'remoteurl_id_idx' => ['remoteurl_id'],
|
||||
'note_id_idx' => ['note_id'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,17 +1,12 @@
|
||||
{% set thumbnail_parameters = {'id': attachment.getId(), 'w': config('thumbnail','width'), 'h': config('thumbnail','height')} %}
|
||||
{% if attachment.getIsLocal() %}
|
||||
{% set handled = false %}
|
||||
{% for block in handle_event('ViewAttachment' ~ attachment.getMimetypeMajor() | capitalize , {'attachment': attachment, 'thumbnail_parameters': thumbnail_parameters}) %}
|
||||
{% set handled = true %}
|
||||
{{ block | raw }}
|
||||
{% endfor %}
|
||||
{% if not handled %}
|
||||
<div>
|
||||
<i> <a href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getTitle() }}</a> </i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% for block in handle_event('ViewRemoteAttachment', {'attachment': attachment, 'thumbnail_parameters': thumbnail_parameters}) %}
|
||||
{{ block | raw }}
|
||||
{% endfor %}
|
||||
{% set handled = false %}
|
||||
{% for block in handle_event('ViewAttachment' ~ attachment.getMimetypeMajor() | capitalize , {'attachment': attachment, 'thumbnail_parameters': thumbnail_parameters}) %}
|
||||
{% set handled = true %}
|
||||
{{ block | raw }}
|
||||
{% endfor %}
|
||||
{% if not handled %}
|
||||
<div>
|
||||
<i> <a href="{{ path('attachment_show', {'id': attachment.getId()}) }}">{{ attachment.getFilename() }}</a> </i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -46,7 +46,7 @@ class AttachmentTest extends GNUsocialTestCase
|
||||
private function testAttachment(string $suffix)
|
||||
{
|
||||
$client = static::createClient();
|
||||
$attachment = DB::findOneBy('attachment', ['title' => '1x1 JPEG image title']);
|
||||
$attachment = DB::findOneBy('attachment', ['filehash' => '5d8ee7ead51a28803b4ee5cb2306a0b90b6ba570f1e5bcc2209926f6ab08e7ea']);
|
||||
$crawler = $client->request('GET', "/attachment/{$attachment->getId()}{$suffix}");
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ class AttachmentTest extends GNUsocialTestCase
|
||||
{
|
||||
$this->testAttachment('');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertSelectorTextContains('figure figcaption', '1x1 JPEG image title');
|
||||
$this->assertSelectorTextContains('figure figcaption', '5d8ee7ead51a28803b4ee5cb2306a0b90b6ba570f1e5bcc2209926f6ab08e7ea');
|
||||
}
|
||||
|
||||
public function testAttachmentView()
|
||||
|
Loading…
Reference in New Issue
Block a user