an event is generated with $args = array("view", * "42"); when an event handler asks $event->page_matches("view/{id}"), it returns * true and sets $event->get_arg('id') = "42" */ class PageRequestEvent extends Event { private string $method; public string $path; /** @var array */ public array $GET; /** @var array */ public array $POST; /** * @var string[] */ public array $args; /** * @var array */ private array $named_args = []; public int $page_num; /** * @param string $method The HTTP method used to make the request * @param string $path The path of the request * @param array $get The GET parameters * @param array $post The POST parameters */ public function __construct(string $method, string $path, array $get, array $post) { global $user; parent::__construct(); global $config; $this->method = $method; // if we're looking at the root of the install, // use the default front page if ($path == "") { $path = $config->get_string(SetupConfig::FRONT_PAGE); } $this->path = $path; $this->GET = $get; $this->POST = $post; // break the path into parts $this->args = explode('/', $path); } public function get_GET(string $key): ?string { if (array_key_exists($key, $this->GET)) { if (is_array($this->GET[$key])) { throw new UserError("GET parameter {$key} is an array, expected single value"); } return $this->GET[$key]; } else { return null; } } public function req_GET(string $key): string { $value = $this->get_GET($key); if ($value === null) { throw new UserError("Missing GET parameter {$key}"); } return $value; } public function get_POST(string $key): ?string { if (array_key_exists($key, $this->POST)) { if (is_array($this->POST[$key])) { throw new UserError("POST parameter {$key} is an array, expected single value"); } return $this->POST[$key]; } else { return null; } } public function req_POST(string $key): string { $value = $this->get_POST($key); if ($value === null) { throw new UserError("Missing POST parameter {$key}"); } return $value; } /** * @return string[]|null */ public function get_POST_array(string $key): ?array { if (array_key_exists($key, $this->POST)) { if (!is_array($this->POST[$key])) { throw new UserError("POST parameter {$key} is a single value, expected array"); } return $this->POST[$key]; } else { return null; } } /** * @return string[] */ public function req_POST_array(string $key): array { $value = $this->get_POST_array($key); if ($value === null) { throw new UserError("Missing POST parameter {$key}"); } return $value; } public function page_starts_with(string $name): bool { return str_starts_with($this->path, $name); } /** * Test if the requested path matches a given pattern. * * If it matches, store the remaining path elements in $args */ public function page_matches( string $name, ?string $method = null, ?bool $authed = null, ?string $permission = null, bool $paged = false, ): bool { global $user; if ($paged) { if ($this->page_matches("$name/{page_num}", $method, $authed, $permission, false)) { $pn = $this->get_arg("page_num"); if (is_numberish($pn)) { return true; } } } assert($method === null || in_array($method, ["GET", "POST", "OPTIONS"])); $authed = $authed ?? $method == "POST"; // method check is fast so do that first if ($method !== null && $this->method !== $method) { return false; } // check if the path matches $parts = explode("/", $name); $part_count = count($parts); if ($part_count != count($this->args)) { return false; } $this->named_args = []; for ($i = 0; $i < $part_count; $i++) { if (str_starts_with($parts[$i], "{")) { $this->named_args[substr($parts[$i], 1, -1)] = $this->args[$i]; } elseif ($parts[$i] != $this->args[$i]) { return false; } } // if we matched the method and the path, but the page requires // authentication and the user is not authenticated, then complain if ($authed && !defined("UNITTEST")) { if (!isset($this->POST["auth_token"])) { throw new PermissionDenied("Permission Denied: Missing CSRF Token"); } if ($this->POST["auth_token"] != $user->get_auth_token()) { throw new PermissionDenied("Permission Denied: Invalid CSRF Token (Go back, refresh the page, and try again?)"); } } if ($permission !== null && !$user->can($permission)) { throw new PermissionDenied("Permission Denied: {$user->name} lacks permission {$permission}"); } return true; } /** * Get the n th argument of the page request (if it exists.) */ public function get_arg(string $n, ?string $default = null): string { if (array_key_exists($n, $this->named_args)) { return rawurldecode($this->named_args[$n]); } elseif ($default !== null) { return $default; } else { throw new UserError("Page argument {$n} is missing"); } } public function get_iarg(string $n, ?int $default = null): int { if (array_key_exists($n, $this->named_args)) { if (is_numberish($this->named_args[$n]) === false) { throw new UserError("Page argument {$n} exists but is not numeric"); } return int_escape($this->named_args[$n]); } elseif ($default !== null) { return $default; } else { throw new UserError("Page argument {$n} is missing"); } } } class CliGenEvent extends Event { public function __construct( public \Symfony\Component\Console\Application $app ) { parent::__construct(); } } /** * A signal that some text needs formatting, the event carries * both the text and the result */ class TextFormattingEvent extends Event { /** * For reference */ public string $original; /** * with formatting applied */ public string $formatted; /** * with formatting removed */ public string $stripped; public function __construct(string $text) { parent::__construct(); // We need to escape before formatting, instead of at display time, // because formatters will add their own HTML tags into the mix and // we don't want to escape those. $h_text = html_escape(trim($text)); $this->original = $h_text; $this->formatted = $h_text; $this->stripped = $h_text; } } /** * A signal that something needs logging */ class LogEvent extends Event { /** * a category, normally the extension name */ public string $section; /** * See python... */ public int $priority = 0; /** * Free text to be logged */ public string $message; /** * The time that the event was created */ public int $time; /** * Extra data to be held separate * * @var string[] */ public array $args; public function __construct(string $section, int $priority, string $message) { parent::__construct(); $this->section = $section; $this->priority = $priority; $this->message = $message; $this->time = time(); } } class DatabaseUpgradeEvent extends Event { } /** * @template T */ abstract class PartListBuildingEvent extends Event { /** @var T[] */ private array $parts = []; /** * @param T $html */ public function add_part(mixed $html, int $position = 50): void { while (isset($this->parts[$position])) { $position++; } $this->parts[$position] = $html; } /** * @return array */ public function get_parts(): array { ksort($this->parts); return $this->parts; } }