API / Minn\Http

Minn\Http

Request, response, routing, and the outgoing client. 37 classes, 122 public methods and 34 internals. This page is the public surface; each class name opens the whole class.

Access enum

public/minn/src/Minn/Http/Access.php · 17 lines · Open the class →

Who a route is for. The six answers every route gives, so the authorization surface of the engine reads as a list of these.

Cases: Public, SignedIn, Cap, Floor, Own, Type, Taxonomy

Args final class

public/minn/src/Minn/Http/Args.php · 149 lines · Open the class →

The parameters a route accepts, as the reference describes them in the REST index: name => {description, type, ...}. The descriptions, types, enums and bounds were captured from the reference, so a client that reads either index is told the same thing, and the router refuses a value the way the reference refuses it, before it judges the caller.

A route declares only what it really reads. An argument published here that the handler ignores would be worse than none at all, because a client reading the index would build a request around it, so each set below names the code that consumes it. The sets keep the reference's order, because a refusal lists the invalid parameters in that order. `_fields` and `_embed` are read everywhere and declared nowhere, as the reference lists neither and validates neither.

constSHARED = array ( 0 => 'context', 1 => 'page', 2 => 'per_page', 3 => 'search', )

The shared collection parameters, judged first: the reference answers a refusal among these alone, and reads the route's own parameters only once these pass.

constHANDLER_VALIDATES = 'handler_validates'

The key an argument carries when its handler judges it (a status the caller may not read is refused before the enum is).

constCONTEXT = array ( 'context' => array ( 'description' => 'Scope under which the request is made; determines fields present in response.', 'type' => 'string', 'enum' => array ( 0 => 'view', 1 => 'embed', 2 => 'edit', ), 'default' => 'view', 'required' => false, ), )

Read by Rest\Context::of(): which view of a resource is wanted.

constFORCE = array ( 'force' => array ( 'description' => 'Whether to bypass Trash and force deletion.', 'type' => 'boolean', 'default' => false, 'required' => false, ), )

Read by Content\PostWriter: whether a delete bypasses the trash.

constUSERS = array ( 'page' => array ( 'description' => 'Current page of the collection.', 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'required' => false, ), 'per_page' => array ( 'description' => 'Maximum number of items to be returned in result set.', 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 100, 'required' => false, ), 'search' => array ( 'description' => 'Limit results to those matching a string.', 'type' => 'string', 'required' => false, ), 'exclude' => array ( 'description' => 'Ensure result set excludes specific IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'default' => array ( ), 'required' => false, ), 'include' => array ( 'description' => 'Limit result set to specific IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'default' => array ( ), 'required' => false, ), 'offset' => array ( 'description' => 'Offset the result set by a specific number of items.', 'type' => 'integer', 'required' => false, ), 'order' => array ( 'default' => 'asc', 'description' => 'Order sort attribute ascending or descending.', 'enum' => array ( 0 => 'asc', 1 => 'desc', ), 'type' => 'string', 'required' => false, ), 'orderby' => array ( 'default' => 'name', 'description' => 'Sort collection by user attribute.', 'enum' => array ( 0 => 'id', 1 => 'include', 2 => 'name', 3 => 'registered_date', 4 => 'slug', 5 => 'include_slugs', 6 => 'email', 7 => 'url', ), 'type' => 'string', 'required' => false, ), 'slug' => array ( 'description' => 'Limit result set to users with one or more specific slugs.', 'type' => 'array', 'items' => array ( 'type' => 'string', ), 'required' => false, ), 'roles' => array ( 'description' => 'Limit result set to users matching at least one specific role provided. Accepts csv list or single role.', 'type' => 'array', 'items' => array ( 'type' => 'string', ), 'required' => false, ), 'capabilities' => array ( 'description' => 'Limit result set to users matching at least one specific capability provided. Accepts csv list or single capability.', 'type' => 'array', 'items' => array ( 'type' => 'string', ), 'required' => false, ), 'who' => array ( 'description' => 'Limit result set to users who are considered authors.', 'type' => 'string', 'enum' => array ( 0 => 'authors', ), 'required' => false, ), 'has_published_posts' => array ( 'description' => 'Limit result set to users who have published posts.', 'type' => array ( 0 => 'boolean', 1 => 'array', ), 'items' => array ( 'type' => 'string', 'enum' => array ( ), ), 'required' => false, ), 'search_columns' => array ( 'default' => array ( ), 'description' => 'Array of column names to be searched.', 'type' => 'array', 'items' => array ( 'enum' => array ( 0 => 'email', 1 => 'name', 2 => 'id', 3 => 'username', 4 => 'slug', ), 'type' => 'string', ), 'required' => false, ), )

Read by Rest\UserCollectionParams: the user collection.

constTERMS = array ( 'page' => array ( 'description' => 'Current page of the collection.', 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'required' => false, ), 'per_page' => array ( 'description' => 'Maximum number of items to be returned in result set.', 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 100, 'required' => false, ), 'search' => array ( 'description' => 'Limit results to those matching a string.', 'type' => 'string', 'required' => false, ), 'exclude' => array ( 'description' => 'Ensure result set excludes specific IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'default' => array ( ), 'required' => false, ), 'include' => array ( 'description' => 'Limit result set to specific IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'default' => array ( ), 'required' => false, ), 'offset' => array ( 'description' => 'Offset the result set by a specific number of items.', 'type' => 'integer', 'required' => false, ), 'order' => array ( 'description' => 'Order sort attribute ascending or descending.', 'type' => 'string', 'default' => 'asc', 'enum' => array ( 0 => 'asc', 1 => 'desc', ), 'required' => false, ), 'orderby' => array ( 'description' => 'Sort collection by term attribute.', 'type' => 'string', 'default' => 'name', 'enum' => array ( 0 => 'id', 1 => 'include', 2 => 'name', 3 => 'slug', 4 => 'include_slugs', 5 => 'term_group', 6 => 'description', 7 => 'count', ), 'required' => false, ), 'hide_empty' => array ( 'description' => 'Whether to hide terms not assigned to any posts.', 'type' => 'boolean', 'default' => false, 'required' => false, ), 'parent' => array ( 'description' => 'Limit result set to terms assigned to a specific parent.', 'type' => 'integer', 'required' => false, ), 'post' => array ( 'description' => 'Limit result set to terms assigned to a specific post.', 'type' => 'integer', 'default' => NULL, 'required' => false, ), 'slug' => array ( 'description' => 'Limit result set to terms with one or more specific slugs.', 'type' => 'array', 'items' => array ( 'type' => 'string', ), 'required' => false, ), )

