diff --git a/app/Exceptions/MicropubUnsupportedModelException.php b/app/Exceptions/MicropubUnsupportedModelException.php deleted file mode 100644 index 660f233f..00000000 --- a/app/Exceptions/MicropubUnsupportedModelException.php +++ /dev/null @@ -1,7 +0,0 @@ -handlerRegistry->getHandler($type); - $dataClass = $handler->dataClass(); - /** @var MicropubData $data */ - $data = $dataClass::fromRequest($request); - $result = $handler->handle($data); - - if ($result['response'] === 'updated') { - return response()->json([ - 'response' => $result['response'], - ], 200)->header('Location', $result['url']); - } + $result = $handler->handle($request->getMicropubData()); + // Return appropriate response based on the handler result return response()->json([ 'response' => $result['response'], 'location' => $result['url'] ?? null, ], 201)->header('Location', $result['url']); - } catch (InvalidTokenScopeException) { - return response()->json([ - 'error' => 'insufficient_scope', - 'error_description' => 'The token does not have the required scope for this request', - ], 401); - } catch (ModelNotFoundException) { - return response()->json([ - 'error' => 'invalid_request', - 'error_description' => 'No known note with given ID', - ], 404); - } catch (MicropubUnsupportedModelException) { - return response()->json([ - 'error' => 'invalid', - 'error_description' => 'This implementation currently only supports the updating of notes', - ], 500); } catch (\InvalidArgumentException $e) { return response()->json([ 'error' => 'invalid_request', @@ -83,10 +57,15 @@ class MicropubController extends Controller ], 400); } catch (MicropubHandlerException) { return response()->json([ - 'error' => 'unsupported_operation', + 'error' => 'Unknown Micropub type', 'error_description' => 'The request could not be processed by this server', ], 500); - } catch (\Exception $e) { + } catch (InvalidTokenScopeException) { + return response()->json([ + 'error' => 'invalid_scope', + 'error_description' => 'The token does not have the required scope for this request', + ], 403); + } catch (\Exception) { return response()->json([ 'error' => 'server_error', 'error_description' => 'An error occurred processing the request', @@ -97,9 +76,10 @@ class MicropubController extends Controller /** * Respond to a GET request to the micropub endpoint. * - * Token validation is handled by the VerifyMicropubToken middleware. - * Supports q=syndicate-to, q=config, and q=geo:, queries. - * The default response returns the token metadata. + * A GET request has been made to `api/post` with an accompanying + * token, here we check whether the token is valid and respond + * appropriately. Further if the request has the query parameter + * syndicate-to we respond with the known syndication endpoints. */ public function get(Request $request): JsonResponse { diff --git a/app/Http/Requests/MicropubRequest.php b/app/Http/Requests/MicropubRequest.php index ab4fde74..cc22dd3e 100644 --- a/app/Http/Requests/MicropubRequest.php +++ b/app/Http/Requests/MicropubRequest.php @@ -5,9 +5,12 @@ declare(strict_types=1); namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Arr; class MicropubRequest extends FormRequest { + protected array $micropubData = []; + public function rules(): array { return [ @@ -15,25 +18,105 @@ class MicropubRequest extends FormRequest ]; } + public function getMicropubData(): array + { + return $this->micropubData; + } + public function getType(): ?string { + // Return consistent type regardless of input format + return $this->micropubData['type'] ?? null; + } + + protected function prepareForValidation(): void + { + // Normalize the request data based on content type if ($this->isJson()) { - $data = $this->json()->all(); + $this->normalizeMicropubJson(); + } else { + $this->normalizeMicropubForm(); + } + } - if (isset($data['action']) && $data['action'] === 'update') { - return 'update'; + private function normalizeMicropubJson(): void + { + $json = $this->json(); + if ($json === null) { + throw new \InvalidArgumentException('`isJson()` passed but there is no json data'); + } + + $data = $json->all(); + + // Convert JSON type (h-entry) to simple type (entry) + if (isset($data['type']) && is_array($data['type'])) { + $type = current($data['type']); + if (str_starts_with($type, 'h-')) { + $this->micropubData['type'] = substr($type, 2); } + } + // Or set the type to update + elseif (isset($data['action']) && $data['action'] === 'update') { + $this->micropubData['type'] = 'update'; + } - if (isset($data['type']) && is_array($data['type'])) { - $type = current($data['type']); - if (str_starts_with($type, 'h-')) { - return substr($type, 2); - } - } + // Add in the token data + $this->micropubData['token_data'] = $data['token_data']; + // Add h-entry values + $this->micropubData['content'] = Arr::get($data, 'properties.content.0'); + $this->micropubData['in-reply-to'] = Arr::get($data, 'properties.in-reply-to.0'); + $this->micropubData['published'] = Arr::get($data, 'properties.published.0'); + $this->micropubData['location'] = $this->getLocationData($data); + $this->micropubData['bookmark-of'] = Arr::get($data, 'properties.bookmark-of.0'); + $this->micropubData['like-of'] = Arr::get($data, 'properties.like-of.0'); + $this->micropubData['mp-syndicate-to'] = Arr::get($data, 'properties.mp-syndicate-to'); + + // Add h-card values + $this->micropubData['name'] = Arr::get($data, 'properties.name.0'); + $this->micropubData['description'] = Arr::get($data, 'properties.description.0'); + $this->micropubData['geo'] = Arr::get($data, 'properties.geo.0'); + + // Add checkin value + $this->micropubData['checkin'] = Arr::get($data, 'checkin'); + $this->micropubData['syndication'] = Arr::get($data, 'properties.syndication.0'); + + // Add photos + $this->micropubData['photos'] = Arr::get($data, 'properties.photo'); + } + + private function normalizeMicropubForm(): void + { + // Convert form h=entry to type=entry + if ($h = $this->input('h')) { + $this->micropubData['type'] = $h; + } + + // Add some fields to the micropub data with default null values + $this->micropubData['in-reply-to'] = null; + $this->micropubData['published'] = null; + $this->micropubData['location'] = null; + $this->micropubData['description'] = null; + $this->micropubData['geo'] = null; + $this->micropubData['latitude'] = null; + $this->micropubData['longitude'] = null; + + // Map form fields to micropub data + foreach ($this->except(['h', 'access_token']) as $key => $value) { + $this->micropubData[$key] = $value; + } + } + + private function getLocationData(array $data): array|string|null + { + if (! Arr::has($data, 'properties.location')) { return null; } - return $this->input('h') ?: null; + if (Arr::has($data, 'properties.location.0')) { + return Arr::get($data, 'properties.location.0'); + } + + return Arr::get($data, 'properties.location'); } } diff --git a/app/Providers/MicropubServiceProvider.php b/app/Providers/MicropubServiceProvider.php index b8ff479c..1002a26d 100644 --- a/app/Providers/MicropubServiceProvider.php +++ b/app/Providers/MicropubServiceProvider.php @@ -4,9 +4,8 @@ declare(strict_types=1); namespace App\Providers; -use App\Services\Micropub\Handlers\CardHandler; -use App\Services\Micropub\Handlers\EntryHandler; -use App\Services\Micropub\Handlers\UpdateHandler; +use App\Services\Micropub\CardHandler; +use App\Services\Micropub\EntryHandler; use App\Services\Micropub\MicropubHandlerRegistry; use Illuminate\Support\ServiceProvider; @@ -20,7 +19,6 @@ class MicropubServiceProvider extends ServiceProvider // Register handlers $registry->register('card', new CardHandler); $registry->register('entry', new EntryHandler); - $registry->register('update', new UpdateHandler); return $registry; }); diff --git a/app/Services/Micropub/Handlers/CardHandler.php b/app/Services/Micropub/CardHandler.php similarity index 61% rename from app/Services/Micropub/Handlers/CardHandler.php rename to app/Services/Micropub/CardHandler.php index 02e3a066..12e283be 100644 --- a/app/Services/Micropub/Handlers/CardHandler.php +++ b/app/Services/Micropub/CardHandler.php @@ -2,28 +2,20 @@ declare(strict_types=1); -namespace App\Services\Micropub\Handlers; +namespace App\Services\Micropub; use App\Exceptions\InvalidTokenScopeException; -use App\Services\Micropub\Data\CardData; -use App\Services\Micropub\Data\MicropubData; use App\Services\PlaceService; class CardHandler implements MicropubHandlerInterface { - public function dataClass(): string - { - return CardData::class; - } - /** * @throws InvalidTokenScopeException */ - public function handle(MicropubData $data): array + public function handle(array $data): array { - assert($data instanceof CardData); - - $scopes = $data->tokenData['scope']; + // Handle h-card requests + $scopes = $data['token_data']['scope']; if (is_string($scopes)) { $scopes = explode(' ', $scopes); } @@ -32,7 +24,7 @@ class CardHandler implements MicropubHandlerInterface throw new InvalidTokenScopeException; } - $location = resolve(PlaceService::class)->createPlace($data->toArray())->uri; + $location = resolve(PlaceService::class)->createPlace($data)->uri; return [ 'response' => 'created', diff --git a/app/Services/Micropub/Data/CardData.php b/app/Services/Micropub/Data/CardData.php deleted file mode 100644 index cab40419..00000000 --- a/app/Services/Micropub/Data/CardData.php +++ /dev/null @@ -1,74 +0,0 @@ -isJson()) { - $data = $request->json()->all(); - - return new static( - tokenData: $data['token_data'], - name: Arr::get($data, 'properties.name.0'), - description: Arr::get($data, 'properties.description.0'), - geo: Arr::get($data, 'properties.geo.0'), - location: Arr::get($data, 'properties.location'), - latitude: null, - longitude: null, - ); - } - - return new static( - tokenData: $request->input('token_data'), - name: $request->input('name'), - description: $request->input('description'), - geo: $request->input('geo'), - location: $request->input('location'), - latitude: $request->input('latitude'), - longitude: $request->input('longitude'), - ); - } - - public static function fromArray(array $data): static - { - return new static( - tokenData: $data['token_data'], - name: $data['name'] ?? null, - description: $data['description'] ?? null, - geo: $data['geo'] ?? null, - location: $data['location'] ?? null, - latitude: $data['latitude'] ?? null, - longitude: $data['longitude'] ?? null, - ); - } - - public function toArray(): array - { - return [ - 'token_data' => $this->tokenData, - 'name' => $this->name, - 'description' => $this->description, - 'geo' => $this->geo, - 'location' => $this->location, - 'latitude' => $this->latitude, - 'longitude' => $this->longitude, - ]; - } -} diff --git a/app/Services/Micropub/Data/EntryData.php b/app/Services/Micropub/Data/EntryData.php deleted file mode 100644 index 8942be78..00000000 --- a/app/Services/Micropub/Data/EntryData.php +++ /dev/null @@ -1,125 +0,0 @@ -isJson()) { - $data = $request->json()->all(); - - $rawContent = Arr::get($data, 'properties.content.0'); - $content = is_array($rawContent) ? ($rawContent['html'] ?? $rawContent['value'] ?? null) : $rawContent; - - return new static( - tokenData: $data['token_data'], - content: $content, - inReplyTo: Arr::get($data, 'properties.in-reply-to.0'), - published: Arr::get($data, 'properties.published.0'), - location: self::extractLocationData($data), - bookmarkOf: Arr::get($data, 'properties.bookmark-of.0'), - likeOf: Arr::get($data, 'properties.like-of.0'), - mpSyndicateTo: Arr::get($data, 'properties.mp-syndicate-to'), - name: Arr::get($data, 'properties.name.0'), - description: Arr::get($data, 'properties.description.0'), - geo: Arr::get($data, 'properties.geo.0'), - checkin: Arr::get($data, 'checkin'), - syndication: Arr::get($data, 'properties.syndication.0'), - photos: Arr::get($data, 'properties.photo'), - ); - } - - return new static( - tokenData: $request->input('token_data'), - content: $request->input('content'), - inReplyTo: $request->input('in-reply-to'), - published: $request->input('published'), - location: $request->input('location'), - bookmarkOf: $request->input('bookmark-of'), - likeOf: $request->input('like-of'), - mpSyndicateTo: $request->input('mp-syndicate-to'), - name: $request->input('name'), - description: $request->input('description'), - geo: $request->input('geo'), - checkin: $request->input('checkin'), - syndication: $request->input('syndication'), - photos: $request->input('photos'), - ); - } - - private static function extractLocationData(array $data): array|string|null - { - if (! Arr::has($data, 'properties.location')) { - return null; - } - - if (Arr::has($data, 'properties.location.0')) { - return Arr::get($data, 'properties.location.0'); - } - - return Arr::get($data, 'properties.location'); - } - - public static function fromArray(array $data): static - { - return new static( - tokenData: $data['token_data'], - content: $data['content'] ?? null, - inReplyTo: $data['in-reply-to'] ?? null, - published: $data['published'] ?? null, - location: $data['location'] ?? null, - bookmarkOf: $data['bookmark-of'] ?? null, - likeOf: $data['like-of'] ?? null, - mpSyndicateTo: $data['mp-syndicate-to'] ?? null, - name: $data['name'] ?? null, - description: $data['description'] ?? null, - geo: $data['geo'] ?? null, - checkin: $data['checkin'] ?? null, - syndication: $data['syndication'] ?? null, - photos: $data['photos'] ?? null, - ); - } - - public function toArray(): array - { - return [ - 'token_data' => $this->tokenData, - 'content' => $this->content, - 'in-reply-to' => $this->inReplyTo, - 'published' => $this->published, - 'location' => $this->location, - 'bookmark-of' => $this->bookmarkOf, - 'like-of' => $this->likeOf, - 'mp-syndicate-to' => $this->mpSyndicateTo, - 'name' => $this->name, - 'description' => $this->description, - 'geo' => $this->geo, - 'checkin' => $this->checkin, - 'syndication' => $this->syndication, - 'photos' => $this->photos, - ]; - } -} diff --git a/app/Services/Micropub/Data/MicropubData.php b/app/Services/Micropub/Data/MicropubData.php deleted file mode 100644 index d8d56a29..00000000 --- a/app/Services/Micropub/Data/MicropubData.php +++ /dev/null @@ -1,16 +0,0 @@ -isJson()) { - $data = $request->json()->all(); - - return new static( - tokenData: $data['token_data'], - updateUrl: $data['url'] ?? null, - updateReplace: $data['replace'] ?? null, - updateAdd: $data['add'] ?? null, - updateDelete: $data['delete'] ?? null, - ); - } - - return new static( - tokenData: $request->input('token_data'), - updateUrl: $request->input('url'), - updateReplace: $request->input('replace'), - updateAdd: $request->input('add'), - updateDelete: $request->input('delete'), - ); - } - - public static function fromArray(array $data): static - { - return new static( - tokenData: $data['token_data'], - updateUrl: $data['update_url'] ?? null, - updateReplace: $data['update_replace'] ?? null, - updateAdd: $data['update_add'] ?? null, - updateDelete: $data['update_delete'] ?? null, - ); - } - - public function toArray(): array - { - return [ - 'token_data' => $this->tokenData, - 'update_url' => $this->updateUrl, - 'update_replace' => $this->updateReplace, - 'update_add' => $this->updateAdd, - 'update_delete' => $this->updateDelete, - ]; - } -} diff --git a/app/Services/Micropub/Handlers/EntryHandler.php b/app/Services/Micropub/EntryHandler.php similarity index 51% rename from app/Services/Micropub/Handlers/EntryHandler.php rename to app/Services/Micropub/EntryHandler.php index ef9740f2..9cdbe789 100644 --- a/app/Services/Micropub/Handlers/EntryHandler.php +++ b/app/Services/Micropub/EntryHandler.php @@ -2,31 +2,22 @@ declare(strict_types=1); -namespace App\Services\Micropub\Handlers; +namespace App\Services\Micropub; use App\Exceptions\InvalidTokenScopeException; use App\Services\ArticleService; use App\Services\BookmarkService; use App\Services\LikeService; -use App\Services\Micropub\Data\EntryData; -use App\Services\Micropub\Data\MicropubData; use App\Services\NoteService; class EntryHandler implements MicropubHandlerInterface { - public function dataClass(): string - { - return EntryData::class; - } - /** * @throws InvalidTokenScopeException */ - public function handle(MicropubData $data): array + public function handle(array $data) { - assert($data instanceof EntryData); - - $scopes = $data->tokenData['scope']; + $scopes = $data['token_data']['scope']; if (is_string($scopes)) { $scopes = explode(' ', $scopes); } @@ -35,12 +26,11 @@ class EntryHandler implements MicropubHandlerInterface throw new InvalidTokenScopeException; } - $dataArray = $data->toArray(); $location = match (true) { - isset($dataArray['like-of']) => resolve(LikeService::class)->create($dataArray)->url, - isset($dataArray['bookmark-of']) => resolve(BookmarkService::class)->create($dataArray)->uri, - isset($dataArray['name']) => resolve(ArticleService::class)->create($dataArray)->link, - default => resolve(NoteService::class)->create($dataArray)->uri, + isset($data['like-of']) => resolve(LikeService::class)->create($data)->url, + isset($data['bookmark-of']) => resolve(BookmarkService::class)->create($data)->uri, + isset($data['name']) => resolve(ArticleService::class)->create($data)->link, + default => resolve(NoteService::class)->create($data)->uri, }; return [ diff --git a/app/Services/Micropub/Handlers/MicropubHandlerInterface.php b/app/Services/Micropub/Handlers/MicropubHandlerInterface.php deleted file mode 100644 index 6afddc5f..00000000 --- a/app/Services/Micropub/Handlers/MicropubHandlerInterface.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ - public function dataClass(): string; - - /** - * Process the request and return a result array with at minimum a - * 'response' key ('created' or 'updated') and a 'url' key pointing to - * the affected resource. - */ - public function handle(MicropubData $data): array; -} diff --git a/app/Services/Micropub/Handlers/UpdateHandler.php b/app/Services/Micropub/Handlers/UpdateHandler.php deleted file mode 100644 index 354cdbdd..00000000 --- a/app/Services/Micropub/Handlers/UpdateHandler.php +++ /dev/null @@ -1,150 +0,0 @@ -tokenData['scope']; - if (is_string($scopes)) { - $scopes = explode(' ', $scopes); - } - - if (! in_array('update', $scopes, true)) { - throw new InvalidTokenScopeException; - } - - $urlPath = parse_url($data->updateUrl, PHP_URL_PATH); - - if (mb_substr($urlPath, 1, 5) !== 'notes') { - throw new MicropubUnsupportedModelException('This implementation currently only supports the updating of notes'); - } - - $note = Note::nb60(basename($urlPath))->firstOrFail(); - - if ($data->updateReplace === null && $data->updateAdd === null && $data->updateDelete === null) { - throw new MicropubHandlerException('Unsupported update operation'); - } - - if ($data->updateReplace !== null) { - foreach ($data->updateReplace as $property => $value) { - match ($property) { - 'content' => $note->note = $value[0], - 'syndication' => $this->applySyndication($note, $value), - default => null, - }; - } - } - - if ($data->updateAdd !== null) { - foreach ($data->updateAdd as $property => $value) { - if ($property === 'syndication') { - $this->applySyndication($note, $value); - } - if ($property === 'photo') { - foreach ($value as $photoURL) { - if (Str::startsWith($photoURL, 'https://')) { - $media = new Media; - $media->path = $photoURL; - $media->type = 'image'; - $media->save(); - $note->media()->save($media); - } - } - } - } - } - - if ($data->updateDelete !== null) { - $this->applyDelete($note, $data->updateDelete); - } - - $note->save(); - - return [ - 'response' => 'updated', - 'url' => $note->uri, - ]; - } - - private function applyDelete(Note $note, array $delete): void - { - if (array_is_list($delete)) { - foreach ($delete as $property) { - match ($property) { - 'syndication' => $this->clearSyndication($note), - 'photo' => $note->media()->delete(), - default => null, - }; - } - - return; - } - - foreach ($delete as $property => $values) { - if ($property === 'syndication') { - $this->removeSyndicationValues($note, $values); - } - if ($property === 'photo') { - $note->media()->whereIn('path', $values)->delete(); - } - } - } - - private function clearSyndication(Note $note): void - { - $note->facebook_url = null; - $note->swarm_url = null; - $note->tweet_id = null; - } - - private function removeSyndicationValues(Note $note, array $urls): void - { - foreach ($urls as $url) { - if (Str::startsWith($url, 'https://www.facebook.com') && $note->facebook_url === $url) { - $note->facebook_url = null; - } elseif (Str::startsWith($url, 'https://www.swarmapp.com') && $note->swarm_url === $url) { - $note->swarm_url = null; - } elseif (Str::startsWith($url, 'https://twitter.com')) { - $note->tweet_id = null; - } - } - } - - private function applySyndication(Note $note, array $urls): void - { - foreach ($urls as $url) { - if (Str::startsWith($url, 'https://www.facebook.com')) { - $note->facebook_url = $url; - } elseif (Str::startsWith($url, 'https://www.swarmapp.com')) { - $note->swarm_url = $url; - } elseif (Str::startsWith($url, 'https://twitter.com')) { - $note->tweet_id = basename(parse_url($url, PHP_URL_PATH)); - } - } - } -} diff --git a/app/Services/Micropub/MicropubHandlerInterface.php b/app/Services/Micropub/MicropubHandlerInterface.php new file mode 100644 index 00000000..82040be9 --- /dev/null +++ b/app/Services/Micropub/MicropubHandlerInterface.php @@ -0,0 +1,10 @@ +dataClass()::fromArray($rawData) - * → $handler->handle($dataObject) - * - * Handlers are registered in MicropubServiceProvider. To support a new - * Micropub post type, create a handler in App\Services\Micropub\Handlers, a - * matching data class in App\Services\Micropub\Data, and register the handler - * here with its type string. - */ class MicropubHandlerRegistry { /** @@ -33,9 +13,6 @@ class MicropubHandlerRegistry */ protected array $handlers = []; - /** - * Register a handler for a given Micropub type string. - */ public function register(string $type, MicropubHandlerInterface $handler): self { $this->handlers[$type] = $handler; @@ -44,8 +21,6 @@ class MicropubHandlerRegistry } /** - * Retrieve the handler for a given type, or throw if none is registered. - * * @throws MicropubHandlerException */ public function getHandler(string $type): MicropubHandlerInterface diff --git a/app/Services/Micropub/UpdateHandler.php b/app/Services/Micropub/UpdateHandler.php new file mode 100644 index 00000000..ee018f19 --- /dev/null +++ b/app/Services/Micropub/UpdateHandler.php @@ -0,0 +1,119 @@ +json([ + 'error' => 'invalid', + 'error_description' => 'This implementation currently only support the updating of notes', + ], 500); + } + + try { + $note = Note::nb60(basename($urlPath))->firstOrFail(); + } catch (ModelNotFoundException) { + return response()->json([ + 'error' => 'invalid_request', + 'error_description' => 'No known note with given ID', + ], 404); + } + + // got the note, are we dealing with a “replace” request? + if (Arr::get($data, 'replace')) { + foreach (Arr::get($data, 'replace') as $property => $value) { + if ($property === 'content') { + $note->note = $value[0]; + } + if ($property === 'syndication') { + foreach ($value as $syndicationURL) { + if (Str::startsWith($syndicationURL, 'https://www.facebook.com')) { + $note->facebook_url = $syndicationURL; + } + if (Str::startsWith($syndicationURL, 'https://www.swarmapp.com')) { + $note->swarm_url = $syndicationURL; + } + if (Str::startsWith($syndicationURL, 'https://twitter.com')) { + $note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH)); + } + } + } + } + $note->save(); + + return [ + 'response' => 'updated', + ]; + } + + // how about “add” + if (Arr::get($data, 'add')) { + foreach (Arr::get($data, 'add') as $property => $value) { + if ($property === 'syndication') { + foreach ($value as $syndicationURL) { + if (Str::startsWith($syndicationURL, 'https://www.facebook.com')) { + $note->facebook_url = $syndicationURL; + } + if (Str::startsWith($syndicationURL, 'https://www.swarmapp.com')) { + $note->swarm_url = $syndicationURL; + } + if (Str::startsWith($syndicationURL, 'https://twitter.com')) { + $note->tweet_id = basename(parse_url($syndicationURL, PHP_URL_PATH)); + } + } + } + if ($property === 'photo') { + foreach ($value as $photoURL) { + if (Str::startsWith($photoURL, 'https://')) { + $media = new Media; + $media->path = $photoURL; + $media->type = 'image'; + $media->save(); + $note->media()->save($media); + } + } + } + } + $note->save(); + + return response()->json([ + 'response' => 'updated', + ]); + } + + return response()->json([ + 'response' => 'error', + 'error_description' => 'unsupported request', + ], 500); + } +} diff --git a/database/migrations/2026_03_14_000000_expand_webmentions_source_target_to_text.php b/database/migrations/2026_03_14_000000_expand_webmentions_source_target_to_text.php deleted file mode 100644 index de643ab0..00000000 --- a/database/migrations/2026_03_14_000000_expand_webmentions_source_target_to_text.php +++ /dev/null @@ -1,26 +0,0 @@ -text('source')->change(); - $table->text('target')->change(); - }); - } - - public function down(): void - { - Schema::table('webmentions', function (Blueprint $table) { - $table->string('source')->change(); - $table->string('target')->change(); - }); - } -}; diff --git a/database/schema/pgsql-schema.sql b/database/schema/pgsql-schema.sql index d1f926a3..6dae72e5 100644 --- a/database/schema/pgsql-schema.sql +++ b/database/schema/pgsql-schema.sql @@ -723,8 +723,8 @@ ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; CREATE TABLE public.webmentions ( id integer NOT NULL, - source text NOT NULL, - target text NOT NULL, + source character varying(255) NOT NULL, + target character varying(255) NOT NULL, commentable_id integer, commentable_type character varying(255), type character varying(255), diff --git a/tests/Feature/MicropubControllerTest.php b/tests/Feature/MicropubControllerTest.php index 10d129c0..38a5feb1 100644 --- a/tests/Feature/MicropubControllerTest.php +++ b/tests/Feature/MicropubControllerTest.php @@ -228,8 +228,8 @@ class MicropubControllerTest extends TestCase ], ['HTTP_Authorization' => 'Bearer ' . $this->getTokenWithIncorrectScope()] ); - $response->assertStatus(401); - $response->assertJson(['error' => 'insufficient_scope']); + $response->assertStatus(403); + $response->assertJson(['error' => 'invalid_scope']); } /** @@ -448,10 +448,10 @@ class MicropubControllerTest extends TestCase ); $response ->assertJson([ - 'error' => 'insufficient_scope', + 'error' => 'invalid_scope', 'error_description' => 'The token does not have the required scope for this request', ]) - ->assertStatus(401); + ->assertStatus(403); } #[Test] @@ -469,7 +469,7 @@ class MicropubControllerTest extends TestCase ); $response ->assertJson([ - 'error' => 'unsupported_operation', + 'error' => 'Unknown Micropub type', 'error_description' => 'The request could not be processed by this server', ]) ->assertStatus(500); @@ -518,6 +518,8 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_updates_existing_note(): void { + $this->markTestSkipped('Update requests are not supported yet'); + $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -532,15 +534,14 @@ class MicropubControllerTest extends TestCase ); $response ->assertJson(['response' => 'updated']) - ->assertSuccessful(); - - $note->refresh(); - $this->assertSame('replaced content', $note->content); + ->assertStatus(200); } #[Test] public function micropub_client_api_request_updates_note_syndication_links(): void { + $this->markTestSkipped('Update requests are not supported yet'); + $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -558,8 +559,7 @@ class MicropubControllerTest extends TestCase ); $response ->assertJson(['response' => 'updated']) - ->assertSuccessful(); - + ->assertStatus(200); $this->assertDatabaseHas('notes', [ 'swarm_url' => 'https://www.swarmapp.com/checkin/123', 'facebook_url' => 'https://www.facebook.com/checkin/123', @@ -569,6 +569,8 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_adds_image_to_note(): void { + $this->markTestSkipped('Update requests are not supported yet'); + $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -583,7 +585,7 @@ class MicropubControllerTest extends TestCase ); $response ->assertJson(['response' => 'updated']) - ->assertSuccessful(); + ->assertStatus(200); $this->assertDatabaseHas('media_endpoint', [ 'path' => 'https://example.org/photo.jpg', ]); @@ -592,6 +594,8 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_returns_error_trying_to_update_non_note_model(): void { + $this->markTestSkipped('Update requests are not supported yet'); + $response = $this->postJson( '/api/post', [ @@ -611,6 +615,8 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_returns_error_trying_to_update_non_existing_note(): void { + $this->markTestSkipped('Update requests are not supported yet'); + $response = $this->postJson( '/api/post', [ @@ -630,6 +636,8 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_returns_error_when_trying_to_update_unsupported_property(): void { + $this->markTestSkipped('Update requests are not supported yet'); + $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -643,13 +651,15 @@ class MicropubControllerTest extends TestCase ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] ); $response - ->assertJson(['error' => 'unsupported_operation']) + ->assertJson(['response' => 'error']) ->assertStatus(500); } #[Test] public function micropub_client_api_request_with_token_with_insufficient_scope_returns_error(): void { + $this->markTestSkipped('Update requests are not supported yet'); + $response = $this->postJson( '/api/post', [ @@ -669,6 +679,8 @@ class MicropubControllerTest extends TestCase #[Test] public function micropub_client_api_request_can_replace_note_syndication_targets(): void { + $this->markTestSkipped('Update requests are not supported yet'); + $note = Note::factory()->create(); $response = $this->postJson( '/api/post', @@ -686,7 +698,7 @@ class MicropubControllerTest extends TestCase ); $response ->assertJson(['response' => 'updated']) - ->assertSuccessful(); + ->assertStatus(200); $this->assertDatabaseHas('notes', [ 'swarm_url' => 'https://www.swarmapp.com/checkin/the-id', 'facebook_url' => 'https://www.facebook.com/post/the-id', @@ -711,116 +723,6 @@ class MicropubControllerTest extends TestCase $this->assertDatabaseHas('notes', ['note' => $note]); } - #[Test] - public function micropub_client_api_request_can_combine_replace_and_add_in_single_update(): void - { - $note = Note::factory()->create(); - $response = $this->postJson( - '/api/post', - [ - 'action' => 'update', - 'url' => $note->uri, - 'replace' => [ - 'content' => ['replaced content'], - ], - 'add' => [ - 'syndication' => ['https://www.swarmapp.com/checkin/123'], - ], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - $response - ->assertJson(['response' => 'updated']) - ->assertSuccessful(); - - $note->refresh(); - $this->assertSame('replaced content', $note->content); - $this->assertDatabaseHas('notes', [ - 'swarm_url' => 'https://www.swarmapp.com/checkin/123', - ]); - } - - #[Test] - public function micropub_client_api_request_can_delete_entire_property(): void - { - $note = Note::factory()->create([ - 'swarm_url' => 'https://www.swarmapp.com/checkin/123', - 'facebook_url' => 'https://www.facebook.com/post/123', - ]); - $response = $this->postJson( - '/api/post', - [ - 'action' => 'update', - 'url' => $note->uri, - 'delete' => ['syndication'], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - $response - ->assertJson(['response' => 'updated']) - ->assertSuccessful(); - - $note->refresh(); - $this->assertNull($note->swarm_url); - $this->assertNull($note->facebook_url); - $this->assertNull($note->tweet_id); - } - - #[Test] - public function micropub_client_api_request_can_delete_specific_syndication_value(): void - { - $note = Note::factory()->create([ - 'swarm_url' => 'https://www.swarmapp.com/checkin/123', - 'facebook_url' => 'https://www.facebook.com/post/123', - ]); - $response = $this->postJson( - '/api/post', - [ - 'action' => 'update', - 'url' => $note->uri, - 'delete' => [ - 'syndication' => ['https://www.swarmapp.com/checkin/123'], - ], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - $response - ->assertJson(['response' => 'updated']) - ->assertSuccessful(); - - $note->refresh(); - $this->assertNull($note->swarm_url); - $this->assertSame('https://www.facebook.com/post/123', $note->facebook_url); - } - - #[Test] - public function micropub_client_api_request_can_delete_photo(): void - { - $note = Note::factory()->create(); - $media = new \App\Models\Media; - $media->path = 'https://example.org/photo.jpg'; - $media->type = 'image'; - $media->save(); - $note->media()->save($media); - - $response = $this->postJson( - '/api/post', - [ - 'action' => 'update', - 'url' => $note->uri, - 'delete' => ['photo'], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - $response - ->assertJson(['response' => 'updated']) - ->assertSuccessful(); - - $this->assertDatabaseMissing('media_endpoint', [ - 'path' => 'https://example.org/photo.jpg', - ]); - } - #[Test] public function micropub_client_api_request_creates_articles_when_it_includes_the_name_property(): void { diff --git a/tests/Unit/Jobs/ProcessWebMentionJobTest.php b/tests/Unit/Jobs/ProcessWebMentionJobTest.php index 549dee60..e3754b86 100644 --- a/tests/Unit/Jobs/ProcessWebMentionJobTest.php +++ b/tests/Unit/Jobs/ProcessWebMentionJobTest.php @@ -195,38 +195,6 @@ class ProcessWebMentionJobTest extends TestCase ]); } - #[Test] - public function webmention_with_long_source_url_gets_saved(): void - { - Queue::fake(); - - $parser = new Parser; - - $html = <<<'HTML' -
- I liked a note. -
- HTML; - $html = str_replace('href="', 'href="' . config('app.url'), $html); - $mock = new MockHandler([ - new Response(200, [], $html), - ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - - $note = Note::factory()->create(); - // Simulate a long brid.gy Bluesky source URL (well over 255 characters) - $source = 'https://brid.gy/comment/bluesky/did:plc:n3jhgiq2ykctnpgzlm6p6b25/at%253A%252F%252Fdid%253Aplc%253An3jhgiq2ykctnpgzlm6p6b25%252Fapp.bsky.feed.post%252F3mfekjuhykb2k/at%253A%252F%252Fdid%253Aplc%253Afsfuwigo5juzdwp23hyianzg%252Fapp.bsky.feed.post%252F3mfemw4rrvs2t'; - - $job = new ProcessWebMention($note, $source); - $job->handle($parser, $client); - - $this->assertGreaterThan(255, strlen($source)); - $this->assertDatabaseHas('webmentions', [ - 'source' => $source, - ]); - } - #[Test] public function webmention_repost_gets_deleted_when_repost_of_value_changes(): void {