'(\d{1,10})', '{slug}' => '([a-z0-9-]{1,120})', '{token}' => '([a-f0-9]{64})', '{kind}' => '(attendees|orders|sales)', '{tab}' => '([a-z]{3,20})', ]; /** The regular expression for one route pattern, e.g. "/events/{id}/edit". */ function route_regex(string $pattern): string { return '#^' . strtr(preg_quote($pattern, '#'), array_combine(array_map(fn($k) => preg_quote($k, '#'), array_keys(ROUTE_PARAMS)), ROUTE_PARAMS)) . '$#'; } /** True when $path is one of the registered routes for $method. The allowlist for routes taken from an address. */ function route_known(array $routes, string $method, string $path): bool { foreach ($routes as $route) { if ($route[0] === $method && preg_match(route_regex((string)$route[1]), $path)) return true; } return false; } function dispatch(array $routes, string $path): void { $method = server('REQUEST_METHOD') ?: 'GET'; if ($method === 'HEAD') $method = 'GET'; $path = '/' . trim($path, '/'); $GLOBALS['gatecraft_path'] = $path; $allowed = []; foreach ($routes as [$m, $pattern, $handler, $perm]) { if (!preg_match(route_regex($pattern), $path, $mm)) continue; if ($m !== $method) { $allowed[] = $m; continue; } // 1. Every POST must carry a valid nonce and session token, or the request stops here. The two exceptions are // authenticated another way: the Stripe webhook by its signature (h_stripe_webhook()), the one-click // unsubscribe by the HMAC-signed link (audience_contact_from_link()). if ($method === 'POST' && !in_array($pattern, ['/webhooks/stripe', '/unsubscribe/one-click'], true)) csrf_verify(); // 2. Routes limited to staff check the capability with current_user_can() (require_perm()). if ($perm !== null) require_perm($perm); $args = array_map(fn($v) => ctype_digit($v) ? (int)$v : $v, array_slice($mm, 1)); try { $fn = str_contains($handler, '\\') ? $handler : __NAMESPACE__ . '\\' . $handler; $fn(...$args); } catch (UserError $ex) { if (str_starts_with(server('HTTP_ACCEPT'), 'application/json')) json_out(['ok' => false, 'error' => error_text($ex)], 422); flash('error', $ex->getMessage()); gatecraft_remember_input($path); $back = safe_next(back_target() ?: (wp_parse_url(server('HTTP_REFERER'), PHP_URL_PATH) . (($qs = wp_parse_url(server('HTTP_REFERER'), PHP_URL_QUERY)) ? '?' . $qs : '')), '/'); redirect($back); } return; } if ($allowed) { if (!headers_sent()) header('Allow: ' . implode(', ', array_unique($allowed))); abort(405); } abort(404, 'We could not find that page.'); }