Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Http/Router/APIRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function addRoute(RouteRegister $route): void
$args[] = [
'methods' => $this->getMethod($method),
'callback' => [$route, 'handleRequest'],
'permission_callback' => '__return_true',
'permission_callback' => $route->getPermissionCallback(),
];
}

Expand Down
2 changes: 2 additions & 0 deletions src/Http/Router/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
* @method static RouteBase isTokenIgnored()
* @method static RouteBase ignoreToken()
* @method static RouteBase getMiddleware()
* @method static RouteBase permission(callable $callback)
* @method static RouteBase getPermission()
* @method static RouteBase getRoutePrefix()
* @method static RouteBase group(Closure $callback)
* @method static RouteBase getRouter()
Expand Down
75 changes: 63 additions & 12 deletions src/Http/Router/RouteBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,28 @@ final class RouteBase

private array $_middleware = [];

/**
* REST permission callback shared by every route in this group.
*
* @var null|callable
*/
private $_permission;

/**
* Enclosing group this route or group was declared in; attributes inherit from it.
*/
private ?self $_parent;

private static ?self $_isGrouped = null;

/**
* Captures the group being declared, so nested routes and groups inherit from it.
*/
public function __construct()
{
$this->_parent = self::$_isGrouped;
}

/**
* Handle static call to route.
*
Expand Down Expand Up @@ -105,7 +125,7 @@ public function noAuth(): self
*/
public function isNoAuth(): ?bool
{
return $this->_noAuth;
return $this->_noAuth ?? $this->_parent?->isNoAuth();
}

/**
Expand All @@ -115,7 +135,7 @@ public function isNoAuth(): ?bool
*/
public function isTokenIgnored(): ?bool
{
return $this->_ignoreToken;
return $this->_ignoreToken ?? $this->_parent?->isTokenIgnored();
}

/**
Expand Down Expand Up @@ -149,7 +169,29 @@ public function middleware(): self
*/
public function getMiddleware(): array
{
return $this->_middleware;
return array_merge($this->_parent?->getMiddleware() ?? [], $this->_middleware);
}

/**
* Sets the REST permission callback for every route in this group.
*
* @param callable $callback receives the WP_REST_Request; return true, false or WP_Error
*
* @return RouteBase
*/
public function permission(callable $callback): self
{
$this->_permission = $callback;

return $this;
}

/**
* Returns the group's REST permission callback, if any.
*/
public function getPermission(): ?callable
{
return $this->_permission ?? $this->_parent?->getPermission();
}

/**
Expand All @@ -159,7 +201,16 @@ public function getMiddleware(): array
*/
public function getRoutePrefix()
{
return $this->_prefix;
$parentPrefix = $this->_parent?->getRoutePrefix();
if ($parentPrefix === null || $parentPrefix === '') {
return $this->_prefix;
}

if ($this->_prefix === null || $this->_prefix === '') {
return $parentPrefix;
}

return rtrim($parentPrefix, '/') . '/' . ltrim($this->_prefix, '/');
}

/**
Expand All @@ -171,9 +222,14 @@ public function getRoutePrefix()
*/
public function group(Closure $callback): self
{
$enclosing = self::$_isGrouped;
self::$_isGrouped = $this;
$callback();
self::$_isGrouped = null;

try {
$callback();
} finally {
self::$_isGrouped = $enclosing;
}

return $this;
}
Expand All @@ -199,11 +255,6 @@ public function getRouter()
*/
private function getRegistrar(): RouteRegister
{
$instance = $this;
if (!\is_null(self::$_isGrouped)) {
$instance = self::$_isGrouped;
}

return new RouteRegister($instance);
return new RouteRegister($this);
}
}
27 changes: 27 additions & 0 deletions src/Http/Router/RouteRegister.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ final class RouteRegister

private array $_middleware = [];

/**
* REST permission callback for this route; falls back to the group's.
*
* @var null|callable
*/
private $_permission;

/**
* Instance of rest request.
*
Expand Down Expand Up @@ -156,6 +163,26 @@ public function middleware(): self
return $this;
}

/**
* Sets the REST permission callback for this route.
*
* @param callable $callback receives the WP_REST_Request; return true, false or WP_Error
*/
public function permission(callable $callback): self
{
$this->_permission = $callback;

return $this;
}

/**
* REST permission callback: the route's own, else its group's, else public.
*/
public function getPermissionCallback(): callable
{
return $this->_permission ?? $this->_routeBase->getPermission() ?? '__return_true';
}

