diff --git a/app/Jobs/ProfilePipeline/HandleUpdateActivity.php b/app/Jobs/ProfilePipeline/HandleUpdateActivity.php new file mode 100644 index 000000000..ffff068e5 --- /dev/null +++ b/app/Jobs/ProfilePipeline/HandleUpdateActivity.php @@ -0,0 +1,88 @@ +payload = $payload; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle(): void + { + $payload = $this->payload; + + if(empty($payload) || !isset($payload['actor'])) { + return; + } + + $profile = Profile::whereRemoteUrl($payload['actor'])->first(); + + if(!$profile || $profile->domain === null || $profile->private_key) { + return; + } + + if($profile->sharedInbox == null || $profile->sharedInbox != $payload['object']['endpoints']['sharedInbox']) { + $profile->sharedInbox = $payload['object']['endpoints']['sharedInbox']; + } + + if($profile->public_key !== $payload['object']['publicKey']['publicKeyPem']) { + $profile->public_key = $payload['object']['publicKey']['publicKeyPem']; + } + + if($profile->bio !== $payload['object']['summary']) { + $len = strlen(strip_tags($payload['object']['summary'])); + if($len) { + if($len > 500) { + $updated = strip_tags($payload['object']['summary']); + $updated = substr($updated, 0, config('pixelfed.max_bio_length')); + $profile->bio = Autolink::create()->autolink($updated); + } else { + $profile->bio = Purify::clean($payload['object']['summary']); + } + } else { + $profile->bio = null; + } + } + + if($profile->name !== $payload['object']['name']) { + $profile->name = Purify::clean(substr($payload['object']['name'], 0, config('pixelfed.max_name_length'))); + } + + if($profile->isDirty()) { + $profile->save(); + } + + RemoteAvatarFetch::dispatch($profile)->onQueue('low'); + + return; + } +} diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index 325d9b0c3..0caf8f25d 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -29,6 +29,7 @@ use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline; use App\Jobs\StoryPipeline\StoryExpire; use App\Jobs\StoryPipeline\StoryFetch; use App\Jobs\StatusPipeline\StatusRemoteUpdatePipeline; +use App\Jobs\ProfilePipeline\HandleUpdateActivity; use App\Util\ActivityPub\Validator\Accept as AcceptValidator; use App\Util\ActivityPub\Validator\Add as AddValidator; @@ -36,6 +37,7 @@ use App\Util\ActivityPub\Validator\Announce as AnnounceValidator; use App\Util\ActivityPub\Validator\Follow as FollowValidator; use App\Util\ActivityPub\Validator\Like as LikeValidator; use App\Util\ActivityPub\Validator\UndoFollow as UndoFollowValidator; +use App\Util\ActivityPub\Validator\UpdatePersonValidator; use App\Services\PollService; use App\Services\FollowerService; @@ -1217,10 +1219,18 @@ class Inbox return; } + if(!Helpers::validateUrl($activity['id'])) { + return; + } + if($activity['type'] === 'Note') { if(Status::whereObjectUrl($activity['id'])->exists()) { StatusRemoteUpdatePipeline::dispatch($activity); } + } else if ($activity['type'] === 'Person') { + if(UpdatePersonValidator::validate($this->payload)) { + HandleUpdateActivity::dispatch($this->payload)->onQueue('low'); + } } } } diff --git a/app/Util/ActivityPub/Validator/UpdatePersonValidator.php b/app/Util/ActivityPub/Validator/UpdatePersonValidator.php new file mode 100644 index 000000000..b8819d8fd --- /dev/null +++ b/app/Util/ActivityPub/Validator/UpdatePersonValidator.php @@ -0,0 +1,119 @@ + 'required', + 'id' => 'required|string|url', + 'type' => [ + 'required', + Rule::in(['Update']) + ], + 'actor' => 'required|url', + 'object' => 'required', + 'object.id' => [ + 'required', + 'url', + 'same:actor', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ], + 'object.type' => [ + 'required', + Rule::in(['Person']) + ], + 'object.publicKey' => 'required', + 'object.publicKey.id' => [ + 'required', + 'url', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ], + 'object.publicKey.owner' => [ + 'required', + 'url', + 'same:actor', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ], + 'object.publicKey.publicKeyPem' => 'required|string', + 'object.url' => [ + 'required', + 'url', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ], + 'object.summary' => 'required|string|nullable', + 'object.preferredUsername' => 'required|string', + 'object.name' => 'required|string|nullable', + 'object.inbox' => [ + 'required', + 'url', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ], + 'object.outbox' => [ + 'required', + 'url', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ], + 'object.following' => [ + 'required', + 'url', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ], + 'object.followers' => [ + 'required', + 'url', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ], + 'object.manuallyApprovesFollowers' => 'required', + 'object.icon' => 'sometimes|nullable', + 'object.icon.type' => 'sometimes|required_with:object.icon.url,object.icon.mediaType|in:Image', + 'object.icon.url' => 'sometimes|required_with:object.icon.type,object.icon.mediaType|url', + 'object.icon.mediaType' => 'sometimes|required_with:object.icon.url,object.icon.type|in:image/jpeg,image/png,image/jpg', + 'object.endpoints' => 'sometimes', + 'object.endpoints.sharedInbox' => [ + 'sometimes', + 'url', + function (string $attribute, mixed $value, Closure $fail) use($payload) { + self::sameHost($attribute, $value, $fail, $payload['actor']); + }, + ] + ])->passes(); + + return $valid; + } + + public static function sameHost(string $attribute, mixed $value, Closure $fail, string $comparedHost) + { + if(empty($value)) { + $fail('The ' . $attribute . ' is invalid or empty'); + } + $host = parse_url($value, PHP_URL_HOST); + $idHost = parse_url($comparedHost, PHP_URL_HOST); + if ($host !== $idHost) { + $fail('The ' . $attribute . ' is invalid'); + } + } +}