Add Micropub q=source and reject malformed requests
Servers supporting updates must support the source content query. It was falling through to the token response, so clients got a 200 with no post in it. Return the raw note as microformats2 JSON, honouring properties[]. Malformed requests could cause a 500 or worse. A replace value sent as a string overwrote the note with its first character, because $value[0] indexes the string. Validate update operations before applying any of them, and return invalid_request for wrongly shaped properties, unknown or malformed q values, and missing update URLs. Share URL resolution between updates and q=source in NoteResolver, which also rejects URLs on other hosts and invalid note IDs. Those IDs used to decode to a real note because invalid characters were skipped. Allow photo objects without alt text, and parse the published date before saving so that an invalid date doesn't leave a stray note behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6e96ca8304
commit
3dce7d3a60
1 changed files with 450 additions and 29 deletions
|
|
@ -12,9 +12,12 @@ use App\Models\Place;
|
|||
use App\Models\SyndicationTarget;
|
||||
use App\Services\Micropub\Data\MicropubData;
|
||||
use App\Services\Micropub\MicropubHandlerRegistry;
|
||||
use App\Services\Micropub\NoteResolver;
|
||||
use App\Services\Micropub\NoteSource;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class MicropubController extends Controller
|
||||
{
|
||||
|
|
@ -46,8 +49,15 @@ class MicropubController extends Controller
|
|||
try {
|
||||
$handler = $this->handlerRegistry->getHandler($type);
|
||||
$dataClass = $handler->dataClass();
|
||||
/** @var MicropubData $data */
|
||||
$data = $dataClass::fromRequest($request);
|
||||
|
||||
try {
|
||||
/** @var MicropubData $data */
|
||||
$data = $dataClass::fromRequest($request);
|
||||
} catch (\TypeError) {
|
||||
// the data objects are typed, so this means a property has the wrong shape
|
||||
return $this->invalidRequest('A property in the request has an invalid value');
|
||||
}
|
||||
|
||||
$result = $handler->handle($data);
|
||||
|
||||
if ($result['response'] === 'updated') {
|
||||
|
|
@ -89,40 +99,36 @@ 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:<lat>,<lng> queries.
|
||||
* Supports q=syndicate-to, q=config, q=source, and q=geo:<lat>,<lng> queries.
|
||||
* The default response returns the token metadata.
|
||||
*/
|
||||
public function get(Request $request): JsonResponse
|
||||
{
|
||||
if ($request->input('q') === 'syndicate-to') {
|
||||
$q = $request->input('q');
|
||||
|
||||
if ($q === 'syndicate-to') {
|
||||
return response()->json([
|
||||
'syndicate-to' => SyndicationTarget::all(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($request->input('q') === 'config') {
|
||||
if ($q === 'config') {
|
||||
return response()->json([
|
||||
'syndicate-to' => SyndicationTarget::all(),
|
||||
'media-endpoint' => route('media-endpoint'),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($request->has('q') && str_starts_with($request->input('q'), 'geo:')) {
|
||||
preg_match_all(
|
||||
'/([0-9.\-]+)/',
|
||||
$request->input('q'),
|
||||
$matches
|
||||
);
|
||||
$distance = (count($matches[0]) === 3) ? 100 * $matches[0][2] : 1000;
|
||||
$places = Place::near(
|
||||
(object) ['latitude' => $matches[0][0], 'longitude' => $matches[0][1]],
|
||||
$distance
|
||||
)->get();
|
||||
if ($q === 'source') {
|
||||
return $this->sourceResponse($request);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'response' => 'places',
|
||||
'places' => $places,
|
||||
]);
|
||||
if (is_string($q) && str_starts_with($q, 'geo:')) {
|
||||
return $this->placesResponse($q);
|
||||
}
|
||||
|
||||
if ($request->has('q')) {
|
||||
return $this->invalidRequest('This server does not know how to handle this q parameter');
|
||||
}
|
||||
|
||||
// the default response is just to return the token data
|
||||
|
|
@ -138,4 +144,59 @@ class MicropubController extends Controller
|
|||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to a q=source query with the microformats2 JSON of a post.
|
||||
*/
|
||||
private function sourceResponse(Request $request): JsonResponse
|
||||
{
|
||||
$url = $request->input('url');
|
||||
|
||||
if (! is_string($url)) {
|
||||
return $this->invalidRequest('The url parameter is required');
|
||||
}
|
||||
|
||||
try {
|
||||
$note = resolve(NoteResolver::class)->fromUrl($url);
|
||||
} catch (MicropubUnsupportedModelException $e) {
|
||||
return $this->invalidRequest($e->getMessage());
|
||||
} catch (ModelNotFoundException) {
|
||||
return $this->invalidRequest('No known note with given ID', 404);
|
||||
}
|
||||
|
||||
$requested = array_values(array_filter(Arr::wrap($request->input('properties')), 'is_string'));
|
||||
|
||||
return response()->json(resolve(NoteSource::class)->toArray($note, $requested));
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to a q=geo:<lat>,<lng> query with the nearby places.
|
||||
*/
|
||||
private function placesResponse(string $q): JsonResponse
|
||||
{
|
||||
preg_match_all('/([0-9.\-]+)/', $q, $matches);
|
||||
|
||||
if (count($matches[0]) < 2 || count(array_filter($matches[0], 'is_numeric')) !== count($matches[0])) {
|
||||
return $this->invalidRequest('The geo query must contain a latitude and longitude');
|
||||
}
|
||||
|
||||
$distance = (count($matches[0]) === 3) ? 100 * $matches[0][2] : 1000;
|
||||
$places = Place::near(
|
||||
(object) ['latitude' => $matches[0][0], 'longitude' => $matches[0][1]],
|
||||
$distance
|
||||
)->get();
|
||||
|
||||
return response()->json([
|
||||
'response' => 'places',
|
||||
'places' => $places,
|
||||
]);
|
||||
}
|
||||
|
||||
private function invalidRequest(string $description, int $status = 400): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => 'invalid_request',
|
||||
'error_description' => $description,
|
||||
], $status);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue