Bring Micropub endpoint closer to the spec
- Detect form-encoded action=... requests, so form-encoded updates work and unhandled actions are no longer created as notes - Default to h-entry when no type is given - Read form-encoded photos from `photo` rather than `photos` - Pass `category` through EntryData so bookmark tags are saved - Accept single values for form-encoded multi-value properties - Return 403 for insufficient_scope and 401 for invalid tokens - Return 400 invalid_request for unsupported post types, update operations and models instead of reporting them as 500s Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cc2ff2b2a2
commit
6e96ca8304
5 changed files with 179 additions and 50 deletions
|
|
@ -64,31 +64,17 @@ class MicropubController extends Controller
|
|||
return response()->json([
|
||||
'error' => 'insufficient_scope',
|
||||
'error_description' => 'The token does not have the required scope for this request',
|
||||
], 401);
|
||||
], 403);
|
||||
} catch (ModelNotFoundException) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => 'No known note with given ID',
|
||||
], 404);
|
||||
} catch (MicropubUnsupportedModelException $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json([
|
||||
'error' => 'invalid',
|
||||
'error_description' => 'This implementation currently only supports the updating of notes',
|
||||
], 500);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
} catch (MicropubUnsupportedModelException|MicropubHandlerException|\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => $e->getMessage(),
|
||||
], 400);
|
||||
} catch (MicropubHandlerException $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json([
|
||||
'error' => 'unsupported_operation',
|
||||
'error_description' => 'The request could not be processed by this server',
|
||||
], 500);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,25 +15,36 @@ class MicropubRequest extends FormRequest
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the request type: the action (e.g. "update") if one is given,
|
||||
* otherwise the microformat type without its "h-" prefix. Per the spec, a
|
||||
* request with no type is treated as an h-entry.
|
||||
*/
|
||||
public function getType(): ?string
|
||||
{
|
||||
if ($this->isJson()) {
|
||||
$data = $this->json()->all();
|
||||
|
||||
if (isset($data['action']) && $data['action'] === 'update') {
|
||||
return 'update';
|
||||
if (isset($data['action']) && is_string($data['action'])) {
|
||||
return $data['action'];
|
||||
}
|
||||
|
||||
if (isset($data['type']) && is_array($data['type'])) {
|
||||
$type = current($data['type']);
|
||||
if (str_starts_with($type, 'h-')) {
|
||||
if (is_string($type) && str_starts_with($type, 'h-')) {
|
||||
return substr($type, 2);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return 'entry';
|
||||
}
|
||||
|
||||
return $this->input('h') ?: null;
|
||||
if ($this->filled('action')) {
|
||||
return $this->input('action');
|
||||
}
|
||||
|
||||
return $this->input('h') ?: 'entry';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class MicropubResponses
|
|||
'response' => 'error',
|
||||
'error' => 'insufficient_scope',
|
||||
'error_description' => 'The token’s scope does not have the necessary requirements.',
|
||||
], 401);
|
||||
], 403);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -29,7 +29,7 @@ class MicropubResponses
|
|||
'response' => 'error',
|
||||
'error' => 'invalid_token',
|
||||
'error_description' => 'The provided token did not pass validation',
|
||||
], 400);
|
||||
], 401);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class EntryData extends MicropubData
|
|||
public readonly mixed $syndication,
|
||||
public readonly ?array $photos,
|
||||
public readonly ?string $postStatus,
|
||||
public readonly ?array $category,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(Request $request): static
|
||||
|
|
@ -51,6 +52,7 @@ class EntryData extends MicropubData
|
|||
syndication: Arr::get($data, 'properties.syndication.0'),
|
||||
photos: Arr::get($data, 'properties.photo'),
|
||||
postStatus: Arr::get($data, 'properties.post-status.0'),
|
||||
category: Arr::get($data, 'properties.category'),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -62,17 +64,27 @@ class EntryData extends MicropubData
|
|||
location: $request->input('location'),
|
||||
bookmarkOf: $request->input('bookmark-of'),
|
||||
likeOf: $request->input('like-of'),
|
||||
mpSyndicateTo: $request->input('mp-syndicate-to'),
|
||||
mpSyndicateTo: self::wrapFormValue($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'),
|
||||
photos: self::wrapFormValue($request->input('photo')),
|
||||
postStatus: $request->input('post-status'),
|
||||
category: self::wrapFormValue($request->input('category')),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form-encoded multi-value properties may be sent with or without the []
|
||||
* suffix, so a single value arrives as a string rather than an array.
|
||||
*/
|
||||
private static function wrapFormValue(mixed $value): ?array
|
||||
{
|
||||
return $value === null ? null : Arr::wrap($value);
|
||||
}
|
||||
|
||||
private static function extractLocationData(array $data): array|string|null
|
||||
{
|
||||
if (! Arr::has($data, 'properties.location')) {
|
||||
|
|
@ -104,6 +116,7 @@ class EntryData extends MicropubData
|
|||
syndication: $data['syndication'] ?? null,
|
||||
photos: $data['photos'] ?? null,
|
||||
postStatus: $data['post-status'] ?? null,
|
||||
category: $data['category'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +138,7 @@ class EntryData extends MicropubData
|
|||
'syndication' => $this->syndication,
|
||||
'photos' => $this->photos,
|
||||
'post-status' => $this->postStatus,
|
||||
'category' => $this->category,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class MicropubHandlerRegistry
|
|||
public function getHandler(string $type): MicropubHandlerInterface
|
||||
{
|
||||
if (! isset($this->handlers[$type])) {
|
||||
throw new MicropubHandlerException("No handler registered for '{$type}'");
|
||||
throw new MicropubHandlerException("Unsupported Micropub request type '{$type}'");
|
||||
}
|
||||
|
||||
return $this->handlers[$type];
|
||||
|
|
|
|||
Loading…
Reference in a new issue