Read by Rest\TermCollectionParams: a term collection, either shape.

constCOMMENTS = array ( 'page' => array ( 'description' => 'Current page of the collection.', 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'required' => false, ), 'per_page' => array ( 'description' => 'Maximum number of items to be returned in result set.', 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 100, 'required' => false, ), 'search' => array ( 'description' => 'Limit results to those matching a string.', 'type' => 'string', 'required' => false, ), 'after' => array ( 'description' => 'Limit response to comments published after a given ISO8601 compliant date.', 'type' => 'string', 'format' => 'date-time', 'required' => false, ), 'author' => array ( 'description' => 'Limit result set to comments assigned to specific user IDs. Requires authorization.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'required' => false, ), 'author_exclude' => array ( 'description' => 'Ensure result set excludes comments assigned to specific user IDs. Requires authorization.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'required' => false, ), 'author_email' => array ( 'default' => NULL, 'description' => 'Limit result set to that from a specific author email. Requires authorization.', 'format' => 'email', 'type' => 'string', 'required' => false, ), 'before' => array ( 'description' => 'Limit response to comments published before a given ISO8601 compliant date.', 'type' => 'string', 'format' => 'date-time', 'required' => false, ), 'exclude' => array ( 'description' => 'Ensure result set excludes specific IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'default' => array ( ), 'required' => false, ), 'include' => array ( 'description' => 'Limit result set to specific IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'default' => array ( ), 'required' => false, ), 'offset' => array ( 'description' => 'Offset the result set by a specific number of items.', 'type' => 'integer', 'required' => false, ), 'order' => array ( 'description' => 'Order sort attribute ascending or descending.', 'type' => 'string', 'default' => 'desc', 'enum' => array ( 0 => 'asc', 1 => 'desc', ), 'required' => false, ), 'orderby' => array ( 'description' => 'Sort collection by comment attribute.', 'type' => 'string', 'default' => 'date_gmt', 'enum' => array ( 0 => 'date', 1 => 'date_gmt', 2 => 'id', 3 => 'include', 4 => 'post', 5 => 'parent', 6 => 'type', ), 'required' => false, ), 'parent' => array ( 'default' => array ( ), 'description' => 'Limit result set to comments of specific parent IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'required' => false, ), 'parent_exclude' => array ( 'default' => array ( ), 'description' => 'Ensure result set excludes specific parent IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'required' => false, ), 'post' => array ( 'default' => array ( ), 'description' => 'Limit result set to comments assigned to specific post IDs.', 'type' => 'array', 'items' => array ( 'type' => 'integer', ), 'required' => false, ), 'status' => array ( 'default' => 'approve', 'description' => 'Limit result set to comments assigned a specific status. Requires authorization.', 'type' => 'string', 'required' => false, ), 'type' => array ( 'default' => 'comment', 'description' => 'Limit result set to comments assigned a specific type. Requires authorization.', 'type' => 'string', 'required' => false, ), 'password' => array ( 'description' => 'The password for the post if it is password protected.', 'type' => 'string', 'required' => false, ), )

Read by Rest\CommentsController::list(): the comment collection.

constUSER_CREATE = array ( 'username' => array ( 'description' => 'Login name for the user.', 'type' => 'string', 'required' => true, ), 'name' => array ( 'description' => 'Display name for the user.', 'type' => 'string', 'required' => false, ), 'first_name' => array ( 'description' => 'First name for the user.', 'type' => 'string', 'required' => false, ), 'last_name' => array ( 'description' => 'Last name for the user.', 'type' => 'string', 'required' => false, ), 'email' => array ( 'description' => 'The email address for the user.', 'type' => 'string', 'format' => 'email', 'required' => true, ), 'url' => array ( 'description' => 'URL of the user.', 'type' => 'string', 'format' => 'uri', 'required' => false, ), 'description' => array ( 'description' => 'Description of the user.', 'type' => 'string', 'required' => false, ), 'locale' => array ( 'description' => 'Locale for the user.', 'type' => 'string', 'required' => false, ), 'nickname' => array ( 'description' => 'The nickname for the user.', 'type' => 'string', 'required' => false, ), 'roles' => array ( 'description' => 'Roles assigned to the user.', 'type' => 'array', 'items' => array ( 'type' => 'string', ), 'required' => false, ), 'password' => array ( 'description' => 'Password for the user (never included).', 'type' => 'string', 'required' => true, ), )

Read by Rest\UsersController::create(): the body of a user create. The locale takes any string here; the reference's enum is the site's installed languages, which is not a constant.

constUSER_EDIT = array ( 'name' => array ( 'description' => 'Display name for the user.', 'type' => 'string', 'required' => false, ), 'first_name' => array ( 'description' => 'First name for the user.', 'type' => 'string', 'required' => false, ), 'last_name' => array ( 'description' => 'Last name for the user.', 'type' => 'string', 'required' => false, ), 'email' => array ( 'description' => 'The email address for the user.', 'type' => 'string', 'format' => 'email', 'required' => false, ), 'url' => array ( 'description' => 'URL of the user.', 'type' => 'string', 'format' => 'uri', 'required' => false, ), 'description' => array ( 'description' => 'Description of the user.', 'type' => 'string', 'required' => false, ), 'locale' => array ( 'description' => 'Locale for the user.', 'type' => 'string', 'required' => false, ), 'nickname' => array ( 'description' => 'The nickname for the user.', 'type' => 'string', 'required' => false, ), 'slug' => array ( 'description' => 'An alphanumeric identifier for the user.', 'type' => 'string', 'required' => false, ), 'roles' => array ( 'description' => 'Roles assigned to the user.', 'type' => 'array', 'items' => array ( 'type' => 'string', ), 'required' => false, ), 'password' => array ( 'description' => 'Password for the user (never included).', 'type' => 'string', 'required' => false, ), 'meta' => array ( 'description' => 'Meta fields.', 'type' => 'object', 'properties' => array ( 'show_admin_bar_front' => array ( 'type' => 'string', 'default' => 'true', ), ), 'required' => false, ), )

Read by Rest\UsersController::update(): the body of a user edit; nothing is required.

constUSER_DELETE = array ( 'force' => array ( 'type' => 'boolean', 'default' => false, 'description' => 'Required to be true, as users do not support trashing.', 'required' => false, ), 'reassign' => array ( 'type' => 'integer', 'description' => 'Reassign the deleted user\'s posts and links to this user ID.', 'required' => true, 'handler_validates' => true, ), )

