only checks write-permission (no trip * scope, and any admin passes), which violates R6; and no stock endpoint can * rename media to the photo-NN cover order at all. Both custom routes require the * configured site OWNER and assert the target is a direct child of the active * trip's dailies container — sharing one guard (EntryScopeGuard) with the save * path so the R6 enforcement points cannot diverge (KTD5). * * Custom-in-repo (NOT GPM-managed): tracked via a `!` negation in user/.gitignore * and deployed with the content push, like cache-on-save. Never in plugins.txt. */ class EntryActionsPlugin extends Plugin { public static function getSubscribedEvents(): array { return [ 'onPluginsInitialized' => ['onPluginsInitialized', 0], 'onApiRegisterRoutes' => ['onApiRegisterRoutes', 0], ]; } /** * Register a lazy PSR-4 autoloader for this plugin's classes. The api router * dispatches from a CACHED route map and instantiates the controller directly * (ApiRouter::handleRoute → `new $controllerClass`) WITHOUT re-firing * onApiRegisterRoutes, so requiring the class only there would leave it * unloaded on cached-route requests. Lazy autoloading fires exactly when the * router constructs the controller — by which point the api plugin's own * autoloader (for AbstractApiController) is already registered. */ public function onPluginsInitialized(): void { spl_autoload_register(static function (string $class): void { $prefix = 'Grav\\Plugin\\EntryActions\\'; if (strncmp($class, $prefix, strlen($prefix)) !== 0) { return; } $rel = substr($class, strlen($prefix)); $file = __DIR__ . '/classes/' . str_replace('\\', '/', $rel) . '.php'; if (is_file($file)) { require_once $file; } }); } public function onApiRegisterRoutes(Event $event): void { $routes = $event['routes']; $routes->delete('/entry/{slug}', [EntryActions\EntryActionsApiController::class, 'deleteEntry']); // Reorder an entry's photos to a client-supplied order → rename to // photo-01..NN so the feed cover (media.images|first) follows the drag. // Nested-static-after-param, same shape as the DELETE above — it only // registers once the API route-map cache is rebuilt (deploy must clear cache). $routes->post('/entry/{slug}/photos/order', [EntryActions\EntryActionsApiController::class, 'reorderPhotos']); } }