20 Commits

Author SHA1 Message Date
tsmethurst
49a80a3c40 [PLUGIN][ActivityPub][TESTS] Add GoToSocial test fixtures 2022-10-21 13:01:32 +02:00
tsmethurst
97114e38e0 [PLUGIN][ActivityPub][TESTS] Replace invalid URL in fixtures 2022-10-21 11:47:27 +01:00
tsmethurst
2df30e2987 [PLUGIN][ActivityPub] Sign outgoing GET requests on behalf of relevant actor 2022-10-21 11:31:35 +01:00
tsmethurst
3b3ded5212 [PLUGIN][ActivityPub] Fix incorrect use of ActivityPubActor::create, should be ::createOrUpdate 2022-10-21 11:31:35 +01:00
tsmethurst
dc240fae49 [DOCKER] Fix incorrect script mount in worker 2022-10-21 11:31:35 +01:00
5cbb1627f2 [COMPONENT][Language] Fix collection query build event incorrectly not setting 'actor_language' join
Thanks to tsmethurst <tobi.smethurst@protonmail.com> for finding the error
2022-10-21 11:31:35 +01:00
46ff8aacd2 [UTIL][TemporaryFile] Silence warnings in critical section inside TemporaryFile 2022-10-21 11:30:47 +01:00
c4d6df4637 [TESTS] Fixup failing tests
Not a permanent solution
2022-10-21 11:30:37 +01:00
053bc38792 [TESTS] Fix tests 2022-10-19 22:39:17 +01:00
2fd46ca886 [TOOLS] Continue raising PHPStan level to 6 2022-10-19 22:39:17 +01:00
c31f3d4997 [TOOLS] Continue raising PHPStan to level 6 2022-10-19 22:39:17 +01:00
Hugo Sales
e6bb418fe6 [TOOLS] Begin raising PHPStan level to 6 2022-10-19 22:38:49 +01:00
fed2242a56 [TOOLS] Raise PHPStan level to 5 and fix associated error, fixing some bugs in the process 2022-10-19 22:38:49 +01:00
edeee49af9 [TOOLS] Fix errors pointed out by PHPStan level 4 2022-10-19 22:38:49 +01:00
4d7742e0e1 [OAuth2] Fix error in plugin install 2022-10-19 22:38:49 +01:00
76f2cdd212 [DEPENDENCIES] Update dependencies 2022-10-19 22:38:44 +01:00
a2aa45fb1f [DOCS] Expand developer Event documentation 2022-04-03 22:05:19 +01:00
d4b7e990ce [CORE][Event] Make all events return \EventResult, enforced at container build time 2022-04-03 21:40:32 +01:00
aef1fac536 [SECURITY] Refactor security hardening code and disable unused stream wrappers
Ensure unwanted enviorment variables are removed from the actual
global environment rather than just the `$_ENV` superglobal variable

Disable stream wrappers, as this is an unexpected feature for most
developers and can be exploited. For instance, `phar://` can be used
to override any class and thus provide code execution (through
`__wakeup` or `__costruct`, for instance). Not a complete solution, as
`php://` can also be abused, but we can't disable it as it gets used
_somewhere_ in our dependencies
2022-04-03 18:02:54 +01:00
556ac85061 [PLUGIN][Pinboard] For tag list request, respond with the most common variant and the corresponding count for each canon tag 2022-04-01 02:10:12 +01:00
165 changed files with 3431 additions and 2206 deletions

View File

@@ -178,7 +178,7 @@ return $config
// There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis. // There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis.
'no_spaces_inside_parenthesis' => true, 'no_spaces_inside_parenthesis' => true,
// Removes `@param`, `@return` and `@var` tags that don't provide any useful information. // Removes `@param`, `@return` and `@var` tags that don't provide any useful information.
'no_superfluous_phpdoc_tags' => true, 'no_superfluous_phpdoc_tags' => false,
// Remove trailing commas in list function calls. // Remove trailing commas in list function calls.
'no_trailing_comma_in_list_call' => true, 'no_trailing_comma_in_list_call' => true,
// PHP single-line arrays should not have trailing comma. // PHP single-line arrays should not have trailing comma.

View File