Read by Rest\UsersController::delete(): the reassignment a user delete requires, and the force it insists on.

constAPPLICATION_PASSWORD = array ( 'app_id' => array ( 'description' => 'A UUID provided by the application to uniquely identify it. It is recommended to use an UUID v5 with the URL or DNS namespace.', 'type' => 'string', 'oneOf' => array ( 0 => array ( 'type' => 'string', 'format' => 'uuid', ), 1 => array ( 'type' => 'string', 'enum' => array ( 0 => '', ), ), ), 'required' => false, ), 'name' => array ( 'description' => 'The name of the application password.', 'type' => 'string', 'minLength' => 1, 'pattern' => '.*\\S.*', 'required' => true, ), )

Read by Rest\ApplicationPasswordsController::create(): the body of a new application password.

constSEARCH = array ( 'context' => array ( 'description' => 'Scope under which the request is made; determines fields present in response.', 'type' => 'string', 'enum' => array ( 0 => 'view', 1 => 'embed', ), 'default' => 'view', 'required' => false, ), 'page' => array ( 'description' => 'Current page of the collection.', 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'required' => false, ), 'per_page' => array ( 'description' => 'Maximum number of items to be returned in result set.', 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 100, 'required' => false, ), 'search' => array ( 'description' => 'Limit results to those matching a string.', 'type' => 'string', 'required' => false, ), 'type' => array ( 'default' => 'post', 'description' => 'Limit results to items of an object type.', 'type' => 'string', 'enum' => array ( 0 => 'post', 1 => 'term', 2 => 'post-format', ), 'required' => false, 'handler_validates' => true, ), 'subtype' => array ( 'default' => 'any', 'description' => 'Limit results to items of one or more object subtypes.', 'type' => 'array', 'items' => array ( 'enum' => array ( 0 => 'post', 1 => 'page', 2 => 'category', 3 => 'post_tag', 4 => 'any', ), 'type' => 'string', ), 'required' => false, 'handler_validates' => true, ), )

Read by Rest\SearchController::list(): the search collection, whose context has no edit view.

staticmerge(array $sets): array

The sets merged into one map, as a route's endpoint publishes them.

@param list<array<string, array<string, mixed>>> $sets
@return array<string, array<string, mixed>>

CertificateName final class

public/minn/src/Minn/Http/CertificateName.php · 56 lines · Open the class →

Whether a TLS certificate names a host: a wildcard only as a whole first label with at least two labels after it, matching exactly one label; an IP address never matches by name; the subjectAltName DNS entries win over the common name when present.

staticvalid(string $reference): bool

Whether a certificate's reference name is one a host can match.

staticmatches(string $host, string $reference): bool

Whether the host matches the reference name.

staticcertificateMatches(string $host, array $certificate): bool

Whether a parsed certificate (openssl_x509_parse) names the host.

@param array<string, mixed> $certificate

CookieText final class

public/minn/src/Minn/Http/CookieText.php · 94 lines · Open the class →

Set-Cookie text and the matching rules a cookie jar applies: parsing a header into name, value and attributes; normalizing attributes (expires and max-age as timestamps, the domain without its leading dot); whether a cookie belongs to a domain and a path (RFC 6265).

staticparse(string $header, string $name = ''): array

A Set-Cookie value split up. With a name given, the first part is all value; a first part without "=" is a value with an empty name.

@return array{name: string, value: string, attributes: array<string, string|true>}
staticnormalizeAttribute(string $name, mixed $value, int $referenceTime): mixed

One attribute normalized, or null when it should go: dates become timestamps, the domain loses its leading dot.

statichostMatches(?string $cookieDomain, string $domain): bool

Whether a host-only cookie with this domain attribute (none means any) belongs to the domain: the same text only.

staticdomainMatches(?string $cookieDomain, string $domain): bool

Whether a cookie with this domain attribute (none means any) belongs to the domain or one of its subdomains (never an IP address).

staticpathMatches(?string $cookiePath, string $requestPath): bool

Whether a cookie with this path attribute (none means any) is sent for the request path.

staticdefaultPath(string $requestPath): string

The path a cookie without one gets: the request path up to its last slash, or "/".

Destination final readonly class

public/minn/src/Minn/Http/Destination.php · 102 lines · Open the class →