public function handleMiddleware(): bool
{
try {
Expand Down
52 changes: 52 additions & 0 deletions tests/Router/RouteDefinitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,58 @@ public function testRouteDefinitionGroupsApplySharedPrefixAndMiddleware(): void
assertSameValue(['auth'], $route->getMiddleware(), 'group middleware was not inherited');
}

public function testRouteDefinitionRouteAttributesInsideAGroupAreKept(): void
{
$router = new Router('api', 'example', 'v1');
$permission = fn () => false;

Route::prefix('demo')->group(function () use ($permission) {
Route::permission($permission)->middleware('audit')->get('entries', fn () => 'ok');
});

$route = $router->getRoute(0);
assertSameValue('demo', $route->getRoutePrefix(), 'group prefix was lost');
assertSameValue($permission, $route->getPermissionCallback(), 'route permission inside a group was dropped');
assertSameValue(['audit'], $route->getMiddleware(), 'route middleware inside a group was dropped');
}

public function testRouteDefinitionNestedGroupsInheritFromTheirParent(): void
{
$router = new Router('api', 'example', 'v1');
$permission = fn () => false;

Route::permission($permission)->prefix('admin')->middleware('auth')->group(function () {
Route::prefix('inner')->middleware('audit')->group(function () {
Route::get('nested', fn () => 'ok');
});
Route::get('sibling', fn () => 'ok');
});

$nested = $router->getRoute(0);
assertSameValue('admin/inner', $nested->getRoutePrefix(), 'nested prefix not joined to the parent');
assertSameValue($permission, $nested->getPermissionCallback(), 'nested group lost the parent permission');
assertSameValue(['auth', 'audit'], $nested->getMiddleware(), 'nested group lost the parent middleware');

$sibling = $router->getRoute(1);
assertSameValue('admin', $sibling->getRoutePrefix(), 'outer group not restored after a nested group');
assertSameValue($permission, $sibling->getPermissionCallback(), 'outer permission not restored after a nested group');
}

public function testRouteDefinitionGroupStateIsClearedWhenTheCallbackThrows(): void
{
$router = new Router('api', 'example', 'v1');

try {
Route::prefix('broken')->group(function () {
throw new \RuntimeException('boom');
});
} catch (\RuntimeException) {
}
Route::get('after', fn () => 'ok');

assertSameValue(null, $router->getRoute(0)->getRoutePrefix(), 'group leaked past a thrown callback');
}

public function testRouteDefinitionPlaceholdersExposeRegexMetadata(): void
{
new Router('ajax', 'example', null);
Expand Down
35 changes: 35 additions & 0 deletions tests/Router/TransportRegistrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ public function testRESTTransportRouteRegistrationPreservesNamespaceVersionPathA
assertTest(\is_callable($registration['args'][0]['permission_callback']), 'REST permission callback is not callable');
}

public function testRESTTransportDefaultsToAPublicPermissionCallback(): void
{
$route = (new RouteBase())->get('entries', fn () => 'ok');

(new APIRouter(new Router('api', 'example', 'v1')))->addRoute($route);

assertSameValue('__return_true', WpKitTestState::$restRoutes[0]['args'][0]['permission_callback'], 'default permission changed');
}

public function testRESTTransportUsesTheGroupPermissionCallback(): void
{
$permission = fn () => false;
$group = (new RouteBase())->permission($permission);
$route = $group->match('GET,POST', 'entries', fn () => 'ok');

(new APIRouter(new Router('api', 'example', 'v1')))->addRoute($route);

foreach (WpKitTestState::$restRoutes[0]['args'] as $args) {
assertSameValue($permission, $args['permission_callback'], 'group permission not applied to every method');
}
}

public function testRESTTransportRoutePermissionOverridesTheGroup(): void
{
$routePermission = fn () => true;
$route = (new RouteBase())
->permission(fn () => false)
->get('entries', fn () => 'ok')
->permission($routePermission);

(new APIRouter(new Router('api', 'example', 'v1')))->addRoute($route);

assertSameValue($routePermission, WpKitTestState::$restRoutes[0]['args'][0]['permission_callback'], 'route permission did not win');
}

public function testRESTTransportWordPressMethodConstantsRemainMapped(): void
{
$router = new Router('api', 'example', 'v1');
Expand Down
Loading