luci-base: dispatcher; improve wildcard routing

When a menu JSON describes an endpoint like

 "admin/app/edit/*" : { ...

and the user navigates to

 admin/app/edit/

instead of the URI which supplies an ID to edit, like

 admin/app/edit/myfoobarthing

we now can use 'alias' and 'rewrite' to redirect
transparently for more generic endpoints.
Without this, it's possible to navigate to

 admin/app/edit/

and the corresponding view does not receive a suitable
path/ID to derive data from, when views use anything
derived via L.env.requestpath.

This menu JSON

  "admin/app/entry/*": {
    "action": {
      "type": "view",
      "path": "app/entry"
    }
  },

  "admin/app/entries": {
    "title": "entries",
    "order": 5,
    "action": {
      "type": "view",
      "path": "app/entries"
    }
  },

  "admin/app/entry": {
    "action": {
      "type": "alias",
      "path": "admin/app/entries"
    }
  },

Produces JSON with a wildcardaction element

  "entry":
  {
    "satisfied": true,
    "wildcard": true,
    "action":
    {
      "type": "alias",
      "path": "admin/app/entries"
    },
    "wildcardaction":
    {
      "type": "view",
      "path": "app/entry"
    }
  },
  "entries":
  {
    "satisfied": true,
    "action":
    {
      "type": "view",
      "path": "app/entries"
    },
    "order": 5,
    "title": "entries"
  },

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
This commit is contained in:
Paul Donald
2026-01-17 20:18:37 +01:00
parent fddc70391f
commit df90c60a72

View File

@@ -388,10 +388,12 @@ function build_pagetree() {
for (let path, spec in data) {
if (type(spec) == 'object') {
let node = tree;
let has_wildcard = false;
for (let s in match(path, /[^\/]+/g)) {
if (s[0] == '*') {
node.wildcard = true;
has_wildcard = true;
break;
}
@@ -405,6 +407,12 @@ function build_pagetree() {
if (type(spec[k]) == t)
node[k] = spec[k];
/* Preserve distinct actions for wildcard vs. base path */
if (has_wildcard && type(spec.action) == 'object')
node.wildcardaction = spec.action;
else if (type(spec.action) == 'object')
node.action = spec.action;
node.satisfied = check_depends(spec);
}
}
@@ -635,16 +643,27 @@ function resolve_page(tree, request_path) {
if (!login && node.auth?.login)
login = true;
/* If this node is marked as wildcard, check if the next segment
* matches a child node. Only apply wildcard behaviour (capturing
* remaining segments as args) if no child matches, allowing
* deeper routes like foo/bar/* to work alongside
* foo/*
*/
if (node.wildcard) {
ctx.request_args = [];
ctx.request_path = ctx.path ? [ ...ctx.path ] : [];
let next_segment = request_path[i + 1];
let has_matching_child = next_segment && node.children?.[next_segment]?.satisfied;
while (++i < length(request_path)) {
push(ctx.request_path, request_path[i]);
push(ctx.request_args, request_path[i]);
if (!has_matching_child) {
ctx.request_args = [];
ctx.request_path = ctx.path ? [ ...ctx.path ] : [];
while (++i < length(request_path)) {
push(ctx.request_path, request_path[i]);
push(ctx.request_args, request_path[i]);
}
break;
}
break;
}
}
@@ -986,6 +1005,11 @@ dispatch = function(_http, path) {
let action = resolved.node.action;
/* If this node matched a wildcard and we have request args,
* prefer the wildcard-specific action when defined. */
if (length(resolved.ctx.request_args) && type(resolved.node.wildcardaction) == 'object')
action = resolved.node.wildcardaction;
if (action?.type == 'arcombine')
action = length(resolved.ctx.request_args) ? action.targets?.[1] : action.targets?.[0];