Where Minn\Http lets a request go. Only http and https; when hosts are named, every hop must start with one of them (and with one of the client's own, when it is bound); and an address outside the public internet (loopback, private and shared ranges, link-local, cloud metadata, reserved) is refused unless its host is listed in $private. A name is resolved here and curl is pinned to the addresses that were checked, so DNS cannot answer one way to the check and another to the connection.

new__construct(array $hosts = array ( ), array $private = array ( ), ?Closure $lookup = NULL, array $within = array ( ))
@param list<string> $hosts URL prefixes every hop must start with; none means any public host
@param list<string> $private host names or addresses allowed to reach a private address
@param (Closure(string): list<string>)|null $lookup a host's addresses; DNS when null
@param list<string> $within URL prefixes the client itself is bound to (an extension's needs.http); none means unbound
methodrefusal(string $url): ?string

Why $url may not be requested, or null when nothing about the URL itself refuses it (a name is judged again once resolved).

methodpinned(Minn\Http\Outbound $request): Minn\Http\Outbound|Minn\Http\Exchange

The request with curl pinned to the addresses its host resolves to, or the failed exchange when the name does not resolve or resolves somewhere private it was not allowed.

statichost(string $url): string

A URL's host, lower-cased, without the brackets around an IPv6 address.

insidestartsWithOne(), allowed(), dns()

DiePage final class

public/minn/src/Minn/Http/DiePage.php · 126 lines · Open the class →

The page wp_die shows a browser, as the reference draws it (probe functions-remaining): its stylesheet word for word, the html element's language (or dir='rtl' for a right-to-left message, which also sets a Tahoma font), the charset, the robots tag, the title and the message as given, then any links. The caller escapes what needs escaping. The stylesheet is kept here with four spaces for each of its tabs.

statichtml(string $languageAttributes, string $direction, string $charset, string $robots, string $title, string $message, string $links): string

The whole page: $languageAttributes go inside <html> unless the direction is 'rtl'; $robots is the printed robots tag.

insidestyles()

Download final readonly class

public/minn/src/Minn/Http/Download.php · 36 lines · Open the class →

A file the engine fetches for itself (a package, a language pack), over its Minn\Http client with the rules a download needs: https at every hop, one of the caller's host prefixes when given, no private address, and a body over the cap fails rather than being truncated. The messages are written for the person who asked for the download.

new__construct(Minn\Http $http = \Minn\Http::__set_state(array( 'within' => array ( ), 'fake' => NULL, 'refusal' => NULL, )))
methodhttps(string $url, int $maxBytes, array $hostPrefixes = array ( ), string $userAgent = 'Minn Engine'): string

The body at $url, following at most five redirects.

@param list<string> $hostPrefixes URL prefixes every hop must start with (none: any https host)

Envelope interface

public/minn/src/Minn/Http/Envelope.php · 9 lines · Open the class →

What runs around a matched route once the router knows the route takes the request: the place a host lets other code refuse, answer or edit it (the runtime's REST server filters). The router hands over the argument check's refusal and the policy's, unthrown, because that code may clear the one and must never run the handler past the other. Without an envelope the router throws them itself.

methodaround(Minn\Http\Matched $matched, Minn\Http\Request $request, ?Minn\RestError $invalid, ?Minn\RestError $refusal, Closure $invoke): Minn\Http\Response

The answer to a matched request, refusals included.

@param Closure(): Response $invoke the route's handler; it may throw RestError

Exchange final readonly class

public/minn/src/Minn/Http/Exchange.php · 74 lines · Open the class →

What came back: the final response's status, headers (repeats as lists), Set-Cookie values, and body, or the transport error.

new__construct(int $code, array $headers, array $cookies, string $body, ?string $error = NULL, array $head = array ( ), int $errno = 0, string $url = '')
@param array<string, string|list<string>> $headers names lower-cased
@param list<string> $cookies raw Set-Cookie header values
@param list<string> $head the final response's status line and header lines as they came
propreadonly int $code
propreadonly array $headers
propreadonly array $cookies
propreadonly string $body
propreadonly ?string $error
propreadonly array $head
propreadonly int $errno
propreadonly string $url
staticfailure(string $error, int $errno, string $url): self

An exchange in which no response arrived.

methodfailed(): bool

Whether the transport failed before any status came back.

methodok(): bool

A response arrived and it was a 2xx.

methodjson(): mixed

The body decoded as JSON, or null when it is not JSON.

methodheader(string $name): ?string

One header by any spelling of its name; a repeated header's values joined with ", ".

methodcookie(string $name): ?string

One cookie's value from the Set-Cookie headers, percent-decoded; the last one set wins.

methodthrow(): self

This exchange when it is ok(), otherwise a RequestFailed exception (a RuntimeException) carrying it, for callers who would rather catch than check.

Failure final class

public/minn/src/Minn/Http/Failure.php · 207 lines · Open the class →

What the public sees when the engine cannot answer: a plain page with no detail, while the detail goes to the log. Installed once per request, it sets PHP's error settings from the debug constants (display stays off unless WP_DEBUG and WP_DEBUG_DISPLAY ask for it) and catches the fatal errors PHP would otherwise print.

staticarmRecovery(): void

Opens the window in which a failure is an extension's fault: while the plugins and the theme's functions file load and the boot actions fire, code that fails fails for every visitor, so pausing it heals the site. A failure after that (a route, a shortcode, a template) answers to one request's input and is never grounds for a pause; it is logged and the request ends in the error page.

staticdisarmRecovery(): void

Closes the window armRecovery() opened.

staticonFatal(callable $recorder): void

What to do with a fatal beyond showing the page: the engine records the extension it came from so the next request loads without it. Installed once the runtime is up, since it needs the database.

@param callable(array{type:int,file:string,line:int,message:string}): void $recorder
staticinstall(array $constants, array $server): void

Sets PHP's error settings from the debug constants as the reference's boot does (Runtime\DebugMode), and installs the error handlers. With WP_DEBUG off, or for a request that answers a machine, errors never display. In the first case the reference leaves php.ini's setting; the engine never shows a visitor PHP's errors. Detail is shown only while display_errors is on.

@param array<string, mixed> $constants the debug and request constants, by name
@param array<string, mixed> $server the request's server variables
staticreport(Throwable $error): Minn\Http\Response

Logs the cause; the page says only that something went wrong.

staticreportJson(Throwable $error): Minn\Http\Response

The same, for a request that asked for JSON: a client that speaks REST is answered in REST, never handed the HTML error page.

staticinternal(): Minn\Http\Response

The 500 page.

staticdatabaseUnavailable(): Minn\Http\Response

The 503 page the reference shows when the database cannot be reached.

staticmaintenance(): Minn\Http\Response

The 503 page while maintenance mode holds; monitors read the status, the retry hint, and the title.

staticdetailed(string $class, string $message, string $file, int $line): Minn\Http\Response

The same page with the cause on it, for a site that asked to see errors. Only ever reached when WP_DEBUG_DISPLAY (or WP_DEBUG) is on: a site that has not asked never learns this much from a response.

insidediscardOutput(), note(), record(), dropin(), page()

Fake final class

public/minn/src/Minn/Http/Fake.php · 94 lines · Open the class →

Answers requests in place of the network, and keeps what was sent. A test makes one and hands its client to the code it tests:

$fake = new Fake(['api.example.com/*' => ['ok' => true]]); $result = (new StatusCheck($fake->http()))->run(); $fake->sent('api.example.com/status'); // the requests that went there

Only that client is answered, so there is nothing to put back and nothing else in the process sees the fake.

new__construct(array $answers = array ( ))

Keys are URL patterns (* matches anything; the scheme and query may be left off); values are an array (sent as JSON), a string (the body), a status code, an Exchange from reply(), or a closure taking the Outbound and returning one of those. A request no pattern matches fails as if nothing were listening.

@param array<string, mixed> $answers
methodhttp(array $within = array ( )): Minn\Http

A client this fake answers, bound to $within as an extension's is by its needs.http. The destination rules still run, but names are not resolved.

@param list<string> $within
methodanswer(Minn\Http\Outbound $request): Minn\Http\Exchange

The answer for one request, which is recorded as sent.

methodsent(string $pattern = '*'): array

The requests sent so far, oldest first; with a pattern, only those whose URL matches it.

@return list<Outbound>
staticreply(array|string $body = '', int $status = 200, array $headers = array ( )): Minn\Http\Exchange

A response made by hand, for a fake to answer with. An array body is sent as JSON.

@param array<string, string|list<string>> $headers name => value; Set-Cookie values become cookies
staticmatches(string $pattern, string $url): bool

Whether a URL matches a pattern in which * stands for anything; the scheme and the query string may be left off.

insideexchange()

Ipv6 final class

public/minn/src/Minn/Http/Ipv6.php · 62 lines · Open the class →

IPv6 addresses as text: written out in full (eight groups, or six and an IPv4 tail), compressed ("::" for the longest run of zero groups, the first when two tie), and checked. Zone suffixes ("%eth0") do not pass.

staticexpand(string $ip): string

The address with "::" expanded into zero groups.

staticcompress(string $ip): string

The address with leading zeros dropped from all-digit groups and the longest zero run as "::".

staticvalid(string $ip): bool

Whether the text is an IPv6 address (with an optional IPv4 tail).

IriParts final readonly class

public/minn/src/Minn/Http/IriParts.php · 138 lines · Open the class →

An IRI (a URL that may carry non-ASCII text) split into its parts and written back normalized: scheme and host in lower case, a scheme's default port dropped, dot segments removed, and every character a URL may not hold percent-encoded. The IRI form keeps non-ASCII characters; the URI form encodes them too.

new__construct(?string $scheme, ?string $userinfo, ?string $host, ?int $port, string $path, ?string $query, ?string $fragment)
propreadonly ?string $scheme
propreadonly ?string $userinfo
propreadonly ?string $host
propreadonly ?int $port
propreadonly string $path
propreadonly ?string $query
propreadonly ?string $fragment
staticparse(string $iri): self

The parts of an IRI (RFC 3986 appendix B).

methodvalid(): bool

Whether the parts make a well-formed IRI: a valid scheme when there is one, no "//" path without an authority.

methodresolve(self $ref): ?self

The reference resolved against this base (RFC 3986 section 5.2), or null when the base has no scheme.

methodwithHost(?string $host): self

The same IRI with another host.

methodwithPath(string $path): self

The same IRI with another path.

methodtoIri(): string

The normalized IRI: non-ASCII characters stay as they are.

methodtoUri(): string

The normalized URI: non-ASCII characters percent-encoded too.

insidewrite(), mergePath(), removeDots(), encode()

Kernel final readonly class

public/minn/src/Minn/Http/Kernel.php · 38 lines · Open the class →

The edge. Turns a request into a response through the router and turns a RestError thrown anywhere underneath into the WordPress error shape.

new__construct(Minn\Http\Router $router)
methodhandle(Minn\Http\Request $request): ?Minn\Http\Response

Routes the request; a thrown failure becomes its response, and null means no route matched.

insideharden()

LegacyHeaders final class

public/minn/src/Minn/Http/LegacyHeaders.php · 60 lines · Open the class →

WP_Http's legacy readers, kept for plugins that still call them (probe http-classes). They read a header block the old way, and decode a chunked body with the reference's quirk. RawResponse does both properly for the engine's own requests.

staticread(array|string $headers): array

A header block (a string, its folded lines joined, or a list of lines) read into the last status line's code and message, lower-cased headers (a repeated one as a list) and the Set-Cookie values. Each status line starts the headers over.

@param string|list<string> $headers
@return array{response: array{code: int, message: string}, headers: array<string, string|list<string>>, cookies: list<string>}
staticdechunk(string $body): string

A chunked body decoded as the reference's WP_Http::chunkTransferDecode decodes one. The CRLF after a chunk is not stepped over, so it decodes only when chunks run into each other and a final 0 ends the body; any other shape comes back as it was.

Location final class

public/minn/src/Minn/Http/Location.php · 85 lines · Open the class →

Where a redirect leads: a Location header made absolute against the URL that sent it.

staticsanitize(string $location): string

A redirect's address made safe, as wp_sanitize_redirect() makes it (probe security-parity): spaces as %20, whole UTF-8 sequences URL-encoded, any other character outside the URL set dropped, and an encoded CR or LF taken out until none is left. Other encoded control characters stay, as they do on the reference.

staticresolve(string $base, string $location): string

The absolute URL $location names, read against $base as RFC 3986 reads a reference: a scheme keeps it as written, "//" takes the base's scheme, a fragment or query alone keeps the base's path, and a relative path joins the base's directory with "." and ".." resolved.

staticsameOrigin(string $one, string $two): bool

Whether two URLs share scheme, host, and port, so credentials may follow from one to the other.

insidewithoutDotSegments()

Matched final readonly class

public/minn/src/Minn/Http/Matched.php · 15 lines · Open the class →

A route the router matched to a request and whose policy it judged: the route, the object and method that answer it, the pattern's captures, and every method the same handler answers on that pattern (the reference lists POST, PUT and PATCH together for one edit handler).

new__construct(Minn\Http\Route $route, object $handler, string $method, array $captures, array $methods)
@param array<string, string> $captures
@param list<string> $methods
propreadonly Minn\Http\Route $route
propreadonly object $handler
propreadonly string $method
propreadonly array $captures
propreadonly array $methods

MediaType final class

public/minn/src/Minn/Http/MediaType.php · 18 lines · Open the class →

Reading the media types a request names in its Accept and Content-Type headers.

staticjson(string $types): bool

Whether a list of media types names JSON: application/json, or any application/…+json vendor type, parameters and case ignored (wp_is_json_media_type(), probe load-remaining).

Method enum

public/minn/src/Minn/Http/Method.php · 33 lines · Open the class →

Cases: Get = 'GET', Head = 'HEAD', Post = 'POST', Put = 'PUT', Patch = 'PATCH', Delete = 'DELETE', Options = 'OPTIONS', Any = '*'

staticfromName(string $name): self

The verb for a name, GET when the name is unknown.

methodmatches(self $declared): bool

HEAD is served by GET handlers; the kernel drops the body.

methodcanonicalRedirects(): bool

Canonical redirects (trailing slash, pretty-URL mapping, 404 guessing) run only for reads; every other method renders the URL as typed.

Outbound final readonly class

public/minn/src/Minn/Http/Outbound.php · 46 lines · Open the class →

One outgoing HTTP request, normalised: the transport needs nothing else.

new__construct(string $method, string $url, array $headers = array ( ), ?string $body = NULL, float $timeout = 5.0, int $redirects = 5, bool $verifySsl = true, string $userAgent = '', ?string $caInfo = NULL, bool $blocking = true, ?Closure $prepare = NULL, ?float $connectTimeout = NULL, ?int $maxBytes = NULL)
@param list<string> $headers "Name: value" lines
@param (Closure(\CurlHandle): void)|null $prepare a last word on the curl handle before it is sent
propreadonly string $method
propreadonly string $url
propreadonly array $headers
propreadonly ?string $body
propreadonly float $timeout
propreadonly int $redirects
propreadonly bool $verifySsl
propreadonly string $userAgent
propreadonly ?string $caInfo
propreadonly bool $blocking
propreadonly ?Closure $prepare
propreadonly ?float $connectTimeout
propreadonly ?int $maxBytes
methodto(string $url, string $method, ?string $body, array $headers): self

The same request sent somewhere else, as the next hop of a redirect.

@param list<string> $headers "Name: value" lines
methodpreparing(Closure $prepare): self

The same request with a last word on the curl handle, run after any it already has.

Policy final readonly class

public/minn/src/Minn/Http/Policy.php · 115 lines · Open the class →

What a route requires of its caller, as data on the route: the router enforces it before the handler runs, so a grep over the attributes is the authorization surface. The two refusals are the reference's: a caller who is not signed in gets the sign-in code (401), a signed-in caller who lacks the capability gets the refusal code (403); how a bad nonce is answered belongs to whoever judges the policy. A policy that names a subject has the record looked up first, and answers the record's 404 before either refusal, which is the reference's order.

Written inline in the attribute with `new`, which is what an attribute argument allows: `policy: new Policy(Access::Cap, 'upload_files', refuse: 'rest_cannot_create', message: '...')`.

new__construct(Minn\Http\Access $access = Minn\Http\Access::Public, ?string $cap = NULL, array $caps = array ( ), ?string $param = NULL, string $signIn = 'rest_not_logged_in', string $signInMessage = 'You are not currently logged in.', string $refuse = 'rest_forbidden', string $message = 'Sorry, you are not allowed to do that.', ?Minn\Http\Policy $edit = NULL, ?Minn\Http\Subject $subject = NULL, ?string $missing = NULL, ?string $missingMessage = NULL, int $signInStatus = 401, ?string $verb = NULL, ?array $opener = NULL)
@param list<string> $caps further capabilities every one of which the caller must hold
propreadonly Minn\Http\Access $access
propreadonly ?string $cap
propreadonly array $caps
propreadonly ?string $param
propreadonly string $signIn
propreadonly string $signInMessage
propreadonly string $refuse
propreadonly string $message
propreadonly ?Minn\Http\Policy $edit
propreadonly ?Minn\Http\Subject $subject
propreadonly ?string $missing
propreadonly ?string $missingMessage
propreadonly int $signInStatus
propreadonly ?string $verb
propreadonly ?array $opener
methodisPublic(): bool

Whether the route is open to anyone with nothing to look up, so the gate need not run at all.

methodmissingCode(): string

The 404 code a missing subject earns.

methodmissingText(): string

The 404 message a missing subject earns.

methodcapabilities(): array

Every capability the policy asks for by name, the one on the object included, so a listing can show what a route takes.

@return list<string>
methodtoArray(): array

The policy as data, for the route catalogue. @return array<string, mixed>

@return array<string, mixed>
methoddescribe(): string

The policy in one line, for the route index and the docs.

Punycode final class

public/minn/src/Minn/Http/Punycode.php · 103 lines · Open the class →

Internationalized host names in ASCII: each label that is not ASCII is written as "xn--" plus its Punycode (RFC 3492) and must stay under 64 bytes. Labels keep their case: there is no Nameprep step, as on the reference ("Bücher" becomes "xn--Bcher-kva").

constPREFIX = 'xn--'
constMAX_LENGTH = 64
statichost(string $hostname): string

A whole host name, label by label.

staticlabel(string $text): string

One label in ASCII.

staticencode(string $input): string

The Punycode of a UTF-8 string (without the prefix).

insidedigits(), digit(), adapt()

RawResponse final class

public/minn/src/Minn/Http/RawResponse.php · 105 lines · Open the class →

An HTTP response as text, the way the Requests library hands it from transport to parser: the status line and headers, a blank line, the body. Built from an exchange, split back up, chunked bodies joined and compressed ones inflated.

staticfromExchange(Minn\Http\Exchange $exchange): string

The exchange as raw response text: its status line, header lines, a blank line, the body.

staticparse(string $raw): array

The text split into protocol, status, header pairs (folded lines joined) and body.

@return array{protocol: float, status: int, headers: list<array{0: string, 1: string}>, body: string}
staticparseHead(string $head): array

A head alone (the body went to a file): protocol, status, header pairs, and an empty body.

@return array{protocol: float, status: int, headers: list<array{0: string, 1: string}>, body: string}
staticunchunk(string $body): string

A chunked body joined; text that is not chunked comes back as it was.

staticinflateLoose(string $data): string|false

A gzip body inflated past its header fields (extra, name, comment, header CRC) and trailer, or a zlib one past its two-byte header, as WP_Http_Encoding::compatible_gzinflate reads them; false when neither inflates.

staticinflate(string $data): string

A gzip or zlib body inflated; anything else (raw deflate included) comes back as it was.

Refusal interface

public/minn/src/Minn/Http/Refusal.php · 5 lines · Open the class →

A refusal a route judges after its arguments and before its policy: the reference turns some requests away at the head of its permission check, before it looks at the caller at all (a post created with an id).

staticrefuse(Minn\Http\Request $request): void

Throws the refusal when the request earns it; returns quietly otherwise.

Request final readonly class

public/minn/src/Minn/Http/Request.php · 145 lines · Open the class →

An immutable picture of the incoming request. Built once from the PHP globals at the edge; handlers only ever see this object.

new__construct(Minn\Http\Method $method, string $path, array $query, array $headers, array $cookies, string $body, bool $secure, string $host, array $form = array ( ), array $files = array ( ), string $remoteAddress = '', array $server = array ( ))
@param array<string, string|array> $query
@param array<string, string> $headers lower-cased names
@param array<string, string> $cookies
@param array<string, mixed> $form decoded form fields, for bodies that are not JSON
@param array<string, array> $files uploaded files, keyed by field
propreadonly Minn\Http\Method $method
propreadonly string $path
propreadonly array $query
propreadonly array $headers
propreadonly array $cookies
propreadonly string $body
propreadonly bool $secure
propreadonly string $host
propreadonly array $form
propreadonly array $files
propreadonly string $remoteAddress
propreadonly array $server

what the server says about itself: software, protocol, and address; diagnostics only

staticfromGlobals(): self

The request PHP received, read once from the superglobals.

methodjson(): array

The JSON body as an array, or the form fields when the body is empty or a form.

methodwithPath(string $path): self

The same request addressed to another path (a REST route carried in ?rest_route=).

methodquery(string $key, ?string $default = NULL): ?string

One query value as a string, or the default when it is absent or not a string.

methodflag(string $key): bool

A query value read as the reference reads a boolean argument: true, 1, yes, on; anything else is false.

methodflagged(string $key): bool

A boolean the query or the body sets, as a DELETE may carry force in either (probes rest-posts-write, rest-terms-controller).

methodhas(string $key): bool

Whether the query carries this key at all, even empty.

methodheader(string $name): ?string

A request header by case-insensitive name, or null.

methodcookie(string $name): ?string

A cookie's value, or null.

methodsegments(): array

The path split into non-empty segments: "/a/b/" becomes ["a", "b"].

methodqueryStringWithout(string ...$keys): string

The query string with the given keys removed, ready to append to a redirect.

RequestFailed final class

public/minn/src/Minn/Http/RequestFailed.php · 7 lines · implements Stringable, Throwable · extends RuntimeException · Open the class →

Thrown by Exchange::throw() when no response arrived or it was not a 2xx; the exchange rides along.

new__construct(Minn\Http\Exchange $reply, string $message)
propreadonly Minn\Http\Exchange $reply

RequestsNames final class

public/minn/src/Minn/Http/RequestsNames.php · 18 lines · Open the class →

The Requests library's PSR-0 class names (Requests_Exception_HTTP_404, Requests_IDNAEncoder, ...) mapped to the PSR-4 names they stand for.

staticmodern(string $legacy): ?string

The namespaced name for a Requests_* name, or null when it is not one.

Response final readonly class

public/minn/src/Minn/Http/Response.php · 133 lines · Open the class →

What a handler returns. Nothing is written to the client until the kernel calls send(), so a response can be inspected, wrapped, or replaced on the way out. Work that belongs after the client has its answer (a cron run a page found due) rides along as afterSend closures.

new__construct(int $status = 200, array $headers = array ( ), string $body = '', array $cookies = array ( ), array $afterSend = array ( ))
@param array<string, string> $headers
@param list<array{0: string, 1: string, 2: array}> $cookies name, value, setcookie options
@param list<Closure(): void> $afterSend run once the response has been handed to the client
propreadonly int $status
propreadonly array $headers
propreadonly string $body
propreadonly array $cookies
propreadonly array $afterSend
statichtml(string $body, int $status = 200): self

An HTML response.

staticjson(mixed $payload, int $status = 200): self

A JSON response with the payload encoded.

staticredirect(string $location, int $status = 301): self

A redirect to a location.

methodwithHeader(string $name, string $value): self

The same response with one header set, after those already set (one already there keeps its place).

methodwithoutHeader(string $name): self

The same response without one header.

methodwithCookie(string $name, string $value, array $options): self

The same response with a cookie to set.

@param array<string, mixed> $options setcookie options: expires, path, secure, httponly, samesite
methodwithoutBody(): self

The same response with an empty body, the HEAD answer.

methodafterSend(Closure $work): self

The same response with work to run once the client has been answered. @param Closure(): void $work

@param Closure(): void $work
staticemitHeader(string $line): void

One header line sent now, as plugin code sends one, ahead of the response's own (which win on a shared name). Nothing outside a web request, or once the head is out.

staticwrite(string $bytes): void

Bytes written to PHP's output now, so into whatever buffers are open: for a page passed through buffers plugin code opened before it (Theme\TemplateBuffer), whose handlers then see it.

methodsendHead(): void

Writes the status, the headers, and the cookies now, and nothing else: for an answer that code outside the engine may finish on its own (an ajax handler that ends the request), which must find them already said.

methodsend(): void

Writes the status, the headers, the cookies, and the body, ends the request for the client, then runs the after-send work in the same process (the server that can close the connection first does).

Route final readonly class

public/minn/src/Minn/Http/Route.php · 75 lines · Open the class →

Declares a handler method as a route. The policy lives here, as metadata the router enforces before the handler runs, so the authorization surface of the engine is a grep away; a route with no policy is one the ratchet in the style suite counts down. The parameter sets it reads live here too, judged before the policy is (the reference refuses a bad argument before it refuses a caller), so the REST index can tell a client what a route takes as well as what it requires of them.

Patterns: "/wp/v2/posts/{id}" captures one segment, "{id:\d+}" constrains it, and "/{path*}" captures the rest of the path (slashes included).

new__construct(Minn\Http\Method $method, string $pattern, ?Minn\Http\Policy $policy = NULL, array $args = array ( ), bool $index = true, array $body = array ( ), ?string $name = NULL, ?string $params = NULL, ?string $refuse = NULL)
@param list<array<string, array<string, mixed>>> $args the parameter sets this route reads, from Args
@param list<array<string, array<string, mixed>>> $body the parameter sets this route reads from the JSON body, from Args or the shape that owns them
propreadonly Minn\Http\Method $method
propreadonly string $pattern
propreadonly ?Minn\Http\Policy $policy
propreadonly array $args
propreadonly bool $index
propreadonly array $body
propreadonly ?string $name
propreadonly ?string $params
propreadonly ?string $refuse
methodarguments(array $captures = array ( )): array

The parameters this route accepts, as the REST index publishes them: from its params source for these captures (a capture the pattern fixes, such as {base:posts}, counts when none is given), or its fixed sets.

@param array<string, string> $captures
@return array<string, array<string, mixed>>
methodbodyArguments(): array

The parameters this route reads from the JSON body, validated before the caller is judged.

@return array<string, array<string, mixed>>
methodregex(): string

The pattern as a regular expression with named captures.

RouteMiss final class

public/minn/src/Minn/Http/RouteMiss.php · 3 lines · implements Stringable, Throwable · extends RuntimeException · Open the class →

A handler declining a request its pattern matched: the router swallows it and goes on to the next route, so a catch-all pattern can leave a path it does not own to whatever registers after it (a plugin's route under the same namespace, say) instead of answering "no route" itself.

RouteParams interface

public/minn/src/Minn/Http/RouteParams.php · 10 lines · Open the class →

The parameters a route takes when they depend on what its captures name (a post type's collection takes its own taxonomies, its own orderby values, whatever plugins add to it), rather than on a fixed set.

staticfor(array $captures): array

The route's parameters for these captures, as the index publishes them; none when the captures name nothing.

@param array<string, string> $captures
@return array<string, array<string, mixed>>

RouteRow final readonly class

public/minn/src/Minn/Http/RouteRow.php · 81 lines · Open the class →

One line of the route table: what a route is, who it is for, and what it takes, read from the attribute and the handler method alone, so the table can be built from the classes without a request, a database, or a site. This is the row the REST index, the docs, and contracts/api/routes.json are written from.

new__construct(string $method, string $pattern, string $name, string $handler, string $summary, ?Minn\Http\Policy $policy, array $args, array $body, bool $index, ?string $params = NULL, string $regex = '')
@param array<string, array<string, mixed>> $args the query parameters the route reads
@param array<string, array<string, mixed>> $body the JSON body parameters it reads
propreadonly string $method
propreadonly string $pattern
propreadonly string $name
propreadonly string $handler
propreadonly string $summary
propreadonly ?Minn\Http\Policy $policy
propreadonly array $args
propreadonly array $body
propreadonly bool $index
propreadonly ?string $params
propreadonly string $regex
staticof(Minn\Http\Route $route, ReflectionMethod $method): self

The row for one attribute on one handler method; the name defaults to the handler.

methodargsFor(string $route): array

The parameters as the index publishes them for one concrete route the pattern takes (a {base} route's parameters are that base's).

@return array<string, array<string, mixed>>
methodtoArray(): array

The row as data, for the JSON catalogue. @return array<string, mixed>

@return array<string, mixed>
insidesummary()

Router final class

public/minn/src/Minn/Http/Router.php · 260 lines · Open the class →

Matches a request to a #[Route] on one of the registered handler objects, has the check refuse a bad argument, has the gate judge the route's policy, and invokes the method with the request plus the named pattern captures. That order is the reference's: an invalid parameter is answered before the caller is looked at. The gate is the one thing a router cannot be built without: a policy nobody judges is a route nobody may call.

new__construct(Closure $gate, ?Closure $check = NULL, ?Minn\Http\Envelope $envelope = NULL)
@param Closure(Policy $policy, Request $request, array<string, string> $captures): void $gate throws when the policy refuses the caller
@param Closure(Route $route, Request $request): void|null $check throws when a declared argument is missing or invalid; judged before the gate
methodregister(object ...$handlers): self

Registers every #[Route] method of the given handlers; returns the router for chaining.

methodroutes(): array

The routes that list in an index: pattern => methods. @return array<string, list<string>>

@return array<string, list<string>>
methodtable(): array

Every registered route as a row: what it is, who it is for, what it takes. The same rows Rest\Catalogue reads from the classes alone.

@return list<RouteRow>
methodallowed(Minn\Http\Request $request): array

The methods the caller may use on this path, for the Allow header: every route matching the path (a route with no captures that names the path exactly claims it alone), its policy judged for the caller without dispatching. A route that states no policy counts for GET only, until it states one. Empty when nothing matched or nothing is allowed, and the header is then left out, as the reference does.

@return list<string> in the reference's order
methodmatching(Minn\Http\Request $request): array

The routes a request's method and path match, policies unjudged, in the order dispatch tries them, with their captures: what a batch reads to tell a route that does not take part from no route at all.

@return list<array{0: Route, 1: array<string, string>}>
methoddispatch(Minn\Http\Request $request): ?Minn\Http\Response

Null when nothing matched, so the caller can fall through.

insideadmits(), arguments(), answer(), enveloped(), methodsOf(), literal(), spells()

StatusText final class

public/minn/src/Minn/Http/StatusText.php · 16 lines · Open the class →

The reason phrase a status line carries for each code, as the reference's table has them (probe functions-remaining): its own wording where it differs from today's RFCs ("Request Entity Too Large", "Reserved" for 306).

staticof(int $code): string

The phrase for a code, or empty for one the table does not know.

Subject enum

public/minn/src/Minn/Http/Subject.php · 46 lines · Open the class →

The record a route capture names, so a policy can have it looked up before the caller is judged: the reference answers "no such post" ahead of "you may not", and the 404 it sends is the record's own. A kind that reads its post type or taxonomy from the {base} capture says so.

Cases: Post, PostParent, Attachment, Block, Navigation, MenuItem, GlobalStyles, GlobalStylesParent, Term, Menu, User, Comment

methodmissingCode(): string

The reference's error code for a record that does not exist.

methodmissingMessage(): string

The reference's message for a record that does not exist.

Transport final class

public/minn/src/Minn/Http/Transport.php · 138 lines · Open the class →

The engine's outgoing HTTP transport over curl: it sends exactly what an Outbound says and nothing else decides. Redirects are followed by curl, so the header lines of every hop arrive in order; only the last response's block is kept, the way plugin code expects to read it. Code calling out goes through Minn\Http, which owns the rules about where a request may go.

staticsend(Minn\Http\Outbound $request): Minn\Http\Exchange

Performs one outgoing request over curl and returns the exchange, a transport error included.

insideoptions(), limit(), lastHead(), lastBlock()

TrustedProxies final readonly class

public/minn/src/Minn/Http/TrustedProxies.php · 115 lines · Open the class →

Which addresses in front of the engine may speak for the client.

By default none do: a forwarded header is a request header, and a request header is the client's word. Behind a proxy or CDN that terminates TLS, `REMOTE_ADDR` is then the proxy's, which makes every visitor look like one address (the sign-in throttle would lock them all out together) and every request look insecure (the auth cookie would lose its Secure flag).

A site says who to believe in `wp-config.php`:

define('MINN_TRUSTED_PROXIES', '10.0.0.0/8, 2400:cb00::/32'); define('MINN_TRUSTED_PROXIES', '*'); // any: only when nothing but the proxy can reach PHP

The client is then the right-most address in `X-Forwarded-For` that is not itself trusted, which is what a proxy chain appends, so a client that sends its own `X-Forwarded-For` cannot put an address the engine believes to the right of the proxy's. `*` trusts whoever connects, not every forwarded hop: the client is the address that proxy appended (with it every hop, the client's own included, would be skipped as trusted, and every visitor would be the proxy again).

staticnone(): self

Nobody in front of the engine speaks for the client.

staticconfigured(): self

What the site declared in wp-config.php, or nobody.

methodtrusts(string $address): bool

Whether the engine believes what this address says about the client.

methodclientAddress(string $remoteAddress, string $forwardedFor): string

The client's address, given who connected and what was forwarded: the right-most forwarded address that is not itself a trusted proxy.

methodforwardedSecure(string $remoteAddress, string $forwardedProto): bool

Whether a forwarded scheme may be believed, and says https.

insidecoversHop(), bare(), covers()