DeleteNote.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace Plugin\DeleteNote;
  20. use ActivityPhp\Type\AbstractObject;
  21. use App\Core\Cache;
  22. use App\Core\DB\DB;
  23. use App\Core\Event;
  24. use function App\Core\I18n\_m;
  25. use App\Core\Modules\NoteHandlerPlugin;
  26. use App\Core\Router\RouteLoader;
  27. use App\Core\Router\Router;
  28. use App\Entity\Activity;
  29. use App\Entity\Actor;
  30. use App\Entity\Note;
  31. use App\Util\Common;
  32. use App\Util\Exception\ClientException;
  33. use DateTime;
  34. use Plugin\ActivityPub\Entity\ActivitypubActivity;
  35. use Symfony\Component\HttpFoundation\Request;
  36. /**
  37. * Delete note plugin main class.
  38. * Adds "delete this note" action to respective note if the user logged in is
  39. * the author.
  40. *
  41. * @package GNUsocial
  42. * @category DeleteNote
  43. *
  44. * @author Eliseu Amaro <mail@eliseuama.ro>
  45. * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
  46. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  47. */
  48. class DeleteNote extends NoteHandlerPlugin
  49. {
  50. public static function cacheKeys(int|Note $note_id): array
  51. {
  52. $note_id = \is_int($note_id) ? $note_id : $note_id->getId();
  53. return [
  54. 'activity' => "deleted-note-activity-{$note_id}",
  55. ];
  56. }
  57. /**
  58. * **Checks actor permissions for the DeleteNote action, deletes given Note
  59. * and creates respective Activity and Notification**
  60. *
  61. * Ensures the given Actor has sufficient permissions to perform the
  62. * deletion.
  63. * If it does, **Undertaker** will carry on, spelling doom for
  64. * the given Note and **everything related to it**
  65. * - Replies and Conversation **are unaffected**, except for the fact that
  66. * this Note no longer exists, of course
  67. * - Replies to this Note **will** remain on the same Conversation, and can
  68. * **still be seen** on that Conversation (potentially separated from a
  69. * parent, this Note)
  70. *
  71. * Replies shouldn't be taken out of context in any additional way, and
  72. * **Undertaker** only calls the methods necessary to accomplish the
  73. * deletion of this Note. Not any other as collateral damage.
  74. *
  75. * Creates the **_delete_ (verb)** Activity, performed on the given **Note
  76. * (object)**, by the given **Actor (subject)**. Launches the
  77. * NewNotification Event, stating who dared to call Undertaker.
  78. *
  79. * @throws \App\Util\Exception\ClientException
  80. * @throws \App\Util\Exception\ServerException
  81. */
  82. private static function undertaker(Actor $actor, Note $note): Activity
  83. {
  84. // Check permissions
  85. if (!$actor->canAdmin($note->getActor())) {
  86. throw new ClientException(_m('You don\'t have permissions to delete this note.'), 401);
  87. }
  88. // Undertaker believes the actor can terminate this note
  89. $activity = $note->delete(actor: $actor, source: 'web');
  90. Cache::delete(self::cacheKeys($note)['activity']);
  91. // Undertaker successful
  92. Event::handle('NewNotification', [$actor, $activity, [], _m('{nickname} deleted note {note_id}.', ['nickname' => $actor->getNickname(), 'note_id' => $activity->getObjectId()])]);
  93. return $activity;
  94. }
  95. /**
  96. * Delegates **DeleteNote::undertaker** to delete the Note provided
  97. *
  98. * Checks whether the Note has already been deleted, only passing on the
  99. * responsibility to undertaker if the Note wasn't.
  100. *
  101. * @throws \App\Util\Exception\ClientException
  102. * @throws \App\Util\Exception\DuplicateFoundException
  103. * @throws \App\Util\Exception\NotFoundException
  104. * @throws \App\Util\Exception\ServerException
  105. */
  106. public static function deleteNote(Note|int $note, Actor|int $actor, string $source = 'web'): ?Activity
  107. {
  108. $actor = \is_int($actor) ? Actor::getById($actor) : $actor;
  109. $note = \is_int($note) ? Note::getById($note) : $note;
  110. // Try to find if note was already deleted
  111. if (\is_null(
  112. Cache::get(
  113. self::cacheKeys($note)['activity'],
  114. fn () => DB::findOneBy(Activity::class, ['verb' => 'delete', 'object_type' => 'note', 'object_id' => $note->getId()], return_null: true),
  115. ),
  116. )) {
  117. // If none found, then undertaker has a job to do
  118. return self::undertaker($actor, $note);
  119. } else {
  120. return null;
  121. }
  122. }
  123. /**
  124. * Adds and connects the _delete_note_action_ route to
  125. * Controller\DeleteNote::class
  126. *
  127. * @return bool Event hook
  128. */
  129. public function onAddRoute(RouteLoader $r)
  130. {
  131. $r->connect(id: 'delete_note_action', uri_path: '/object/note/{note_id<\d+>}/delete', target: Controller\DeleteNote::class);
  132. return Event::next;
  133. }
  134. /**
  135. * **Catches AddExtraNoteActions Event**
  136. *
  137. * Adds an anchor link to the route _delete_note_action_ in the **Note card
  138. * template**. More specifically, in the **note_actions block**.
  139. *
  140. * @throws \App\Util\Exception\DuplicateFoundException
  141. * @throws \App\Util\Exception\NotFoundException
  142. * @throws \App\Util\Exception\ServerException
  143. *
  144. * @return bool Event hook
  145. */
  146. public function onAddExtraNoteActions(Request $request, Note $note, array &$actions)
  147. {
  148. if (\is_null($actor = Common::actor())) {
  149. return Event::next;
  150. }
  151. if (
  152. // Only add action if note wasn't already deleted!
  153. \is_null(Cache::get(
  154. self::cacheKeys($note)['activity'],
  155. fn () => DB::findOneBy(Activity::class, ['verb' => 'delete', 'object_type' => 'note', 'object_id' => $note->getId()], return_null: true),
  156. ))
  157. // And has permissions
  158. && $actor->canAdmin($note->getActor())) {
  159. $delete_action_url = Router::url('delete_note_action', ['note_id' => $note->getId()]);
  160. $query_string = $request->getQueryString();
  161. $delete_action_url .= '?from=' . mb_substr($query_string, 2);
  162. $actions[] = [
  163. 'title' => _m('Delete note'),
  164. 'classes' => '',
  165. 'url' => $delete_action_url,
  166. ];
  167. }
  168. return Event::next;
  169. }
  170. // ActivityPub handling and processing for Delete note is below
  171. /**
  172. * ActivityPub Inbox handler for Delete activities
  173. *
  174. * @param Actor $actor Actor who authored the activity
  175. * @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
  176. * @param mixed $type_object Activity's Object
  177. * @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
  178. *
  179. * @return bool Returns `Event::stop` if handled, `Event::next` otherwise
  180. */
  181. private function activitypub_handler(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): bool
  182. {
  183. if ($type_activity->get('type') !== 'Delete'
  184. || !($type_object instanceof Note)) {
  185. return Event::next;
  186. }
  187. $activity = self::deleteNote($type_object, $actor, source: 'ActivityPub');
  188. if (!\is_null($activity)) {
  189. // Store ActivityPub Activity
  190. $ap_act = \Plugin\ActivityPub\Entity\ActivitypubActivity::create([
  191. 'activity_id' => $activity->getId(),
  192. 'activity_uri' => $type_activity->get('id'),
  193. 'created' => new DateTime($type_activity->get('published') ?? 'now'),
  194. 'modified' => new DateTime(),
  195. ]);
  196. DB::persist($ap_act);
  197. }
  198. return Event::stop;
  199. }
  200. /**
  201. * Convert an Activity Streams 2.0 Delete into the appropriate Delete entities
  202. *
  203. * @param Actor $actor Actor who authored the activity
  204. * @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
  205. * @param \ActivityPhp\Type\AbstractObject $type_object Activity Streams 2.0 Object
  206. * @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
  207. *
  208. * @return bool Returns `Event::stop` if handled, `Event::next` otherwise
  209. */
  210. public function onNewActivityPubActivity(Actor $actor, AbstractObject $type_activity, AbstractObject $type_object, ?ActivitypubActivity &$ap_act): bool
  211. {
  212. return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
  213. }
  214. /**
  215. * Convert an Activity Streams 2.0 formatted activity with a known object into Entities
  216. *
  217. * @param Actor $actor Actor who authored the activity
  218. * @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
  219. * @param mixed $type_object Object
  220. * @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
  221. *
  222. * @return bool Returns `Event::stop` if handled, `Event::next` otherwise
  223. */
  224. public function onNewActivityPubActivityWithObject(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): bool
  225. {
  226. return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
  227. }
  228. /**
  229. * Translate GNU social internal verb 'delete' to Activity Streams 2.0 'Delete'
  230. *
  231. * @param string $verb GNU social's internal verb
  232. * @param null|string $gs_verb_to_activity_stream_two_verb Resulting Activity Streams 2.0 verb
  233. *
  234. * @return bool Returns `Event::stop` if handled, `Event::next` otherwise
  235. */
  236. public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): bool
  237. {
  238. if ($verb === 'delete') {
  239. $gs_verb_to_activity_stream_two_verb = 'Delete';
  240. return Event::stop;
  241. }
  242. return Event::next;
  243. }
  244. }