Each handler now declares the data class it needs via dataClass(). The controller builds the appropriate typed DTO from the raw request array before calling handle(), giving handlers typed property access instead of raw array lookups. Handlers moved to App\Services\Micropub\Handlers, data objects to App\Services\Micropub\Data. MicropubHandlerRegistry and the interface are documented with flow diagrams and guidance for adding new types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
149 lines
5.3 KiB
PHP
149 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Exceptions\InvalidTokenScopeException;
|
|
use App\Exceptions\MicropubHandlerException;
|
|
use App\Exceptions\MicropubUnsupportedModelException;
|
|
use App\Http\Requests\MicropubRequest;
|
|
use App\Models\Place;
|
|
use App\Models\SyndicationTarget;
|
|
use App\Services\Micropub\MicropubHandlerRegistry;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Lcobucci\JWT\Token;
|
|
|
|
class MicropubController extends Controller
|
|
{
|
|
protected MicropubHandlerRegistry $handlerRegistry;
|
|
|
|
public function __construct(MicropubHandlerRegistry $handlerRegistry)
|
|
{
|
|
$this->handlerRegistry = $handlerRegistry;
|
|
}
|
|
|
|
/**
|
|
* Respond to a POST request to the micropub endpoint.
|
|
*
|
|
* The request is initially processed by the MicropubRequest form request
|
|
* class. The normalizes the data, so we can pass it into the handlers for
|
|
* the different micropub requests, h-entry or h-card, for example.
|
|
*/
|
|
public function post(MicropubRequest $request): JsonResponse
|
|
{
|
|
$type = $request->getType();
|
|
|
|
if (! $type) {
|
|
return response()->json([
|
|
'error' => 'invalid_request',
|
|
'error_description' => 'Microformat object type is missing, for example: h-entry or h-card',
|
|
], 400);
|
|
}
|
|
|
|
try {
|
|
$handler = $this->handlerRegistry->getHandler($type);
|
|
$dataClass = $handler->dataClass();
|
|
$data = $dataClass::fromArray($request->getMicropubData());
|
|
$result = $handler->handle($data);
|
|
|
|
if ($result['response'] === 'updated') {
|
|
return response()->json([
|
|
'response' => $result['response'],
|
|
], 200)->header('Location', $result['url']);
|
|
}
|
|
|
|
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',
|
|
'error_description' => $e->getMessage(),
|
|
], 400);
|
|
} catch (MicropubHandlerException) {
|
|
return response()->json([
|
|
'error' => 'unsupported_operation',
|
|
'error_description' => 'The request could not be processed by this server',
|
|
], 500);
|
|
} catch (\Exception $e) {
|
|
return response()->json([
|
|
'error' => 'server_error',
|
|
'error_description' => 'An error occurred processing the request',
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Respond to a GET request to the micropub endpoint.
|
|
*
|
|
* 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
|
|
{
|
|
if ($request->input('q') === 'syndicate-to') {
|
|
return response()->json([
|
|
'syndicate-to' => SyndicationTarget::all(),
|
|
]);
|
|
}
|
|
|
|
if ($request->input('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();
|
|
|
|
return response()->json([
|
|
'response' => 'places',
|
|
'places' => $places,
|
|
]);
|
|
}
|
|
|
|
// the default response is just to return the token data
|
|
/** @var Token $tokenData */
|
|
$tokenData = $request->input('token_data');
|
|
|
|
return response()->json([
|
|
'response' => 'token',
|
|
'token' => [
|
|
'me' => $tokenData['me'],
|
|
'scope' => $tokenData['scope'],
|
|
'client_id' => $tokenData['client_id'],
|
|
],
|
|
]);
|
|
}
|
|
}
|