@@ -34,10 +34,11 @@ use Component\Attachment\Entity as E;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EventResult;
class Attachment extends Component class Attachment extends Component
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('note_attachment_show', '/object/note/{note_id<\d+>}/attachment/{attachment_id<\d+>}', [C\Attachment::class, 'attachmentShowWithNote']); $r->connect('note_attachment_show', '/object/note/{note_id<\d+>}/attachment/{attachment_id<\d+>}', [C\Attachment::class, 'attachmentShowWithNote']);
$r->connect('note_attachment_view', '/object/note/{note_id<\d+>}/attachment/{attachment_id<\d+>}/view', [C\Attachment::class, 'attachmentViewWithNote']); $r->connect('note_attachment_view', '/object/note/{note_id<\d+>}/attachment/{attachment_id<\d+>}/view', [C\Attachment::class, 'attachmentViewWithNote']);
@@ -51,13 +52,13 @@ class Attachment extends Component
* *
* This can be used in the future to deduplicate images by visual content * This can be used in the future to deduplicate images by visual content
*/ */
public function onHashFile(string $filename, ?string &$out_hash): bool public function onHashFile(string $filename, ?string &$out_hash): EventResult
{ {
$out_hash = hash_file(E\Attachment::FILEHASH_ALGO, $filename); $out_hash = hash_file(E\Attachment::FILEHASH_ALGO, $filename);
return Event::stop; return Event::stop;
} }
public function onNoteDeleteRelated(Note &$note, Actor $actor): bool public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult
{ {
Cache::delete("note-attachments-{$note->getId()}"); Cache::delete("note-attachments-{$note->getId()}");
foreach ($note->getAttachments() as $attachment) { foreach ($note->getAttachments() as $attachment) {
@@ -68,7 +69,7 @@ class Attachment extends Component
return Event::next; return Event::next;
} }
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult
{ {
if (!\in_array('attachment_to_note', $note_qb->getAllAliases())) { if (!\in_array('attachment_to_note', $note_qb->getAllAliases())) {
$note_qb->leftJoin( $note_qb->leftJoin(
@@ -84,7 +85,7 @@ class Attachment extends Component
/** /**
* Populate $note_expr with the criteria for looking for notes with attachments * Populate $note_expr with the criteria for looking for notes with attachments
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{ {
$include_term = str_contains($term, ':') ? explode(':', $term)[1] : $term; $include_term = str_contains($term, ':') ? explode(':', $term)[1] : $term;
if (Formatting::startsWith($term, ['note-types:', 'notes-incude:', 'note-filter:'])) { if (Formatting::startsWith($term, ['note-types:', 'notes-incude:', 'note-filter:'])) {

View File

@@ -32,15 +32,17 @@ use Component\Attachment\Entity\Attachment;
use Component\Attachment\Entity\AttachmentThumbnail; use Component\Attachment\Entity\AttachmentThumbnail;
use Component\Avatar\Controller as C; use Component\Avatar\Controller as C;
use Component\Avatar\Exception\NoAvatarException; use Component\Avatar\Exception\NoAvatarException;
use EventResult;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Avatar extends Component class Avatar extends Component
{ {
public function onInitializeComponent() public function onInitializeComponent(): EventResult
{ {
return EventResult::next;
} }
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('avatar_actor', '/actor/{actor_id<\d+>}/avatar/{size<full|big|medium|small>?medium}', [Controller\Avatar::class, 'avatar_view']); $r->connect('avatar_actor', '/actor/{actor_id<\d+>}/avatar/{size<full|big|medium|small>?medium}', [Controller\Avatar::class, 'avatar_view']);
$r->connect('avatar_default', '/avatar/default/{size<full|big|medium|small>?medium}', [Controller\Avatar::class, 'default_avatar_view']); $r->connect('avatar_default', '/avatar/default/{size<full|big|medium|small>?medium}', [Controller\Avatar::class, 'default_avatar_view']);
@@ -49,9 +51,11 @@ class Avatar extends Component
} }
/** /**
* @param SettingsTabsType $tabs
*
* @throws \App\Util\Exception\ClientException * @throws \App\Util\Exception\ClientException
*/ */
public function onPopulateSettingsTabs(Request $request, string $section, &$tabs): bool public function onPopulateSettingsTabs(Request $request, string $section, &$tabs): EventResult
{ {
if ($section === 'profile') { if ($section === 'profile') {
$tabs[] = [ $tabs[] = [
@@ -64,7 +68,7 @@ class Avatar extends Component
return Event::next; return Event::next;
} }
public function onAvatarUpdate(int $actor_id): bool public function onAvatarUpdate(int $actor_id): EventResult
{ {
Cache::delete("avatar-{$actor_id}"); Cache::delete("avatar-{$actor_id}");
foreach (['full', 'big', 'medium', 'small'] as $size) { foreach (['full', 'big', 'medium', 'small'] as $size) {
@@ -127,6 +131,8 @@ class Avatar extends Component
* *
* Returns the avatar file's hash, mimetype, title and path. * Returns the avatar file's hash, mimetype, title and path.
* Ensures exactly one cached value exists * Ensures exactly one cached value exists
*
* @return array{id: null|int, filename: null|string, title: string, mimetype: string, filepath?: string}
*/ */
public static function getAvatarFileInfo(int $actor_id, string $size = 'medium'): array public static function getAvatarFileInfo(int $actor_id, string $size = 'medium'): array
{ {

View File

@@ -39,6 +39,7 @@ use Component\Circle\Entity\ActorCircleSubscription;
use Component\Circle\Entity\ActorTag; use Component\Circle\Entity\ActorTag;
use Component\Collection\Util\MetaCollectionTrait; use Component\Collection\Util\MetaCollectionTrait;
use Component\Tag\Tag; use Component\Tag\Tag;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -53,12 +54,13 @@ use Symfony\Component\HttpFoundation\Request;
*/ */
class Circle extends Component class Circle extends Component
{ {
/** @phpstan-use MetaCollectionTrait<ActorCircle> */
use MetaCollectionTrait; use MetaCollectionTrait;
public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/'; public const TAG_CIRCLE_REGEX = '/' . Nickname::BEFORE_MENTIONS . '@#([\pL\pN_\-\.]{1,64})/';
protected const SLUG = 'circle'; protected const SLUG = 'circle';
protected const PLURAL_SLUG = 'circles'; protected const PLURAL_SLUG = 'circles';
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('actor_circle_view_by_circle_id', '/circle/{circle_id<\d+>}', [CircleController\Circle::class, 'circleById']); $r->connect('actor_circle_view_by_circle_id', '/circle/{circle_id<\d+>}', [CircleController\Circle::class, 'circleById']);
// View circle members by (tagger id or nickname) and tag // View circle members by (tagger id or nickname) and tag
@@ -93,7 +95,7 @@ class Circle extends Component
]; ];
} }
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): EventResult
{ {
if ($section === 'profile' && \in_array($request->get('_route'), ['person_actor_settings', 'group_actor_settings'])) { if ($section === 'profile' && \in_array($request->get('_route'), ['person_actor_settings', 'group_actor_settings'])) {
$tabs[] = [ $tabs[] = [
@@ -106,7 +108,10 @@ class Circle extends Component
return Event::next; return Event::next;
} }
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): bool /**
* @param Actor[] $targets
*/
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): EventResult
{ {
$circles = $actor->getCircles(); $circles = $actor->getCircles();
foreach ($circles as $circle) { foreach ($circles as $circle) {
@@ -118,6 +123,9 @@ class Circle extends Component
// Meta Collection ------------------------------------------------------------------- // Meta Collection -------------------------------------------------------------------
/**
* @param array<string, mixed> $vars
*/
private function getActorIdFromVars(array $vars): int private function getActorIdFromVars(array $vars): int
{ {
$id = $vars['request']->get('id', null); $id = $vars['request']->get('id', null);
@@ -129,7 +137,7 @@ class Circle extends Component
return $user->getId(); return $user->getId();
} }
public static function createCircle(Actor|int $tagger_id, string $tag): int public static function createCircle(Actor|int $tagger_id, string $tag): int|null
{ {
$tagger_id = \is_int($tagger_id) ? $tagger_id : $tagger_id->getId(); $tagger_id = \is_int($tagger_id) ? $tagger_id : $tagger_id->getId();
$circle = ActorCircle::create([ $circle = ActorCircle::create([
@@ -145,7 +153,10 @@ class Circle extends Component
return $circle->getId(); return $circle->getId();
} }
protected function createCollection(Actor $owner, array $vars, string $name) /**
* @param array<string, mixed> $vars
*/
protected function createCollection(Actor $owner, array $vars, string $name): void
{ {
$this->createCircle($owner, $name); $this->createCircle($owner, $name);
DB::persist(ActorTag::create([ DB::persist(ActorTag::create([
@@ -155,7 +166,12 @@ class Circle extends Component
])); ]));
} }
protected function removeItem(Actor $owner, array $vars, $items, array $collections) /**
* @param array<string, mixed> $vars
* @param array<int> $items
* @param array<mixed> $collections
*/
protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool
{ {
$tagger_id = $owner->getId(); $tagger_id = $owner->getId();
$tagged_id = $this->getActorIdFromVars($vars); $tagged_id = $this->getActorIdFromVars($vars);
@@ -168,9 +184,15 @@ class Circle extends Component
DB::removeBy(ActorTag::class, ['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag]); DB::removeBy(ActorTag::class, ['tagger' => $tagger_id, 'tagged' => $tagged_id, 'tag' => $tag]);
} }
Cache::delete(Actor::cacheKeys($tagger_id)['circles']); Cache::delete(Actor::cacheKeys($tagger_id)['circles']);
return true;
} }
protected function addItem(Actor $owner, array $vars, $items, array $collections) /**
* @param array<string, mixed> $vars
* @param array<int> $items
* @param array<mixed> $collections
*/
protected function addItem(Actor $owner, array $vars, array $items, array $collections): void
{ {
$tagger_id = $owner->getId(); $tagger_id = $owner->getId();
$tagged_id = $this->getActorIdFromVars($vars); $tagged_id = $this->getActorIdFromVars($vars);
@@ -187,8 +209,10 @@ class Circle extends Component
/** /**
* @see MetaCollectionPlugin->shouldAddToRightPanel * @see MetaCollectionPlugin->shouldAddToRightPanel
*
* @param array<string, mixed> $vars
*/ */
protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool
{ {
return \in_array($vars['path'], ['actor_view_nickname', 'actor_view_id']); return \in_array($vars['path'], ['actor_view_nickname', 'actor_view_id']);
} }
@@ -201,8 +225,10 @@ class Circle extends Component
* itself, and from every Actor that is a part of its ActorCircle. * itself, and from every Actor that is a part of its ActorCircle.
* *
* @param Actor $owner the Actor, and by extension its own circle of Actors * @param Actor $owner the Actor, and by extension its own circle of Actors
* @param null|array $vars Page vars sent by AppendRightPanelBlock event * @param null|array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param bool $ids_only true if only the Collections ids are to be returned * @param bool $ids_only true if only the Collections ids are to be returned
*
* @return ($ids_only is true ? int[] : ActorCircle[])
*/ */
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
{ {
@@ -218,7 +244,7 @@ class Circle extends Component
return $ids_only ? array_map(fn ($x) => $x->getId(), $circles) : $circles; return $ids_only ? array_map(fn ($x) => $x->getId(), $circles) : $circles;
} }
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering) public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult
{ {
DB::persist(Feed::create([ DB::persist(Feed::create([
'actor_id' => $actor_id, 'actor_id' => $actor_id,

View File

@@ -38,6 +38,8 @@ class Circle extends CircleController
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
*
* @return ControllerResultType
*/ */
public function circleById(int|ActorCircle $circle_id): array public function circleById(int|ActorCircle $circle_id): array
{ {
@@ -57,11 +59,17 @@ class Circle extends CircleController
} }
} }
/**
* @return ControllerResultType
*/
public function circleByTaggerIdAndTag(int $tagger_id, string $tag): array public function circleByTaggerIdAndTag(int $tagger_id, string $tag): array
{ {
return $this->circleById(ActorCircle::getByPK(['tagger' => $tagger_id, 'tag' => $tag])); return $this->circleById(ActorCircle::getByPK(['tagger' => $tagger_id, 'tag' => $tag]));
} }
/**
* @return ControllerResultType
*/
public function circleByTaggerNicknameAndTag(string $tagger_nickname, string $tag): array public function circleByTaggerNicknameAndTag(string $tagger_nickname, string $tag): array
{ {
return $this->circleById(ActorCircle::getByPK(['tagger' => LocalUser::getByNickname($tagger_nickname)->getId(), 'tag' => $tag])); return $this->circleById(ActorCircle::getByPK(['tagger' => LocalUser::getByNickname($tagger_nickname)->getId(), 'tag' => $tag]));

View File

@@ -31,16 +31,20 @@ use App\Entity\LocalUser;
use Component\Circle\Entity\ActorCircle; use Component\Circle\Entity\ActorCircle;
use Component\Collection\Util\Controller\MetaCollectionController; use Component\Collection\Util\Controller\MetaCollectionController;
/**
* @extends MetaCollectionController<Circles>
*/
class Circles extends MetaCollectionController class Circles extends MetaCollectionController
{ {
protected const SLUG = 'circle'; protected const SLUG = 'circle';
protected const PLURAL_SLUG = 'circles'; protected const PLURAL_SLUG = 'circles';
protected string $page_title = 'Actor circles'; protected string $page_title = 'Actor circles';
public function createCollection(int $owner_id, string $name) public function createCollection(int $owner_id, string $name): bool
{ {
return \Component\Circle\Circle::createCircle($owner_id, $name); return !\is_null(\Component\Circle\Circle::createCircle($owner_id, $name));
} }
public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
{ {
return Router::url( return Router::url(
@@ -49,21 +53,26 @@ class Circles extends MetaCollectionController
); );
} }
public function getCollectionItems(int $owner_id, $collection_id): array /**
* @return Circles[]
*/
public function getCollectionItems(int $owner_id, int $collection_id): array
{ {
$notes = []; // TODO: Use Feed::query return []; // TODO
return [
'_template' => 'collection/notes.html.twig',
'notes' => $notes,
];
} }
/**
* @return Circles[]
*/
public function feedByCircleId(int $circle_id) public function feedByCircleId(int $circle_id)
{ {
// Owner id isn't used // Owner id isn't used
return $this->getCollectionItems(0, $circle_id); return $this->getCollectionItems(0, $circle_id);
} }
/**
* @return Circles[]
*/
public function feedByTaggerIdAndTag(int $tagger_id, string $tag) public function feedByTaggerIdAndTag(int $tagger_id, string $tag)
{ {
// Owner id isn't used // Owner id isn't used
@@ -71,6 +80,9 @@ class Circles extends MetaCollectionController
return $this->getCollectionItems($tagger_id, $circle_id); return $this->getCollectionItems($tagger_id, $circle_id);
} }
/**
* @return Circles[]
*/
public function feedByTaggerNicknameAndTag(string $tagger_nickname, string $tag) public function feedByTaggerNicknameAndTag(string $tagger_nickname, string $tag)
{ {
$tagger_id = LocalUser::getByNickname($tagger_nickname)->getId(); $tagger_id = LocalUser::getByNickname($tagger_nickname)->getId();
@@ -82,12 +94,13 @@ class Circles extends MetaCollectionController
{ {
return DB::findBy(ActorCircle::class, ['tagger' => $owner_id], order_by: ['id' => 'desc']); return DB::findBy(ActorCircle::class, ['tagger' => $owner_id], order_by: ['id' => 'desc']);
} }
public function getCollectionBy(int $owner_id, int $collection_id): ActorCircle
public function getCollectionBy(int $owner_id, int $collection_id): self
{ {
return DB::findOneBy(ActorCircle::class, ['id' => $collection_id, 'actor_id' => $owner_id]); return DB::findOneBy(ActorCircle::class, ['id' => $collection_id, 'actor_id' => $owner_id]);
} }
public function setCollectionName(int $actor_id, string $actor_nickname, ActorCircle $collection, string $name) public function setCollectionName(int $actor_id, string $actor_nickname, ActorCircle $collection, string $name): void
{ {
foreach ($collection->getActorTags(db_reference: true) as $at) { foreach ($collection->getActorTags(db_reference: true) as $at) {
$at->setTag($name); $at->setTag($name);
@@ -96,7 +109,7 @@ class Circles extends MetaCollectionController
Cache::delete(Actor::cacheKeys($actor_id)['circles']); Cache::delete(Actor::cacheKeys($actor_id)['circles']);
} }
public function removeCollection(int $actor_id, string $actor_nickname, ActorCircle $collection) public function removeCollection(int $actor_id, string $actor_nickname, ActorCircle $collection): void
{ {
foreach ($collection->getActorTags(db_reference: true) as $at) { foreach ($collection->getActorTags(db_reference: true) as $at) {
DB::remove($at); DB::remove($at);

View File

@@ -25,6 +25,7 @@ use App\Core\Cache;
use App\Core\DB; use App\Core\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Core\Router; use App\Core\Router;
use App\Entity\Actor;
use DateTimeInterface; use DateTimeInterface;
/** /**
@@ -144,6 +145,9 @@ class ActorCircle extends Entity
return $this->tag; return $this->tag;
} }
/**
* @return ActorTag[]
*/
public function getActorTags(bool $db_reference = false): array public function getActorTags(bool $db_reference = false): array
{ {
$handle = fn () => DB::findBy('actor_tag', ['tagger' => $this->getTagger(), 'tag' => $this->getTag()]); $handle = fn () => DB::findBy('actor_tag', ['tagger' => $this->getTagger(), 'tag' => $this->getTag()]);
@@ -156,7 +160,10 @@ class ActorCircle extends Entity
); );
} }
public function getTaggedActors() /**
* @return Actor[]
*/
public function getTaggedActors(): array
{ {
return Cache::get( return Cache::get(
"circle-{$this->getId()}-tagged-actors", "circle-{$this->getId()}-tagged-actors",
@@ -170,6 +177,9 @@ class ActorCircle extends Entity
); );
} }
/**
* @return Actor[]
*/
public function getSubscribedActors(?int $offset = null, ?int $limit = null): array public function getSubscribedActors(?int $offset = null, ?int $limit = null): array
{ {
return Cache::get( return Cache::get(

View File

@@ -7,14 +7,18 @@ namespace Component\Circle\Form;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Util\Form\ArrayTransformer; use App\Util\Form\ArrayTransformer;
use Component\Circle\Entity\ActorTag;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
abstract class SelfTagsForm abstract class SelfTagsForm
{ {
/** /**
* @return array [Form (add), ?Form (existing)] * @param ActorTag[] $actor_self_tags
*
* @return array{FormInterface, ?FormInterface} [Form (add), ?Form (existing)]
*/ */
public static function handleTags( public static function handleTags(
Request $request, Request $request,

View File

@@ -14,6 +14,7 @@ use Component\Subscription\Entity\ActorSubscription;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EventResult;
class Collection extends Component class Collection extends Component
{ {
@@ -22,6 +23,11 @@ class Collection extends Component
* *
* Supports a variety of query terms and is used both in feeds and * Supports a variety of query terms and is used both in feeds and
* in search. Uses query builders to allow for extension * in search. Uses query builders to allow for extension
*
* @param array<string, OrderByType> $note_order_by
* @param array<string, OrderByType> $actor_order_by
*
* @return array{notes: null|Note[], actors: null|Actor[]}
*/ */
public static function query(string $query, int $page, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array public static function query(string $query, int $page, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array
{ {
@@ -64,7 +70,7 @@ class Collection extends Component
return ['notes' => $notes ?? null, 'actors' => $actors ?? null]; return ['notes' => $notes ?? null, 'actors' => $actors ?? null];
} }
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult
{ {
$note_aliases = $note_qb->getAllAliases(); $note_aliases = $note_qb->getAllAliases();
if (!\in_array('subscription', $note_aliases)) { if (!\in_array('subscription', $note_aliases)) {
@@ -79,8 +85,11 @@ class Collection extends Component
/** /**
* Convert $term to $note_expr and $actor_expr, search criteria. Handles searching for text * Convert $term to $note_expr and $actor_expr, search criteria. Handles searching for text
* notes, for different types of actors and for the content of text notes * notes, for different types of actors and for the content of text notes
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr) public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{ {
if (str_contains($term, ':')) { if (str_contains($term, ':')) {
$term = explode(':', $term); $term = explode(':', $term);

View File

@@ -4,6 +4,9 @@ declare(strict_types = 1);
namespace Component\Collection\Util\Controller; namespace Component\Collection\Util\Controller;
/**
* @extends OrderedCollection<\Component\Circle\Entity\ActorCircle>
*/
class CircleController extends OrderedCollection class CircleController extends OrderedCollection
{ {
} }

View File

@@ -6,15 +6,25 @@ namespace Component\Collection\Util\Controller;
use App\Core\Controller; use App\Core\Controller;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use Component\Collection\Collection as CollectionModule; use Component\Collection\Collection as CollectionComponent;
class Collection extends Controller /**
* @template T
*/
abstract class Collection extends Controller
{ {
/**
* @param array<string, OrderByType> $note_order_by
* @param array<string, OrderByType> $actor_order_by
*
* @return array{notes: null|Note[], actors: null|Actor[]}
*/
public function query(string $query, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array public function query(string $query, ?string $locale = null, ?Actor $actor = null, array $note_order_by = [], array $actor_order_by = []): array
{ {
$actor ??= Common::actor(); $actor ??= Common::actor();
$locale ??= Common::currentLanguage()->getLocale(); $locale ??= Common::currentLanguage()->getLocale();
return CollectionModule::query($query, $this->int('page') ?? 1, $locale, $actor, $note_order_by, $actor_order_by); return CollectionComponent::query($query, $this->int('page') ?? 1, $locale, $actor, $note_order_by, $actor_order_by);
} }
} }

View File

@@ -38,12 +38,23 @@ use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use Functional as F; use Functional as F;
/**
* @template T
*
* @extends OrderedCollection<T>
*/
abstract class FeedController extends OrderedCollection abstract class FeedController extends OrderedCollection
{ {
/** /**
* Post-processing of the result of a feed controller, to remove any * Post-processing of the result of a feed controller, to remove any
* notes or actors the user specified, as well as format the raw * notes or actors the user specified, as well as format the raw
* list of notes into a usable format * list of notes into a usable format
*
* @template NA of Note|Actor
*
* @param NA[] $result
*
* @return NA[]
*/ */
protected function postProcess(array $result): array protected function postProcess(array $result): array
{ {
@@ -58,6 +69,9 @@ abstract class FeedController extends OrderedCollection
return $result; return $result;
} }
/**
* @param Note[] $notes
*/
private static function enforceScope(array &$notes, ?Actor $actor, ?Actor $in = null): void private static function enforceScope(array &$notes, ?Actor $actor, ?Actor $in = null): void
{ {
$notes = F\select($notes, fn (Note $n) => $n->isVisibleTo($actor, $in)); $notes = F\select($notes, fn (Note $n) => $n->isVisibleTo($actor, $in));

View File

@@ -39,8 +39,14 @@ use App\Util\Common;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @template T of object
*
* @extends FeedController<T>
*/
abstract class MetaCollectionController extends FeedController abstract class MetaCollectionController extends FeedController
{ {
protected const SLUG = 'collectionsEntry'; protected const SLUG = 'collectionsEntry';
@@ -48,17 +54,36 @@ abstract class MetaCollectionController extends FeedController
protected string $page_title = 'Collections'; protected string $page_title = 'Collections';
abstract public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string; abstract public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string;
abstract public function getCollectionItems(int $owner_id, $collection_id): array;
abstract public function getCollectionsByActorId(int $owner_id): array;
abstract public function getCollectionBy(int $owner_id, int $collection_id);
abstract public function createCollection(int $owner_id, string $name);
/**
* @return T[]
*/
abstract public function getCollectionItems(int $owner_id, int $collection_id): array;
/**
* @return T[]
*/
abstract public function getCollectionsByActorId(int $owner_id): array;
/**
* @return T A collection
*/
abstract public function getCollectionBy(int $owner_id, int $collection_id): object;
abstract public function createCollection(int $owner_id, string $name): bool;
/**
* @return ControllerResultType
*/
public function collectionsViewByActorNickname(Request $request, string $nickname): array public function collectionsViewByActorNickname(Request $request, string $nickname): array
{ {
$user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]); $user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]);
return self::collectionsView($request, $user->getId(), $nickname); return self::collectionsView($request, $user->getId(), $nickname);
} }
/**
* @return ControllerResultType
*/
public function collectionsViewByActorId(Request $request, int $id): array public function collectionsViewByActorId(Request $request, int $id): array
{ {
return self::collectionsView($request, $id, null); return self::collectionsView($request, $id, null);
@@ -70,7 +95,7 @@ abstract class MetaCollectionController extends FeedController
* @param int $id actor id * @param int $id actor id
* @param ?string $nickname actor nickname * @param ?string $nickname actor nickname
* *
* @return array twig template options * @return ControllerResultType twig template options
*/ */
public function collectionsView(Request $request, int $id, ?string $nickname): array public function collectionsView(Request $request, int $id, ?string $nickname): array
{ {
@@ -113,34 +138,23 @@ abstract class MetaCollectionController extends FeedController
// the functions and passing that class to the template. // the functions and passing that class to the template.
// This is suggested at https://web.archive.org/web/20220226132328/https://stackoverflow.com/questions/3595727/twig-pass-function-into-template/50364502 // This is suggested at https://web.archive.org/web/20220226132328/https://stackoverflow.com/questions/3595727/twig-pass-function-into-template/50364502
$fn = new class($id, $nickname, $request, $this, static::SLUG) { $fn = new class($id, $nickname, $request, $this, static::SLUG) {
private $id; public function __construct(private int $id, private string $nickname, private Request $request, private object $parent, private string $slug)
private $nick;
private $request;
private $parent;
private $slug;
public function __construct($id, $nickname, $request, $parent, $slug)
{ {
$this->id = $id;
$this->nick = $nickname;
$this->request = $request;
$this->parent = $parent;
$this->slug = $slug;
} }
// there's already an injected function called path, // there's already an injected function called path,
// that maps to Router::url(name, args), but since // that maps to Router::url(name, args), but since
// I want to preserve nicknames, I think it's better // I want to preserve nicknames, I think it's better
// to use that getUrl function // to use that getUrl function
public function getUrl($cid) public function getUrl(int $cid): string
{ {
return $this->parent->getCollectionUrl($this->id, $this->nick, $cid); return $this->parent->getCollectionUrl($this->id, $this->nickname, $cid);
} }
// There are many collections in this page and we need two // There are many collections in this page and we need two
// forms for each one of them: one form to edit the collection's // forms for each one of them: one form to edit the collection's
// name and another to remove the collection. // name and another to remove the collection.
// creating the edit form // creating the edit form
public function editForm($collection) public function editForm(object $collection): FormView
{ {
$edit = Form::create([ $edit = Form::create([
['name', TextType::class, [ ['name', TextType::class, [
@@ -159,7 +173,7 @@ abstract class MetaCollectionController extends FeedController
]); ]);
$edit->handleRequest($this->request); $edit->handleRequest($this->request);
if ($edit->isSubmitted() && $edit->isValid()) { if ($edit->isSubmitted() && $edit->isValid()) {
$this->parent->setCollectionName($this->id, $this->nick, $collection, $edit->getData()['name']); $this->parent->setCollectionName($this->id, $this->nickname, $collection, $edit->getData()['name']);
DB::flush(); DB::flush();
throw new RedirectException(); throw new RedirectException();
} }
@@ -167,7 +181,7 @@ abstract class MetaCollectionController extends FeedController
} }
// creating the remove form // creating the remove form
public function rmForm($collection) public function rmForm(object $collection): FormView
{ {
$rm = Form::create([ $rm = Form::create([
['remove_' . $collection->getId(), SubmitType::class, [ ['remove_' . $collection->getId(), SubmitType::class, [
@@ -180,7 +194,7 @@ abstract class MetaCollectionController extends FeedController
]); ]);
$rm->handleRequest($this->request); $rm->handleRequest($this->request);
if ($rm->isSubmitted()) { if ($rm->isSubmitted()) {
$this->parent->removeCollection($this->id, $this->nick, $collection); $this->parent->removeCollection($this->id, $this->nickname, $collection);
DB::flush(); DB::flush();
throw new RedirectException(); throw new RedirectException();
} }
@@ -198,12 +212,18 @@ abstract class MetaCollectionController extends FeedController
]; ];
} }
/**
* @return ControllerResultType
*/
public function collectionsEntryViewNotesByNickname(Request $request, string $nickname, int $cid): array public function collectionsEntryViewNotesByNickname(Request $request, string $nickname, int $cid): array
{ {
$user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]); $user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]);
return self::collectionsEntryViewNotesByActorId($request, $user->getId(), $cid); return self::collectionsEntryViewNotesByActorId($request, $user->getId(), $cid);
} }
/**
* @return ControllerResultType
*/
public function collectionsEntryViewNotesByActorId(Request $request, int $id, int $cid): array public function collectionsEntryViewNotesByActorId(Request $request, int $id, int $cid): array
{ {
$collection = $this->getCollectionBy($id, $cid); $collection = $this->getCollectionBy($id, $cid);

View File

@@ -4,6 +4,11 @@ declare(strict_types = 1);
namespace Component\Collection\Util\Controller; namespace Component\Collection\Util\Controller;
class OrderedCollection extends Collection /**
* @template T
*
* @extends Collection<T>
*/
abstract class OrderedCollection extends Collection
{ {
} }

View File

@@ -39,11 +39,15 @@ use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use App\Util\Formatting; use App\Util\Formatting;
use EventResult;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @template T
* */
trait MetaCollectionTrait trait MetaCollectionTrait
{ {
//protected const SLUG = 'collection'; //protected const SLUG = 'collection';
@@ -53,39 +57,43 @@ trait MetaCollectionTrait
* create a collection owned by Actor $owner. * create a collection owned by Actor $owner.
* *
* @param Actor $owner The collection's owner * @param Actor $owner The collection's owner
* @param array $vars Page vars sent by AppendRightPanelBlock event * @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param string $name Collection's name * @param string $name Collection's name
*/ */
abstract protected function createCollection(Actor $owner, array $vars, string $name); abstract protected function createCollection(Actor $owner, array $vars, string $name): void;
/** /**
* remove item from collections. * remove item from collections.
* *
* @param Actor $owner Current user * @param Actor $owner Current user
* @param array $vars Page vars sent by AppendRightPanelBlock event * @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param array $items Array of collections's ids to remove the current item from * @param int[] $items Array of collections's ids to remove the current item from
* @param array $collections List of ids of collections owned by $owner * @param int[] $collections List of ids of collections owned by $owner
*/ */
abstract protected function removeItem(Actor $owner, array $vars, array $items, array $collections); abstract protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool;
/** /**
* add item to collections. * add item to collections.
* *
* @param Actor $owner Current user * @param Actor $owner Current user
* @param array $vars Page vars sent by AppendRightPanelBlock event * @param array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param array $items Array of collections's ids to add the current item to * @param int[] $items Array of collections's ids to add the current item to
* @param array $collections List of ids of collections owned by $owner * @param int[] $collections List of ids of collections owned by $owner
*/ */
abstract protected function addItem(Actor $owner, array $vars, array $items, array $collections); abstract protected function addItem(Actor $owner, array $vars, array $items, array $collections): void;
/** /**
* Check the route to determine whether the widget should be added * Check the route to determine whether the widget should be added
*
* @param array<string, mixed> $vars
*/ */
abstract protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool; abstract protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool;
/** /**
* Get array of collections's owned by $actor * Get array of collections's owned by $actor
* *
* @param Actor $owner Collection's owner * @param Actor $owner Collection's owner
* @param ?array $vars Page vars sent by AppendRightPanelBlock event * @param null|array<string, mixed> $vars Page vars sent by AppendRightPanelBlock event
* @param bool $ids_only if true, the function must return only the primary key or each collections * @param bool $ids_only if true, the function must return only the primary key or each collections
*
* @return int[]|T[]
*/ */
abstract protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array; abstract protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array;
@@ -93,8 +101,11 @@ trait MetaCollectionTrait
* Append Collections widget to the right panel. * Append Collections widget to the right panel.
* It's compose of two forms: one to select collections to add * It's compose of two forms: one to select collections to add
* the current item to, and another to create a new collection. * the current item to, and another to create a new collection.
*
* @param array<string, mixed> $vars
* @param string[] $res
*/ */
public function onAppendRightPanelBlock(Request $request, $vars, &$res): bool public function onAppendRightPanelBlock(Request $request, array $vars, array &$res): EventResult
{ {
$user = Common::actor(); $user = Common::actor();
if (\is_null($user)) { if (\is_null($user)) {
@@ -186,7 +197,10 @@ trait MetaCollectionTrait
return Event::next; return Event::next;
} }
public function onEndShowStyles(array &$styles, string $route): bool /**
* @param string[] $styles
*/
public function onEndShowStyles(array &$styles, string $route): EventResult
{ {
$styles[] = 'components/Collection/assets/css/widget.css'; $styles[] = 'components/Collection/assets/css/widget.css';
$styles[] = 'components/Collection/assets/css/pages.css'; $styles[] = 'components/Collection/assets/css/pages.css';

View File

@@ -32,6 +32,9 @@ abstract class Parser
{ {
/** /**
* Merge $parts into $criteria_arr * Merge $parts into $criteria_arr
*
* @param mixed[] $parts
* @param Criteria[] $criteria_arr
*/ */
private static function connectParts(array &$parts, array &$criteria_arr, string $last_op, mixed $eb, bool $force = false): void private static function connectParts(array &$parts, array &$criteria_arr, string $last_op, mixed $eb, bool $force = false): void
{ {

View File

@@ -46,6 +46,9 @@ use Component\Conversation\Entity\ConversationMute;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Conversation extends FeedController class Conversation extends FeedController
{ {
/** /**
@@ -55,7 +58,10 @@ class Conversation extends FeedController
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* *
* @return array Array containing keys: 'notes' (all known notes in the given Conversation), 'should_format' (boolean, stating if onFormatNoteList events may or not format given notes), 'page_title' (used as the title header) * @return ControllerResultType Array containing keys: 'notes' (all known
* notes in the given Conversation), 'should_format' (boolean, stating if
* onFormatNoteList events may or not format given notes), 'page_title'
* (used as the title header)
*/ */
public function showConversation(Request $request, int $conversation_id): array public function showConversation(Request $request, int $conversation_id): array
{ {
@@ -83,7 +89,7 @@ class Conversation extends FeedController
* @throws NoSuchNoteException * @throws NoSuchNoteException
* @throws ServerException * @throws ServerException
* *
* @return array * @return ControllerResultType
*/ */
public function addReply(Request $request) public function addReply(Request $request)
{ {
@@ -103,7 +109,7 @@ class Conversation extends FeedController
* @throws \App\Util\Exception\RedirectException * @throws \App\Util\Exception\RedirectException
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* *
* @return array Array containing templating where the form is to be rendered, and the form itself * @return ControllerResultType Array containing templating where the form is to be rendered, and the form itself
*/ */
public function muteConversation(Request $request, int $conversation_id) public function muteConversation(Request $request, int $conversation_id)
{ {

View File

@@ -40,12 +40,13 @@ use App\Util\Common;
use App\Util\Formatting; use App\Util\Formatting;
use Component\Conversation\Entity\Conversation as ConversationEntity; use Component\Conversation\Entity\Conversation as ConversationEntity;
use Component\Conversation\Entity\ConversationMute; use Component\Conversation\Entity\ConversationMute;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Conversation extends Component class Conversation extends Component
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('conversation', '/conversation/{conversation_id<\d+>}', [Controller\Conversation::class, 'showConversation']); $r->connect('conversation', '/conversation/{conversation_id<\d+>}', [Controller\Conversation::class, 'showConversation']);
$r->connect('conversation_mute', '/conversation/{conversation_id<\d+>}/mute', [Controller\Conversation::class, 'muteConversation']); $r->connect('conversation_mute', '/conversation/{conversation_id<\d+>}/mute', [Controller\Conversation::class, 'muteConversation']);
@@ -95,16 +96,16 @@ class Conversation extends Component
* action, if a user is logged in. * action, if a user is logged in.
* *
* @param \App\Entity\Note $note The Note being rendered * @param \App\Entity\Note $note The Note being rendered
* @param array $actions Contains keys 'url' (linking 'conversation_reply_to' * @param array{url: string, title: string, classes: string, id: string} $actions
* route), 'title' (used as title for aforementioned url), * Contains keys 'url' (linking 'conversation_reply_to' route),
* 'classes' (CSS styling classes used to visually inform the user of action context), * 'title' (used as title for aforementioned url), 'classes' (CSS styling
* 'id' (HTML markup id used to redirect user to this anchor upon performing the action) * classes used to visually inform the user of action context), 'id' (HTML
* markup id used to redirect user to this anchor upon performing the
* action)
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
*
* @return bool EventHook
*/ */
public function onAddNoteActions(Request $request, Note $note, array &$actions): bool public function onAddNoteActions(Request $request, Note $note, array &$actions): EventResult
{ {
if (\is_null(Common::user())) { if (\is_null(Common::user())) {
return Event::next; return Event::next;
@@ -139,12 +140,13 @@ class Conversation extends Component
/** /**
* Append on note information about user actions. * Append on note information about user actions.
* *
* @param array $vars Contains information related to Note currently being rendered * @param array<string, mixed> $vars Contains information related to Note currently being rendered
* @param array $result Contains keys 'actors', and 'action'. Needed to construct a string, stating who ($result['actors']), has already performed a reply ($result['action']), in the given Note (vars['note']) * @param array{actors: Actor[], action: string} $result
* *cContains keys 'actors', and 'action'. Needed to construct a string,
* @return bool EventHook * stating who ($result['actors']), has already performed a reply
* ($result['action']), in the given Note (vars['note'])
*/ */
public function onAppendCardNote(array $vars, array &$result): bool public function onAppendCardNote(array $vars, array &$result): EventResult
{ {
if (str_contains($vars['request']->getPathInfo(), 'conversation')) { if (str_contains($vars['request']->getPathInfo(), 'conversation')) {
return Event::next; return Event::next;
@@ -194,10 +196,8 @@ class Conversation extends Component
* *
* @param \App\Entity\Actor $actor The Actor currently attempting to post a Note * @param \App\Entity\Actor $actor The Actor currently attempting to post a Note
* @param null|\App\Entity\Actor $context_actor The 'owner' of the current route (e.g. Group or Actor), used to target it * @param null|\App\Entity\Actor $context_actor The 'owner' of the current route (e.g. Group or Actor), used to target it
*
* @return bool EventHook
*/ */
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): bool public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): EventResult
{ {
$to_note_id = $this->getReplyToIdFromRequest($request); $to_note_id = $this->getReplyToIdFromRequest($request);
if (!\is_null($to_note_id)) { if (!\is_null($to_note_id)) {
@@ -211,14 +211,12 @@ class Conversation extends Component
/** /**
* Posting event to add extra information to Component\Posting form data * Posting event to add extra information to Component\Posting form data
* *
* @param array $data Transport data to be filled with reply_to_id * @param array{reply_to_id: int} $data Transport data to be filled with reply_to_id
* *
* @throws \App\Util\Exception\ClientException * @throws \App\Util\Exception\ClientException
* @throws \App\Util\Exception\NoSuchNoteException * @throws \App\Util\Exception\NoSuchNoteException
*
* @return bool EventHook
*/ */
public function onPostingModifyData(Request $request, Actor $actor, array &$data): bool public function onPostingModifyData(Request $request, Actor $actor, array &$data): EventResult
{ {
$to_note_id = $this->getReplyToIdFromRequest($request); $to_note_id = $this->getReplyToIdFromRequest($request);
if (!\is_null($to_note_id)) { if (!\is_null($to_note_id)) {
@@ -231,8 +229,10 @@ class Conversation extends Component
/** /**
* Add minimal Note card to RightPanel template * Add minimal Note card to RightPanel template
*
* @param string[] $elements
*/ */
public function onPrependPostingForm(Request $request, array &$elements): bool public function onPrependPostingForm(Request $request, array &$elements): EventResult
{ {
$elements[] = Formatting::twigRenderFile('cards/blocks/note_compact_wrapper.html.twig', ['note' => Note::getById((int) $request->query->get('reply_to_id'))]); $elements[] = Formatting::twigRenderFile('cards/blocks/note_compact_wrapper.html.twig', ['note' => Note::getById((int) $request->query->get('reply_to_id'))]);
return Event::next; return Event::next;
@@ -244,10 +244,8 @@ class Conversation extends Component
* *
* @param \App\Entity\Note $note Note being deleted * @param \App\Entity\Note $note Note being deleted
* @param \App\Entity\Actor $actor Actor that performed the delete action * @param \App\Entity\Actor $actor Actor that performed the delete action
*
* @return bool EventHook
*/ */
public function onNoteDeleteRelated(Note &$note, Actor $actor): bool public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult
{ {
// Ensure we have the most up to date replies // Ensure we have the most up to date replies
Cache::delete(Note::cacheKeys($note->getId())['replies']); Cache::delete(Note::cacheKeys($note->getId())['replies']);
@@ -260,13 +258,13 @@ class Conversation extends Component
* Adds extra actions related to Conversation Component, that act upon/from the given Note. * Adds extra actions related to Conversation Component, that act upon/from the given Note.
* *
* @param \App\Entity\Note $note Current Note being rendered * @param \App\Entity\Note $note Current Note being rendered
* @param array $actions Containing 'url' (Controller connected route), 'title' (used in anchor link containing the url), ?'classes' (CSS classes required for styling, if needed) * @param array{url: string, title: string, classes?: string} $actions Containing 'url' (Controller connected
* route), 'title' (used in anchor link containing the url), ?'classes' (CSS classes required for styling, if
* needed)
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
*
* @return bool EventHook
*/ */
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): bool public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): EventResult
{ {
if (\is_null($user = Common::user())) { if (\is_null($user = Common::user())) {
return Event::next; return Event::next;
@@ -299,10 +297,8 @@ class Conversation extends Component
* Prevents new Notifications to appear for muted conversations * Prevents new Notifications to appear for muted conversations
* *
* @param Activity $activity Notification Activity * @param Activity $activity Notification Activity
*
* @return bool EventHook
*/ */
public function onNewNotificationShould(Activity $activity, Actor $actor): bool public function onNewNotificationShould(Activity $activity, Actor $actor): EventResult
{ {
if ($activity->getObjectType() === 'note' && ConversationMute::isMuted($activity, $actor)) { if ($activity->getObjectType() === 'note' && ConversationMute::isMuted($activity, $actor)) {
return Event::stop; return Event::stop;

View File

@@ -41,10 +41,15 @@ use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController; use Component\Collection\Util\Controller\FeedController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Feeds extends FeedController class Feeds extends FeedController
{ {
/** /**
* The Planet feed represents every local post. Which is what this instance has to share with the universe. * The Planet feed represents every local post. Which is what this instance has to share with the universe.
*
* @return ControllerResultType
*/ */
public function public(Request $request): array public function public(Request $request): array
{ {
@@ -60,6 +65,8 @@ class Feeds extends FeedController
/** /**
* The Home feed represents everything that concerns a certain actor (its subscriptions) * The Home feed represents everything that concerns a certain actor (its subscriptions)
*
* @return ControllerResultType
*/ */
public function home(Request $request): array public function home(Request $request): array
{ {

View File

@@ -27,10 +27,11 @@ use App\Core\Event;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router;
use Component\Feed\Controller as C; use Component\Feed\Controller as C;
use EventResult;
class Feed extends Component class Feed extends Component
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('feed_public', '/feed/public', [C\Feeds::class, 'public']); $r->connect('feed_public', '/feed/public', [C\Feeds::class, 'public']);
$r->connect('feed_home', '/feed/home', [C\Feeds::class, 'home']); $r->connect('feed_home', '/feed/home', [C\Feeds::class, 'home']);

View File

@@ -54,6 +54,7 @@ use Component\FreeNetwork\Util\WebfingerResource;
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor; use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor;
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceNote; use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceNote;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use EventResult;
use Exception; use Exception;
use const PREG_SET_ORDER; use const PREG_SET_ORDER;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
@@ -79,13 +80,13 @@ class FreeNetwork extends Component
public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize'; public const OAUTH_AUTHORIZE_REL = 'http://apinamespace.org/oauth/authorize';
private static array $protocols = []; private static array $protocols = [];
public function onInitializeComponent(): bool public function onInitializeComponent(): EventResult
{ {
Event::handle('AddFreeNetworkProtocol', [&self::$protocols]); Event::handle('AddFreeNetworkProtocol', [&self::$protocols]);
return Event::next; return Event::next;
} }
public function onAddRoute(Router $m): bool public function onAddRoute(Router $m): EventResult
{ {
// Feeds // Feeds
$m->connect('feed_network', '/feed/network', [Feeds::class, 'network']); $m->connect('feed_network', '/feed/network', [Feeds::class, 'network']);
@@ -111,7 +112,7 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering) public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult
{ {
DB::persist(\App\Entity\Feed::create(['actor_id' => $actor_id, 'url' => Router::url($route = 'feed_network'), 'route' => $route, 'title' => _m('Meteorites'), 'ordering' => $ordering++])); DB::persist(\App\Entity\Feed::create(['actor_id' => $actor_id, 'url' => Router::url($route = 'feed_network'), 'route' => $route, 'title' => _m('Meteorites'), 'ordering' => $ordering++]));
DB::persist(\App\Entity\Feed::create(['actor_id' => $actor_id, 'url' => Router::url($route = 'feed_clique'), 'route' => $route, 'title' => _m('Planetary System'), 'ordering' => $ordering++])); DB::persist(\App\Entity\Feed::create(['actor_id' => $actor_id, 'url' => Router::url($route = 'feed_clique'), 'route' => $route, 'title' => _m('Planetary System'), 'ordering' => $ordering++]));
@@ -119,7 +120,7 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onStartGetProfileAcctUri(Actor $profile, &$acct): bool public function onStartGetProfileAcctUri(Actor $profile, &$acct): EventResult
{ {
$wfr = new WebFingerResourceActor($profile); $wfr = new WebFingerResourceActor($profile);
try { try {
@@ -147,7 +148,7 @@ class FreeNetwork extends Component
* @throws NoSuchActorException * @throws NoSuchActorException
* @throws ServerException * @throws ServerException
*/ */
public function onEndGetWebFingerResource(string $resource, ?WebfingerResource &$target = null, array $args = []): bool public function onEndGetWebFingerResource(string $resource, ?WebfingerResource &$target = null, array $args = []): EventResult
{ {
// * Either we didn't find the profile, then we want to make // * Either we didn't find the profile, then we want to make
// the $profile variable null for clarity. // the $profile variable null for clarity.
@@ -223,7 +224,7 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onStartHostMetaLinks(array &$links): bool public function onStartHostMetaLinks(array &$links): EventResult
{ {
foreach (Discovery::supportedMimeTypes() as $type) { foreach (Discovery::supportedMimeTypes() as $type) {
$links[] = new XML_XRD_Element_Link( $links[] = new XML_XRD_Element_Link(
@@ -243,8 +244,10 @@ class FreeNetwork extends Component
/** /**
* Add a link header for LRDD Discovery * Add a link header for LRDD Discovery
*
* @param mixed $action
*/ */
public function onStartShowHTML($action): bool public function onStartShowHTML($action): EventResult
{ {
if ($action instanceof ShowstreamAction) { if ($action instanceof ShowstreamAction) {
$resource = $action->getTarget()->getUri(); $resource = $action->getTarget()->getUri();
@@ -257,13 +260,13 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onStartDiscoveryMethodRegistration(Discovery $disco): bool public function onStartDiscoveryMethodRegistration(Discovery $disco): EventResult
{ {
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodWebfinger'); $disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodWebfinger');
return Event::next; return Event::next;
} }
public function onEndDiscoveryMethodRegistration(Discovery $disco): bool public function onEndDiscoveryMethodRegistration(Discovery $disco): EventResult
{ {
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodHostMeta'); $disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodHostMeta');
$disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodLinkHeader'); $disco->registerMethod('\Component\FreeNetwork\Util\LrddMethod\LrddMethodLinkHeader');
@@ -275,7 +278,7 @@ class FreeNetwork extends Component
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*/ */
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?Response &$response = null): bool public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?Response &$response = null): EventResult
{ {
if (!\in_array($route, ['freenetwork_hostmeta', 'freenetwork_hostmeta_format', 'freenetwork_webfinger', 'freenetwork_webfinger_format', 'freenetwork_ownerxrd'])) { if (!\in_array($route, ['freenetwork_hostmeta', 'freenetwork_hostmeta_format', 'freenetwork_webfinger', 'freenetwork_webfinger_format', 'freenetwork_ownerxrd'])) {
return Event::next; return Event::next;
@@ -343,6 +346,7 @@ class FreeNetwork extends Component
* @param string $preMention Character(s) that signals a mention ('@', '!'...) * @param string $preMention Character(s) that signals a mention ('@', '!'...)
* *
* @return array the matching URLs (without @ or acct:) and each respective position in the given string * @return array the matching URLs (without @ or acct:) and each respective position in the given string
*
* @example.com/mublog/user * @example.com/mublog/user
*/ */
public static function extractUrlMentions(string $text, string $preMention = '@'): array public static function extractUrlMentions(string $text, string $preMention = '@'): array
@@ -374,9 +378,10 @@ class FreeNetwork extends Component
* @param $mentions * @param $mentions
* *
* @return bool hook return value * @return bool hook return value
*
* @example.com/mublog/user * @example.com/mublog/user
*/ */
public function onEndFindMentions(Actor $sender, string $text, array &$mentions): bool public function onEndFindMentions(Actor $sender, string $text, array &$mentions): EventResult
{ {
$matches = []; $matches = [];
@@ -495,6 +500,9 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
/**
* @param Actor[] $targets
*/
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
{ {
foreach (self::$protocols as $protocol) { foreach (self::$protocols as $protocol) {
@@ -516,8 +524,11 @@ class FreeNetwork extends Component
/** /**
* Add fediverse: query expression * Add fediverse: query expression
* // TODO: adding WebFinger would probably be nice * // TODO: adding WebFinger would probably be nice
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{ {
if (Formatting::startsWith($term, ['fediverse:'])) { if (Formatting::startsWith($term, ['fediverse:'])) {
foreach (self::$protocols as $protocol) { foreach (self::$protocols as $protocol) {
@@ -530,7 +541,7 @@ class FreeNetwork extends Component
return Event::next; return Event::next;
} }
public function onPluginVersion(array &$versions): bool public function onPluginVersion(array &$versions): EventResult
{ {
$versions[] = [ $versions[] = [
'name' => 'WebFinger', 'name' => 'WebFinger',

View File

@@ -61,6 +61,8 @@ class Group extends Controller
* *
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupCreate(Request $request): array public function groupCreate(Request $request): array
{ {
@@ -89,6 +91,8 @@ class Group extends Controller
* @throws NicknameTooLongException * @throws NicknameTooLongException
* @throws NotFoundException * @throws NotFoundException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupSettings(Request $request, int $id): array public function groupSettings(Request $request, int $id): array
{ {

View File

@@ -40,10 +40,15 @@ use Component\Subscription\Entity\ActorSubscription;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class GroupFeed extends FeedController class GroupFeed extends FeedController
{ {
/** /**
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupView(Request $request, Actor $group): array public function groupView(Request $request, Actor $group): array
{ {
@@ -96,6 +101,8 @@ class GroupFeed extends FeedController
/** /**
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupViewId(Request $request, int $id): array public function groupViewId(Request $request, int $id): array
{ {
@@ -119,6 +126,8 @@ class GroupFeed extends FeedController
* *
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function groupViewNickname(Request $request, string $nickname): array public function groupViewNickname(Request $request, string $nickname): array
{ {

View File

@@ -33,11 +33,12 @@ use App\Util\Nickname;
use Component\Group\Controller as C; use Component\Group\Controller as C;
use Component\Group\Entity\LocalGroup; use Component\Group\Entity\LocalGroup;
use Component\Notification\Notification; use Component\Notification\Notification;
use EventResult;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Group extends Component class Group extends Component
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect(id: 'group_actor_view_id', uri_path: '/group/{id<\d+>}', target: [C\GroupFeed::class, 'groupViewId']); $r->connect(id: 'group_actor_view_id', uri_path: '/group/{id<\d+>}', target: [C\GroupFeed::class, 'groupViewId']);
$r->connect(id: 'group_actor_view_nickname', uri_path: '/!{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\GroupFeed::class, 'groupViewNickname']); $r->connect(id: 'group_actor_view_nickname', uri_path: '/!{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\GroupFeed::class, 'groupViewNickname']);
@@ -49,8 +50,10 @@ class Group extends Component
/** /**
* Enqueues a notification for an Actor (such as person or group) which means * Enqueues a notification for an Actor (such as person or group) which means
* it shows up in their home feed and such. * it shows up in their home feed and such.
*
* @param Actor[] $targets
*/ */
public function onNewNotificationStart(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): bool public function onNewNotificationStart(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): EventResult
{ {
foreach ($targets as $target) { foreach ($targets as $target) {
if ($target->isGroup()) { if ($target->isGroup()) {
@@ -69,8 +72,11 @@ class Group extends Component
/** /**
* Add an <a href=group_actor_settings> to the profile card for groups, if the current actor can access them * Add an <a href=group_actor_settings> to the profile card for groups, if the current actor can access them
*
* @param array<string, mixed> $vars
* @param string[] $res
*/ */
public function onAppendCardProfile(array $vars, array &$res): bool public function onAppendCardProfile(array $vars, array &$res): EventResult
{ {
$actor = Common::actor(); $actor = Common::actor();
$group = $vars['actor']; $group = $vars['actor'];
@@ -108,7 +114,10 @@ class Group extends Component
return null; return null;
} }
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): bool /**
* @param Actor[] $targets
*/
public function onPostingFillTargetChoices(Request $request, Actor $actor, array &$targets): EventResult
{ {
$group = $this->getGroupFromContext($request); $group = $this->getGroupFromContext($request);
if (!\is_null($group)) { if (!\is_null($group)) {
@@ -127,7 +136,7 @@ class Group extends Component
* *
* @param null|Actor $context_actor Actor group, if current route is part of an existing Group set of routes * @param null|Actor $context_actor Actor group, if current route is part of an existing Group set of routes
*/ */
public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): bool public function onPostingGetContextActor(Request $request, Actor $actor, ?Actor &$context_actor): EventResult
{ {
$ctx = $this->getGroupFromContext($request); $ctx = $this->getGroupFromContext($request);
if (!\is_null($ctx)) { if (!\is_null($ctx)) {

View File

@@ -100,6 +100,8 @@ class Language extends Controller
* @throws NoLoggedInUser * @throws NoLoggedInUser
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function sortLanguages(Request $request): array public function sortLanguages(Request $request): array
{ {

View File

@@ -119,6 +119,9 @@ class ActorLanguage extends Entity
) ?: [Language::getByLocale(Common::config('site', 'language'))]; ) ?: [Language::getByLocale(Common::config('site', 'language'))];
} }
/**
* @return int[]
*/
public static function getActorRelatedLanguagesIds(Actor $actor): array public static function getActorRelatedLanguagesIds(Actor $actor): array
{ {
return Cache::getList( return Cache::getList(

View File

@@ -134,6 +134,9 @@ class Language extends Entity
return self::getById($note->getLanguageId()); return self::getById($note->getLanguageId());
} }
/**
* @return array<string, string>
*/
public static function getLanguageChoices(): array public static function getLanguageChoices(): array
{ {
$langs = Cache::getHashMap( $langs = Cache::getHashMap(
@@ -144,6 +147,8 @@ class Language extends Entity
return array_merge(...F\map(array_values($langs), fn ($l) => $l->toChoiceFormat())); return array_merge(...F\map(array_values($langs), fn ($l) => $l->toChoiceFormat()));
} }
/**
* @return array<string, string> */
public function toChoiceFormat(): array public function toChoiceFormat(): array
{ {
return [_m($this->getLongDisplay()) => $this->getLocale()]; return [_m($this->getLongDisplay()) => $this->getLocale()];
@@ -152,6 +157,8 @@ class Language extends Entity
/** /**
* Get all the available languages as well as the languages $actor * Get all the available languages as well as the languages $actor
* prefers and are appropriate for posting in/to $context_actor * prefers and are appropriate for posting in/to $context_actor
*
* @return array{array<string, string>, array<string, string>}
*/ */
public static function getSortedLanguageChoices(?Actor $actor, ?Actor $context_actor, ?bool $use_short_display): array public static function getSortedLanguageChoices(?Actor $actor, ?Actor $context_actor, ?bool $use_short_display): array
{ {

View File

@@ -33,18 +33,22 @@ use Component\Language\Entity\ActorLanguage;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Language extends Component class Language extends Component
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('settings_sort_languages', '/settings/sort_languages', [C\Language::class, 'sortLanguages']); $r->connect('settings_sort_languages', '/settings/sort_languages', [C\Language::class, 'sortLanguages']);
return Event::next; return Event::next;
} }
public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): bool /**
* @param Note[] $notes
*/
public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): EventResult
{ {
if (\is_null($actor)) { if (\is_null($actor)) {
return Event::next; return Event::next;
@@ -59,8 +63,11 @@ class Language extends Component
/** /**
* Populate $note_expr or $actor_expr with an expression to match a language * Populate $note_expr or $actor_expr with an expression to match a language
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{ {
$search_term = str_contains($term, ':') ? explode(':', $term)[1] : $term; $search_term = str_contains($term, ':') ? explode(':', $term)[1] : $term;
@@ -103,7 +110,7 @@ class Language extends Component
return Event::next; return Event::next;
} }
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult
{ {
$note_aliases = $note_qb->getAllAliases(); $note_aliases = $note_qb->getAllAliases();
if (!\in_array('note_language', $note_aliases)) { if (!\in_array('note_language', $note_aliases)) {
@@ -116,7 +123,7 @@ class Language extends Component
$note_qb->leftJoin('Component\Language\Entity\Language', 'note_actor_language', Expr\Join::WITH, 'note_actor_language.id = actor_language.language_id'); $note_qb->leftJoin('Component\Language\Entity\Language', 'note_actor_language', Expr\Join::WITH, 'note_actor_language.id = actor_language.language_id');
} }
$actor_aliases = $note_qb->getAllAliases(); $actor_aliases = $actor_qb->getAllAliases();
if (!\in_array('actor_language', $actor_aliases)) { if (!\in_array('actor_language', $actor_aliases)) {
$actor_qb->leftJoin('Component\Language\Entity\ActorLanguage', 'actor_language', Expr\Join::WITH, 'actor.id = actor_language.actor_id'); $actor_qb->leftJoin('Component\Language\Entity\ActorLanguage', 'actor_language', Expr\Join::WITH, 'actor.id = actor_language.actor_id');
} }

View File

@@ -104,7 +104,6 @@ class EditFeeds extends Controller
$feed->setUrl($fd[$md5 . '-url']); $feed->setUrl($fd[$md5 . '-url']);
$feed->setOrdering($fd[$md5 . '-order']); $feed->setOrdering($fd[$md5 . '-order']);
$feed->setTitle($fd[$md5 . '-title']); $feed->setTitle($fd[$md5 . '-title']);
DB::merge($feed);
} }
DB::flush(); DB::flush();
Cache::delete($key); Cache::delete($key);
@@ -119,7 +118,6 @@ class EditFeeds extends Controller
/** @var SubmitButton $remove_button */ /** @var SubmitButton $remove_button */
$remove_button = $form->get($remove_id); $remove_button = $form->get($remove_id);
if ($remove_button->isClicked()) { if ($remove_button->isClicked()) {
// @phpstan-ignore-next-line -- Doesn't quite understand that _this_ $opts for the current $form_definitions does have 'data'
DB::remove(DB::getReference('feed', ['actor_id' => $user->getId(), 'url' => $opts['data']])); DB::remove(DB::getReference('feed', ['actor_id' => $user->getId(), 'url' => $opts['data']]));
DB::flush(); DB::flush();
Cache::delete($key); Cache::delete($key);

View File

@@ -32,21 +32,24 @@ use App\Entity\Feed;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\NotFoundException; use App\Util\Exception\NotFoundException;
use Component\LeftPanel\Controller as C; use Component\LeftPanel\Controller as C;
use EventResult;
class LeftPanel extends Component class LeftPanel extends Component
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('edit_feeds', '/edit-feeds', C\EditFeeds::class); $r->connect('edit_feeds', '/edit-feeds', C\EditFeeds::class);
return Event::next; return Event::next;
} }
/** /**
* @param array<string, string> $route_params
*
* @throws \App\Util\Exception\DuplicateFoundException * @throws \App\Util\Exception\DuplicateFoundException
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
*/ */
public function onAppendFeed(Actor $actor, string $title, string $route, array $route_params): bool public function onAppendFeed(Actor $actor, string $title, string $route, array $route_params): EventResult
{ {
$cache_key = Feed::cacheKey($actor); $cache_key = Feed::cacheKey($actor);
$feeds = Feed::getFeeds($actor); $feeds = Feed::getFeeds($actor);

View File

@@ -84,6 +84,8 @@ class NoteToLink extends Entity
* Create an instance of NoteToLink or fill in the * Create an instance of NoteToLink or fill in the
* properties of $obj with the associative array $args. Doesn't * properties of $obj with the associative array $args. Doesn't
* persist the result * persist the result
*
* @param (array{link_id: int, note_id: int} & array<string, mixed>) $args
*/ */
public static function create(array $args, bool $_delegated_call = false): static public static function create(array $args, bool $_delegated_call = false): static
{ {

View File

@@ -31,6 +31,7 @@ use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\HTML; use App\Util\HTML;
use Component\Link\Entity\NoteToLink; use Component\Link\Entity\NoteToLink;
use EventResult;
use InvalidArgumentException; use InvalidArgumentException;
class Link extends Component class Link extends Component
@@ -38,7 +39,7 @@ class Link extends Component
/** /**
* Note that this persists both a Link and a NoteToLink * Note that this persists both a Link and a NoteToLink
* *
* @return [Entity\Link, NoteToLink] * @return array{ link: ?Entity\Link, note_to_link: ?NoteToLink }
*/ */
public static function maybeCreateLink(string $url, int $note_id): array public static function maybeCreateLink(string $url, int $note_id): array
{ {
@@ -53,8 +54,10 @@ class Link extends Component
/** /**
* Extract URLs from $content and create the appropriate Link and NoteToLink entities * Extract URLs from $content and create the appropriate Link and NoteToLink entities
*
* @param array{ignoreLinks?: string[]} $process_note_content_extra_args
*/ */
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $process_note_content_extra_args = []): bool public function onProcessNoteContent(Note $note, string $content, string $content_type, array $process_note_content_extra_args = []): EventResult
{ {
$ignore = $process_note_content_extra_args['ignoreLinks'] ?? []; $ignore = $process_note_content_extra_args['ignoreLinks'] ?? [];
if (Common::config('attachments', 'process_links')) { if (Common::config('attachments', 'process_links')) {
@@ -65,13 +68,13 @@ class Link extends Component
if (\in_array($match, $ignore)) { if (\in_array($match, $ignore)) {
continue; continue;
} }
self::maybeCreateLink($match, $note_id); self::maybeCreateLink($match, $note->getId());
} }
} }
return Event::next; return Event::next;
} }
public function onRenderPlainTextNoteContent(string &$text): bool public function onRenderPlainTextNoteContent(string &$text): EventResult
{ {
$text = $this->replaceURLs($text); $text = $this->replaceURLs($text);
return Event::next; return Event::next;
@@ -149,7 +152,12 @@ class Link extends Component
public const URL_SCHEME_NO_DOMAIN = 4; public const URL_SCHEME_NO_DOMAIN = 4;
public const URL_SCHEME_COLON_COORDINATES = 8; public const URL_SCHEME_COLON_COORDINATES = 8;
public function URLSchemes($filter = null) /**
* @param self::URL_SCHEME_COLON_COORDINATES|self::URL_SCHEME_COLON_DOUBLE_SLASH|self::URL_SCHEME_NO_DOMAIN|self::URL_SCHEME_SINGLE_COLON $filter
*
* @return string[]
*/
public function URLSchemes(?int $filter = null): array
{ {
// TODO: move these to config // TODO: move these to config
$schemes = [ $schemes = [
@@ -196,6 +204,7 @@ class Link extends Component
* Intermediate callback for `replaceURLs()`, which helps resolve some * Intermediate callback for `replaceURLs()`, which helps resolve some
* ambiguous link forms before passing on to the final callback. * ambiguous link forms before passing on to the final callback.
* *
* @param string[] $matches
* @param callable(string $text): string $callback: return replacement text * @param callable(string $text): string $callback: return replacement text
*/ */
private function callbackHelper(array $matches, callable $callback): string private function callbackHelper(array $matches, callable $callback): string
@@ -275,7 +284,7 @@ class Link extends Component
return HTML::html(['a' => ['attrs' => $attrs, $url]], options: ['indent' => false]); return HTML::html(['a' => ['attrs' => $attrs, $url]], options: ['indent' => false]);
} }
public function onNoteDeleteRelated(Note &$note, Actor $actor): bool public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult
{ {
DB::wrapInTransaction(fn () => NoteToLink::removeWhereNoteId($note->getId())); DB::wrapInTransaction(fn () => NoteToLink::removeWhereNoteId($note->getId()));
return Event::next; return Event::next;

View File

@@ -44,6 +44,8 @@ class Feed extends Controller
{ {
/** /**
* Everything with attention to current user * Everything with attention to current user
*
* @return ControllerResultType
*/ */
public function notifications(Request $request): array public function notifications(Request $request): array
{ {

View File

@@ -117,7 +117,7 @@ class Notification extends Entity
/** /**
* Pull the complete list of known activity context notifications for this activity. * Pull the complete list of known activity context notifications for this activity.
* *
* @return array of integer actor ids (also group profiles) * @return int[] actor ids (also group profiles)
*/ */
public static function getNotificationTargetIdsByActivity(int|Activity $activity_id): array public static function getNotificationTargetIdsByActivity(int|Activity $activity_id): array
{ {
@@ -129,11 +129,17 @@ class Notification extends Entity
return $targets; return $targets;
} }
/**
* @return int[]
*/
public function getNotificationTargetsByActivity(int|Activity $activity_id): array public function getNotificationTargetsByActivity(int|Activity $activity_id): array
{ {
return DB::findBy(Actor::class, ['id' => $this->getNotificationTargetIdsByActivity($activity_id)]); return DB::findBy(Actor::class, ['id' => $this->getNotificationTargetIdsByActivity($activity_id)]);
} }
/**
* @return int[]
*/
public static function getAllActivitiesTargetedAtActor(Actor $actor): array public static function getAllActivitiesTargetedAtActor(Actor $actor): array
{ {
return DB::dql(<<<'EOF' return DB::dql(<<<'EOF'

View File

@@ -34,12 +34,13 @@ use App\Entity\LocalUser;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use Component\FreeNetwork\FreeNetwork; use Component\FreeNetwork\FreeNetwork;
use Component\Notification\Controller\Feed; use Component\Notification\Controller\Feed;
use EventResult;
use Exception; use Exception;
use Throwable; use Throwable;
class Notification extends Component class Notification extends Component
{ {
public function onAddRoute(Router $m): bool public function onAddRoute(Router $m): EventResult
{ {
$m->connect('feed_notifications', '/feed/notifications', [Feed::class, 'notifications']); $m->connect('feed_notifications', '/feed/notifications', [Feed::class, 'notifications']);
return Event::next; return Event::next;
@@ -48,7 +49,7 @@ class Notification extends Component
/** /**
* @throws ServerException * @throws ServerException
*/ */
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): bool public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult
{ {
DB::persist(\App\Entity\Feed::create([ DB::persist(\App\Entity\Feed::create([
'actor_id' => $actor_id, 'actor_id' => $actor_id,
@@ -72,10 +73,10 @@ class Notification extends Component
* *
* @param Actor $sender The one responsible for this activity, take care not to include it in targets * @param Actor $sender The one responsible for this activity, take care not to include it in targets
* @param Activity $activity The activity responsible for the object being given to known to targets * @param Activity $activity The activity responsible for the object being given to known to targets
* @param array $targets Attentions, Mentions, any other source. Should never be empty, you usually want to register an attention to every $sender->getSubscribers() * @param non-empty-array<Actor|int> $targets Attentions, Mentions, any other source. Should never be empty, you usually want to register an attention to every $sender->getSubscribers()
* @param null|string $reason An optional reason explaining why this notification exists * @param null|string $reason An optional reason explaining why this notification exists
*/ */
public function onNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool public function onNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): EventResult
{ {
// Ensure targets are all actor objects and unique // Ensure targets are all actor objects and unique
$effective_targets = []; $effective_targets = [];
@@ -102,13 +103,20 @@ class Notification extends Component
return Event::next; return Event::next;
} }
public function onQueueNotificationLocal(Actor $sender, Activity $activity, Actor $target, ?string $reason, array &$retry_args): bool /**
* @param mixed[] $retry_args
*/
public function onQueueNotificationLocal(Actor $sender, Activity $activity, Actor $target, ?string $reason, array &$retry_args): EventResult
{ {
// TODO: use https://symfony.com/doc/current/notifier.html // TODO: use https://symfony.com/doc/current/notifier.html
return Event::stop; return Event::stop;
} }
public function onQueueNotificationRemote(Actor $sender, Activity $activity, array $targets, ?string $reason, array &$retry_args): bool /**
* @param Actor[] $targets
* @param mixed[] $retry_args
*/
public function onQueueNotificationRemote(Actor $sender, Activity $activity, array $targets, ?string $reason, array &$retry_args): EventResult
{ {
if (FreeNetwork::notify($sender, $activity, $targets, $reason)) { if (FreeNetwork::notify($sender, $activity, $targets, $reason)) {
return Event::stop; return Event::stop;
@@ -121,7 +129,9 @@ class Notification extends Component
* Bring given Activity to Targets' knowledge. * Bring given Activity to Targets' knowledge.
* This will flush a Notification to DB. * This will flush a Notification to DB.
* *
* @return true if successful, false otherwise * @param Actor[] $targets
*
* @return bool true if successful, false otherwise
*/ */
public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool
{ {

View File

@@ -34,11 +34,16 @@ use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController; use Component\Collection\Util\Controller\FeedController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class PersonFeed extends FeedController class PersonFeed extends FeedController
{ {
/** /**
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function personViewId(Request $request, int $id): array public function personViewId(Request $request, int $id): array
{ {
@@ -62,6 +67,8 @@ class PersonFeed extends FeedController
* *
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function personViewNickname(Request $request, string $nickname): array public function personViewNickname(Request $request, string $nickname): array
{ {
@@ -73,6 +80,9 @@ class PersonFeed extends FeedController
return $this->personView($request, $person); return $this->personView($request, $person);
} }
/**
* @return ControllerResultType
*/
public function personView(Request $request, Actor $person): array public function personView(Request $request, Actor $person): array
{ {
return [ return [

View File

@@ -88,6 +88,8 @@ class PersonSettings extends Controller
* @throws NoLoggedInUser * @throws NoLoggedInUser
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function allSettings(Request $request, LanguageController $language): array public function allSettings(Request $request, LanguageController $language): array
{ {
@@ -205,6 +207,8 @@ class PersonSettings extends Controller
* @throws \Doctrine\DBAL\Exception * @throws \Doctrine\DBAL\Exception
* @throws NoLoggedInUser * @throws NoLoggedInUser
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType[]
*/ */
private static function notifications(Request $request): array private static function notifications(Request $request): array
{ {

View File

@@ -26,10 +26,11 @@ use App\Core\Modules\Component;
use App\Core\Router; use App\Core\Router;
use App\Util\Nickname; use App\Util\Nickname;
use Component\Person\Controller as C; use Component\Person\Controller as C;
use EventResult;
class Person extends Component class Person extends Component
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect(id: 'person_actor_view_id', uri_path: '/person/{id<\d+>}', target: [C\PersonFeed::class, 'personViewId']); $r->connect(id: 'person_actor_view_id', uri_path: '/person/{id<\d+>}', target: [C\PersonFeed::class, 'personViewId']);
$r->connect(id: 'person_actor_view_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\PersonFeed::class, 'personViewNickname'], options: ['is_system_path' => false]); $r->connect(id: 'person_actor_view_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}', target: [C\PersonFeed::class, 'personViewNickname'], options: ['is_system_path' => false]);

View File

@@ -33,10 +33,6 @@ class PersonSettingsTest extends GNUsocialTestCase
{ {
use AssertThrows; use AssertThrows;
/**
* @covers \App\Controller\PersonSettings::allSettings
* @covers \App\Controller\PersonSettings::personalInfo
*/
public function testPersonalInfo() public function testPersonalInfo()
{ {
$client = static::createClient(); $client = static::createClient();
@@ -64,10 +60,6 @@ class PersonSettingsTest extends GNUsocialTestCase
// static::assertSame('908555842', $changed_user->getPhoneNumber()->getNationalNumber()); // static::assertSame('908555842', $changed_user->getPhoneNumber()->getNationalNumber());
} }
/**
* @covers \App\Controller\PersonSettings::account
* @covers \App\Controller\PersonSettings::allSettings
*/
public function testEmail() public function testEmail()
{ {
$client = static::createClient(); $client = static::createClient();
@@ -86,10 +78,6 @@ class PersonSettingsTest extends GNUsocialTestCase
static::assertSame($changed_user->getIncomingEmail(), 'incoming@provider.any'); static::assertSame($changed_user->getIncomingEmail(), 'incoming@provider.any');
} }
/**
* @covers \App\Controller\PersonSettings::account
* @covers \App\Controller\PersonSettings::allSettings
*/
public function testCorrectPassword() public function testCorrectPassword()
{ {
$client = static::createClient(); $client = static::createClient();
@@ -108,10 +96,6 @@ class PersonSettingsTest extends GNUsocialTestCase
static::assertTrue($changed_user->checkPassword('this is some test password')); static::assertTrue($changed_user->checkPassword('this is some test password'));
} }
/**
* @covers \App\Controller\PersonSettings::account
* @covers \App\Controller\PersonSettings::allSettings
*/
public function testAccountWrongPassword() public function testAccountWrongPassword()
{ {
$client = static::createClient(); $client = static::createClient();
@@ -130,10 +114,6 @@ class PersonSettingsTest extends GNUsocialTestCase
} }
// TODO: First actually implement this functionality // TODO: First actually implement this functionality
// /**
// * @covers \App\Controller\PersonSettings::allSettings
// * @covers \App\Controller\PersonSettings::notifications
// */
// public function testNotifications() // public function testNotifications()
// { // {
// $client = static::createClient(); // $client = static::createClient();

View File

@@ -33,6 +33,7 @@ use App\Core\Router;
use App\Core\VisibilityScope; use App\Core\VisibilityScope;
use App\Entity\Activity; use App\Entity\Activity;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\BugFoundException; use App\Util\Exception\BugFoundException;
@@ -43,10 +44,13 @@ use App\Util\Exception\ServerException;
use App\Util\Formatting; use App\Util\Formatting;
use App\Util\HTML; use App\Util\HTML;
use Component\Attachment\Entity\ActorToAttachment; use Component\Attachment\Entity\ActorToAttachment;
use Component\Attachment\Entity\Attachment;
use Component\Attachment\Entity\AttachmentToNote; use Component\Attachment\Entity\AttachmentToNote;
use Component\Conversation\Conversation; use Component\Conversation\Conversation;
use Component\Language\Entity\Language; use Component\Language\Entity\Language;
use Component\Notification\Entity\Attention; use Component\Notification\Entity\Attention;
use EventResult;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -54,7 +58,7 @@ class Posting extends Component
{ {
public const route = 'posting_form_action'; public const route = 'posting_form_action';
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect(self::route, '/form/posting', Controller\Posting::class); $r->connect(self::route, '/form/posting', Controller\Posting::class);
return Event::next; return Event::next;
@@ -64,13 +68,15 @@ class Posting extends Component
* HTML render event handler responsible for adding and handling * HTML render event handler responsible for adding and handling
* the result of adding the note submission form, only if a user is logged in * the result of adding the note submission form, only if a user is logged in
* *
* @param array{post_form?: FormInterface} $res
*
* @throws BugFoundException * @throws BugFoundException
* @throws ClientException * @throws ClientException
* @throws DuplicateFoundException * @throws DuplicateFoundException
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*/ */
public function onAddMainRightPanelBlock(Request $request, array &$res): bool public function onAddMainRightPanelBlock(Request $request, array &$res): EventResult
{ {
if (\is_null($user = Common::user()) || preg_match('(feed|conversation|group|view)', $request->get('_route')) === 0) { if (\is_null($user = Common::user()) || preg_match('(feed|conversation|group|view)', $request->get('_route')) === 0) {
return Event::next; return Event::next;
@@ -82,9 +88,25 @@ class Posting extends Component
} }
/** /**
* @param Actor $actor The Actor responsible for the creation of this Note
* @param null|string $content The raw text content
* @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...)
* @param null|string $locale Note's written text language, set by the default Actor language or upon filling
* @param null|VisibilityScope $scope The visibility of this Note
* @param Actor[]|int[] $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and target
* @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
* @param UploadedFile[] $attachments UploadedFile[] to be stored as GSFiles associated to this note
* @param array<array{Attachment, string}> $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
* @param array{note?: Note, content?: string, content_type?: string, extra_args?: array<string, mixed>} $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
* @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification
* @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent
* @param string $source The source of this Note
*
* @throws ClientException * @throws ClientException
* @throws DuplicateFoundException * @throws DuplicateFoundException
* @throws ServerException * @throws ServerException
*
* @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>}
*/ */
public static function storeLocalArticle( public static function storeLocalArticle(
Actor $actor, Actor $actor,
@@ -147,11 +169,11 @@ class Posting extends Component
* @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...) * @param string $content_type Indicating one of the various supported content format (Plain Text, Markdown, LaTeX...)
* @param null|string $locale Note's written text language, set by the default Actor language or upon filling * @param null|string $locale Note's written text language, set by the default Actor language or upon filling
* @param null|VisibilityScope $scope The visibility of this Note * @param null|VisibilityScope $scope The visibility of this Note
* @param array $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and target * @param Actor[]|int[] $attentions Actor|int[]: In Group/To Person or Bot, registers an attention between note and targte
* @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself * @param null|int|Note $reply_to The soon-to-be Note parent's id, if it's a Reply itself
* @param array $attachments UploadedFile[] to be stored as GSFiles associated to this note * @param UploadedFile[] $attachments UploadedFile[] to be stored as GSFiles associated to this note
* @param array $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note * @param array<array{Attachment, string}> $processed_attachments Array of [Attachment, Attachment's name][] to be associated to this $actor and Note
* @param array $process_note_content_extra_args Extra arguments for the event ProcessNoteContent * @param array{note?: Note, content?: string, content_type?: string, extra_args?: array<string, mixed>} $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
* @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification * @param bool $flush_and_notify True if the newly created Note activity should be passed on as a Notification
* @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent * @param null|string $rendered The Note's content post RenderNoteContent event, which sanitizes and processes the raw content sent
* @param string $source The source of this Note * @param string $source The source of this Note
@@ -160,7 +182,7 @@ class Posting extends Component
* @throws DuplicateFoundException * @throws DuplicateFoundException
* @throws ServerException * @throws ServerException
* *
* @return array [Activity, Note, Effective Attentions] * @return array{\App\Entity\Activity, \App\Entity\Note, array<int, \App\Entity\Actor>}
*/ */
public static function storeLocalNote( public static function storeLocalNote(
Actor $actor, Actor $actor,
@@ -179,6 +201,8 @@ class Posting extends Component
): array { ): array {
$scope ??= VisibilityScope::EVERYWHERE; // TODO: If site is private, default to LOCAL $scope ??= VisibilityScope::EVERYWHERE; // TODO: If site is private, default to LOCAL
$reply_to_id = \is_null($reply_to) ? null : (\is_int($reply_to) ? $reply_to : $reply_to->getId()); $reply_to_id = \is_null($reply_to) ? null : (\is_int($reply_to) ? $reply_to : $reply_to->getId());
/** @var array<int, array{ mentioned?: array<int, Actor|LocalUser> }> $mentions */
$mentions = []; $mentions = [];
if (\is_null($rendered) && !empty($content)) { if (\is_null($rendered) && !empty($content)) {
Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $locale, &$mentions]); Event::handle('RenderNoteContent', [$content, $content_type, &$rendered, $actor, $locale, &$mentions]);
@@ -298,7 +322,10 @@ class Posting extends Component
return [$activity, $note, $effective_attentions]; return [$activity, $note, $effective_attentions];
} }
public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?string $language = null, array &$mentions = []) /**
* @param array<int, \App\Entity\Actor> $mentions
*/
public function onRenderNoteContent(string $content, string $content_type, ?string &$rendered, Actor $author, ?string $language = null, array &$mentions = []): EventResult
{ {
switch ($content_type) { switch ($content_type) {
case 'text/plain': case 'text/plain':

View File

@@ -38,12 +38,17 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<\App\Entity\Note>
*/
class Search extends FeedController class Search extends FeedController
{ {
/** /**
* Handle a search query * Handle a search query
*
* @return ControllerResultType
*/ */
public function handle(Request $request) public function handle(Request $request): array
{ {
$actor = Common::actor(); $actor = Common::actor();
$language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null; $language = !\is_null($actor) ? $actor->getTopLanguage()->getLocale() : null;

View File

@@ -27,10 +27,12 @@ use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Modules\Component; use App\Core\Modules\Component;
use App\Core\Router;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use App\Util\Formatting; use App\Util\Formatting;
use EventResult;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormView;
@@ -39,9 +41,10 @@ use Symfony\Component\HttpFoundation\Request;
class Search extends Component class Search extends Component
{ {
public function onAddRoute($r) public function onAddRoute(Router $r): EventResult
{ {
$r->connect('search', '/search', Controller\Search::class); $r->connect('search', '/search', Controller\Search::class);
return EventResult::next;
} }
/** /**
@@ -131,9 +134,11 @@ class Search extends Component
/** /**
* Add the search form to the site header * Add the search form to the site header
* *
* @param string[] $elements
*
* @throws RedirectException * @throws RedirectException
*/ */
public function onPrependRightPanelBlock(Request $request, array &$elements): bool public function onPrependRightPanelBlock(Request $request, array &$elements): EventResult
{ {
$elements[] = Formatting::twigRenderFile('cards/search/view.html.twig', ['search' => self::searchForm($request)]); $elements[] = Formatting::twigRenderFile('cards/search/view.html.twig', ['search' => self::searchForm($request)]);
return Event::next; return Event::next;
@@ -142,11 +147,9 @@ class Search extends Component
/** /**
* Output our dedicated stylesheet * Output our dedicated stylesheet
* *
* @param array $styles stylesheets path * @param string[] $styles stylesheets path
*
* @return bool hook value; true means continue processing, false means stop
*/ */
public function onEndShowStyles(array &$styles, string $route): bool public function onEndShowStyles(array &$styles, string $route): EventResult
{ {
$styles[] = 'components/Search/assets/css/view.css'; $styles[] = 'components/Search/assets/css/view.css';
return Event::next; return Event::next;

View File

@@ -45,6 +45,8 @@ class Subscribers extends CircleController
{ {
/** /**
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function subscribersByActor(Request $request, Actor $actor): array public function subscribersByActor(Request $request, Actor $actor): array
{ {
@@ -61,6 +63,8 @@ class Subscribers extends CircleController
/** /**
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function subscribersByActorId(Request $request, int $id): array public function subscribersByActorId(Request $request, int $id): array
{ {
@@ -78,6 +82,8 @@ class Subscribers extends CircleController
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
* @throws RedirectException * @throws RedirectException
*
* @return ControllerResultType
*/ */
public function subscribersAdd(Request $request, int $object_id): array public function subscribersAdd(Request $request, int $object_id): array
{ {
@@ -126,6 +132,8 @@ class Subscribers extends CircleController
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
* @throws RedirectException * @throws RedirectException
*
* @return ControllerResultType
*/ */
public function subscribersRemove(Request $request, int $object_id): array public function subscribersRemove(Request $request, int $object_id): array
{ {

View File

@@ -38,6 +38,8 @@ class Subscriptions extends CircleController
/** /**
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*
* @return ControllerResultType
*/ */
public function subscriptionsByActorId(Request $request, int $id): array public function subscriptionsByActorId(Request $request, int $id): array
{ {
@@ -48,7 +50,10 @@ class Subscriptions extends CircleController
return $this->subscriptionsByActor($request, $actor); return $this->subscriptionsByActor($request, $actor);
} }
public function subscriptionsByActor(Request $request, Actor $actor) /**
* @return ControllerResultType
*/
public function subscriptionsByActor(Request $request, Actor $actor): array
{ {
return [ return [
'_template' => 'collection/actors.html.twig', '_template' => 'collection/actors.html.twig',

View File

@@ -39,11 +39,12 @@ use App\Util\Exception\ServerException;
use Component\Notification\Entity\Attention; use Component\Notification\Entity\Attention;
use Component\Subscription\Controller\Subscribers as SubscribersController; use Component\Subscription\Controller\Subscribers as SubscribersController;
use Component\Subscription\Controller\Subscriptions as SubscriptionsController; use Component\Subscription\Controller\Subscriptions as SubscriptionsController;
use EventResult;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Subscription extends Component class Subscription extends Component
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect(id: 'actor_subscribe_add', uri_path: '/actor/subscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersAdd']); $r->connect(id: 'actor_subscribe_add', uri_path: '/actor/subscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersAdd']);
$r->connect(id: 'actor_subscribe_remove', uri_path: '/actor/unsubscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersRemove']); $r->connect(id: 'actor_subscribe_remove', uri_path: '/actor/unsubscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersRemove']);
@@ -57,6 +58,8 @@ class Subscription extends Component
* *
* @param Actor|int|LocalUser $subject The Actor who subscribed or unsubscribed * @param Actor|int|LocalUser $subject The Actor who subscribed or unsubscribed
* @param Actor|int|LocalUser $object The Actor who was subscribed or unsubscribed from * @param Actor|int|LocalUser $object The Actor who was subscribed or unsubscribed from
*
* @return array{bool, bool}
*/ */
public static function refreshSubscriptionCount(int|Actor|LocalUser $subject, int|Actor|LocalUser $object): array public static function refreshSubscriptionCount(int|Actor|LocalUser $subject, int|Actor|LocalUser $object): array
{ {
@@ -177,16 +180,15 @@ class Subscription extends Component
* **unsubscribe** a given **Actor**. * **unsubscribe** a given **Actor**.
* *
* @param Actor $object The Actor on which the action is to be performed * @param Actor $object The Actor on which the action is to be performed
* @param array $actions An array containing all actions added to the * @param array<array{url: string, title: string, classes: string, id: string}> $actions
* An array containing all actions added to the
* current profile, this event adds an action to it * current profile, this event adds an action to it
* *
* @throws DuplicateFoundException * @throws DuplicateFoundException
* @throws NotFoundException * @throws NotFoundException
* @throws ServerException * @throws ServerException
*
* @return bool EventHook
*/ */
public function onAddProfileActions(Request $request, Actor $object, array &$actions): bool public function onAddProfileActions(Request $request, Actor $object, array &$actions): EventResult
{ {
// Action requires a user to be logged in // Action requires a user to be logged in
// We know it's a LocalUser, which has the same id as Actor // We know it's a LocalUser, which has the same id as Actor

View File

@@ -15,7 +15,12 @@ class Tag extends Controller
// TODO: Use Feed::query // TODO: Use Feed::query
// TODO: If ?canonical=something, respect // TODO: If ?canonical=something, respect
// TODO: Allow to set locale of tag being selected // TODO: Allow to set locale of tag being selected
private function process(null|string|array $tag_single_or_multi, string $key, string $query, string $template, bool $include_locale = false) /**
* @param (null|string|string[]) $tag_single_or_multi
*
* @return ControllerResultType
*/
private function process(null|string|array $tag_single_or_multi, string $key, string $query, string $template, bool $include_locale = false): array
{ {
$actor = Common::actor(); $actor = Common::actor();
$page = $this->int('page') ?: 1; $page = $this->int('page') ?: 1;
@@ -46,7 +51,10 @@ class Tag extends Controller
]; ];
} }
public function single_note_tag(string $tag) /**
* @return ControllerResultType
*/
public function single_note_tag(string $tag): array
{ {
return $this->process( return $this->process(
tag_single_or_multi: $tag, tag_single_or_multi: $tag,
@@ -57,7 +65,10 @@ class Tag extends Controller
); );
} }
public function multi_note_tags(string $tags) /**
* @return ControllerResultType
*/
public function multi_note_tags(string $tags): array
{ {
return $this->process( return $this->process(
tag_single_or_multi: explode(',', $tags), tag_single_or_multi: explode(',', $tags),

View File

@@ -134,6 +134,9 @@ class NoteTag extends Entity
return "note-tags-{$note_id}"; return "note-tags-{$note_id}";
} }
/**
* @return NoteTag[]
*/
public static function getByNoteId(int $note_id): array public static function getByNoteId(int $note_id): array
{ {
return Cache::getList(self::cacheKey($note_id), fn () => DB::dql('SELECT nt FROM note_tag AS nt JOIN note AS n WITH n.id = nt.note_id WHERE n.id = :id', ['id' => $note_id])); return Cache::getList(self::cacheKey($note_id), fn () => DB::dql('SELECT nt FROM note_tag AS nt JOIN note AS n WITH n.id = nt.note_id WHERE n.id = :id', ['id' => $note_id]));

View File

@@ -119,6 +119,8 @@ class NoteTagBlock extends Entity
/** /**
* Check whether $note_tag is considered blocked by one of * Check whether $note_tag is considered blocked by one of
* $note_tag_blocks * $note_tag_blocks
*
* @param NoteTagBlock[] $note_tag_blocks
*/ */
public static function checkBlocksNoteTag(NoteTag $note_tag, array $note_tag_blocks): bool public static function checkBlocksNoteTag(NoteTag $note_tag, array $note_tag_blocks): bool
{ {

View File

@@ -42,6 +42,7 @@ use Component\Tag\Entity\NoteTag;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -60,14 +61,17 @@ class Tag extends Component
public const TAG_REGEX = '/(^|\\s)(#[\\pL\\pN_\\-]{1,64})/u'; // Brion Vibber 2011-02-23 v2:classes/Notice.php:367 function saveTags public const TAG_REGEX = '/(^|\\s)(#[\\pL\\pN_\\-]{1,64})/u'; // Brion Vibber 2011-02-23 v2:classes/Notice.php:367 function saveTags
public const TAG_SLUG_REGEX = '[A-Za-z0-9]{1,64}'; public const TAG_SLUG_REGEX = '[A-Za-z0-9]{1,64}';
public function onAddRoute($r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('single_note_tag', '/note-tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_note_tag']); $r->connect('single_note_tag', '/note-tag/{tag<' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'single_note_tag']);
$r->connect('multi_note_tags', '/note-tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']); $r->connect('multi_note_tags', '/note-tags/{tags<(' . self::TAG_SLUG_REGEX . ',)+' . self::TAG_SLUG_REGEX . '>}', [Controller\Tag::class, 'multi_note_tags']);
return Event::next; return Event::next;
} }
public static function maybeCreateTag(string $tag, int $note_id, ?int $lang_id): ?NoteTag /**
* @param array{tag_use_canonical?: bool} $extra_args
*/
public static function maybeCreateTag(string $tag, int $note_id, ?int $lang_id, array $extra_args = []): ?NoteTag
{ {
if (!self::validate($tag)) { if (!self::validate($tag)) {
return null; // Ignore invalid tag candidates return null; // Ignore invalid tag candidates
@@ -117,8 +121,10 @@ class Tag extends Component
/** /**
* Process note by extracting any tags present * Process note by extracting any tags present
*
* @param array{TagProcessed?: bool} $extra_args
*/ */
public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): bool public function onProcessNoteContent(Note $note, string $content, string $content_type, array $extra_args): EventResult
{ {
if ($extra_args['TagProcessed'] ?? false) { if ($extra_args['TagProcessed'] ?? false) {
return Event::next; return Event::next;
@@ -135,7 +141,7 @@ class Tag extends Component
return Event::next; return Event::next;
} }
public function onRenderPlainTextNoteContent(string &$text, ?string $locale = null): bool public function onRenderPlainTextNoteContent(string &$text, ?string $locale = null): EventResult
{ {
$text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $locale), $text); $text = preg_replace_callback(self::TAG_REGEX, fn ($m) => $m[1] . self::tagLink($m[2], $locale), $text);
return Event::next; return Event::next;
@@ -212,8 +218,11 @@ class Tag extends Component
* Populate $note_expr with an expression to match a tag, if the term looks like a tag * Populate $note_expr with an expression to match a tag, if the term looks like a tag
* *
* $term /^(note|tag|people|actor)/ means we want to match only either a note or an actor * $term /^(note|tag|people|actor)/ means we want to match only either a note or an actor
*
* @param mixed $note_expr
* @param mixed $actor_expr
*/ */
public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): bool public function onCollectionQueryCreateExpression(ExpressionBuilder $eb, string $term, ?string $locale, ?Actor $actor, &$note_expr, &$actor_expr): EventResult
{ {
if (!str_contains($term, ':')) { if (!str_contains($term, ':')) {
return Event::next; return Event::next;
@@ -252,7 +261,7 @@ class Tag extends Component
return Event::next; return Event::next;
} }
public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool public function onCollectionQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): EventResult
{ {
if (!\in_array('note_tag', $note_qb->getAllAliases())) { if (!\in_array('note_tag', $note_qb->getAllAliases())) {
$note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id'); $note_qb->leftJoin(NoteTag::class, 'note_tag', Expr\Join::WITH, 'note_tag.note_id = note.id');
@@ -263,13 +272,20 @@ class Tag extends Component
return Event::next; return Event::next;
} }
public function onPostingAddFormEntries(Request $request, Actor $actor, array &$form_params): bool /**
* @param array{string, class-string, array<string, mixed>} $form_params
*/
public function onPostingAddFormEntries(Request $request, Actor $actor, array &$form_params): EventResult
{ {
$form_params[] = ['tag_use_canonical', CheckboxType::class, ['required' => false, 'data' => true, 'label' => _m('Make note tags canonical'), 'help' => _m('Canonical tags will be treated as a version of an existing tag with the same root/stem (e.g. \'#great_tag\' will be considered as a version of \'#great\', if it already exists)')]]; $form_params[] = ['tag_use_canonical', CheckboxType::class, ['required' => false, 'data' => true, 'label' => _m('Make note tags canonical'), 'help' => _m('Canonical tags will be treated as a version of an existing tag with the same root/stem (e.g. \'#great_tag\' will be considered as a version of \'#great\', if it already exists)')]];
return Event::next; return Event::next;
} }
public function onAddExtraArgsToNoteContent(Request $request, Actor $actor, array $data, array &$extra_args): bool /**
* @param array{tag_use_canonical?: bool} $data
* @param array{tag_use_canonical?: bool} $extra_args
*/
public function onAddExtraArgsToNoteContent(Request $request, Actor $actor, array $data, array &$extra_args): EventResult
{ {
if (!isset($data['tag_use_canonical'])) { if (!isset($data['tag_use_canonical'])) {
throw new ClientException(_m('Missing Use Canonical preference for Tags.')); throw new ClientException(_m('Missing Use Canonical preference for Tags.'));

View File

@@ -75,7 +75,7 @@
"friendsofphp/php-cs-fixer": "^3.2.1", "friendsofphp/php-cs-fixer": "^3.2.1",
"jchook/phpunit-assert-throws": "^1.0", "jchook/phpunit-assert-throws": "^1.0",
"niels-de-blaauw/php-doc-check": "^0.2.2", "niels-de-blaauw/php-doc-check": "^0.2.2",
"phpstan/phpstan": "dev-master", "phpstan/phpstan": "1.9.x-dev",
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"symfony/browser-kit": "^6.0", "symfony/browser-kit": "^6.0",
"symfony/css-selector": "^6.0", "symfony/css-selector": "^6.0",
@@ -103,6 +103,9 @@
"files": [ "files": [
"src/Core/I18n/I18n.php" "src/Core/I18n/I18n.php"
], ],
"classmap": [
"src/Core/Event/EventResult.php"
],
"psr-4": { "psr-4": {
"App\\": "src/", "App\\": "src/",
"Plugin\\": "plugins/", "Plugin\\": "plugins/",

2642
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ services:
# this creates a service per class whose id is the fully-qualified class name # this creates a service per class whose id is the fully-qualified class name
App\: App\:
resource: '../src/*' resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Routes}' exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Routes,Core/Event/EventResult.php}'
App\Test\Fixtures\: App\Test\Fixtures\:
resource: '../tests/fixtures/*' resource: '../tests/fixtures/*'

View File

@@ -93,7 +93,7 @@ services:
- ./docker/php/entrypoint.sh:/entrypoint.sh - ./docker/php/entrypoint.sh:/entrypoint.sh
- ./docker/db/wait_for_db.sh:/wait_for_db.sh - ./docker/db/wait_for_db.sh:/wait_for_db.sh
- ./docker/social/install.sh:/var/entrypoint.d/social_install.sh - ./docker/social/install.sh:/var/entrypoint.d/social_install.sh
- ./docker/social/worker.sh:/var/entrypoint.d/social_worker.sh - ./docker/worker/worker.sh:/var/entrypoint.d/social_worker.sh
# Main files # Main files
- .:/var/www/social - .:/var/www/social
- /var/www/social/docker # exclude docker folder - /var/www/social/docker # exclude docker folder

View File

@@ -45,10 +45,10 @@ Example 1: Adding elements to the core UI
* @param array $vars Input from the caller/emitter * @param array $vars Input from the caller/emitter
* @param array $res I/O parameter used to accumulate or return values from the listener to the emitter * @param array $res I/O parameter used to accumulate or return values from the listener to the emitter
* *
* @return bool true if not handled or if the handling should be accumulated with other listeners, * @return \EventResult true if not handled or if the handling should be accumulated with other listeners,
* false if handled well enough and no other listeners are needed * false if handled well enough and no other listeners are needed
*/ */
public function onViewAttachmentImage(array $vars, array &$res): bool public function onViewAttachmentImage(array $vars, array &$res): \EventResult
{ {
$res[] = Formatting::twigRenderFile('imageEncoder/imageEncoderView.html.twig', ['attachment' => $vars['attachment'], 'thumbnail_parameters' => $vars['thumbnail_parameters']]); $res[] = Formatting::twigRenderFile('imageEncoder/imageEncoderView.html.twig', ['attachment' => $vars['attachment'], 'thumbnail_parameters' => $vars['thumbnail_parameters']]);
return Event::stop; return Event::stop;
@@ -74,11 +74,25 @@ Event::handle('ResizerAvailable', [&$event_map]);
/** /**
* @param array $event_map output * @param array $event_map output
* *
* @return bool event hook * @return \EventResult event hook
*/ */
public function onResizerAvailable(array &$event_map): bool public function onResizerAvailable(array &$event_map): \EventResult
{ {
$event_map['image'] = 'ResizeImagePath'; $event_map['image'] = 'ResizeImagePath';
return Event::next; return Event::next;
} }
``` ```
Example 3: Default action
-----
An event can be emited to perform an action, but still have a fallback as such:
> Event emitter
```php
if (Event::handle('EventName', $args) !== Event::stop): \EventResult
{
// Do default action, as no-one claimed authority on handling this event
}
```

View File

@@ -1,5 +1,7 @@
parameters: parameters:
level: 4 level: 6
tmpDir: /var/www/social/var/cache/phpstan
inferPrivatePropertyTypeFromConstructor: true
bootstrapFiles: bootstrapFiles:
- config/bootstrap.php - config/bootstrap.php
paths: paths:
@@ -15,6 +17,13 @@ parameters:
earlyTerminatingMethodCalls: earlyTerminatingMethodCalls:
App\Core\Log: App\Core\Log:
- unexpected_exception - unexpected_exception
typeAliases:
ControllerResultType: '(array{_template: string} | array{_redirect: string}) & array<string, mixed>'
CacheKeysType: 'array<string, string>'
SettingsTabsType: 'array<array{title: string, desc: string, id: string, controller: ControllerResultType}>'
OrderByType: "'ASC' | 'DESC' | 'asc' | 'desc'"
ModuleVersionType: 'array{name: string, version: string, author: string, rawdescription: string}'
ignoreErrors: ignoreErrors:
- -
message: '/Access to an undefined property App\\Util\\Bitmap::\$\w+/' message: '/Access to an undefined property App\\Util\\Bitmap::\$\w+/'
@@ -35,10 +44,41 @@ parameters:
paths: paths:
- * - *
# - -
# message: '/has no return typehint specified/' message: '/::onCollectionQueryCreateExpression\(\) has parameter \$(actor|note)_expr with no type specified\./'
# paths: paths:
# - tests/* - *
-
message: '/::schemaDef\(\) return type has no value type specified in iterable type array\./'
paths:
- *
-
message: '/has no return type specified\./'
paths:
- *
-
message: '/with no value type specified in iterable type (array|iterable)\.|type has no value type specified in iterable type (array|iterable)\./'
paths:
- *
-
message: '/never returns array{_redirect: string} so it can be removed from the return type\./'
paths:
- *
-
message: '/but returns array<string, array<int, mixed>\|string>\./'
paths:
- plugins/AttachmentCollections/Controller/AttachmentCollections.php
- plugins/Bundles/Controller/BundleCollection.php
-
message: '/has parameter \$.+ with no type specified./'
paths:
- tests/*
services: services:
- -

View File

@@ -50,6 +50,7 @@ use App\Util\Exception\BugFoundException;
use Component\Collection\Util\Controller\OrderedCollection; use Component\Collection\Util\Controller\OrderedCollection;
use Component\FreeNetwork\Entity\FreeNetworkActorProtocol; use Component\FreeNetwork\Entity\FreeNetworkActorProtocol;
use Component\FreeNetwork\Util\Discovery; use Component\FreeNetwork\Util\Discovery;
use EventResult;
use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use Plugin\ActivityPub\Controller\Inbox; use Plugin\ActivityPub\Controller\Inbox;
@@ -122,14 +123,14 @@ class ActivityPub extends Plugin
], ],
]; ];
public function onInitializePlugin(): bool public function onInitializePlugin(): EventResult
{ {
Event::handle('ActivityStreamsTwoContext', [&self::$activity_streams_two_context]); Event::handle('ActivityStreamsTwoContext', [&self::$activity_streams_two_context]);
self::$activity_streams_two_context = array_unique(self::$activity_streams_two_context, \SORT_REGULAR); self::$activity_streams_two_context = array_unique(self::$activity_streams_two_context, \SORT_REGULAR);
return Event::next; return Event::next;
} }
public function onQueueActivitypubInbox(ActivitypubActor $ap_actor, Actor $actor, string|AbstractObject $type): bool public function onQueueActivitypubInbox(ActivitypubActor $ap_actor, Actor $actor, string|AbstractObject $type): EventResult
{ {
// TODO: Check if Actor has authority over payload // TODO: Check if Actor has authority over payload
@@ -157,7 +158,7 @@ class ActivityPub extends Plugin
* *
* @param Router $r the router that was initialized * @param Router $r the router that was initialized
*/ */
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect( $r->connect(
'activitypub_inbox', 'activitypub_inbox',
@@ -183,7 +184,7 @@ class ActivityPub extends Plugin
/** /**
* Fill Actor->getUrl() calls with correct URL coming from ActivityPub * Fill Actor->getUrl() calls with correct URL coming from ActivityPub
*/ */
public function onStartGetActorUri(Actor $actor, int $type, ?string &$url): bool public function onStartGetActorUri(Actor $actor, int $type, ?string &$url): EventResult
{ {
if ( if (
// Is remote? // Is remote?
@@ -203,7 +204,7 @@ class ActivityPub extends Plugin
/** /**
* Fill Actor->canAdmin() for Actors that came from ActivityPub * Fill Actor->canAdmin() for Actors that came from ActivityPub
*/ */
public function onFreeNetworkActorCanAdmin(Actor $actor, Actor $other, bool &$canAdmin): bool public function onFreeNetworkActorCanAdmin(Actor $actor, Actor $other, bool &$canAdmin): EventResult
{ {
// Are both in AP? // Are both in AP?
if ( if (
@@ -223,7 +224,7 @@ class ActivityPub extends Plugin
* *
* @throws Exception * @throws Exception
*/ */
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): bool public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): EventResult
{ {
if (\count(array_intersect(self::$accept_headers, $accept_header)) === 0) { if (\count(array_intersect(self::$accept_headers, $accept_header)) === 0) {
return Event::next; return Event::next;
@@ -262,7 +263,7 @@ class ActivityPub extends Plugin
/** /**
* Add ActivityStreams 2 Extensions * Add ActivityStreams 2 Extensions
*/ */
public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): bool public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): EventResult
{ {
switch ($type_name) { switch ($type_name) {
case 'Person': case 'Person':
@@ -280,9 +281,11 @@ class ActivityPub extends Plugin
/** /**
* Let FreeNetwork Component know we exist and which class to use to call the freeNetworkDistribute method * Let FreeNetwork Component know we exist and which class to use to call the freeNetworkDistribute method
*/ */
public function onAddFreeNetworkProtocol(array &$protocols): bool public function onAddFreeNetworkProtocol(array &$protocols): EventResult
{ {
if (!\in_array('\Plugin\ActivityPub\ActivityPub', $protocols)) {
$protocols[] = '\Plugin\ActivityPub\ActivityPub'; $protocols[] = '\Plugin\ActivityPub\ActivityPub';
}
return Event::next; return Event::next;
} }
@@ -293,11 +296,11 @@ class ActivityPub extends Plugin
* *
* @return bool true if imported, false otherwise * @return bool true if imported, false otherwise
*/ */
public static function freeNetworkGrabRemote(string $uri): bool public static function freeNetworkGrabRemote(string $uri, ?Actor $on_behalf_of = null): bool
{ {
if (Common::isValidHttpUrl($uri)) { if (Common::isValidHttpUrl($uri)) {
try { try {
$object = self::getObjectByUri($uri); $object = self::getObjectByUri($uri, try_online: true, on_behalf_of: $on_behalf_of);
if (!\is_null($object)) { if (!\is_null($object)) {
if ($object instanceof Type\AbstractObject) { if ($object instanceof Type\AbstractObject) {
if (\in_array($object->get('type'), array_keys(Model\Actor::$_as2_actor_type_to_gs_actor_type))) { if (\in_array($object->get('type'), array_keys(Model\Actor::$_as2_actor_type_to_gs_actor_type))) {
@@ -321,7 +324,7 @@ class ActivityPub extends Plugin
string $inbox, string $inbox,
array $to_actors, array $to_actors,
array &$retry_args, array &$retry_args,
): bool { ): EventResult {
try { try {
$data = Model::toType($activity); $data = Model::toType($activity);
if ($sender->isGroup()) { // When the sender is a group, if ($sender->isGroup()) { // When the sender is a group,
@@ -426,7 +429,7 @@ class ActivityPub extends Plugin
/** /**
* Add activity+json mimetype to WebFinger * Add activity+json mimetype to WebFinger
*/ */
public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): bool public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): EventResult
{ {
if ($object->isPerson()) { if ($object->isPerson()) {
$link = new XML_XRD_Element_Link( $link = new XML_XRD_Element_Link(
@@ -442,7 +445,7 @@ class ActivityPub extends Plugin
/** /**
* When FreeNetwork component asks us to help with identifying Actors from XRDs * When FreeNetwork component asks us to help with identifying Actors from XRDs
*/ */
public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): bool public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): EventResult
{ {
$addr = null; $addr = null;
foreach ($xrd->aliases as $alias) { foreach ($xrd->aliases as $alias) {
@@ -473,7 +476,7 @@ class ActivityPub extends Plugin
/** /**
* When FreeNetwork component asks us to help with identifying Actors from URIs * When FreeNetwork component asks us to help with identifying Actors from URIs
*/ */
public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): bool public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): EventResult
{ {
try { try {
if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) { if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) {
@@ -537,7 +540,7 @@ class ActivityPub extends Plugin
* *
* @return null|Actor|mixed|Note got from URI * @return null|Actor|mixed|Note got from URI
*/ */
public static function getObjectByUri(string $resource, bool $try_online = true): mixed public static function getObjectByUri(string $resource, bool $try_online = true, ?Actor $on_behalf_of = null): mixed
{ {
// Try known object // Try known object
$known_object = DB::findOneBy(ActivitypubObject::class, ['object_uri' => $resource], return_null: true); $known_object = DB::findOneBy(ActivitypubObject::class, ['object_uri' => $resource], return_null: true);
@@ -553,7 +556,7 @@ class ActivityPub extends Plugin
// Try Actor // Try Actor
try { try {
return Explorer::getOneFromUri($resource, try_online: false); return Explorer::getOneFromUri($resource, try_online: false, on_behalf_of: $on_behalf_of);
} catch (\Exception) { } catch (\Exception) {
// Ignore, this is brute forcing, it's okay not to find // Ignore, this is brute forcing, it's okay not to find
} }
@@ -589,7 +592,7 @@ class ActivityPub extends Plugin
throw new Exception("Remote resource {$resource} not found without online resources."); throw new Exception("Remote resource {$resource} not found without online resources.");
} }
$response = HTTPClient::get($resource, ['headers' => self::HTTP_CLIENT_HEADERS]); $response = Explorer::get($resource, $on_behalf_of);
// If it was deleted // If it was deleted
if ($response->getStatusCode() == 410) { if ($response->getStatusCode() == 410) {
//$obj = Type::create('Tombstone', ['id' => $resource]); //$obj = Type::create('Tombstone', ['id' => $resource]);

View File

@@ -32,12 +32,14 @@ declare(strict_types = 1);
namespace Plugin\ActivityPub\Controller; namespace Plugin\ActivityPub\Controller;
use ActivityPhp\Type\AbstractObject;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB; use App\Core\DB;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Queue; use App\Core\Queue;
use App\Core\Router; use App\Core\Router;
use App\Entity\Actor;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use Exception; use Exception;
@@ -95,10 +97,12 @@ class Inbox extends Controller
return $error('Actor not found in the request.'); return $error('Actor not found in the request.');
} }
$to_actor = $this->deriveActivityTo($type);
try { try {
$resource_parts = parse_url($type->get('actor')); $resource_parts = parse_url($type->get('actor'));
if ($resource_parts['host'] !== Common::config('site', 'server')) { if ($resource_parts['host'] !== Common::config('site', 'server')) {
$actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor'))); $actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor'), try_online: true, on_behalf_of: $to_actor));
$ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()]); $ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()]);
} else { } else {
throw new Exception('Only remote actors can use this endpoint.'); throw new Exception('Only remote actors can use this endpoint.');
@@ -136,7 +140,7 @@ class Inbox extends Controller
// If the signature fails verification the first time, update profile as it might have changed public key // If the signature fails verification the first time, update profile as it might have changed public key
if ($verified !== 1) { if ($verified !== 1) {
try { try {
$res = Explorer::getRemoteActorActivity($ap_actor->getUri()); $res = Explorer::getRemoteActorActivity($ap_actor->getUri(), $to_actor);
if (\is_null($res)) { if (\is_null($res)) {
return $error('Invalid remote actor (null response).'); return $error('Invalid remote actor (null response).');
} }
@@ -169,4 +173,68 @@ class Inbox extends Controller
return new TypeResponse($type, status: 202); return new TypeResponse($type, status: 202);
} }
/**
* Poke at the given AbstractObject to find out who it is 'to'.
* Function will check through the 'to', 'cc', and 'object' fields
* of the given type (in that order) to check if if points to anyone
* on our instance. The first found candidate will be returned.
*
* @param AbstractObject $type
*
* @return Actor|null The discovered actor, if found. null otherwise.
*
* @throws Exception
*/
private function deriveActivityTo(AbstractObject $type): Actor|null
{
foreach (['to', 'cc'] as $field) {
foreach ((array) $type->get($field) as $uri) {
$actor = self::uriToMaybeLocalActor($uri);
if (!\is_null($actor)) {
return $actor;
}
}
}
// if it's not to or cc anyone we have to dive deeper
if ($type->has('object')) {
// the 'object' field might just be a uri of one
// of our Actors, if this is a follow or block
$object = $type->get('object');
if (\is_string($object)) {
$actor = self::uriToMaybeLocalActor($object);
if (!\is_null($actor)) {
return $actor;
}
} else if ($object instanceof AbstractObject) {
// if the inner object is also a Type, repeat the process
return $this->deriveActivityTo($object);
}
}
return null;
}
/**
* Get local actor that owns or corresponds to given uri.
*
* @param string $uri
*
* @return Actor|null
*/
private static function uriToMaybeLocalActor(string $uri): Actor|null
{
$parsed = parse_url($uri);
// check if this uri belongs to us
if ($parsed['host'] === Common::config('site', 'server')) {
// it is our uri so we should be able to get
// the actor without making any remote calls
$actor = Explorer::getLocalActorForPath($parsed['path']);
if (!\is_null($actor)) {
return $actor;
}
}
return null;
}
} }

View File

@@ -3,9 +3,9 @@
"@context": [ "@context": [
"https://www.w3.org/ns/activitystreams" "https://www.w3.org/ns/activitystreams"
], ],
"id": "https://another_instance.gnusocial.test/activity/41362", "id": "https://another-instance.gnusocial.test/activity/41362",
"published": "2022-03-20T17:54:15+00:00", "published": "2022-03-20T17:54:15+00:00",
"actor": "https://another_instance.gnusocial.test/actor/43", "actor": "https://another-instance.gnusocial.test/actor/43",
"to": [ "to": [
"https://www.w3.org/ns/activitystreams#Public" "https://www.w3.org/ns/activitystreams#Public"
], ],

View File

@@ -20,23 +20,23 @@
} }
} }
], ],
"id": "https://another_instance.gnusocial.test/actor/43", "id": "https://another-instance.gnusocial.test/actor/43",
"inbox": "https://another_instance.gnusocial.test/actor/43/inbox.json", "inbox": "https://another-instance.gnusocial.test/actor/43/inbox.json",
"outbox": "https://another_instance.gnusocial.test/actor/43/outbox.json", "outbox": "https://another-instance.gnusocial.test/actor/43/outbox.json",
"following": "https://another_instance.gnusocial.test/actor/43/subscriptions", "following": "https://another-instance.gnusocial.test/actor/43/subscriptions",
"followers": "https://another_instance.gnusocial.test/actor/43/subscribers", "followers": "https://another-instance.gnusocial.test/actor/43/subscribers",
"liked": "https://another_instance.gnusocial.test/actor/43/favourites", "liked": "https://another-instance.gnusocial.test/actor/43/favourites",
"preferredUsername": "alice", "preferredUsername": "alice",
"publicKey": { "publicKey": {
"id": "https://another_instance.gnusocial.test/actor/43#public-key", "id": "https://another-instance.gnusocial.test/actor/43#public-key",
"owner": "https://another_instance.gnusocial.test/actor/43", "owner": "https://another-instance.gnusocial.test/actor/43",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArRHP8lxqj1HqFL4q7CKh\noDyBvuhoaoWo/AGdjiWu5AEODL6utaQX+bCJApH9+XNICCkWvayKupgOvLPqBxkh\nl4TPUjlb1iCt+iZeMB8ftude4epaUNUDdqK1zG3g8z8AdF3nx9/cHI+8UY7+JAh6\naZ5EBi+wNYtl4UoDfizmLeRmmGIam5UQ6x2RseYCevIm1BBCZZHLdIaoPJphyjLW\n8sRJtHL4D3m28NkGG8GizctXHbMl7+RlVJ8HyQSr5taRMF+CmZ9ZDFqF2ewc9Pmw\nOMG4o/6m50Q2ELz23O8idjGxKgG7iGdEa3c5cQZTCQ0+2N77L+iS029AV9AKyZMi\nCwIDAQAB\n-----END PUBLIC KEY-----\n" "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArRHP8lxqj1HqFL4q7CKh\noDyBvuhoaoWo/AGdjiWu5AEODL6utaQX+bCJApH9+XNICCkWvayKupgOvLPqBxkh\nl4TPUjlb1iCt+iZeMB8ftude4epaUNUDdqK1zG3g8z8AdF3nx9/cHI+8UY7+JAh6\naZ5EBi+wNYtl4UoDfizmLeRmmGIam5UQ6x2RseYCevIm1BBCZZHLdIaoPJphyjLW\n8sRJtHL4D3m28NkGG8GizctXHbMl7+RlVJ8HyQSr5taRMF+CmZ9ZDFqF2ewc9Pmw\nOMG4o/6m50Q2ELz23O8idjGxKgG7iGdEa3c5cQZTCQ0+2N77L+iS029AV9AKyZMi\nCwIDAQAB\n-----END PUBLIC KEY-----\n"
}, },
"name": "Alice P. Hacker", "name": "Alice P. Hacker",
"published": "2022-02-23T17:20:30+00:00", "published": "2022-02-23T17:20:30+00:00",
"updated": "2022-02-25T02:12:48+00:00", "updated": "2022-02-25T02:12:48+00:00",
"url": "https://another_instance.gnusocial.test/@alice", "url": "https://another-instance.gnusocial.test/@alice",
"endpoints": { "endpoints": {
"sharedInbox": "https://another_instance.gnusocial.test/inbox.json" "sharedInbox": "https://another-instance.gnusocial.test/inbox.json"
} }
} }

View File

@@ -25,7 +25,7 @@
"id": "https://instance.gnusocial.test/object/note/1340", "id": "https://instance.gnusocial.test/object/note/1340",
"published": "2022-03-16T21:54:43+00:00", "published": "2022-03-16T21:54:43+00:00",
"attributedTo": "https://instance.gnusocial.test/actor/42", "attributedTo": "https://instance.gnusocial.test/actor/42",
"content": "<p>This is a public root note with a mention to @<span class=\"h-card\"><a href=\"https://another_instance.gnusocial.test/actor/43\" class=\"h-card u-url p-nickname mention\">alice@another_instance.gnusocial.test</a></span>.</p>", "content": "<p>This is a public root note with a mention to @<span class=\"h-card\"><a href=\"https://another-instance.gnusocial.test/actor/43\" class=\"h-card u-url p-nickname mention\">alice@another_instance.gnusocial.test</a></span>.</p>",
"mediaType": "text/html", "mediaType": "text/html",
"source": { "source": {
"content": "This is a public root note with a mention to @alice@another_instance.gnusocial.test.", "content": "This is a public root note with a mention to @alice@another_instance.gnusocial.test.",
@@ -35,7 +35,7 @@
"tag": [ "tag": [
{ {
"type": "Mention", "type": "Mention",
"href": "https://another_instance.gnusocial.test/actor/43", "href": "https://another-instance.gnusocial.test/actor/43",
"name": "@alice@another_instance.gnusocial.test" "name": "@alice@another_instance.gnusocial.test"
} }
], ],

View File

@@ -0,0 +1,14 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://gotosocial.org/users/the_mighty_zork",
"id": "https://gotosocial.org/01E41WF691G30VVAV6TZXW10VT",
"object": {
"actor": "http://example.org/users/some_user",
"id": "http://example.org/users/some_user/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3",
"object": "https://gotosocial.org/users/the_mighty_zork",
"to": "https://gotosocial.org/users/the_mighty_zork",
"type": "Follow"
},
"to": "http://example.org/users/some_user",
"type": "Accept"
}

View File

@@ -0,0 +1,10 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://example.org/users/the_mighty_zork",
"cc": "https://example.org/users/the_mighty_zork",
"id": "https://example.org/users/the_mighty_zork/statuses/01G74JJ1KS331G2JXHRMZCE0ER",
"object": "https://example.org/users/the_mighty_zork/statuses/01FCTA44PW9H1TB328S9AQXKDS",
"published": "2022-06-09T13:12:00Z",
"to": "https://example.org/users/the_mighty_zork/followers",
"type": "Announce"
}

View File

@@ -0,0 +1,44 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://example.org/users/admin",
"cc": [
"https://example.org/users/admin/followers",
"https://example.org/users/the_mighty_zork"
],
"id": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/activity",
"object": {
"attachment": [],
"attributedTo": "https://example.org/users/admin",
"cc": [
"https://example.org/users/admin/followers",
"https://example.org/users/the_mighty_zork"
],
"content": "hi @the_mighty_zork welcome to the instance!",
"id": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0",
"inReplyTo": "https://example.org/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
"published": "2021-11-20T13:32:16Z",
"replies": {
"first": {
"id": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/replies?page=true",
"next": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/replies?only_other_accounts=false\\u0026page=true",
"partOf": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/replies",
"type": "CollectionPage"
},
"id": "https://example.org/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0/replies",
"type": "Collection"
},
"sensitive": false,
"summary": "",
"tag": {
"href": "https://example.org/users/the_mighty_zork",
"name": "@the_mighty_zork@localhost:8080",
"type": "Mention"
},
"to": "https://www.w3.org/ns/activitystreams#Public",
"type": "Note",
"url": "https://example.org/@admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0"
},
"published": "2021-11-20T13:32:16Z",
"to": "https://www.w3.org/ns/activitystreams#Public",
"type": "Create"
}

View File

@@ -0,0 +1,8 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://gotosocial.org/users/the_mighty_zork",
"id": "https://gotosocial.org/users/the_mighty_zork/follow/01F8PY8RHWRQZV038T4E8T9YK8",
"object": "https://example.com/users/admin",
"to": "https://example.org/users/admin",
"type": "Follow"
}

View File

@@ -0,0 +1,14 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://gotosocial.org/users/the_mighty_zork",
"id": "https://gotosocial.org/01WKYFGS71GG2SXJ8T4FG9VRN2",
"object": {
"actor": "http://example.org/users/some_user",
"id": "http://example.org/users/some_user/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3",
"object": "https://gotosocial.org/users/the_mighty_zork",
"to": "https://gotosocial.org/users/the_mighty_zork",
"type": "Follow"
},
"to": "http://example.org/users/some_user",
"type": "Reject"
}

View File

@@ -0,0 +1,53 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"http://joinmastodon.org/ns"
],
"actor": "https://example.org/users/the_mighty_zork",
"bcc": "https://example.org/users/the_mighty_zork/followers",
"id": "https://example.org/users/the_mighty_zork#updates/011HHHD988G37MD88E1YAF03E4",
"object": {
"discoverable": true,
"featured": "https://example.org/users/the_mighty_zork/collections/featured",
"followers": "https://example.org/users/the_mighty_zork/followers",
"following": "https://example.org/users/the_mighty_zork/following",
"icon": {
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://example.org/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"
},
"id": "https://example.org/users/the_mighty_zork",
"image": {
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://example.org/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"
},
"inbox": "https://example.org/users/the_mighty_zork/inbox",
"manuallyApprovesFollowers": false,
"name": "original zork (he/they)",
"outbox": "https://example.org/users/the_mighty_zork/outbox",
"preferredUsername": "the_mighty_zork",
"publicKey": {
"id": "https://example.org/users/the_mighty_zork/main-key",
"owner": "https://example.org/users/the_mighty_zork",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
},
"summary": "\\u003cp\\u003ehey yo this is my profile!\\u003c/p\\u003e",
"tag": {
"icon": {
"mediaType": "image/png",
"type": "Image",
"url": "https://example.org/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"
},
"id": "https://example.org/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
"name": ":rainbow:",
"type": "Emoji",
"updated": "2021-09-20T12:40:37+02:00"
},
"type": "Person",
"url": "https://example.org/@the_mighty_zork"
},
"to": "https://www.w3.org/ns/activitystreams#Public",
"type": "Update"
}

View File

@@ -0,0 +1,44 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"http://joinmastodon.org/ns"
],
"attachment": {
"blurhash": "LNJRdVM{00Rj%Mayt7j[4nWBofRj",
"mediaType": "image/jpeg",
"name": "Black and white image of some 50's style text saying: Welcome On Board",
"type": "Document",
"url": "https://example.org/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg"
},
"attributedTo": "https://example.org/users/admin",
"cc": "https://example.org/users/admin/followers",
"content": "hello world! #welcome ! first post on the instance :rainbow: !",
"id": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
"published": "2021-10-20T11:36:45Z",
"replies": {
"first": {
"id": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/replies?page=true",
"next": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/replies?only_other_accounts=false\\u0026page=true",
"partOf": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/replies",
"type": "CollectionPage"
},
"id": "https://example.org/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R/replies",
"type": "Collection"
},
"sensitive": false,
"summary": "",
"tag": {
"icon": {
"mediaType": "image/png",
"type": "Image",
"url": "https://example.org/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"
},
"id": "https://example.org/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
"name": ":rainbow:",
"type": "Emoji",
"updated": "2021-09-20T10:40:37Z"
},
"to": "https://www.w3.org/ns/activitystreams#Public",
"type": "Note",
"url": "https://example.org/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R"
}

View File

@@ -0,0 +1,46 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"http://joinmastodon.org/ns",
"https://w3id.org/security/v1"
],
"discoverable": true,
"featured": "https://example.org/users/the_mighty_zork/collections/featured",
"followers": "https://example.org/users/the_mighty_zork/followers",
"following": "https://example.org/users/the_mighty_zork/following",
"icon": {
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://example.org/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg"
},
"id": "https://example.org/users/the_mighty_zork",
"image": {
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://example.org/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg"
},
"inbox": "https://example.org/users/the_mighty_zork/inbox",
"manuallyApprovesFollowers": false,
"name": "original zork (he/they)",
"outbox": "https://example.org/users/the_mighty_zork/outbox",
"preferredUsername": "the_mighty_zork",
"publicKey": {
"id": "https://example.org/users/the_mighty_zork/main-key",
"owner": "https://example.org/users/the_mighty_zork",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n"
},
"summary": "<p>hey yo this is my profile!</p>",
"tag": {
"icon": {
"mediaType": "image/png",
"type": "Image",
"url": "https://example.org/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"
},
"id": "https://example.org/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
"name": ":rainbow:",
"type": "Emoji",
"updated": "2021-09-20T12:40:37+02:00"
},
"type": "Person",
"url": "https://example.org/@the_mighty_zork"
}

View File

@@ -68,7 +68,7 @@ class GSObjectNoteTest extends GNUsocialTestCase
self::bootKernel(); self::bootKernel();
$actor_uri = 'https://instance.gnusocial.test/actor/42'; $actor_uri = 'https://instance.gnusocial.test/actor/42';
$another_actor_uri = 'https://another_instance.gnusocial.test/actor/43'; $another_actor_uri = 'https://another-instance.gnusocial.test/actor/43';
$object_uri = 'https://instance.gnusocial.test/object/note/1340'; $object_uri = 'https://instance.gnusocial.test/object/note/1340';
$note = ActivityPub::getObjectByUri($object_uri, try_online: false); $note = ActivityPub::getObjectByUri($object_uri, try_online: false);
static::assertInstanceOf(Note::class, $note); static::assertInstanceOf(Note::class, $note);
@@ -77,7 +77,7 @@ class GSObjectNoteTest extends GNUsocialTestCase
static::assertSame('text/plain', $note->getContentType()); static::assertSame('text/plain', $note->getContentType());
static::assertSame('This is a public root note with a mention to @alice@another_instance.gnusocial.test.', $note->getContent()); static::assertSame('This is a public root note with a mention to @alice@another_instance.gnusocial.test.', $note->getContent());
// TODO: implement proper sanitization rules // TODO: implement proper sanitization rules
//static::assertSame('<p>This is a public root note with a mention to @<span class=\"h-card\"><a href=\"https://another_instance.gnusocial.test/actor/43\" class=\"h-card u-url p-nickname mention\">alice@another_instance.gnusocial.test</a></span>.</p>', $note->getRendered()); //static::assertSame('<p>This is a public root note with a mention to @<span class=\"h-card\"><a href=\"https://another-instance.gnusocial.test/actor/43\" class=\"h-card u-url p-nickname mention\">alice@another_instance.gnusocial.test</a></span>.</p>', $note->getRendered());
static::assertSame('ActivityPub', $note->getSource()); static::assertSame('ActivityPub', $note->getSource());
static::assertNull($note->getReplyTo()); static::assertNull($note->getReplyTo());
static::assertFalse($note->getIsLocal()); static::assertFalse($note->getIsLocal());

View File

@@ -37,6 +37,7 @@ use App\Core\HTTPClient;
use App\Core\Log; use App\Core\Log;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\NoSuchActorException; use App\Util\Exception\NoSuchActorException;
use App\Util\Nickname; use App\Util\Nickname;
@@ -49,6 +50,7 @@ use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/** /**
* ActivityPub's own Explorer * ActivityPub's own Explorer
@@ -66,6 +68,8 @@ class Explorer
* Shortcut function to get a single profile from its URL. * Shortcut function to get a single profile from its URL.
* *
* @param bool $try_online whether to try online grabbing, defaults to true * @param bool $try_online whether to try online grabbing, defaults to true
* @param Actor $on_behalf_of AP Actor on behalf of whom any remote lookups are to be performed, defaults to null.
* If null, outgoing GET request(s) will not be http signed.
* *
* @throws ClientExceptionInterface * @throws ClientExceptionInterface
* @throws NoSuchActorException * @throws NoSuchActorException
@@ -73,9 +77,9 @@ class Explorer
* @throws ServerExceptionInterface * @throws ServerExceptionInterface
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
*/ */
public static function getOneFromUri(string $uri, bool $try_online = true): Actor public static function getOneFromUri(string $uri, bool $try_online = true, ?Actor $on_behalf_of = null): Actor
{ {
$actors = (new self())->lookup($uri, $try_online); $actors = (new self())->lookup($uri, $try_online, $on_behalf_of);
switch (\count($actors)) { switch (\count($actors)) {
case 1: case 1:
return $actors[0]; return $actors[0];
@@ -93,6 +97,8 @@ class Explorer
* *
* @param string $uri User's url * @param string $uri User's url
* @param bool $try_online whether to try online grabbing, defaults to true * @param bool $try_online whether to try online grabbing, defaults to true
* @param Actor $on_behalf_of AP Actor on behalf of whom the lookup is being performed, defaults to null.
* If null, outgoing GET request(s) will not be http signed.
* *
* @throws ClientExceptionInterface * @throws ClientExceptionInterface
* @throws NoSuchActorException * @throws NoSuchActorException
@@ -102,7 +108,7 @@ class Explorer
* *
* @return array of Actor objects * @return array of Actor objects
*/ */
public function lookup(string $uri, bool $try_online = true): array public function lookup(string $uri, bool $try_online = true, ?Actor $on_behalf_of = null): array
{ {
if (\in_array($uri, ActivityPub::PUBLIC_TO)) { if (\in_array($uri, ActivityPub::PUBLIC_TO)) {
return []; return [];
@@ -111,7 +117,7 @@ class Explorer
Log::debug('ActivityPub Explorer: Started now looking for ' . $uri); Log::debug('ActivityPub Explorer: Started now looking for ' . $uri);
$this->discovered_actors = []; $this->discovered_actors = [];
return $this->_lookup($uri, $try_online); return $this->_lookup($uri, $try_online, $on_behalf_of);
} }
/** /**
@@ -121,6 +127,8 @@ class Explorer
* *
* @param string $uri User's url * @param string $uri User's url
* @param bool $try_online whether to try online grabbing, defaults to true * @param bool $try_online whether to try online grabbing, defaults to true
* @param Actor $on_behalf_of Actor on behalf of whom the lookup is being performed, defaults to null.
* If null, outgoing GET request(s) will not be http signed.
* *
* @throws ClientExceptionInterface * @throws ClientExceptionInterface
* @throws NoSuchActorException * @throws NoSuchActorException
@@ -130,13 +138,13 @@ class Explorer
* *
* @return array of Actor objects * @return array of Actor objects
*/ */
private function _lookup(string $uri, bool $try_online = true): array private function _lookup(string $uri, bool $try_online = true, ?Actor $on_behalf_of = null): array
{ {
$grab_known = $this->grabKnownActor($uri); $grab_known = $this->grabKnownActor($uri);
// First check if we already have it locally and, if so, return it. // First check if we already have it locally and, if so, return it.
// If the known fetch fails and remote grab is required: store locally and return. // If the known fetch fails and remote grab is required: store locally and return.
if (!$grab_known && (!$try_online || !$this->grabRemoteActor($uri))) { if (!$grab_known && (!$try_online || !$this->grabRemoteActor($uri, $on_behalf_of))) {
throw new NoSuchActorException('Actor not found.'); throw new NoSuchActorException('Actor not found.');
} }
@@ -158,32 +166,25 @@ class Explorer
{ {
Log::debug('ActivityPub Explorer: Searching locally for ' . $uri . ' offline.'); Log::debug('ActivityPub Explorer: Searching locally for ' . $uri . ' offline.');
// Try local if (!Common::isValidHttpUrl($uri)) {
if (Common::isValidHttpUrl($uri)) { Log::debug('ActivityPub Explorer: URI ' . $uri . ' was not a valid http url.');
// This means $uri is a valid url return false;
$resource_parts = parse_url($uri);
// TODO: Use URLMatcher
if ($resource_parts['host'] === Common::config('site', 'server')) {
$str = $resource_parts['path'];
// actor_view_nickname
$renick = '/\/@(' . Nickname::DISPLAY_FMT . ')\/?/m';
// actor_view_id
$reuri = '/\/actor\/(\d+)\/?/m';
if (preg_match_all($renick, $str, $matches, \PREG_SET_ORDER, 0) === 1) {
$this->discovered_actors[] = DB::findOneBy(
LocalUser::class,
['nickname' => $matches[0][1]],
)->getActor();
return true;
} elseif (preg_match_all($reuri, $str, $matches, \PREG_SET_ORDER, 0) === 1) {
$this->discovered_actors[] = Actor::getById((int) $matches[0][1]);
return true;
} }
// Check if uri corresponds to local actor
$resource_parts = parse_url($uri);
if ($resource_parts['host'] === Common::config('site', 'server')) {
$actor = $this::getLocalActorForPath($resource_parts['path']);
if (!\is_null($actor)) {
Log::debug('ActivityPub Explorer: Found local ActivityPub Actor for ' . $uri);
$this->discovered_actors[] = $actor;
return true;
} else {
Log::debug('ActivityPub Explorer: Unable to find a known local ActivityPub Actor for ' . $uri);
} }
} }
// Try standard ActivityPub route // URI isn't for a local actor, try to get by URI more generally
// Is this a known filthy little mudblood?
$aprofile = DB::findOneBy(ActivitypubActor::class, ['uri' => $uri], return_null: true); $aprofile = DB::findOneBy(ActivitypubActor::class, ['uri' => $uri], return_null: true);
if (!\is_null($aprofile)) { if (!\is_null($aprofile)) {
Log::debug('ActivityPub Explorer: Found a known ActivityPub Actor for ' . $uri); Log::debug('ActivityPub Explorer: Found a known ActivityPub Actor for ' . $uri);
@@ -201,6 +202,8 @@ class Explorer
* $this->discovered_actor_profiles * $this->discovered_actor_profiles
* *
* @param string $uri User's url * @param string $uri User's url
* @param Actor $on_behalf_of Actor on behalf of whom http GET requests are to be made, defaults to null.
* If null, outgoing GET request(s) will not be http signed.
* *
* @throws ClientExceptionInterface * @throws ClientExceptionInterface
* @throws NoSuchActorException * @throws NoSuchActorException
@@ -210,10 +213,9 @@ class Explorer
* *
* @return bool success state * @return bool success state
*/ */
private function grabRemoteActor(string $uri): bool private function grabRemoteActor(string $uri, ?Actor $on_behalf_of = null): bool
{ {
Log::debug('ActivityPub Explorer: Trying to grab a remote actor for ' . $uri); $response = $this->get($uri, $on_behalf_of);
$response = HTTPClient::get($uri, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
$res = json_decode($response->getContent(), true); $res = json_decode($response->getContent(), true);
if ($response->getStatusCode() == 410) { // If it was deleted if ($response->getStatusCode() == 410) { // If it was deleted
return true; // Nothing to add. return true; // Nothing to add.
@@ -227,7 +229,7 @@ class Explorer
if ($res['type'] === 'OrderedCollection') { // It's a potential collection of actors!!! if ($res['type'] === 'OrderedCollection') { // It's a potential collection of actors!!!
Log::debug('ActivityPub Explorer: Found a collection of actors for ' . $uri); Log::debug('ActivityPub Explorer: Found a collection of actors for ' . $uri);
$this->travelCollection($res['first']); $this->travelCollection($res['first'], $on_behalf_of);
return true; return true;
} else { } else {
try { try {
@@ -249,15 +251,19 @@ class Explorer
/** /**
* Allows the Explorer to transverse a collection of persons. * Allows the Explorer to transverse a collection of persons.
* *
* @param Actor $on_behalf_of Actor on behalf of whom http GET requests are to be made, defaults to null.
* If null, outgoing GET request(s) will not be http signed.
* @param string $uri Collection's url
*
* @throws ClientExceptionInterface * @throws ClientExceptionInterface
* @throws NoSuchActorException * @throws NoSuchActorException
* @throws RedirectionExceptionInterface * @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface * @throws ServerExceptionInterface
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
*/ */
private function travelCollection(string $uri): bool private function travelCollection(string $uri, ?Actor $on_behalf_of = null): bool
{ {
$response = HTTPClient::get($uri, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]); $response = $this->get($uri, $on_behalf_of);
$res = json_decode($response->getContent(), true); $res = json_decode($response->getContent(), true);
if (!isset($res['orderedItems'])) { if (!isset($res['orderedItems'])) {
@@ -266,22 +272,47 @@ class Explorer
// Accumulate findings // Accumulate findings
foreach ($res['orderedItems'] as $actor_uri) { foreach ($res['orderedItems'] as $actor_uri) {
$this->_lookup($actor_uri); $this->_lookup($actor_uri, true, $on_behalf_of);
} }
// Go through entire collection // Go through entire collection
if (!\is_null($res['next'])) { if (!\is_null($res['next'])) {
$this->travelCollection($res['next']); $this->travelCollection($res['next'], $on_behalf_of);
} }
return true; return true;
} }
/**
* Perform an http GET request to the given uri. Will be http-signed on behalf of given Actor, if provided.
*
* @param Actor $on_behalf_of Actor on behalf of whom http GET requests are to be made, defaults to null.
* If null, outgoing GET request(s) will not be http signed.
* @param string $uri uri of remote resource, expected to return an Activity/Object of some kind.
*
* @return ResponseInterface The http response, for further processing.
*/
public static function get(string $uri, ?Actor $on_behalf_of = null): ResponseInterface
{
$headers = [];
if (!\is_null($on_behalf_of)) {
// sign the http GET request
$headers = HTTPSignature::sign($on_behalf_of, $uri, body: false, addlHeaders: [], method: 'get');
} else {
// just do a bare request
$headers = ACTIVITYPUB::HTTP_CLIENT_HEADERS;
}
return HTTPClient::get($uri, ['headers' => $headers]);
}
/** /**
* Get a remote user array from its URL (this function is only used for * Get a remote user array from its URL (this function is only used for
* profile updating and shall not be used for anything else) * profile updating and shall not be used for anything else)
* *
* @param string $uri User's url * @param string $uri User's url
* @param Actor $on_behalf_of Actor on behalf of whom http GET requests are to be made, defaults to null.
* If null, outgoing GET request(s) will not be http signed.
* *
* @throws ClientExceptionInterface * @throws ClientExceptionInterface
* @throws Exception * @throws Exception
@@ -292,9 +323,9 @@ class Explorer
* @return null|string If it is able to fetch, false if it's gone * @return null|string If it is able to fetch, false if it's gone
* // Exceptions when network issues or unsupported Activity format * // Exceptions when network issues or unsupported Activity format
*/ */
public static function getRemoteActorActivity(string $uri): string|null public static function getRemoteActorActivity(string $uri, ?Actor $on_behalf_of = null): string|null
{ {
$response = HTTPClient::get($uri, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]); $response = Explorer::get($uri, $on_behalf_of);
// If it was deleted // If it was deleted
if ($response->getStatusCode() == 410) { if ($response->getStatusCode() == 410) {
return null; return null;
@@ -303,4 +334,37 @@ class Explorer
} }
return $response->getContent(); return $response->getContent();
} }
/**
* Parse the given path and return the actor it corresponds to.
*
* @param String $path Path on *this instance*. Will be parsed with regular expressions.
* Something like `/actor/1` or `/object/note/1`.
*
* @return Actor|null The actor corresponding to/owning the given uri, null if not found.
*/
public static function getLocalActorForPath(string $path): Actor|null
{
// TODO: Use URLMatcher
// actor_view_nickname
$renick = '/\/@(' . Nickname::DISPLAY_FMT . ')\/?/';
if (preg_match_all($renick, $path, $matches, \PREG_SET_ORDER, 0) === 1) {
return DB::findOneBy(LocalUser::class, ['nickname' => $matches[0][1]])->getActor();
}
// actor_view_id
$reuri = '/\/actor\/(\d+)\/?/';
if (preg_match_all($reuri, $path, $matches, \PREG_SET_ORDER, 0) === 1) {
return Actor::getById((int) $matches[0][1]);
}
// note / page / article match
$renote = '/\/object\/(?:note|page|article)\/(\d+)\/?/';
if (preg_match_all($renote, $path, $matches, \PREG_SET_ORDER, 0) === 1) {
return Note::getById((int) $matches[0][1])->getActor();
}
return null;
}
} }

View File

@@ -47,13 +47,13 @@ class HTTPSignature
* *
* @return array Headers to be used in request * @return array Headers to be used in request
*/ */
public static function sign(Actor $user, string $url, string|bool $body = false, array $addlHeaders = []): array public static function sign(Actor $user, string $url, string|bool $body = false, array $addlHeaders = [], string $method = 'post'): array
{ {
$digest = false; $digest = false;
if ($body) { if ($body) {
$digest = self::_digest($body); $digest = self::_digest($body);
} }
$headers = self::_headersToSign($url, $digest); $headers = self::_headersToSign($url, $digest, $method);
$headers = array_merge($headers, $addlHeaders); $headers = array_merge($headers, $addlHeaders);
$stringToSign = self::_headersToSigningString($headers); $stringToSign = self::_headersToSigningString($headers);
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers))); $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
@@ -81,14 +81,23 @@ class HTTPSignature
} }
/** /**
* Return a canonical array of http headers, ready to be signed.
*
* @param string $uri uri of destination
* @param string|bool $digest digest of the request body to add to the `Digest` header (optional).
* @param string $method http method (GET, POST, etc) that the request will use.
* This will be used in the `(request-target)` part of the signature.
*
* @return array Headers to be signed.
*
* @throws Exception * @throws Exception
*/ */
protected static function _headersToSign(string $url, string|bool $digest = false): array protected static function _headersToSign(string $url, string|bool $digest = false, string $method): array
{ {
$date = new DateTime('UTC'); $date = new DateTime('UTC');
$headers = [ $headers = [
'(request-target)' => 'post ' . parse_url($url, \PHP_URL_PATH), '(request-target)' => strtolower($method) . ' ' . parse_url($url, \PHP_URL_PATH),
'Date' => $date->format('D, d M Y H:i:s \G\M\T'), 'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
'Host' => parse_url($url, \PHP_URL_HOST), 'Host' => parse_url($url, \PHP_URL_HOST),
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/activity+json, application/json', 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/activity+json, application/json',

View File

@@ -118,7 +118,7 @@ class Actor extends Model
'modified' => new DateTime(), 'modified' => new DateTime(),
]; ];
if (isset($options['objects']['Actor'])) { if (isset($options['objects']['Actor'])) {
$actor = GSActor::create($actor_map, $options['objects']['Actor']); $actor = GSActor::createOrUpdate($options['objects']['Actor'], $actor_map);
} else { } else {
$actor = GSActor::create($actor_map); $actor = GSActor::create($actor_map);
DB::persist($actor); DB::persist($actor);
@@ -133,7 +133,7 @@ class Actor extends Model
'url' => $object->get('url') ?? null, 'url' => $object->get('url') ?? null,
]; ];
if (isset($options['objects']['ActivitypubActor'])) { if (isset($options['objects']['ActivitypubActor'])) {
$ap_actor = ActivitypubActor::create($ap_actor_map, $options['objects']['ActivitypubActor']); $ap_actor = ActivitypubActor::createOrUpdate($options['objects']['ActivitypubActor'], $ap_actor_map);
} else { } else {
$ap_actor = ActivitypubActor::create($ap_actor_map); $ap_actor = ActivitypubActor::create($ap_actor_map);
DB::persist($ap_actor); DB::persist($ap_actor);
@@ -145,7 +145,7 @@ class Actor extends Model
'public_key' => ($object->has('publicKey') && isset($object->get('publicKey')['publicKeyPem'])) ? $object->get('publicKey')['publicKeyPem'] : null, 'public_key' => ($object->has('publicKey') && isset($object->get('publicKey')['publicKeyPem'])) ? $object->get('publicKey')['publicKeyPem'] : null,
]; ];
if (isset($options['objects']['ActivitypubRsa'])) { if (isset($options['objects']['ActivitypubRsa'])) {
$apRSA = ActivitypubRsa::create($ap_rsa_map, $options['objects']['ActivitypubRsa']); $apRSA = ActivitypubRsa::createOrUpdate($options['objects']['ActivitypubRsa'], $ap_rsa_map);
} else { } else {
$apRSA = ActivitypubRsa::create($ap_rsa_map); $apRSA = ActivitypubRsa::create($ap_rsa_map);
DB::persist($apRSA); DB::persist($apRSA);

View File

@@ -41,6 +41,7 @@ use App\Entity\Feed;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Util\Nickname; use App\Util\Nickname;
use Component\Collection\Util\MetaCollectionTrait; use Component\Collection\Util\MetaCollectionTrait;
use EventResult;
use Plugin\AttachmentCollections\Controller\AttachmentCollections as AttachmentCollectionsController; use Plugin\AttachmentCollections\Controller\AttachmentCollections as AttachmentCollectionsController;
use Plugin\AttachmentCollections\Entity\AttachmentCollection; use Plugin\AttachmentCollections\Entity\AttachmentCollection;
use Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry; use Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry;
@@ -48,10 +49,15 @@ use Symfony\Component\HttpFoundation\Request;
class AttachmentCollections extends Plugin class AttachmentCollections extends Plugin
{ {
/** @phpstan-use MetaCollectionTrait<AttachmentCollection> */
use MetaCollectionTrait; use MetaCollectionTrait;
protected const SLUG = 'collection'; protected const SLUG = 'collection';
protected const PLURAL_SLUG = 'collections'; protected const PLURAL_SLUG = 'collections';
protected function createCollection(Actor $owner, array $vars, string $name)
/**
* @param array<string, mixed> $vars
*/
protected function createCollection(Actor $owner, array $vars, string $name): void
{ {
$col = AttachmentCollection::create([ $col = AttachmentCollection::create([
'name' => $name, 'name' => $name,
@@ -64,7 +70,13 @@ class AttachmentCollections extends Plugin
'attachment_collection_id' => $col->getId(), 'attachment_collection_id' => $col->getId(),
])); ]));
} }
protected function removeItem(Actor $owner, array $vars, array $items, array $collections)
/**
* @param array<string, mixed> $vars
* @param int[] $items
* @param array<string, mixed> $collections
*/
protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool
{ {
return DB::dql(<<<'EOF' return DB::dql(<<<'EOF'
DELETE FROM \Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry AS entry DELETE FROM \Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry AS entry
@@ -82,7 +94,12 @@ class AttachmentCollections extends Plugin
]); ]);
} }
protected function addItem(Actor $owner, array $vars, array $items, array $collections) /**
* @param array<string, mixed> $vars
* @param int[] $items
* @param array<string, mixed> $collections
*/
protected function addItem(Actor $owner, array $vars, array $items, array $collections): void
{ {
foreach ($items as $id) { foreach ($items as $id) {
// prevent user from putting something in a collection (s)he doesn't own: // prevent user from putting something in a collection (s)he doesn't own:
@@ -96,11 +113,19 @@ class AttachmentCollections extends Plugin
} }
} }
protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool /**
* @param array<string, mixed> $vars
*/
protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool
{ {
return $vars['path'] === 'note_attachment_show'; return $vars['path'] === 'note_attachment_show';
} }
/**
* @param array<string, mixed> $vars
*
* @return ($ids_only is true ? int[] : AttachmentCollection[])
*/
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
{ {
if (\is_null($vars)) { if (\is_null($vars)) {
@@ -122,7 +147,7 @@ class AttachmentCollections extends Plugin
return array_map(fn ($x) => $x['attachment_collection_id'], $res); return array_map(fn ($x) => $x['attachment_collection_id'], $res);
} }
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
// View all collections by actor id and nickname // View all collections by actor id and nickname
$r->connect( $r->connect(
@@ -149,7 +174,7 @@ class AttachmentCollections extends Plugin
return Event::next; return Event::next;
} }
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering) public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult
{ {
DB::persist(Feed::create([ DB::persist(Feed::create([
'actor_id' => $actor_id, 'actor_id' => $actor_id,

View File

@@ -28,15 +28,21 @@ use App\Core\Router;
use Component\Collection\Util\Controller\MetaCollectionController; use Component\Collection\Util\Controller\MetaCollectionController;
use Plugin\AttachmentCollections\Entity\AttachmentCollection; use Plugin\AttachmentCollections\Entity\AttachmentCollection;
/**
* @extends MetaCollectionController<AttachmentCollection>
*/
class AttachmentCollections extends MetaCollectionController class AttachmentCollections extends MetaCollectionController
{ {
public function createCollection(int $owner_id, string $name) public function createCollection(int $owner_id, string $name): bool
{ {
DB::persist(AttachmentCollection::create([ DB::persist(AttachmentCollection::create([
'name' => $name, 'name' => $name,
'actor_id' => $owner_id, 'actor_id' => $owner_id,
])); ]));
return true;
} }
public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
{ {
if (\is_null($owner_nickname)) { if (\is_null($owner_nickname)) {
@@ -50,7 +56,12 @@ class AttachmentCollections extends MetaCollectionController
['nickname' => $owner_nickname, 'cid' => $collection_id], ['nickname' => $owner_nickname, 'cid' => $collection_id],
); );
} }
public function getCollectionItems(int $owner_id, $collection_id): array
/**
* FIXME return value not consistent with base class
*/
// @phpstan-disable-next-line
public function getCollectionItems(int $owner_id, int $collection_id): array
{ {
[$attachs, $notes] = DB::dql( [$attachs, $notes] = DB::dql(
<<<'EOF' <<<'EOF'
@@ -63,28 +74,30 @@ class AttachmentCollections extends MetaCollectionController
EOF, EOF,
['cid' => $collection_id], ['cid' => $collection_id],
); );
return [
'_template' => 'AttachmentCollections/collection_entry_view.html.twig', return ['_template' => 'AttachmentCollections/collection_entry_view.html.twig', 'attachments' => array_values($attachs), 'bare_notes' => array_values($notes)];
'attachments' => array_values($attachs),
'bare_notes' => array_values($notes),
];
} }
/**
* @return AttachmentCollection[]
*/
public function getCollectionsByActorId(int $owner_id): array public function getCollectionsByActorId(int $owner_id): array
{ {
return DB::findBy(AttachmentCollection::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']); return DB::findBy(AttachmentCollection::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']);
} }
public function getCollectionBy(int $owner_id, int $collection_id): AttachmentCollection public function getCollectionBy(int $owner_id, int $collection_id): AttachmentCollection
{ {
return DB::findOneBy(AttachmentCollection::class, ['id' => $collection_id]); return DB::findOneBy(AttachmentCollection::class, ['id' => $collection_id]);
} }
public function setCollectionName(int $actor_id, string $actor_nickname, AttachmentCollection $collection, string $name) public function setCollectionName(int $actor_id, string $actor_nickname, AttachmentCollection $collection, string $name): void
{ {
$collection->setName($name); $collection->setName($name);
DB::persist($collection); DB::persist($collection);
} }
public function removeCollection(int $actor_id, string $actor_nickname, AttachmentCollection $collection) public function removeCollection(int $actor_id, string $actor_nickname, AttachmentCollection $collection): void
{ {
DB::remove($collection); DB::remove($collection);
} }

View File

@@ -28,11 +28,16 @@ use App\Core\Event;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use App\Util\Common; use App\Util\Common;
use App\Util\Formatting; use App\Util\Formatting;
use EventResult;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class AttachmentShowRelated extends Plugin class AttachmentShowRelated extends Plugin
{ {
public function onAppendRightPanelBlock(Request $request, $vars, &$res): bool /**
* @param array<string, mixed> $vars
* @param string[] $res
*/
public function onAppendRightPanelBlock(Request $request, array $vars, array &$res): EventResult
{ {
if ($vars['path'] === 'note_attachment_show') { if ($vars['path'] === 'note_attachment_show') {
$related_notes = DB::dql('select n from attachment_to_note an ' $related_notes = DB::dql('select n from attachment_to_note an '
@@ -50,11 +55,9 @@ class AttachmentShowRelated extends Plugin
/** /**
* Output our dedicated stylesheet * Output our dedicated stylesheet
* *
* @param array $styles stylesheets path * @param string[] $styles stylesheets path
*
* @return bool hook value; true means continue processing, false means stop
*/ */
public function onEndShowStyles(array &$styles, string $path): bool public function onEndShowStyles(array &$styles, string $path): EventResult
{ {
if ($path === 'note_attachment_show') { if ($path === 'note_attachment_show') {
$styles[] = '/assets/default_theme/pages/feeds.css'; $styles[] = '/assets/default_theme/pages/feeds.css';

View File

@@ -38,6 +38,7 @@ use function App\Core\I18n\_m;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use App\Util\Formatting; use App\Util\Formatting;
use EventResult;
use FFMpeg\FFProbe as ffprobe; use FFMpeg\FFProbe as ffprobe;
use SplFileInfo; use SplFileInfo;
@@ -53,7 +54,10 @@ class AudioEncoder extends Plugin
return GSFile::mimetypeMajor($mimetype) === 'audio'; return GSFile::mimetypeMajor($mimetype) === 'audio';
} }
public function onFileMetaAvailable(array &$event_map, string $mimetype): bool /**
* @param array<string, callable> $event_map
*/
public function onFileMetaAvailable(array &$event_map, string $mimetype): EventResult
{ {
if (!self::shouldHandle($mimetype)) { if (!self::shouldHandle($mimetype)) {
return Event::next; return Event::next;
@@ -89,8 +93,11 @@ class AudioEncoder extends Plugin
/** /**
* Generates the view for attachments of type Video * Generates the view for attachments of type Video
*
* @param (array{attachment: \Component\Attachment\Entity\Attachment, note: \App\Entity\Note, title: string} & array<string, mixed>) $vars
* @param array<string> $res
*/ */
public function onViewAttachment(array $vars, array &$res): bool public function onViewAttachment(array $vars, array &$res): EventResult
{ {
if (!self::shouldHandle($vars['attachment']->getMimetype())) { if (!self::shouldHandle($vars['attachment']->getMimetype())) {
return Event::next; return Event::next;
@@ -108,9 +115,11 @@ class AudioEncoder extends Plugin
} }
/** /**
* @param ModuleVersionType[] $versions
*
* @throws ServerException * @throws ServerException
*/ */
public function onPluginVersion(array &$versions): bool public function onPluginVersion(array &$versions): EventResult
{ {
$versions[] = [ $versions[] = [
'name' => 'AudioEncoder', 'name' => 'AudioEncoder',

View File

@@ -23,22 +23,27 @@ declare(strict_types = 1);
namespace Plugin\Blog; namespace Plugin\Blog;
use App\Core\Event; use App\Core\Event;
use function App\Core\I18n\_m;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use App\Core\Router; use App\Core\Router;
use App\Util\Common; use App\Util\Common;
use App\Util\HTML; use App\Util\HTML;
use EventResult;
use Plugin\Blog\Controller as C; use Plugin\Blog\Controller as C;
use function App\Core\I18n\_m;
class Blog extends Plugin class Blog extends Plugin
{ {
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect(id: 'blog_post', uri_path: '/blog/post', target: [C\Post::class, 'makePost']); $r->connect(id: 'blog_post', uri_path: '/blog/post', target: [C\Post::class, 'makePost']);
return Event::next; return Event::next;
} }
public function onAppendCardProfile(array $vars, array &$res): bool /**
* @param (array{actor: \App\Entity\Actor} & array<string, mixed>) $vars
* @param array<string> $res
*/
public function onAppendCardProfile(array $vars, array &$res): EventResult
{ {
$actor = Common::actor(); $actor = Common::actor();
$group = $vars['actor']; $group = $vars['actor'];

View File

@@ -32,11 +32,17 @@ use Symfony\Component\HttpFoundation\Request;
class Bundles extends Plugin class Bundles extends Plugin
{ {
/**
* @phpstan-use MetaCollectionTrait<BundleCollection>
*/
use MetaCollectionTrait; use MetaCollectionTrait;
protected const SLUG = 'bundle'; protected const SLUG = 'bundle';
protected const PLURAL_SLUG = 'bundles'; protected const PLURAL_SLUG = 'bundles';
protected function createCollection(Actor $owner, array $vars, string $name) /**
* @param array<string, mixed> $vars
*/
protected function createCollection(Actor $owner, array $vars, string $name): void
{ {
$column = BundleCollection::create([ $column = BundleCollection::create([
'name' => $name, 'name' => $name,
@@ -49,7 +55,12 @@ class Bundles extends Plugin
])); ]));
} }
protected function removeItem(Actor $owner, array $vars, array $items, array $collections) /**
* @param array<string, mixed> $vars
* @param array<int> $items
* @param array<mixed> $collections
*/
protected function removeItem(Actor $owner, array $vars, array $items, array $collections): bool
{ {
return DB::dql(<<<'EOF' return DB::dql(<<<'EOF'
DELETE FROM \Plugin\BlogCollections\Entity\BlogCollectionEntry AS entry DELETE FROM \Plugin\BlogCollections\Entity\BlogCollectionEntry AS entry
@@ -66,7 +77,12 @@ class Bundles extends Plugin
]); ]);
} }
protected function addItem(Actor $owner, array $vars, array $items, array $collections) /**
* @param array<string, mixed> $vars
* @param array<int> $items
* @param array<int> $collections
*/
protected function addItem(Actor $owner, array $vars, array $items, array $collections): void
{ {
foreach ($items as $id) { foreach ($items as $id) {
// prevent user from putting something in a collection (s)he doesn't own: // prevent user from putting something in a collection (s)he doesn't own:
@@ -79,12 +95,22 @@ class Bundles extends Plugin
} }
} }
protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool /**
* @param array<string, mixed> $vars
*/
protected function shouldAddToRightPanel(Actor $user, array $vars, Request $request): bool
{ {
// TODO: Implement shouldAddToRightPanel() method. // TODO: Implement shouldAddToRightPanel() method.
return false; return false;
} }
/**
* FIXME incompatible return type
*
* @param null|array<string, mixed> $vars
*
* @return BundleCollection[]|int[]
*/
protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array
{ {
if (\is_null($vars)) { if (\is_null($vars)) {

View File

@@ -26,11 +26,14 @@ namespace Plugin\Bundles\Controller;
use App\Core\DB; use App\Core\DB;
use App\Core\Router; use App\Core\Router;
use Component\Collection\Util\Controller\MetaCollectionController; use Component\Collection\Util\Controller\MetaCollectionController;
use Plugin\Bundles\Entity\BundleCollection; use Plugin\Bundles\Entity\BundleCollection as BundleCollectionEntity;
class BundleCollections extends MetaCollectionController /**
* @extends MetaCollectionController<BundleCollectionEntity>
*/
class BundleCollection extends MetaCollectionController
{ {
public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string
{ {
if (\is_null($owner_nickname)) { if (\is_null($owner_nickname)) {
return Router::url( return Router::url(
@@ -44,7 +47,8 @@ class BundleCollections extends MetaCollectionController
); );
} }
public function getCollectionItems(int $owner_id, $collection_id): array // FIXME
public function getCollectionItems(int $owner_id, int $collection_id): array
{ {
[$notes] = DB::dql( [$notes] = DB::dql(
<<<'EOF' <<<'EOF'
@@ -65,19 +69,21 @@ class BundleCollections extends MetaCollectionController
public function getCollectionsByActorId(int $owner_id): array public function getCollectionsByActorId(int $owner_id): array
{ {
return DB::findBy(BundleCollection::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']); return DB::findBy(BundleCollectionEntity::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']);
} }
public function getCollectionBy(int $owner_id, int $collection_id) public function getCollectionBy(int $owner_id, int $collection_id): BundleCollectionEntity
{ {
return DB::findOneBy(BundleCollection::class, ['id' => $collection_id]); return DB::findOneBy(BundleCollectionEntity::class, ['id' => $collection_id]);
} }
public function createCollection(int $owner_id, string $name) public function createCollection(int $owner_id, string $name): bool
{ {
DB::persist(BundleCollection::create([ DB::persist(BundleCollectionEntity::create([
'name' => $name, 'name' => $name,
'actor_id' => $owner_id, 'actor_id' => $owner_id,
])); ]));
return true;
} }
} }

View File

@@ -58,7 +58,7 @@ class Cover
* @throws ClientException Invalid form * @throws ClientException Invalid form
* @throws ServerException Invalid file type * @throws ServerException Invalid file type
* *
* @return array template * @return ControllerResultType
*/ */
public static function coverSettings(Request $request): array public static function coverSettings(Request $request): array
{ {
@@ -112,7 +112,9 @@ class Cover
DB::flush(); DB::flush();
// Only delete files if the commit went through // Only delete files if the commit went through
if ($old_file != null) { if ($old_file != null) {
@unlink($old_file); foreach ($old_file as $f) {
@unlink($f->getPath());
}
} }
throw new RedirectException(); throw new RedirectException();
} }
@@ -128,7 +130,9 @@ class Cover
$old_file = $cover->delete(); $old_file = $cover->delete();
DB::remove($cover); DB::remove($cover);
DB::flush(); DB::flush();
@unlink($old_file); foreach ($old_file as $f) {
@unlink($f->getPath());
}
throw new RedirectException(); throw new RedirectException();
} }
$removeForm = $form2->createView(); $removeForm = $form2->createView();

View File

@@ -27,6 +27,7 @@ use App\Core\Event;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use App\Core\Router; use App\Core\Router;
use App\Util\Common; use App\Util\Common;
use EventResult;
use Plugin\Cover\Controller as C; use Plugin\Cover\Controller as C;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -44,17 +45,18 @@ class Cover extends Plugin
{ {
/** /**
* Map URLs to actions * Map URLs to actions
*
* @return bool hook value; true means continue processing, false means stop
*/ */
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('settings_profile_cover', 'settings/cover', [Controller\Cover::class, 'coversettings']); $r->connect('settings_profile_cover', 'settings/cover', [Controller\Cover::class, 'coversettings']);
$r->connect('cover', '/cover', [Controller\Cover::class, 'cover']); $r->connect('cover', '/cover', [Controller\Cover::class, 'cover']);
return Event::next; return Event::next;
} }
public function onPopulateSettingsTabs(Request $request, string $section, &$tabs) /**
* @param SettingsTabsType $tabs
*/
public function onPopulateSettingsTabs(Request $request, string $section, &$tabs): EventResult
{ {
if ($section === 'profile') { if ($section === 'profile') {
$tabs[] = [ $tabs[] = [
@@ -72,7 +74,7 @@ class Cover extends Plugin
* *
* @return bool hook value; true means continue processing, false means stop. * @return bool hook value; true means continue processing, false means stop.
* *
* public function onStartTwigPopulateVars(array &$vars): bool * public function onStartTwigPopulateVars(array &$vars): \EventResult
* { * {
* if (Common::user() != null) { * if (Common::user() != null) {
* $cover = DB::find('cover', ['actor_id' => Common::user()->getId()]); * $cover = DB::find('cover', ['actor_id' => Common::user()->getId()]);
@@ -88,11 +90,9 @@ class Cover extends Plugin
/** /**
* Output our dedicated stylesheet * Output our dedicated stylesheet
* *
* @param array $styles stylesheets path * @param string[] $styles stylesheets path
*
* @return bool hook value; true means continue processing, false means stop
*/ */
public function onStartShowStyles(array &$styles): bool public function onStartShowStyles(array &$styles): EventResult
{ {
$styles[] = 'assets/css/cover.css'; $styles[] = 'assets/css/cover.css';
return Event::next; return Event::next;

View File

@@ -117,7 +117,7 @@ class Cover extends Entity
/** /**
* Delete this cover and the corresponding attachment and thumbnails, which this owns * Delete this cover and the corresponding attachment and thumbnails, which this owns
* *
* @return array attachments deleted (if delete_attachments_now is true) * @return Attachment[] attachments deleted (if delete_attachments_now is true)
*/ */
public function delete(bool $flush = false, bool $delete_attachments_now = false, bool $cascading = false): array public function delete(bool $flush = false, bool $delete_attachments_now = false, bool $cascading = false): array
{ {

View File

@@ -34,6 +34,7 @@ use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use DateTime; use DateTime;
use EventResult;
use Plugin\ActivityPub\Entity\ActivitypubActivity; use Plugin\ActivityPub\Entity\ActivitypubActivity;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -132,10 +133,8 @@ class DeleteNote extends NoteHandlerPlugin
/** /**
* Adds and connects the _delete_note_action_ route to * Adds and connects the _delete_note_action_ route to
* Controller\DeleteNote::class * Controller\DeleteNote::class
*
* @return bool Event hook
*/ */
public function onAddRoute(Router $r) public function onAddRoute(Router $r): EventResult
{ {
$r->connect(id: 'delete_note_action', uri_path: '/object/note/{note_id<\d+>}/delete', target: Controller\DeleteNote::class); $r->connect(id: 'delete_note_action', uri_path: '/object/note/{note_id<\d+>}/delete', target: Controller\DeleteNote::class);
@@ -152,9 +151,9 @@ class DeleteNote extends NoteHandlerPlugin
* @throws \App\Util\Exception\NotFoundException * @throws \App\Util\Exception\NotFoundException
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* *
* @return bool Event hook * @params string[] $actions
*/ */
public function onAddExtraNoteActions(Request $request, Note $note, array &$actions) public function onAddExtraNoteActions(Request $request, Note $note, array &$actions): EventResult
{ {
if (\is_null($actor = Common::actor())) { if (\is_null($actor = Common::actor())) {
return Event::next; return Event::next;
@@ -192,10 +191,8 @@ class DeleteNote extends NoteHandlerPlugin
* @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity * @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
* @param mixed $type_object Activity's Object * @param mixed $type_object Activity's Object
* @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity * @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
*
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
*/ */
private function activitypub_handler(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): bool private function activitypub_handler(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): EventResult
{ {
if ($type_activity->get('type') !== 'Delete' if ($type_activity->get('type') !== 'Delete'
|| !($type_object instanceof Note)) { || !($type_object instanceof Note)) {
@@ -224,10 +221,8 @@ class DeleteNote extends NoteHandlerPlugin
* @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity * @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
* @param \ActivityPhp\Type\AbstractObject $type_object Activity Streams 2.0 Object * @param \ActivityPhp\Type\AbstractObject $type_object Activity Streams 2.0 Object
* @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity * @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
*
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
*/ */
public function onNewActivityPubActivity(Actor $actor, AbstractObject $type_activity, AbstractObject $type_object, ?ActivitypubActivity &$ap_act): bool public function onNewActivityPubActivity(Actor $actor, AbstractObject $type_activity, AbstractObject $type_object, ?ActivitypubActivity &$ap_act): EventResult
{ {
return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act); return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
} }
@@ -239,10 +234,8 @@ class DeleteNote extends NoteHandlerPlugin
* @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity * @param \ActivityPhp\Type\AbstractObject $type_activity Activity Streams 2.0 Activity
* @param mixed $type_object Object * @param mixed $type_object Object
* @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity * @param null|\Plugin\ActivityPub\Entity\ActivitypubActivity $ap_act Resulting ActivitypubActivity
*
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
*/ */
public function onNewActivityPubActivityWithObject(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): bool public function onNewActivityPubActivityWithObject(Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): EventResult
{ {
return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act); return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
} }
@@ -252,10 +245,8 @@ class DeleteNote extends NoteHandlerPlugin
* *
* @param string $verb GNU social's internal verb * @param string $verb GNU social's internal verb
* @param null|string $gs_verb_to_activity_stream_two_verb Resulting Activity Streams 2.0 verb * @param null|string $gs_verb_to_activity_stream_two_verb Resulting Activity Streams 2.0 verb
*
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
*/ */
public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): bool public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): EventResult
{ {
if ($verb === 'delete') { if ($verb === 'delete') {
$gs_verb_to_activity_stream_two_verb = 'Delete'; $gs_verb_to_activity_stream_two_verb = 'Delete';

View File

@@ -31,16 +31,15 @@ use App\Util\Exception\RedirectException;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use App\Util\Formatting; use App\Util\Formatting;
use Component\Group\Controller as ComponentGroupController; use Component\Group\Controller as ComponentGroupController;
use EventResult;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Directory extends Plugin class Directory extends Plugin
{ {
/** /**
* Map Directory routes to its corresponding Controllers * Map Directory routes to its corresponding Controllers
*
* @return bool
*/ */
public function onAddRoute(Router $r) public function onAddRoute(Router $r): EventResult
{ {
$r->connect('directory_people', '/directory/people', [Controller\Directory::class, 'people']); $r->connect('directory_people', '/directory/people', [Controller\Directory::class, 'people']);
$r->connect('directory_groups', '/directory/groups', [Controller\Directory::class, 'groups']); $r->connect('directory_groups', '/directory/groups', [Controller\Directory::class, 'groups']);
@@ -52,10 +51,8 @@ class Directory extends Plugin
* Add Links to main navigation card * Add Links to main navigation card
* *
* @param array $res out menu items * @param array $res out menu items
*
* @return bool hook value; true means continue processing, false means stop
*/ */
public function onAddMainNavigationItem(array $vars, array &$res): bool public function onAddMainNavigationItem(array $vars, array &$res): EventResult
{ {
$res[] = ['title' => 'People', 'path' => Router::url($path_id = 'directory_people', []), 'path_id' => $path_id]; $res[] = ['title' => 'People', 'path' => Router::url($path_id = 'directory_people', []), 'path_id' => $path_id];
$res[] = ['title' => 'Groups', 'path' => Router::url($path_id = 'directory_groups', []), 'path_id' => $path_id]; $res[] = ['title' => 'Groups', 'path' => Router::url($path_id = 'directory_groups', []), 'path_id' => $path_id];
@@ -69,10 +66,8 @@ class Directory extends Plugin
* *
* @throws RedirectException * @throws RedirectException
* @throws ServerException * @throws ServerException
*
* @return bool EventHook
*/ */
public function onPrependActorsCollection(Request $request, array &$elements): bool public function onPrependActorsCollection(Request $request, array &$elements): EventResult
{ {
if (\is_null($actor = Common::actor())) { if (\is_null($actor = Common::actor())) {
return Event::next; return Event::next;

View File

@@ -36,10 +36,11 @@ namespace Plugin\EmailNotifications;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use EventResult;
class EmailNotifications extends Plugin class EmailNotifications extends Plugin
{ {
public function onAddNotificationTransport(&$form_defs): bool public function onAddNotificationTransport(array &$form_defs): EventResult
{ {
$form_defs['Email'] = $form_defs['placeholder']; $form_defs['Email'] = $form_defs['placeholder'];
$form_defs['Email'][] = $form_defs['placeholder']['save']('Email', 'save_email'); $form_defs['Email'][] = $form_defs['placeholder']['save']('Email', 'save_email');

View File

@@ -216,7 +216,7 @@ class OEmbed extends Controller
/** /**
* Placeholder * Placeholder
*/ */
public function init_document($type) public function init_document(string $type): void
{ {
throw new NotImplementedException; throw new NotImplementedException;
// switch ($type) { // switch ($type) {
@@ -243,7 +243,7 @@ class OEmbed extends Controller
/** /**
* Placeholder * Placeholder
*/ */
public function end_document($type) public function end_document(string $type): void
{ {
throw new NotImplementedException; throw new NotImplementedException;
// switch ($type) { // switch ($type) {

View File

@@ -57,6 +57,7 @@ use App\Util\TemporaryFile;
use Component\Attachment\Entity\Attachment; use Component\Attachment\Entity\Attachment;
use Component\Link\Entity\Link; use Component\Link\Entity\Link;
use Embed\Embed as LibEmbed; use Embed\Embed as LibEmbed;
use EventResult;
use Exception; use Exception;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
@@ -111,7 +112,7 @@ class Embed extends Plugin
* *
* @throws Exception * @throws Exception
*/ */
public function onAddRoute(Router $m): bool public function onAddRoute(Router $m): EventResult
{ {
$m->connect('oembed', 'main/oembed', Controller\OEmbed::class); $m->connect('oembed', 'main/oembed', Controller\OEmbed::class);
return Event::next; return Event::next;
@@ -120,7 +121,7 @@ class Embed extends Plugin
/** /**
* Insert oembed and opengraph tags in all HTML head elements * Insert oembed and opengraph tags in all HTML head elements
*/ */
public function onShowHeadElements(Request $request, array &$result): bool public function onShowHeadElements(Request $request, array &$result): EventResult
{ {
$matches = []; $matches = [];
preg_match(',/?([^/]+)/?(.*),', $request->getPathInfo(), $matches); preg_match(',/?([^/]+)/?(.*),', $request->getPathInfo(), $matches);
@@ -146,7 +147,7 @@ class Embed extends Plugin
/** /**
* Show this attachment enhanced with the corresponding Embed data, if available * Show this attachment enhanced with the corresponding Embed data, if available
*/ */
public function onViewLink(array $vars, array &$res): bool public function onViewLink(array $vars, array &$res): EventResult
{ {
$link = $vars['link']; $link = $vars['link'];
try { try {
@@ -177,7 +178,7 @@ class Embed extends Plugin
* *
* @throws DuplicateFoundException * @throws DuplicateFoundException
*/ */
public function onNewLinkFromNote(Link $link, Note $note): bool public function onNewLinkFromNote(Link $link, Note $note): EventResult
{ {
// Only handle text mime // Only handle text mime
$mimetype = $link->getMimetype(); $mimetype = $link->getMimetype();
@@ -368,7 +369,7 @@ class Embed extends Plugin
return HTTPClient::get($url)->getContent(); return HTTPClient::get($url)->getContent();
} }
public function onAttachmentGetBestTitle(Attachment $attachment, Note $note, ?string &$title) public function onAttachmentGetBestTitle(Attachment $attachment, Note $note, ?string &$title): EventResult
{ {
try { try {
$embed = DB::findOneBy('attachment_embed', ['attachment_id' => $attachment->getId()]); $embed = DB::findOneBy('attachment_embed', ['attachment_id' => $attachment->getId()]);
@@ -386,10 +387,8 @@ class Embed extends Plugin
* @param array $versions inherited from parent * @param array $versions inherited from parent
* *
* @throws ServerException * @throws ServerException
*
* @return bool true hook value
*/ */
public function onPluginVersion(array &$versions): bool public function onPluginVersion(array &$versions): EventResult
{ {
$versions[] = [ $versions[] = [
'name' => 'Embed', 'name' => 'Embed',

View File

@@ -43,6 +43,7 @@ final class EmbedTest extends TestCase
*/ */
public function testEmbed(string $url, string $expectedType) public function testEmbed(string $url, string $expectedType)
{ {
static::markTestIncomplete();
// try { // try {
// $data = EmbedHelper::getObject($url); // $data = EmbedHelper::getObject($url);
// static::assertSame($expectedType, $data->type); // static::assertSame($expectedType, $data->type);

View File

@@ -26,6 +26,7 @@ namespace Plugin\Favourite\Controller;
use App\Core\DB; use App\Core\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Router; use App\Core\Router;
use App\Entity\Activity; use App\Entity\Activity;
@@ -41,10 +42,12 @@ use App\Util\Exception\RedirectException;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use Component\Collection\Util\Controller\FeedController; use Component\Collection\Util\Controller\FeedController;
use Component\Notification\Entity\Attention; use Component\Notification\Entity\Attention;
use function App\Core\I18n\_m;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
/**
* @extends FeedController<Note>
*/
class Favourite extends FeedController class Favourite extends FeedController
{ {
/** /**

View File

@@ -45,6 +45,7 @@ use App\Util\Common;
use App\Util\Nickname; use App\Util\Nickname;
use Component\Notification\Entity\Attention; use Component\Notification\Entity\Attention;
use DateTime; use DateTime;
use EventResult;
use Plugin\Favourite\Entity\NoteFavourite; use Plugin\Favourite\Entity\NoteFavourite;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -122,10 +123,8 @@ class Favourite extends NoteHandlerPlugin
* *
* @param Note $note Current Note being rendered * @param Note $note Current Note being rendered
* @param array $actions Array containing all Note actions to be rendered * @param array $actions Array containing all Note actions to be rendered
*
* @return bool Event hook, Event::next (true) is returned to allow Event to be handled by other handlers
*/ */
public function onAddNoteActions(Request $request, Note $note, array &$actions): bool public function onAddNoteActions(Request $request, Note $note, array &$actions): EventResult
{ {
if (\is_null($user = Common::user())) { if (\is_null($user = Common::user())) {
return Event::next; return Event::next;
@@ -168,10 +167,8 @@ class Favourite extends NoteHandlerPlugin
* *
* @param array $vars Array containing necessary info to process event. In this case, contains the current Note being rendered * @param array $vars Array containing necessary info to process event. In this case, contains the current Note being rendered
* @param array $result Contains a hashmap for each Activity performed on Note (object) * @param array $result Contains a hashmap for each Activity performed on Note (object)
*
* @return bool Event hook, Event::next (true) is returned to allow Event to be handled by other handlers
*/ */
public function onAppendCardNote(array $vars, array &$result): bool public function onAppendCardNote(array $vars, array &$result): EventResult
{ {
// If note is the original and user isn't the one who repeated, append on end "user repeated this" // If note is the original and user isn't the one who repeated, append on end "user repeated this"
// If user is the one who repeated, append on end "you repeated this, remove repeat?" // If user is the one who repeated, append on end "you repeated this, remove repeat?"
@@ -197,7 +194,7 @@ class Favourite extends NoteHandlerPlugin
/** /**
* Deletes every favourite entity in table related to a deleted Note * Deletes every favourite entity in table related to a deleted Note
*/ */
public function onNoteDeleteRelated(Note &$note, Actor $actor): bool public function onNoteDeleteRelated(Note &$note, Actor $actor): EventResult
{ {
$note_favourites_list = NoteFavourite::getNoteFavourites($note); $note_favourites_list = NoteFavourite::getNoteFavourites($note);
foreach ($note_favourites_list as $favourite_entity) { foreach ($note_favourites_list as $favourite_entity) {
@@ -210,7 +207,7 @@ class Favourite extends NoteHandlerPlugin
/** /**
* Maps Routes to their respective Controllers * Maps Routes to their respective Controllers
*/ */
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
// Add/remove note to/from favourites // Add/remove note to/from favourites
$r->connect(id: 'favourite_add', uri_path: '/object/note/{id<\d+>}/favour', target: [Controller\Favourite::class, 'favouriteAddNote']); $r->connect(id: 'favourite_add', uri_path: '/object/note/{id<\d+>}/favour', target: [Controller\Favourite::class, 'favouriteAddNote']);
@@ -233,7 +230,7 @@ class Favourite extends NoteHandlerPlugin
* *
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
*/ */
public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): bool public function onCreateDefaultFeeds(int $actor_id, LocalUser $user, int &$ordering): EventResult
{ {
DB::persist(Feed::create([ DB::persist(Feed::create([
'actor_id' => $actor_id, 'actor_id' => $actor_id,
@@ -270,10 +267,8 @@ class Favourite extends NoteHandlerPlugin
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
*
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
*/ */
private function activitypub_handler(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, mixed $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): bool private function activitypub_handler(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, mixed $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): EventResult
{ {
if (!\in_array($type_activity->get('type'), ['Like', 'Undo'])) { if (!\in_array($type_activity->get('type'), ['Like', 'Undo'])) {
return Event::next; return Event::next;
@@ -329,7 +324,7 @@ class Favourite extends NoteHandlerPlugin
return Event::stop; return Event::stop;
} }
public function onActivityPubNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool public function onActivityPubNewNotification(Actor $sender, Activity $activity, array $targets, ?string $reason = null): EventResult
{ {
switch ($activity->getVerb()) { switch ($activity->getVerb()) {
case 'favourite': case 'favourite':
@@ -363,10 +358,8 @@ class Favourite extends NoteHandlerPlugin
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
*
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
*/ */
public function onNewActivityPubActivity(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, \ActivityPhp\Type\AbstractObject $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): bool public function onNewActivityPubActivity(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, \ActivityPhp\Type\AbstractObject $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): EventResult
{ {
return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act); return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
} }
@@ -387,10 +380,8 @@ class Favourite extends NoteHandlerPlugin
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
*
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
*/ */
public function onNewActivityPubActivityWithObject(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, mixed $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): bool public function onNewActivityPubActivityWithObject(Actor $actor, \ActivityPhp\Type\AbstractObject $type_activity, mixed $type_object, ?\Plugin\ActivityPub\Entity\ActivitypubActivity &$ap_act): EventResult
{ {
return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act); return $this->activitypub_handler($actor, $type_activity, $type_object, $ap_act);
} }
@@ -400,10 +391,8 @@ class Favourite extends NoteHandlerPlugin
* *
* @param string $verb GNU social's internal verb * @param string $verb GNU social's internal verb
* @param null|string $gs_verb_to_activity_stream_two_verb Resulting Activity Streams 2.0 verb * @param null|string $gs_verb_to_activity_stream_two_verb Resulting Activity Streams 2.0 verb
*
* @return bool Returns `Event::stop` if handled, `Event::next` otherwise
*/ */
public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): bool public function onGSVerbToActivityStreamsTwoActivityType(string $verb, ?string &$gs_verb_to_activity_stream_two_verb): EventResult
{ {
if ($verb === 'favourite') { if ($verb === 'favourite') {
$gs_verb_to_activity_stream_two_verb = 'Like'; $gs_verb_to_activity_stream_two_verb = 'Like';

View File

@@ -31,11 +31,13 @@ use App\Core\Modules\Plugin;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use EventResult;
/** /**
* Check attachment file size quotas * Check attachment file size quotas
* *
* @package GNUsocial * @package GNUsocial
*
* @ccategory Attachment * @ccategory Attachment
* *
* @author Hugo Sales <hugo@hsal.es> * @author Hugo Sales <hugo@hsal.es>
@@ -57,7 +59,7 @@ class FileQuota extends Plugin
* @throws ClientException * @throws ClientException
* @throws ServerException * @throws ServerException
*/ */
public function onEnforceUserFileQuota(int $filesize, int $user_id): bool public function onEnforceUserFileQuota(int $filesize, int $user_id): EventResult
{ {
$query = <<<'END' $query = <<<'END'
select sum(at.size) as total select sum(at.size) as total
@@ -101,10 +103,8 @@ class FileQuota extends Plugin
* Adds this plugin's version information to $versions array * Adds this plugin's version information to $versions array
* *
* @param array $versions inherited from parent * @param array $versions inherited from parent
*
* @return bool true hook value
*/ */
public function onPluginVersion(array &$versions): bool public function onPluginVersion(array &$versions): EventResult
{ {
$versions[] = [ $versions[] = [
'name' => 'FileQuota', 'name' => 'FileQuota',

View File

@@ -32,6 +32,7 @@ use App\Util\Exception\ServerException;
use App\Util\Exception\TemporaryFileException; use App\Util\Exception\TemporaryFileException;
use App\Util\Formatting; use App\Util\Formatting;
use App\Util\TemporaryFile; use App\Util\TemporaryFile;
use EventResult;
use Exception; use Exception;
use Jcupitt\Vips; use Jcupitt\Vips;
use SplFileInfo; use SplFileInfo;
@@ -59,7 +60,7 @@ class ImageEncoder extends Plugin
return GSFile::mimetypeMajor($mimetype) === 'image'; return GSFile::mimetypeMajor($mimetype) === 'image';
} }
public function onFileMetaAvailable(array &$event_map, string $mimetype): bool public function onFileMetaAvailable(array &$event_map, string $mimetype): EventResult
{ {
if (!self::shouldHandle($mimetype)) { if (!self::shouldHandle($mimetype)) {
return Event::next; return Event::next;
@@ -68,7 +69,7 @@ class ImageEncoder extends Plugin
return Event::next; return Event::next;
} }
public function onFileSanitizerAvailable(array &$event_map, string $mimetype): bool public function onFileSanitizerAvailable(array &$event_map, string $mimetype): EventResult
{ {
if (!self::shouldHandle($mimetype)) { if (!self::shouldHandle($mimetype)) {
return Event::next; return Event::next;
@@ -77,7 +78,7 @@ class ImageEncoder extends Plugin
return Event::next; return Event::next;
} }
public function onFileResizerAvailable(array &$event_map, string $mimetype): bool public function onFileResizerAvailable(array &$event_map, string $mimetype): EventResult
{ {
if (!self::shouldHandle($mimetype)) { if (!self::shouldHandle($mimetype)) {
return Event::next; return Event::next;
@@ -179,7 +180,7 @@ class ImageEncoder extends Plugin
/** /**
* Generates the view for attachments of type Image * Generates the view for attachments of type Image
*/ */
public function onViewAttachment(array $vars, array &$res): bool public function onViewAttachment(array $vars, array &$res): EventResult
{ {
if (!self::shouldHandle($vars['attachment']->getMimetype())) { if (!self::shouldHandle($vars['attachment']->getMimetype())) {
return Event::next; return Event::next;
@@ -257,10 +258,8 @@ class ImageEncoder extends Plugin
* Adds this plugin's version information to $versions array * Adds this plugin's version information to $versions array
* *
* @param array $versions inherited from parent * @param array $versions inherited from parent
*
* @return bool true hook value
*/ */
public function onPluginVersion(array &$versions): bool public function onPluginVersion(array &$versions): EventResult
{ {
$versions[] = [ $versions[] = [
'name' => 'ImageEncoder', 'name' => 'ImageEncoder',

View File

@@ -33,17 +33,19 @@ namespace Plugin\LatexNotes;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use EventResult;
use PhpLatex_Parser; use PhpLatex_Parser;
use PhpLatex_Renderer_Html; use PhpLatex_Renderer_Html;
class LatexNotes extends Plugin class LatexNotes extends Plugin
{ {
public function onPostingAvailableContentTypes(array &$types): bool public function onPostingAvailableContentTypes(array &$types): EventResult
{ {
$types['LaTeX'] = 'application/x-latex'; $types['LaTeX'] = 'application/x-latex';
return Event::next; return Event::next;
} }
public function onRenderNoteContent($content, $content_type, &$rendered): bool
public function onRenderNoteContent(string $content, string $content_type, string &$rendered): EventResult
{ {
if ($content_type !== 'application/x-latex') { if ($content_type !== 'application/x-latex') {
return Event::next; return Event::next;

View File

@@ -33,16 +33,18 @@ namespace Plugin\MarkdownNotes;
use App\Core\Event; use App\Core\Event;
use App\Core\Modules\Plugin; use App\Core\Modules\Plugin;
use EventResult;
use Parsedown; use Parsedown;
class MarkdownNotes extends Plugin class MarkdownNotes extends Plugin
{ {
public function onPostingAvailableContentTypes(array &$types): bool public function onPostingAvailableContentTypes(array &$types): EventResult
{ {
$types['Markdown'] = 'text/markdown'; $types['Markdown'] = 'text/markdown';
return Event::next; return Event::next;
} }
public function onRenderNoteContent($content, $content_type, &$rendered): bool
public function onRenderNoteContent(string $content, string $content_type, string &$rendered): EventResult
{ {
if ($content_type !== 'text/markdown') { if ($content_type !== 'text/markdown') {
return Event::next; return Event::next;

View File

@@ -40,6 +40,7 @@ use App\Util\Exception\BugFoundException;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Formatting; use App\Util\Formatting;
use App\Util\Functional as GSF; use App\Util\Functional as GSF;
use EventResult;
use Functional as F; use Functional as F;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -94,7 +95,7 @@ class NoteTypeFeedFilter extends Plugin
* *
* Includes if any positive type matches, but removes if any negated matches * Includes if any positive type matches, but removes if any negated matches
*/ */
public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): bool public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): EventResult
{ {
$types = $this->normalizeTypesList(\is_null($request->get('note-types')) ? [] : explode(',', $request->get('note-types'))); $types = $this->normalizeTypesList(\is_null($request->get('note-types')) ? [] : explode(',', $request->get('note-types')));
$notes = F\select( $notes = F\select(
@@ -139,7 +140,7 @@ class NoteTypeFeedFilter extends Plugin
/** /**
* Draw the media feed navigation. * Draw the media feed navigation.
*/ */
public function onAddFeedActions(Request $request, bool $is_not_empty, &$res): bool public function onAddFeedActions(Request $request, bool $is_not_empty, array &$res): EventResult
{ {
$qs = []; $qs = [];
$query_string = $request->getQueryString(); $query_string = $request->getQueryString();
@@ -181,10 +182,8 @@ class NoteTypeFeedFilter extends Plugin
* Output our dedicated stylesheet * Output our dedicated stylesheet
* *
* @param array $styles stylesheets path * @param array $styles stylesheets path
*
* @return bool hook value; true means continue processing, false means stop
*/ */
public function onEndShowStyles(array &$styles, string $route): bool public function onEndShowStyles(array &$styles, string $route): EventResult
{ {
$styles[] = 'plugins/NoteTypeFeedFilter/assets/css/noteTypeFeedFilter.css'; $styles[] = 'plugins/NoteTypeFeedFilter/assets/css/noteTypeFeedFilter.css';
return Event::next; return Event::next;

View File

@@ -36,6 +36,9 @@ use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use Plugin\OAuth2\Util\Token; use Plugin\OAuth2\Util\Token;
/**
* @extends Token<AccessToken>
*/
class AccessToken extends Token implements AccessTokenEntityInterface class AccessToken extends Token implements AccessTokenEntityInterface
{ {
// {{{ Autocode // {{{ Autocode

View File

@@ -36,6 +36,9 @@ use League\OAuth2\Server\Entities\AuthCodeEntityInterface;
use Plugin\OAuth2\Repository; use Plugin\OAuth2\Repository;
use Plugin\OAuth2\Util\Token; use Plugin\OAuth2\Util\Token;
/**
* @extends Token<AuthCode>
*/
class AuthCode extends Token implements AuthCodeEntityInterface class AuthCode extends Token implements AuthCodeEntityInterface
{ {
// {{{ Autocode // {{{ Autocode

View File

@@ -38,6 +38,7 @@ use App\Core\Modules\Plugin;
use App\Core\Router; use App\Core\Router;
use App\Util\Common; use App\Util\Common;
use DateInterval; use DateInterval;
use EventResult;
use Exception; use Exception;
use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\CryptKey;
@@ -67,7 +68,7 @@ class OAuth2 extends Plugin
/** /**
* @throws Exception * @throws Exception
*/ */
public function onInitializePlugin() public function onInitializePlugin(): EventResult
{ {
self::$authorization_server = new AuthorizationServer( self::$authorization_server = new AuthorizationServer(
new Repository\Client, new Repository\Client,
@@ -86,6 +87,7 @@ class OAuth2 extends Plugin
), ),
new DateInterval('PT1H'), new DateInterval('PT1H'),
); );
return Event::next;
} }
/** /**
@@ -94,7 +96,7 @@ class OAuth2 extends Plugin
* *
* @param Router $r the router that was initialized * @param Router $r the router that was initialized
*/ */
public function onAddRoute(Router $r): bool public function onAddRoute(Router $r): EventResult
{ {
$r->connect('oauth2_mastodon_api_apps', '/api/v1/apps', C\Client::class, ['http-methods' => ['POST']]); $r->connect('oauth2_mastodon_api_apps', '/api/v1/apps', C\Client::class, ['http-methods' => ['POST']]);
$r->connect('oauth2_client', '/oauth/client', C\Client::class, ['http-methods' => ['POST']]); $r->connect('oauth2_client', '/oauth/client', C\Client::class, ['http-methods' => ['POST']]);
@@ -103,7 +105,7 @@ class OAuth2 extends Plugin
return Event::next; return Event::next;
} }
public function onEndHostMetaLinks(array &$links): bool public function onEndHostMetaLinks(array &$links): EventResult
{ {
$links[] = new XML_XRD_Element_Link(self::OAUTH_REQUEST_TOKEN_REL, Router::url('oauth2_client', type: Router::ABSOLUTE_URL)); $links[] = new XML_XRD_Element_Link(self::OAUTH_REQUEST_TOKEN_REL, Router::url('oauth2_client', type: Router::ABSOLUTE_URL));
$links[] = new XML_XRD_Element_Link(self::OAUTH_AUTHORIZE_REL, Router::url('oauth2_authorize', type: Router::ABSOLUTE_URL)); $links[] = new XML_XRD_Element_Link(self::OAUTH_AUTHORIZE_REL, Router::url('oauth2_authorize', type: Router::ABSOLUTE_URL));

View File

@@ -45,11 +45,17 @@ class RefreshToken implements RefreshTokenRepositoryInterface
DB::persist($refreshTokenEntity); DB::persist($refreshTokenEntity);
} }
/**
* @param string $tokenId
*/
public function revokeRefreshToken($tokenId) public function revokeRefreshToken($tokenId)
{ {
// Some logic to revoke the auth token in a database // Some logic to revoke the auth token in a database
} }
/**
* @param string $tokenId
*/
public function isRefreshtokenRevoked($tokenId): bool public function isRefreshtokenRevoked($tokenId): bool
{ {
return false; // The auth token has not been revoked return false; // The auth token has not been revoked

View File

@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
cd /var/www/social/file || exit 1 mkdir /var/www/social/file && cd /var/www/social/file || exit 1
mkdir -p oauth && cd oauth || exit 1 mkdir -p oauth && cd oauth || exit 1

Some files were not shown because too many files have changed in this diff Show More