diff --git a/components/Conversation/Conversation.php b/components/Conversation/Conversation.php index 18fe79d9ef..ec44b47e29 100644 --- a/components/Conversation/Conversation.php +++ b/components/Conversation/Conversation.php @@ -23,6 +23,7 @@ declare(strict_types = 1); namespace Component\Conversation; +use App\Core\Cache; use App\Core\DB\DB; use App\Core\Event; use function App\Core\I18n\_m; @@ -179,4 +180,16 @@ class Conversation extends Component } return Event::next; } + + public function onNoteDeleteRelated(Note &$note, Actor $actor): bool + { + Cache::delete("note-replies-{$note->getId()}"); + DB::wrapInTransaction(function () use ($note) { + foreach ($note->getReplies() as $reply) { + $reply->setReplyTo(null); + } + }); + Cache::delete("note-replies-{$note->getId()}"); + return Event::next; + } } diff --git a/plugins/DeleteNote/Controller/DeleteNote.php b/plugins/DeleteNote/Controller/DeleteNote.php index b61621eebb..f8b3074e48 100644 --- a/plugins/DeleteNote/Controller/DeleteNote.php +++ b/plugins/DeleteNote/Controller/DeleteNote.php @@ -43,6 +43,7 @@ class DeleteNote extends Controller { /** * Create delete note view + * * @throws ClientException * @throws NoLoggedInUser * @throws RedirectException @@ -57,17 +58,6 @@ class DeleteNote extends Controller throw new NoSuchNoteException(); } - // Only let the original actor delete it - // TODO: should be anyone with permissions to do this? Admins and what not - $actor = $user->getActor(); - $actor_id = $actor->getId(); - if ($note->getActor()->getId() !== $actor_id) { - // Log this shenanigans and get the user redirected - Log::warning("Actor {$actor_id} attempted to delete note {$note_id} without any permissions to do so)"); - throw new RedirectException('root'); - } - - // We made sure that the note can be deleted, lets make the form $form_delete = Form::create([ ['delete_note', SubmitType::class, [ @@ -81,7 +71,7 @@ class DeleteNote extends Controller $form_delete->handleRequest($request); if ($form_delete->isSubmitted()) { - if (!\is_null(\Plugin\DeleteNote\DeleteNote::deleteNote(note_id: $note_id, actor_id: $actor_id))) { + if (!\is_null(\Plugin\DeleteNote\DeleteNote::deleteNote(note_id: $note_id, actor_id: $user->getId()))) { DB::flush(); } else { throw new ClientException(_m('Note already deleted!')); @@ -91,7 +81,7 @@ class DeleteNote extends Controller // Prevent open redirect if (!\is_null($from = $this->string('from'))) { if (Router::isAbsolute($from)) { - Log::warning("Actor {$actor_id} attempted to delete to a note and then get redirected to another host, or the URL was invalid ({$from})"); + Log::warning("Actor {$user->getId()} attempted to delete to a note and then get redirected to another host, or the URL was invalid ({$from})"); throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive) } else { // TODO anchor on element id diff --git a/plugins/DeleteNote/DeleteNote.php b/plugins/DeleteNote/DeleteNote.php index 3456f3cdd2..bb57dd41ed 100644 --- a/plugins/DeleteNote/DeleteNote.php +++ b/plugins/DeleteNote/DeleteNote.php @@ -23,7 +23,6 @@ namespace Plugin\DeleteNote; use App\Core\DB\DB; use App\Core\Event; -use App\Util\Exception\BugFoundException; use function App\Core\I18n\_m; use App\Core\Modules\NoteHandlerPlugin; use App\Core\Router\RouteLoader; @@ -31,11 +30,7 @@ use App\Core\Router\Router; use App\Entity\Activity; use App\Entity\Actor; use App\Entity\Note; -use App\Util\Exception\DuplicateFoundException; -use App\Util\Exception\NotFoundException; -use Component\Attachment\Entity\Attachment; -use Component\Attachment\Entity\AttachmentToNote; -use Plugin\DeleteNote\Entity\DeleteNote as DeleteEntity; +use App\Util\Exception\ClientException; use Symfony\Component\HttpFoundation\Request; /** @@ -51,104 +46,36 @@ use Symfony\Component\HttpFoundation\Request; */ class DeleteNote extends NoteHandlerPlugin { - /** - * Delete the given Note - * - * Bear in mind, in GNU social deleting a Note only replaces its content with a tombstone - * @param DeleteEntity $deleteNote - * @return bool - * @throws BugFoundException - */ - private static function undertaker(DeleteEntity $deleteNote): bool + private static function undertaker(Actor $actor, Note $note): Activity { - $note = Note::getById($deleteNote->getNoteId()); - $actor = Actor::getById($deleteNote->getActorId()); - // Only let the original actor delete it - // TODO: should be anyone with permissions to do this? Admins and what not + // TODO: Let actors of appropriate role do this as well if ($note->getActor()->getId() !== $actor->getId()) { - return false; + throw new ClientException(_m('You don\'t have permissions to delete this note.'), 401); } - // Create note tombstone to be rendered in a bit - $time_deleted = $deleteNote->getCreated(); - $deletion_time = date_format($time_deleted, 'Y/m/d H:i:s'); - $note->setModified($time_deleted); - $note_tombstone = "Actor {$actor->getUrl()} deleted this note at {$deletion_time}"; - $note->setContent($note_tombstone); - - // TODO: set note url with a new route, stating the note was deleted - - // Get note attachments - $note_attachments = $note->getAttachments(); - // Remove every relation this note has to its attachments - AttachmentToNote::removeWhereNoteId($note->getId()); - // Iterate through all note attachments to decrement their lives - foreach ($note_attachments as $attachment_entity) { - if ($attachment_entity->livesDecrementAndGet() <= 0) { - // Remove attachment from DB if there are no lives remaining - DB::remove($attachment_entity); - } else { - // This means it can live... for now - DB::merge($attachment_entity); - } - } - // Flush DB, a lot of stuff happened - DB::flush(); - - // Get the note rendered with tombstone text - // TODO: not sure if I put the actor as a mention here - $mentions = []; - $rendered = null; - Event::handle('RenderNoteContent', [$note_tombstone, 'text/plain', &$rendered, $actor, $note->getLanguageLocale(), &$mentions]); - $note->setRendered($rendered); - - // Apply changes to Note and flush - DB::merge($note); - DB::flush(); + // Undertaker believes the actor can terminate this note + $activity = $note->delete(actor: $actor, source: 'web'); // Undertaker successful - return true; + Event::handle('NewNotification', [$actor, $activity, [], "{$actor->getNickname()} deleted note {$activity->getObjectId()}"]); + return $activity; } public static function deleteNote(int $note_id, int $actor_id, string $source = 'web'): ?Activity { - $opts = ['note_id' => $note_id, 'actor_id' => $actor_id]; - $activity = null; - // Try and find if note was already deleted - try { - DB::findOneBy('delete_note', $opts); - } catch (DuplicateFoundException $e) { - } catch (NotFoundException $e) { + if (\is_null(DB::findOneBy(Activity::class, ['verb' => 'delete', 'object_type' => 'note', 'object_id' => $note_id], return_null: true))) { // If none found, then undertaker has a job to do - $delete_entity = DeleteEntity::create($opts); - if (self::undertaker($delete_entity)) { - // Undertaker believes the actor can terminate this note - // We should persist this entity then - DB::persist($delete_entity); - - // TODO: "the server MAY replace the object with a Tombstone of the object" - // not sure if I can do that yet? - $activity = Activity::create([ - 'actor_id' => $actor_id, - 'verb' => 'delete', - 'object_type' => 'note', - 'object_id' => $note_id, - 'source' => $source, - ]); - DB::persist($activity); - - Event::handle('NewNotification', [$actor = Actor::getById($actor_id), $activity, [], "{$actor->getNickname()} deleted note {$note_id}"]); - } + return self::undertaker(Actor::getById($actor_id), Note::getById($note_id)); + } else { + return null; } - return $activity; } public function onAddRoute(RouteLoader $r) { $r->connect(id: 'delete_note_action', uri_path: '/object/note/{note_id<\d+>}/delete', target: Controller\DeleteNote::class); - //$r->connect(id: 'note_deleted', uri_path: '/object/note/{note_id<\d+>}/404', target: Controller\DeleteNote::class); return Event::next; } @@ -156,10 +83,8 @@ class DeleteNote extends NoteHandlerPlugin public function onAddExtraNoteActions(Request $request, Note $note, array &$actions) { // Only add action if note wasn't already deleted! - try { - DB::findOneBy('delete_note', ['note_id' => $note->getId()]); - } catch (NotFoundException $e) { - $delete_action_url = Router::url('delete_note', ['note_id' => $note->getId()]); + if (\is_null(DB::findOneBy(Activity::class, ['verb' => 'delete', 'object_type' => 'note', 'object_id' => $note->getId()], return_null: true))) { + $delete_action_url = Router::url('delete_note_action', ['note_id' => $note->getId()]); $query_string = $request->getQueryString(); $delete_action_url .= '?from=' . mb_substr($query_string, 2); $actions[] = [ @@ -167,7 +92,6 @@ class DeleteNote extends NoteHandlerPlugin 'classes' => '', 'url' => $delete_action_url, ]; - } catch (DuplicateFoundException $e) { } return Event::next; diff --git a/plugins/DeleteNote/Entity/DeleteNote.php b/plugins/DeleteNote/Entity/DeleteNote.php deleted file mode 100644 index 1200cd6feb..0000000000 --- a/plugins/DeleteNote/Entity/DeleteNote.php +++ /dev/null @@ -1,129 +0,0 @@ -. - -// }}} - -namespace Plugin\DeleteNote\Entity; - -use App\Core\Entity; - -/** - * DeleteNote entity - * - * Stores the Note id and Actor who performed the action upon deletion of Note - * - * @package GNUsocial - * @category DeleteNote - * - * @author Eliseu Amaro - * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org - * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later - */ -class DeleteNote extends Entity -{ - // {{{ Autocode - // @codeCoverageIgnoreStart - private int $id; - private int $note_id; - private int $actor_id; - private \DateTimeInterface $created; - private \DateTimeInterface $modified; - - public function setId(int $id): self - { - $this->id = $id; - return $this; - } - - public function getId(): int - { - return $this->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 setActorId(int $actor_id): self - { - $this->actor_id = $actor_id; - return $this; - } - - public function getActorId(): int - { - return $this->actor_id; - } - - public function setCreated(\DateTimeInterface $created): self - { - $this->created = $created; - return $this; - } - - public function getCreated(): \DateTimeInterface - { - return $this->created; - } - - 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' => 'delete_note', - 'fields' => [ - 'id' => ['type' => 'serial', 'not null' => true, 'description' => 'Serial identifier'], - 'note_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'Deleted Note identifier'], - 'actor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'Actor who deleted the Note'], - 'created' => ['type' => 'datetime', 'not null' => true, 'description' => 'date this record was created', 'default' => 'CURRENT_TIMESTAMP'], - 'modified' => ['type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified', 'default' => 'CURRENT_TIMESTAMP'], - ], - 'primary key' => ['id'], - 'foreign keys' => [ - 'note_id_to_id_fkey' => ['note', ['note_id' => 'id']], - 'actor_id_to_id_fkey' => ['actor', ['actor_id' => 'id']], - ], - 'indexes' => [ - 'deleted_note_id_idx' => ['note_id'], - ], - ]; - } -} diff --git a/src/Controller/Note.php b/src/Controller/Note.php index 3fbb645c54..146fc1287d 100644 --- a/src/Controller/Note.php +++ b/src/Controller/Note.php @@ -26,6 +26,7 @@ namespace App\Controller; use App\Core\Controller; use App\Core\DB\DB; use function App\Core\I18n\_m; +use App\Entity\Activity; use App\Util\Common; use App\Util\Exception\ClientException; use Symfony\Component\HttpFoundation\Request; @@ -37,9 +38,13 @@ class Note extends Controller */ private function note(int $id, callable $handle) { - $note = DB::findOneBy('note', ['id' => $id]); - if (empty($note)) { - throw new ClientException(_m('No such note.'), 404); + $note = DB::findOneBy('note', ['id' => $id], return_null: true); + if (\is_null($note)) { + if (!\is_null(DB::findOneBy(Activity::class, ['verb' => 'delete', 'object_type' => 'note', 'object_id' => $id], return_null: true))) { + throw new ClientException(_m('Note deleted.'), 410); + } else { + throw new ClientException(_m('No such note.'), 404); + } } else { if ($note->isVisibleTo(Common::actor())) { return $handle($note); diff --git a/src/Entity/Note.php b/src/Entity/Note.php index a675a84a5f..79f255861b 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -35,6 +35,7 @@ use Component\Avatar\Avatar; use Component\Conversation\Entity\Conversation; use Component\Language\Entity\Language; use DateTimeInterface; +use function App\Core\I18n\_m; /** * Entity for notices @@ -475,22 +476,18 @@ class Note extends Entity return $mentioned; } - public function delete(?int $actor_id = null, string $source = 'web'): bool + public function delete(?Actor $actor = null, string $source = 'web'): Activity { - if (Event::handle('NoteDeleteRelated', [&$this]) === Event::next) { - DB::persist( - Activity::create([ - 'actor_id' => $actor_id ?? $this->getActorId(), - 'verb' => 'delete', - 'object_type' => 'note', - 'object_id' => $this->getId(), - 'source' => $source, - ]), - ); - DB::remove($this); - return true; - } - return false; + Event::handle('NoteDeleteRelated', [&$this, $actor]); + DB::persist($activity = Activity::create([ + 'actor_id' => $actor->getId(), + 'verb' => 'delete', + 'object_type' => 'note', + 'object_id' => $this->getId(), + 'source' => $source, + ])); + DB::remove(DB::findOneBy(self::class, ['id' => $this->id])); + return $activity; } public static function schemaDef(): array