�PNG  IHDR22?�� pHYs  �� OiCCPPhotoshop ICC profilexڝSgTS�=���BK���KoR RB���&*! J�!��Q�EEȠ�����Q, � ��!���������{�kּ������>���� �H3Q5� �B�������.@� $p�d!s�#�~<<+"��x� �M��0���B�\���t�8K�@z�B�@F���&S�`�cb�P-`'������{[�!�� e�Dh;��V�EX0fK�9�-0IWfH���� � 0Q��){`�##x��F�W<�+��*x��<�$9E�[-qWW.(�I+6aa�@.�y�2�4���������x����6��_-��"bb���ϫp@�t~��,/��;�m��%�h^ �u��f�@����W�p�~<�5�j>{�-�]c�K'Xt����o��(�h���w��?�G�%�fI�q^D$.Tʳ?�D��*�A��,���� �`6�B$��BB d�r`)��B(�Ͱ*`/�@4�Qh��p.�U�=p�a��(�� A�a!ڈb�X#����!�H�$ ɈQ"K�5H1R�T UH�=r9�\F��;�2����G1���Q=� �C��7�F� �dt1�����r�=�6��Ыhڏ>C�0��3�l0.��B�8, �c˱"� ���V����cϱw�E� 6wB aAHXLXN�H� $4� 7 �Q�'"��K�&���b21�XH,#��/{�C�7$�C2'��I��T��F�nR#�,��4H#���dk�9�, +ȅ����3��!�[ �b@q��S�(R�jJ��4�e�2AU��Rݨ�T5�ZB���R�Q��4u�9̓IK�����hh�i��t�ݕN��W���G���w ��Ljg(�gw��L�Ӌ�T071���oUX*�*|�� �J�&�*/T����ު U�U�T��^S}�FU3S� Ԗ�U��P�SSg�;���g�oT?�~Y��Y�L�OC�Q��_�� c�x,!k ��u�5�&���|v*�����=���9C3J3W�R�f?�q��tN �(���~���)�)�4L�1e\k����X�H�Q�G�6������E�Y��A�J'\'Gg����S�Sݧ �M=:��.�k���Dw�n��^��Lo��y���}/�T�m���G X� $� �<�5qo</���QC]�@C�a�a�ᄑ��<��F�F�i�\�$�m�mƣ&&!&KM�M�RM��)�;L;L���͢�֙5�=1�2��כ߷`ZxZ,����eI��Z�Yn�Z9Y�XUZ]�F���%ֻ�����N�N���gð�ɶ�����ۮ�m�}agbg�Ů��}�}��= ���Z~s�r:V:ޚΜ�?}����/gX���3��)�i�S��Ggg�s�󈋉K��.�>.���Ƚ�Jt�q]�z���������ۯ�6�i�ܟ�4�)�Y3s���C�Q��? ��0k߬~OCO�g��#/c/�W�װ��w��a�>�>r��>�<7�2�Y_�7��ȷ�O�o�_��C#�d�z����%g��A�[��z|!��?:�e����A���AA�������!h�쐭!��Α�i�P~���a�a��~ '���W�?�p�X�1�5w��Cs�D�D�Dޛg1O9�-J5*>�.j<�7�4�?�.fY��X�XIlK9.*�6nl������� �{�/�]py�����.,:�@L�N8��A*��%�w%� y��g"/�6ш�C\*N�H*Mz�쑼5y$�3�,幄'���L Lݛ:��v m2=:�1����qB�!M��g�g�fvˬe����n��/��k���Y- �B��TZ(�*�geWf�͉�9���+��̳�ې7�����ᒶ��KW-X潬j9�������(�x��oʿ�ܔ���Ĺd�f�f���-�[����n �ڴ �V����E�/��(ۻ��C���<��e����;?T�T�T�T6��ݵa��n��{��4���[���>ɾ�UUM�f�e�I���?�������m]�Nmq����#�׹���=TR��+�G�����w- 6 U����#pDy��� �� :�v�{���vg/jB��F�S��[b[�O�>����z�G��499�?r����C�d�&����ˮ/~�����јѡ�򗓿m|������������x31^�V���w�w��O�| (�h���SЧ��������c3-� cHRMz%������u0�`:�o�_�F5IDATx��ytUս�?�;���fN$$��@B�!�k��� -*�gE���j�O�kQ�ʠ`U�B_ �AÐ0%�Ȕ�@r3ޛ;߳��$�@��v����^�s����������$��ߢ !�W��5�̱'-٘������4�>�Rƫ$�G��0U�H}�_ _� #��k�MJ/=�9Du *ڲ�k�PQ;0��j|�*�$�^컂脛y���>�z×� B��� ��(�:$�ʔ_�6��C!B����MVx��a��ζyY����j��ƋY2�9�F�{r��r�딟F��Q��hY6��� x��[l��RC��������nt@��;P��3��� F�w��7��:�P���ɋ'���mj���Bƪ�$�Q�L��y��߷g��\���z����v5�p�v$�v櫻�`6~K �Y�X����,�:��"�o�e0II"(��,������l_�C���/4�+f�E�!q��:����u����c��樼.UE�@� �]K�W���$-�h]�JG���Dz)�`��.�A/ ���mᏣc�3jK� �!�>���$�Q�!�_�? \�C2(E �M$Z I{�U���s�ǣ���=��@Բe ޻��W�����@ �w����}�9v,)��.}*|"�s��di���,���G�]W�AC�������|<����[��ߕ������K�*�G nߴ!��‹��J�!��6(L�a)��Am��������;adf��:�xm��nh�;֦I��(���o�C~ډa?�=��s����h��E������xp����)���_���t��W�����´;8���`dz=(��@@�+,�;�`�fhl��XȌ�3,uI�~z(J����� �ZX�A����H'�À�㜦�82|��a�n�z2��I���� ���EY�h�ε<�0�q�{��C��v�˅��?"@��χ�x�WS+��N�~�KQ�J�8ل�� _.W��� !TU{�����,��"�� �;�������HI S� ��))�o�1�H�D� ���� (� ;����"Ҕ��,���� �F=���O��&�Ph>�`���y��߲ &S��P�<��'��1s�1cĂ�N����2)����@�u��tv��<�+�-&�����h��[!FSG���' � z z0��<����8d&�����E0�,��<��"!P�b4��� �{S�����X����.�囤���T��hEF��X�q0"-<����h���]�Py*����F⩍��@v�˂�Y08�� .q��a��s����(!<̫1�3�L�H��7sYW�`��qtu�\7��?�@�Y�� %Ka�x�GZ�*W�P��y�����N0r9��Z�c�%���q,��k�����!!,��>���IB��� �x1�Xu�Y���v-���ֺ��N������JP��a�2`�e5��w��u;�6�i���̃��(K���ťa��(l51��JL��O'%���]���ٖ뿉!��{*�p+f ���i8��D�ȑ���ܩ�M��V��ӟz��x�7Z�1i}�^͜���6e����x��)u�d�ˏ�V�eG�\�X1�����=n�U'�~Y����L�\,�(d!x ,�:�TW8� M`r���@��r+v�Q0��>^;˴�VbF��('����� �*O��i��j�Ӆ����^�S3&� ���{������.ی�� N;��NU%d0�Ce\r"�C�b��&t7N�7��� �ȪD�[%l�۪�$��'m?�A9tI�9>��NVL ��� 6v~M�����ъ�B���0����9$&=ώm��W�0���L���&��E���f�f��'+�9��hv��7,���M��6�D�&�^� �����8 D֮�����o��k^z��O��hP�8q�5d*��YVq3�{��������#LX4�?��jP=3�~�؅c�ݰ�̱�ұ��8̲��2�G�����j��έ�9��w7���U�X�w�85C��?!I&���o&����L���I#�^�eV�,�^t5�k� �xs˛\��U�8��."���{���ǵ�l4��ݼ�;��r"���G�/~���gJV�N糦��ZZ^k+NN�D��I$�=OE�z����|*�*{��%k���G��>�`9��b;�s���8,�qs��c��.b��q�%�>r��n�R�Ƨ�%�,�A��le#�8���}; 21�0�b�&�QX�f���q�00����$>�h�N�'�a3��A���/�r ��L5�8�GԲ'�����w;��c#�q1���h$*��V^�k��QĽ�b:i������� ����H F#�_���,��]g����Ը����`RUU���� ��"�qD���>-��ϤI�~��3�̴�����NZ[[555�eeek7�m����YF#[��Đ��R��i��L�H�r ��@��J-�Pŝ��_|�-��6y��ks�V�U~��6���w.����(��0A� 4��f��� /���C�xŊ�ٳ����z=6�����rg���mKo��o�/����a�g���?'9%9��P��斓��~���L{�� �H��{AW��B��$����v�1b�&Mb�ر��ر#�0`�`xMBp! )Z�Nmo�mZ�g�}�eذaWTVV ���'99��C5��&�&+5�bT>|8��PUUETT����&���t��~TE%��'������b�tEGG;RSS�S�N�v��ѯ��t����u1@�ޚ"f��͈���r'L�Ph��m��ƌ�74o��`�x�q�P���NRSS�X,�����l&&&��MFFA!-��`��ҷ�n��Yuu�����x<I�LB%����KԒ50�!�3�bz�����ʸKL���;---�l6[��`��������Ȏ;�����egg4��!D�T��jWujyZ��������K����%����p�^���X,6��sV=�ܖ����6�3�X ���4ҿ��A2�&����_k�;d�� �VfOIEND�B`� 0x0x
PHP 8.1.34
Preview: Request.php Size: 62.62 KB
/home2/astrociv/public_html/asquarebuildcon.in/wp-content/plugins/imunify-security/inc/App/Defender/Request.php
<?php
/**
 * Copyright (с) Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2025 All Rights Reserved
 *
 * Licensed under CLOUD LINUX LICENSE AGREEMENT
 * https://www.cloudlinux.com/legal/
 */

namespace CloudLinux\Imunify\App\Defender;

/**
 * Request class for handling HTTP request data.
 *
 * Provides a clean abstraction over global variables like $_SERVER, $_GET, $_POST, etc.
 * This makes the code more testable and provides a consistent interface.
 *
 * @since 2.1.0
 */
class Request {

	const MAX_KEY_DEPTH = 5;
	const MAX_KEY_COUNT = 200;

	/**
	 * Request headers PHP exposes in $_SERVER without the HTTP_ prefix.
	 *
	 * @since 4.1.0
	 *
	 * @var string[]
	 */
	const CGI_HEADERS = array( 'CONTENT_TYPE', 'CONTENT_LENGTH' );

	/**
	 * Maximum bytes of php://input read when parsing a JSON body.
	 */
	const MAX_JSON_BODY_BYTES = 1048576;

	/** Cap on the raw body exposed under RAW_BODY_KEY; bounds scan-all regex cost/ReDoS on the hot path (matches getFileContent()'s 8 KB default). */
	const RAW_BODY_INSPECT_BYTES = 8192;

	/** Synthetic ARGS key holding the (capped) raw body of a fail-closed JSON request so scan-all rules can still inspect it; hidden from getArgNames() when injected. */
	const RAW_BODY_KEY = '__waf_raw_body__';

	/**
	 * HTTP methods a REST override may resolve to.
	 *
	 * Scoped to the verbs the rule format defines for `method`. A plugin can
	 * register a route for any string, so an unlisted override may still
	 * dispatch -- but no rule can target it, and falling back to the physical
	 * method is the safe direction.
	 *
	 * @since 4.1.1
	 *
	 * @var string[]
	 */
	const OVERRIDABLE_METHODS = array( 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS' );

	/**
	 * REST URL prefix assumed when WordPress is not loaded far enough to ask.
	 *
	 * @since 4.1.1
	 */
	const DEFAULT_REST_PREFIX = 'wp-json';

	/**
	 * Standard core scripts that bootstrap WordPress REST dispatch outside index.php.
	 *
	 * @since 4.1.1
	 *
	 * @var string[]
	 */
	const CORE_REST_ENTRY_SCRIPTS = array(
		'wp-blog-header.php',
		'wp-trackback.php',
		'wp-signup.php',
		'wp-activate.php',
	);

	/**
	 * Physical request method reported by the server.
	 *
	 * @var string
	 */
	private $method;

	/**
	 * Method WordPress will dispatch this request as; null until resolved.
	 *
	 * @since 4.1.1
	 *
	 * @var string|null
	 */
	private $effective_method = null;

	/**
	 * Whether WordPress will dispatch this request through the REST server;
	 * null until resolved.
	 *
	 * Resolving this can scan every rewrite rule, and Defender asks once per
	 * WAF rule, so the answer is memoized. The inputs it reads ($server, $get,
	 * $form_post) are all fixed in the constructor.
	 *
	 * @since 4.1.1
	 *
	 * @var bool|null
	 */
	private $is_rest_request = null;

	/**
	 * Normalised forms of the request path for REQUEST_URI matching; null until
	 * resolved.
	 *
	 * Defender resolves REQUEST_URI once per WAF rule, and building these reads
	 * home_url(), so they are memoized. The inputs ($server) are fixed in the
	 * constructor.
	 *
	 * @var string[]|null
	 */
	private $normalized_uri_candidates = null;

	/**
	 * Server variables.
	 *
	 * @var array
	 */
	private $server;

	/**
	 * GET parameters.
	 *
	 * @var array
	 */
	private $get;

	/**
	 * POST parameters.
	 *
	 * @var array
	 */
	private $post;

	/**
	 * Form POST parameters supplied by PHP before JSON body parsing.
	 *
	 * WordPress routing reads $_POST, not JSON-decoded request arguments.
	 *
	 * @since 4.1.1
	 *
	 * @var array
	 */
	private $form_post;

	/**
	 * Cookies.
	 *
	 * @var array
	 */
	private $cookies;

	/**
	 * File uploads.
	 *
	 * @var array
	 */
	private $files;

	/**
	 * Memoised getFileContent() results, keyed by "field|limit".
	 *
	 * @since 4.1.1
	 *
	 * @var array
	 */
	private $fileContentCache = array();

	/**
	 * Recursion bound for walking a $_FILES entry's tmp_name tree.
	 *
	 * Mirrors ValueResolver's own leaf-walk ceiling — the two walk the same
	 * plugin-mutable structure — but is declared here so Request does not take
	 * a dependency on a class that already depends on it. PHP stops building
	 * $_FILES at max_input_nesting_level - 1 (63 by default), so no real request
	 * comes near this.
	 *
	 * @since 4.1.1
	 *
	 * @var int
	 */
	const UPLOAD_PARTS_MAX_DEPTH = 1024;

	/**
	 * Recursion bound for the null-byte strip over a $_FILES sub-value.
	 *
	 * Must stay at or above ValueResolver's leaf-extraction ceiling, so a leaf
	 * the strip skips is also one no matcher can be handed.
	 *
	 * @since 4.1.1
	 *
	 * @var int
	 */
	const FILES_STRIP_MAX_DEPTH = 1024;

	/**
	 * Whether the RAW_BODY_KEY entry in $this->post is a synthetic sentinel we
	 * injected (only synthetic entries are hidden from getArgNames()).
	 *
	 * @var bool
	 */
	private $raw_body_fail_closed = false;

	/**
	 * Throttled-error payload captured when a JSON body fails closed, for the
	 * owner (Plugin) to emit via Debug::sendThrottledError(); null until then.
	 *
	 * @var array|null
	 */
	private $fail_closed_report = null;

	/**
	 * Constructor.
	 *
	 * @param array $server  Server variables (defaults to $_SERVER).
	 * @param array $get     GET parameters (defaults to $_GET).
	 * @param array $post    POST parameters (defaults to $_POST).
	 * @param array $cookies Cookies (defaults to $_COOKIE).
	 * @param array $files   File uploads (defaults to $_FILES).
	 */
	public function __construct( $server = null, $get = null, $post = null, $cookies = null, $files = null ) {
		// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
		$this->server    = null !== $server ? $server : $_SERVER;
		$this->get       = null !== $get ? $get : $_GET;
		$this->post      = null !== $post ? $post : $_POST;
		$this->form_post = $this->post;
		$this->cookies   = null !== $cookies ? $cookies : $_COOKIE;
		$this->files     = null !== $files ? $files : $_FILES;
		// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended

		$this->method = $this->extractPhysicalMethod( $this->server );

		if ( empty( $this->post ) ) {
			$this->maybeParseJsonBody();
		}
	}

	/**
	 * Parse JSON request body into $this->post when $_POST is empty.
	 *
	 * PHP only populates $_POST for application/x-www-form-urlencoded and
	 * multipart/form-data. For application/json requests (used by WordPress
	 * REST API), $_POST is empty and the body is only available via
	 * php://input. This method bridges that gap so ARGS:field conditions
	 * work on JSON request bodies.
	 *
	 * Guards:
	 * - Only runs when $_POST is empty (avoids double-parsing form submissions).
	 * - Only for application/json and +json suffix content types.
	 * - Caps raw input at 1 MB.
	 * - Fails closed when the body cannot be decoded into usable parameters.
	 *
	 * @since 3.0.2
	 */
	private function maybeParseJsonBody() {
		$content_type = isset( $this->server['CONTENT_TYPE'] )
			? $this->server['CONTENT_TYPE']
			: ( isset( $this->server['HTTP_CONTENT_TYPE'] ) ? $this->server['HTTP_CONTENT_TYPE'] : '' );

		$parts     = explode( ';', $content_type, 2 );
		$mime_type = strtolower( trim( $parts[0] ) );
		if ( ! $this->isJsonContentType( $mime_type ) ) {
			return;
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading php://input stream, not a filesystem file.
		$raw = $this->readInput( self::MAX_JSON_BODY_BYTES );
		// Use a strict check rather than empty(): a legitimately empty or
		// unreadable body must return, but the single-character body "0" is a
		// valid bare JSON number that must proceed to the fail-closed branch
		// below (empty("0") === true would otherwise let it slip past).
		if ( false === $raw || '' === $raw ) {
			return;
		}

		// Note: on PHP 5.6, json_decode() may return null without setting
		// json_last_error() when the depth limit is exceeded. The is_array()
		// guard below handles this correctly — null fails the check.
		$decoded    = json_decode( $raw, true, 64 );
		$json_error = json_last_error();
		if ( is_array( $decoded ) ) {
			if ( ! empty( $decoded ) ) {
				$this->post = $decoded;
			}
			// An empty JSON object {} or array [] decodes to an empty array:
			// there are no key/value parameters to inspect, so $this->post
			// stays empty. This is the single fail-open case we accept -- the
			// body carries no content for ARGS rules to act on.
			return;
		}

		// Fail closed: Content-Type claims JSON but the body did not decode into
		// usable parameters (truncated/oversized, malformed, too deep, the `null`
		// literal, or a bare scalar). Leaving $this->post empty would fail open,
		// so expose a capped slice of the raw body under the synthetic sentinel
		// key for scan-all rules; the flag marks it synthetic so getArgNames()
		// can hide it (see RAW_BODY_KEY / RAW_BODY_INSPECT_BYTES).
		$body_bytes                 = strlen( $raw );
		$this->post                 = array( self::RAW_BODY_KEY => substr( $raw, 0, self::RAW_BODY_INSPECT_BYTES ) );
		$this->raw_body_fail_closed = true;

		$this->reportFailClosed( $mime_type, $body_bytes, $json_error );
	}

	/**
	 * Capture the throttled-error payload for a fail-closed JSON body.
	 *
	 * Records (does not emit) the diagnostic metadata so the owner that holds a
	 * Debug handle can throttle and send it after construction -- see
	 * isRawBodyFailClosed() and failClosed*(). The raw body is never reported:
	 * it is attacker-controlled and may carry payloads or PII.
	 *
	 * @since 4.0.2
	 *
	 * @param string $mime_type  Lowercased JSON content type that was detected.
	 * @param int    $body_bytes Length of the raw body that was read.
	 * @param int    $json_error json_last_error() value from the failed decode.
	 *
	 * @return void
	 */
	private function reportFailClosed( $mime_type, $body_bytes, $json_error ) {
		$truncated = $body_bytes >= self::MAX_JSON_BODY_BYTES;

		if ( $truncated ) {
			$reason = 'oversize';
		} elseif ( JSON_ERROR_DEPTH === $json_error ) {
			$reason = 'depth';
		} elseif ( JSON_ERROR_NONE === $json_error ) {
			// Valid JSON that is not an object/array (null literal or bare scalar).
			// On PHP 5.6 a depth overflow also lands here, reported as non_array.
			$reason = 'non_array';
		} else {
			$reason = 'malformed';
		}

		// Error code embeds the reason so each reason throttles/groups on its own.
		$this->fail_closed_report = array(
			'code'    => 'waf_json_body_fail_closed_' . $reason,
			'message' => 'WAF: JSON request body failed closed (reason: ' . $reason . '); raw body exposed to ARGS rules',
			'context' => array(
				'reason'       => $reason,
				'content_type' => $mime_type,
				'body_bytes'   => $body_bytes,
				'truncated'    => $truncated,
				'json_error'   => $json_error,
			),
		);
	}

	/**
	 * Determine whether a MIME type denotes a JSON request body.
	 *
	 * Matches the exact application/json type plus any structured-syntax
	 * suffix per RFC 6839 (e.g. application/ld+json, application/vnd.api+json),
	 * mirroring how framework body parsers detect JSON.
	 *
	 * @since 4.0.2
	 *
	 * @param string $mime_type Lowercased MIME type with parameters stripped.
	 *
	 * @return bool True if the type denotes JSON.
	 */
	private function isJsonContentType( $mime_type ) {
		if ( 'application/json' === $mime_type ) {
			return true;
		}

		return '+json' === substr( $mime_type, -5 );
	}

	/**
	 * Read raw request body from php://input.
	 *
	 * Extracted as a method so unit tests can override the input source
	 * without requiring a real php://input stream.
	 *
	 * @since 3.0.2
	 *
	 * @param int $max_length Maximum bytes to read.
	 *
	 * @return string|false Raw body content, or false on failure.
	 */
	protected function readInput( $max_length ) {
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading php://input stream.
		return file_get_contents( 'php://input', false, null, 0, $max_length );
	}

	/**
	 * Get the physical request method reported by the server.
	 *
	 * @return string The HTTP method (GET, POST, PUT, DELETE, etc.).
	 */
	public function getMethod() {
		return $this->method;
	}

	/**
	 * Check if the physical request method matches the given method.
	 *
	 * @param string $method The method to check against.
	 *
	 * @return bool True if the request method matches, false otherwise.
	 */
	public function isMethod( $method ) {
		return strtolower( $this->method ) === strtolower( $method );
	}

	/**
	 * Get the method WordPress will dispatch this request as.
	 *
	 * Identical to getMethod() for ordinary requests, because WordPress ignores
	 * method overrides outside the REST server. On a REST request it mirrors
	 * WP_REST_Server::serve_request(), which prefers the query parameter
	 * `_method` and only then the X-HTTP-Method-Override header. A rule scoped
	 * to that logical verb can be evaluated in addition to a rule scoped to the
	 * physical method. Overrides outside
	 * OVERRIDABLE_METHODS resolve to the physical method -- WordPress finds no
	 * route for them, so they must not be able to switch a rule off.
	 *
	 * Note that WordPress never reads `_method` from the request body, so
	 * neither does this.
	 *
	 * @since 4.1.1
	 *
	 * @return string The HTTP method (GET, POST, PUT, DELETE, etc.).
	 */
	public function getEffectiveMethod() {
		if ( null === $this->effective_method ) {
			$this->effective_method = $this->resolveEffectiveMethod();
		}

		return $this->effective_method;
	}

	/**
	 * Check if the effective request method matches the given method.
	 *
	 * @since 4.1.1
	 *
	 * @param string $method The method to check against.
	 *
	 * @return bool True if the effective method matches, false otherwise.
	 */
	public function isEffectiveMethod( $method ) {
		$effective = strtoupper( $this->getEffectiveMethod() );
		$method    = strtoupper( $method );

		return $effective === $method
			|| ( 'HEAD' === $effective && 'GET' === $method && $this->isRestRequest() );
	}

	/**
	 * Get a GET parameter.
	 *
	 * @param string $key     The parameter key.
	 * @param mixed  $default Default value if the parameter doesn't exist.
	 *
	 * @return mixed The parameter value or default.
	 */
	public function get( $key, $default = null ) {
		return isset( $this->get[ $key ] ) ? $this->get[ $key ] : $default;
	}

	/**
	 * Check if a GET parameter exists.
	 *
	 * @param string $key The parameter key.
	 *
	 * @return bool True if the parameter exists, false otherwise.
	 */
	public function hasGet( $key ) {
		return isset( $this->get[ $key ] );
	}

	/**
	 * Get a POST parameter.
	 *
	 * @param string $key     The parameter key.
	 * @param mixed  $default Default value if the parameter doesn't exist.
	 *
	 * @return mixed The parameter value or default.
	 */
	public function post( $key, $default = null ) {
		return isset( $this->post[ $key ] ) ? $this->post[ $key ] : $default;
	}

	/**
	 * Check if a POST parameter exists.
	 *
	 * @param string $key The parameter key.
	 *
	 * @return bool True if the parameter exists, false otherwise.
	 */
	public function hasPost( $key ) {
		return isset( $this->post[ $key ] );
	}

	/**
	 * Get a cookie.
	 *
	 * @param string $key     The cookie key.
	 * @param mixed  $default Default value if the cookie doesn't exist.
	 *
	 * @return mixed The cookie value or default.
	 */
	public function cookie( $key, $default = null ) {
		return isset( $this->cookies[ $key ] ) ? $this->cookies[ $key ] : $default;
	}

	/**
	 * Check if a cookie exists.
	 *
	 * @param string $key The cookie key.
	 *
	 * @return bool True if the cookie exists, false otherwise.
	 */
	public function hasCookie( $key ) {
		return isset( $this->cookies[ $key ] );
	}

	/**
	 * Get all GET parameters.
	 *
	 * @return array All GET parameters.
	 */
	public function getAllGet() {
		return $this->get;
	}

	/**
	 * Get all POST parameters.
	 *
	 * Note: for a fail-closed JSON body this includes the synthetic RAW_BODY_KEY
	 * sentinel. Callers enumerating real parameter names (e.g. incident logging)
	 * should consult isRawBodyFailClosed() and skip RAW_BODY_KEY so the synthetic
	 * key is not recorded as a real POST parameter.
	 *
	 * @return array All POST parameters.
	 */
	public function getAllPost() {
		return $this->post;
	}

	/**
	 * Whether the request body failed closed and RAW_BODY_KEY is synthetic.
	 *
	 * True only when maybeParseJsonBody() injected the sentinel because the JSON
	 * body could not be decoded; false when RAW_BODY_KEY (if present) is a real
	 * decoded parameter. Lets name-enumerating callers hide the synthetic key the
	 * same way getArgNames() does.
	 *
	 * @since 4.0.2
	 *
	 * @return bool
	 */
	public function isRawBodyFailClosed() {
		return $this->raw_body_fail_closed;
	}

	/**
	 * Throttled-error message for a fail-closed JSON body (empty if none).
	 *
	 * @since 4.0.2
	 *
	 * @return string
	 */
	public function failClosedMessage() {
		return null === $this->fail_closed_report ? '' : $this->fail_closed_report['message'];
	}

	/**
	 * Throttled-error code for a fail-closed JSON body (empty if none).
	 *
	 * @since 4.0.2
	 *
	 * @return string
	 */
	public function failClosedCode() {
		return null === $this->fail_closed_report ? '' : $this->fail_closed_report['code'];
	}

	/**
	 * Throttled-error context (reason/size metadata) for a fail-closed JSON body.
	 *
	 * @since 4.0.2
	 *
	 * @return array
	 */
	public function failClosedContext() {
		return null === $this->fail_closed_report ? array() : $this->fail_closed_report['context'];
	}

	/**
	 * Get all GET and POST parameters merged.
	 *
	 * POST values take precedence over GET when keys overlap.
	 *
	 * @since 3.0.0
	 *
	 * @return array All GET and POST parameters.
	 */
	public function getAllArgs() {
		return array_merge( $this->get, $this->post );
	}

	/**
	 * Values of every GET and POST parameter, GET first. A name sent in both
	 * sources contributes both values, so neither hides the other.
	 *
	 * @return array<int, mixed>
	 */
	public function getAllArgValues() {
		return array_merge( array_values( $this->get ), array_values( $this->post ) );
	}

	/**
	 * Check if any GET or POST parameters exist.
	 *
	 * @since 3.0.2
	 *
	 * @return bool True if at least one parameter exists.
	 */
	public function hasAnyArgs() {
		return ! empty( $this->get ) || ! empty( $this->post );
	}

	/**
	 * Get all cookies.
	 *
	 * @return array All cookies.
	 */
	public function getAllCookies() {
		return $this->cookies;
	}

	/**
	 * Get all file uploads.
	 *
	 * @return array All file uploads.
	 */
	public function getAllFiles() {
		return $this->files;
	}

	/**
	 * Get all server variables.
	 *
	 * @return array All server variables.
	 */
	public function getAllServer() {
		return $this->server;
	}

	/**
	 * Get the request URI.
	 *
	 * @return string The request URI.
	 */
	public function getUri() {
		return isset( $this->server['REQUEST_URI'] ) ? $this->server['REQUEST_URI'] : '';
	}

	/**
	 * The request path normalised the way a web server maps a URL to a file.
	 *
	 * Query and fragment are split off the raw (undecoded) REQUEST_URI first, so a
	 * percent-encoded ? or # in the path cannot pose as a delimiter. The raw path
	 * is then segmented on '/', each segment has its ';' matrix parameters
	 * stripped and is decoded once, as the server decodes it. An encoded slash
	 * stays inside its segment, as on Apache with AllowEncodedSlashes Off. Dot
	 * segments are then removed (RFC 3986). The decoded query is appended
	 * unchanged. The pass is linear, so there is no length cut-off: a cap would
	 * let a padded path skip normalisation while the server still maps it.
	 *
	 * @return string
	 */
	public function getNormalizedUri() {
		return $this->normalizeUri( false );
	}

	/**
	 * Same as getNormalizedUri(), except that a decoded %2f or %5c also separates
	 * segments before dot segments are removed. nginx, OpenLiteSpeed and Apache
	 * with AllowEncodedSlashes On resolve /x/..%2fwp-admin/post.php to
	 * /wp-admin/post.php this way.
	 *
	 * @return string
	 */
	public function getNormalizedUriWithEncodedSlashes() {
		return $this->normalizeUri( true );
	}

	/**
	 * Every normalised form of the request path a REQUEST_URI rule should be
	 * matched against, in addition to the raw URI:
	 *
	 * - the path as the server resolves it, with and without encoded slashes
	 *   as separators;
	 * - those paths relative to the WordPress home and site paths, for an
	 *   install in a subdirectory or core in its own directory;
	 * - the executed script relative to ABSPATH;
	 * - any of the above with a leading /index.php front-controller segment
	 *   removed, as WordPress routes /index.php/wp-json/... to the REST API;
	 * - a path under a custom REST prefix (rest_url_prefix filter) rewritten
	 *   to /wp-json/, the prefix the rules spell;
	 * - a /wp-json/ path, or a rest_route query value, with the route
	 *   lowercased, as WordPress matches REST routes case-insensitively.
	 *
	 * A rule written for /wp-admin/post.php then also matches
	 * /blog/wp-admin/post.php.
	 *
	 * @return string[] Distinct candidates.
	 */
	public function getNormalizedUriCandidates() {
		if ( null !== $this->normalized_uri_candidates ) {
			return $this->normalized_uri_candidates;
		}

		$paths      = array( $this->getNormalizedUri(), $this->getNormalizedUriWithEncodedSlashes() );
		$candidates = $paths;
		foreach ( $this->installBasePaths() as $base ) {
			foreach ( $paths as $path ) {
				$relative = self::stripBasePath( $path, $base );
				if ( null !== $relative ) {
					$candidates[] = $relative;
				}
			}
		}
		$script = $this->getScriptUriFromAbspath();
		if ( null !== $script ) {
			$candidates[] = $script;
		}

		foreach ( $candidates as $candidate ) {
			if ( 0 === strpos( $candidate, '/index.php/' ) ) {
				$candidates[] = (string) substr( $candidate, strlen( '/index.php' ) );
			}
		}

		$custom_prefix = $this->restUrlPrefix();
		if ( self::DEFAULT_REST_PREFIX !== $custom_prefix ) {
			foreach ( $candidates as $candidate ) {
				$route = self::stripBasePath( $candidate, $custom_prefix );
				if ( null !== $route ) {
					$candidates[] = '/' . self::DEFAULT_REST_PREFIX . $route;
				}
			}
		}

		$rest_prefix = '/' . self::DEFAULT_REST_PREFIX . '/';
		foreach ( $candidates as $candidate ) {
			if ( 0 !== strpos( $candidate, $rest_prefix ) ) {
				continue;
			}
			$qpos  = strpos( $candidate, '?' );
			$route = false === $qpos ? $candidate : substr( $candidate, 0, $qpos );
			$query = false === $qpos ? '' : substr( $candidate, $qpos );
			$lower = $rest_prefix . strtolower( (string) substr( $route, strlen( $rest_prefix ) ) );
			if ( $lower !== $route ) {
				$candidates[] = $lower . $query;
			}
		}

		foreach ( $candidates as $candidate ) {
			$lower = preg_replace_callback(
				'/([?&]rest_route=)([^&]*)/',
				function ( $m ) {
					return $m[1] . strtolower( $m[2] );
				},
				$candidate
			);
			if ( is_string( $lower ) && $lower !== $candidate ) {
				$candidates[] = $lower;
			}
		}

		$this->normalized_uri_candidates = array_values( array_unique( $candidates ) );
		return $this->normalized_uri_candidates;
	}

	/**
	 * Normalise the request path; see getNormalizedUri().
	 *
	 * @param bool $split_encoded_slashes Treat a decoded / or \ as a separator.
	 *
	 * @return string
	 */
	private function normalizeUri( $split_encoded_slashes ) {
		list( $raw_path, $raw_query ) = $this->splitRawUri();

		$leading  = ( '/' === substr( $raw_path, 0, 1 ) );
		$trailing = false;
		$out      = array();
		foreach ( explode( '/', $raw_path ) as $seg ) {
			$semi = strpos( $seg, ';' );
			if ( false !== $semi ) {
				$seg = substr( $seg, 0, $semi );
			}
			$seg   = urldecode( $seg );
			$parts = $split_encoded_slashes ? preg_split( '#[/\\\\]#', $seg ) : array( $seg );
			foreach ( (array) $parts as $part ) {
				$trailing = ( '' === $part || '.' === $part || '..' === $part );
				if ( '' === $part || '.' === $part ) {
					continue;
				}
				if ( '..' === $part ) {
					array_pop( $out );
					continue;
				}
				$out[] = $part;
			}
		}

		$path = ( $leading ? '/' : '' ) . implode( '/', $out );
		if ( $trailing && '' !== $path && '/' !== substr( $path, -1 ) ) {
			$path .= '/';
		}

		return $path . $this->decodeQuery( $raw_query );
	}

	/**
	 * Split the raw REQUEST_URI into its path and its query (with the leading
	 * '?'), dropping any fragment and an absolute-form scheme and authority.
	 *
	 * @return string[] Raw path and raw query; both undecoded.
	 */
	private function splitRawUri() {
		$raw  = $this->getUri();
		$hash = strpos( $raw, '#' );
		if ( false !== $hash ) {
			$raw = substr( $raw, 0, $hash );
		}
		$qpos      = strpos( $raw, '?' );
		$raw_path  = ( false === $qpos ) ? $raw : substr( $raw, 0, $qpos );
		$raw_query = ( false === $qpos ) ? '' : substr( $raw, $qpos );
		if ( preg_match( '#^[a-zA-Z][a-zA-Z0-9+.\-]*://[^/]*(/.*)?$#', $raw_path, $m ) ) {
			$raw_path = isset( $m[1] ) ? $m[1] : '/';
		}

		return array( $raw_path, $raw_query );
	}

	/**
	 * Decode a raw query the way the raw REQUEST_URI candidate is decoded.
	 *
	 * @param string $raw_query Raw query, including the leading '?', or ''.
	 *
	 * @return string
	 */
	private function decodeQuery( $raw_query ) {
		return '' === $raw_query ? '' : urldecode( urldecode( $raw_query ) );
	}

	/**
	 * The decoded home and site paths of the install, without slashes, when
	 * WordPress does not sit at the root: "blog" for https://example.org/blog.
	 *
	 * @return string[]
	 */
	private function installBasePaths() {
		$bases = array( $this->homeBasePath() );
		if ( function_exists( 'site_url' ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url is not available in the isolated Request unit environment.
			$path = parse_url( (string) site_url(), PHP_URL_PATH );
			if ( is_string( $path ) ) {
				$bases[] = trim( $path, '/' );
			}
		}

		$decoded = array();
		foreach ( $bases as $base ) {
			$base = rawurldecode( $base );
			if ( '' !== $base ) {
				$decoded[] = $base;
			}
		}

		return array_values( array_unique( $decoded ) );
	}

	/**
	 * A normalised path relative to a base path of the install:
	 * /blog/wp-admin/post.php becomes /wp-admin/post.php for "blog".
	 *
	 * @param string $path Normalised path, possibly with a query.
	 * @param string $base Base path without slashes.
	 *
	 * @return string|null Null when the path is outside the base path.
	 */
	private static function stripBasePath( $path, $base ) {
		$prefix = '/' . $base;
		$length = strlen( $prefix );
		if ( 0 !== strncasecmp( $path, $prefix, $length ) ) {
			return null;
		}

		$rest = (string) substr( $path, $length );
		if ( '' === $rest ) {
			return '/';
		}
		if ( '/' === $rest[0] ) {
			return $rest;
		}
		if ( '?' === $rest[0] ) {
			return '/' . $rest;
		}

		return null;
	}

	/**
	 * The REST URL prefix without slashes, "wp-json" unless filtered.
	 *
	 * @return string
	 */
	private function restUrlPrefix() {
		$prefix = function_exists( 'rest_get_url_prefix' ) ? trim( (string) rest_get_url_prefix(), '/' ) : '';

		return '' === $prefix ? self::DEFAULT_REST_PREFIX : $prefix;
	}

	/**
	 * The executed script as a path relative to ABSPATH, with the decoded query:
	 * /wp-admin/post.php?action=edit. This is the file the server runs, whatever
	 * form the request path took and wherever WordPress is installed. The root
	 * index.php is left out: every rewritten front-end URL runs it, so a rule
	 * aimed at a direct /index.php request would match them all.
	 *
	 * @return string|null Null when SCRIPT_FILENAME is not a file inside ABSPATH.
	 */
	private function getScriptUriFromAbspath() {
		if ( ! defined( 'ABSPATH' )
			|| ! isset( $this->server['SCRIPT_FILENAME'] )
			|| ! is_string( $this->server['SCRIPT_FILENAME'] ) ) {
			return null;
		}

		$script = str_replace( '\\', '/', $this->server['SCRIPT_FILENAME'] );
		$base   = rtrim( str_replace( '\\', '/', (string) ABSPATH ), '/' ) . '/';
		if ( 0 !== strpos( $script, $base ) ) {
			return null;
		}

		$relative = (string) substr( $script, strlen( $base ) );
		if ( '' === $relative || 'index.php' === $relative ) {
			return null;
		}

		list( , $raw_query ) = $this->splitRawUri();
		return '/' . $relative . $this->decodeQuery( $raw_query );
	}

	/**
	 * Resolve lazily because this Request predates WordPress routing.
	 *
	 * @since 4.1.3
	 *
	 * @return string|null Nonempty scalar route, or null before/without routing.
	 */
	public function getRestRoute() {
		global $wp;

		if ( ! is_object( $wp ) || ! isset( $wp->query_vars['rest_route'] ) ) {
			return null;
		}

		$route = $wp->query_vars['rest_route'];
		return is_string( $route ) && ! empty( $route ) ? $route : null;
	}

	/**
	 * Check if a file upload exists.
	 *
	 * @param string $key The file key.
	 *
	 * @return bool True if the file exists, false otherwise.
	 */
	public function hasFile( $key ) {
		return isset( $this->files[ $key ] ) && ! empty( $this->files[ $key ]['name'] );
	}

	/**
	 * Get a file upload.
	 *
	 * @param string $key The file key.
	 *
	 * @return array|null The file data or null if not found.
	 */
	public function getFile( $key ) {
		return isset( $this->files[ $key ] ) ? $this->files[ $key ] : null;
	}

	/**
	 * Get the original filename of an uploaded file.
	 *
	 * @since 3.0.2
	 *
	 * @param string $key The file field name.
	 *
	 * @return string|null Original filename, or null if not found.
	 */
	public function getFileName( $key ) {
		$file = $this->getFile( $key );
		if ( ! isset( $file['name'] ) ) {
			return null;
		}
		// Strip null bytes that could cause regex matching to silently truncate.
		return self::stripNullBytes( $file['name'] );
	}

	/**
	 * Get the client-provided MIME type of an uploaded file.
	 *
	 * WARNING: This value comes directly from $_FILES['type'] and is trivially
	 * spoofable by the client. WAF rules should use :name as the primary gate
	 * and treat :type only as a supplementary signal.
	 *
	 * @since 3.0.2
	 *
	 * @param string $key The file field name.
	 *
	 * @return string|null MIME type, or null if not found.
	 */
	public function getFileType( $key ) {
		$file = $this->getFile( $key );
		if ( ! isset( $file['type'] ) ) {
			return null;
		}
		// Strip null bytes that could cause regex matching to silently truncate.
		return self::stripNullBytes( $file['type'] );
	}

	/**
	 * Get the client-supplied relative path of an uploaded file.
	 *
	 * PHP 8.1+ adds ``full_path`` to every $_FILES entry. For a directory
	 * upload (``<input type="file" webkitdirectory>``) it carries the path the
	 * client chose, and unlike ``name`` it is NOT reduced to a basename — so it
	 * is the one sub-key that can still hold separators when ``name`` cannot.
	 * That makes it attacker-controlled in the same way ``name`` is, which is
	 * why it is a first-class sub-selector rather than something a rule can
	 * only reach through the raw entry.
	 *
	 * Goes through the same shape-preserving null-byte strip as
	 * getFileName()/getFileType() so every FILES sub-selector a rule can name
	 * is normalised the same way.
	 *
	 * Falls back to ``name`` when the sub-key is absent. This plugin supports
	 * PHP 7.4, where $_FILES has no full_path at all, and a selector that
	 * silently resolves to null there is worse than a narrow one: ``exists``
	 * and ``not_exists`` would BOTH be false for a request carrying an upload,
	 * so a rule gated on either never fires and the virtual patch is quietly
	 * inert on every pre-8.1 host. Degrading to the basename keeps extension
	 * rules working; only the separators full_path uniquely carries are lost.
	 *
	 * @since 4.1.1
	 *
	 * @param string $key The file field name.
	 *
	 * @return string|array|null Client-supplied path, the filename on PHP < 8.1,
	 *                           or null if there is no such upload.
	 */
	public function getFileFullPath( $key ) {
		$file = $this->getFile( $key );
		if ( ! isset( $file['full_path'] ) ) {
			return $this->getFileName( $key );
		}
		// Strip null bytes that could cause regex matching to silently truncate.
		return self::stripNullBytes( $file['full_path'] );
	}

	/**
	 * Strip null bytes from a $_FILES sub-value of any shape.
	 *
	 * A file input can be single (``zz``), multi (``zz[]``) or nested
	 * (``zz[x][]``); PHP mirrors that shape inside every $_FILES sub-key, so
	 * the sub-value is a string, a flat array, or a nested array. Recursing
	 * keeps the leaf filenames intact: str_replace() applied to a nested array
	 * casts each inner array to the literal string "Array" (and emits an
	 * attacker-triggerable "Array to string conversion" warning), which
	 * silently destroys the very value a FILES rule matches on.
	 *
	 * @since 4.1.1
	 *
	 * @param mixed $value A $_FILES sub-value.
	 * @param int   $depth Remaining recursion budget.
	 *
	 * @return mixed The value with null bytes removed, shape preserved.
	 */
	private static function stripNullBytes( $value, $depth = self::FILES_STRIP_MAX_DEPTH ) {
		if ( is_array( $value ) ) {
			if ( $depth <= 0 ) {
				return $value;
			}

			$stripped = array();
			foreach ( $value as $key => $item ) {
				$stripped[ $key ] = self::stripNullBytes( $item, $depth - 1 );
			}

			return $stripped;
		}

		return is_string( $value ) ? str_replace( "\0", '', $value ) : $value;
	}

	/**
	 * Read the first N bytes of an uploaded file's content.
	 *
	 * Validates that the file was genuinely uploaded via HTTP POST
	 * before reading, to prevent path-traversal attacks.
	 *
	 * @since 3.0.2
	 *
	 * @param string $key   The file field name.
	 * @param int    $limit Max bytes to read (default 8192).
	 *
	 * @return string|null File content, or null if not found/unreadable.
	 */
	public function getFileContent( $key, $limit = 8192 ) {
		$file = $this->getFile( $key );
		if ( ! isset( $file['tmp_name'] ) ) {
			return null;
		}

		// An uploaded temp file cannot change within a request, and every rule
		// carrying a :content condition re-resolves it. Without this, a ruleset
		// with N such rules re-reads every part N times — and scan-all made that
		// per-request cost max_file_uploads (20) times larger.
		$cacheKey = $key . '|' . (int) $limit;
		if ( array_key_exists( $cacheKey, $this->fileContentCache ) ) {
			return $this->fileContentCache[ $cacheKey ];
		}

		$content = $this->readUploadedParts(
			$file['tmp_name'],
			isset( $file['error'] ) ? $file['error'] : null,
			$limit
		);

		$this->fileContentCache[ $cacheKey ] = $content;

		return $content;
	}

	/**
	 * Read one upload, or every part of a multi/nested one.
	 *
	 * A multi-file input (``doc[]``) mirrors its shape into tmp_name and error
	 * alike, so both arrive as arrays. Reading tmp_name as a scalar made the
	 * error check see ``(int) array`` — always 1, never UPLOAD_ERR_OK — and
	 * return null, so :content silently skipped every multi-file upload:
	 * renaming a form part from ``file`` to ``file[]`` took its bytes out of
	 * scan-all's view entirely. Recursing keeps the two shapes in step.
	 *
	 * The is_uploaded_file() guard is applied per part, so a $_FILES entry a
	 * plugin rewrote cannot turn a rule into an arbitrary-file-read primitive.
	 * PHP caps a request at max_file_uploads parts (20 by default), which
	 * bounds the total read at that many times $limit.
	 *
	 * Bounded by $depth for the same reason ValueResolver bounds its own leaf
	 * walk: both recurse over the same plugin-mutable $_FILES, so both need the
	 * same defensive story. A real request cannot come close — PHP stops at
	 * max_input_nesting_level - 1.
	 *
	 * @since 4.1.1
	 *
	 * @param mixed $tmpName tmp_name sub-value: a path or an array of them.
	 * @param mixed $error   Matching error sub-value, or null when absent.
	 * @param int   $limit   Max bytes to read per part.
	 * @param int   $depth   Remaining recursion budget.
	 *
	 * @return string|array|null Bytes, a shape-preserving array of them, or null.
	 */
	private function readUploadedParts( $tmpName, $error, $limit, $depth = self::UPLOAD_PARTS_MAX_DEPTH ) {
		if ( is_array( $tmpName ) ) {
			if ( $depth <= 0 ) {
				return null;
			}

			$parts = array();

			foreach ( $tmpName as $index => $part ) {
				if ( is_array( $error ) ) {
					// A missing per-part code is not a licence to read the part.
					$partError = array_key_exists( $index, $error )
						? $error[ $index ]
						: UPLOAD_ERR_NO_FILE;
				} else {
					// tmp_name an array beside a scalar error is a shape only a
					// plugin rewriting $_FILES can produce. Apply the one code to
					// every part rather than treating each as uncheckable.
					$partError = $error;
				}

				$content = $this->readUploadedParts( $part, $partError, $limit, $depth - 1 );

				if ( null !== $content ) {
					$parts[ $index ] = $content;
				}
			}

			return empty( $parts ) ? null : $parts;
		}

		if ( null !== $error && UPLOAD_ERR_OK !== (int) $error ) {
			return null;
		}

		if ( ! is_string( $tmpName ) || '' === $tmpName ) {
			return null;
		}

		if ( ! $this->isUploadedFile( $tmpName ) ) {
			return null;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- reading uploaded temp file, not a WP filesystem operation.
		$fh = fopen( $tmpName, 'rb' );
		if ( false === $fh ) {
			return null;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fread -- reading uploaded temp file.
		$content = fread( $fh, $limit );
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- reading uploaded temp file.
		fclose( $fh );

		if ( false === $content ) {
			return null;
		}

		return $content;
	}

	/**
	 * Check whether a file path points to a genuinely uploaded file.
	 *
	 * Wraps is_uploaded_file() so that unit tests can override this
	 * method to allow reading non-uploaded temp files.
	 *
	 * @since 3.0.2
	 *
	 * @param string $path Temporary file path.
	 *
	 * @return bool True if the file was uploaded via HTTP POST.
	 */
	protected function isUploadedFile( $path ) {
		return is_uploaded_file( $path );
	}

	/**
	 * Resolve a nested GET parameter value using bracket-path segments.
	 *
	 * Navigates $this->get[$rootKey][$path[0]][$path[1]]... to the leaf value.
	 *
	 * @since 3.0.0
	 *
	 * @param string $rootKey Root parameter key.
	 * @param array  $path    Array of bracket-path segments.
	 *
	 * @return mixed|null The leaf value, or null if the path doesn't exist.
	 */
	public function resolveNestedGet( $rootKey, array $path ) {
		return self::resolveNestedPath( $this->get, $rootKey, $path );
	}

	/**
	 * Resolve a nested POST parameter value using bracket-path segments.
	 *
	 * Navigates $this->post[$rootKey][$path[0]][$path[1]]... to the leaf value.
	 *
	 * @since 3.0.0
	 *
	 * @param string $rootKey Root parameter key.
	 * @param array  $path    Array of bracket-path segments.
	 *
	 * @return mixed|null The leaf value, or null if the path doesn't exist.
	 */
	public function resolveNestedPost( $rootKey, array $path ) {
		return self::resolveNestedPath( $this->post, $rootKey, $path );
	}

	/**
	 * Navigate a nested array by root key and path segments.
	 *
	 * @since 3.0.0
	 *
	 * @param array  $data    The source array ($_GET or $_POST).
	 * @param string $rootKey Root key in $data.
	 * @param array  $path    Ordered path segments to traverse.
	 *
	 * @return mixed|null The leaf value, or null if any segment is missing.
	 */
	private static function resolveNestedPath( array $data, $rootKey, array $path ) {
		if ( ! isset( $data[ $rootKey ] ) ) {
			return null;
		}

		$current = $data[ $rootKey ];

		foreach ( $path as $segment ) {
			if ( ! is_array( $current ) || ! isset( $current[ $segment ] ) ) {
				return null;
			}
			$current = $current[ $segment ];
		}

		return $current;
	}

	/**
	 * Recursively extract all leaf string values from a (possibly nested) array.
	 *
	 * Used for scan-all ARGS evaluation when parameter values may be PHP arrays
	 * produced by bracket-notation form fields (e.g., param[key]=val).
	 *
	 * @since 3.0.0
	 *
	 * @param array $data     The array to walk.
	 * @param int   $maxDepth Maximum recursion depth (default 5).
	 * @param int   $maxCount Maximum number of leaf values to return (default 100).
	 *
	 * @return string[] Flat array of leaf string values.
	 */
	public static function extractLeafValues( array $data, $maxDepth = 5, $maxCount = 100 ) {
		$leaves = array();
		self::walkLeaves( $data, $maxDepth, $maxCount, $leaves, 0 );
		return $leaves;
	}

	/**
	 * Recursive helper for extractLeafValues().
	 *
	 * @param array $data     Current array level.
	 * @param int   $maxDepth Maximum recursion depth.
	 * @param int   $maxCount Maximum leaf count.
	 * @param array $leaves   Collected leaves (passed by reference).
	 * @param int   $depth    Current depth.
	 */
	private static function walkLeaves( array $data, $maxDepth, $maxCount, array &$leaves, $depth ) {
		if ( $depth >= $maxDepth ) {
			return;
		}

		foreach ( $data as $value ) {
			if ( count( $leaves ) >= $maxCount ) {
				return;
			}

			if ( is_array( $value ) ) {
				self::walkLeaves( $value, $maxDepth, $maxCount, $leaves, $depth + 1 );
			} elseif ( is_string( $value ) ) {
				$leaves[] = $value;
			}
		}
	}

	/**
	 * Values of the GET and POST parameters whose names match a regex pattern,
	 * GET first. A name sent in both sources contributes both values. Names
	 * are matched as strings: PHP turns a numeric name such as ?123=x into an
	 * integer key.
	 *
	 * @param string $pattern Regex pattern to match against parameter names (without delimiters/anchors).
	 *
	 * @return array<int, mixed>
	 */
	public function getMatchingArgValues( $pattern ) {
		$regex = self::argNameRegex( $pattern );
		if ( null === $regex ) {
			return array();
		}

		$values = array();
		foreach ( array( $this->get, $this->post ) as $source ) {
			foreach ( $source as $key => $value ) {
				if ( preg_match( $regex, (string) $key ) ) {
					$values[] = $value;
				}
			}
		}

		return $values;
	}

	/**
	 * Anchored regex for a parameter-name pattern, or null when it is invalid.
	 *
	 * @param string $pattern Regex pattern (without delimiters/anchors).
	 *
	 * @return string|null
	 */
	private static function argNameRegex( $pattern ) {
		$regex = '#^(?:' . str_replace( '#', '\\#', $pattern ) . ')$#';

		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- intentional; invalid regex must be silently skipped.
		if ( false === @preg_match( $regex, '' ) ) {
			return null;
		}

		return $regex;
	}

	/**
	 * Get all cookie values whose names match a regex pattern.
	 *
	 * @since 3.0.0
	 *
	 * @param string $pattern Regex pattern to match against cookie names (without delimiters/anchors).
	 *
	 * @return array<string, mixed> Associative array of matching cookie name => value pairs.
	 */
	public function getMatchingCookies( $pattern ) {
		$regex = '#^(?:' . str_replace( '#', '\\#', $pattern ) . ')$#';

		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- intentional; invalid regex must be silently skipped.
		if ( false === @preg_match( $regex, '' ) ) {
			return array();
		}

		$results = array();

		foreach ( $this->cookies as $key => $value ) {
			if ( is_string( $key ) && preg_match( $regex, $key ) ) {
				$results[ $key ] = $value;
			}
		}

		return $results;
	}

	/**
	 * Get a request header.
	 *
	 * @param string $key The header key.
	 *
	 * @return string|null The header value or null if not found.
	 */
	public function getHeader( $key ) {
		$normalized = strtoupper( str_replace( '-', '_', $key ) );

		if ( isset( $this->server[ 'HTTP_' . $normalized ] ) ) {
			return $this->server[ 'HTTP_' . $normalized ];
		}

		if ( in_array( $normalized, self::CGI_HEADERS, true ) && isset( $this->server[ $normalized ] ) ) {
			return $this->server[ $normalized ];
		}

		return null;
	}

	/**
	 * Convert a $_SERVER header key to its Title-Case header name.
	 *
	 * @since 4.1.0
	 *
	 * @param string $key Server key with any HTTP_ prefix already removed.
	 *
	 * @return string Header name (e.g. USER_AGENT => User-Agent).
	 */
	private static function headerNameFromKey( $key ) {
		return str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', $key ) ) ) );
	}

	/**
	 * Check if a request header exists.
	 *
	 * @since 3.0.0
	 *
	 * @param string $key The header key (e.g. 'User-Agent', 'X-Custom').
	 *
	 * @return bool True if the header exists, false otherwise.
	 */
	public function hasHeader( $key ) {
		return null !== $this->getHeader( $key );
	}

	/**
	 * Get all header values whose names match a regex pattern.
	 *
	 * PHP normalises header names to HTTP_UPPER_CASE in $_SERVER, so this
	 * method converts them back to Title-Case (e.g. HTTP_USER_AGENT → User-Agent)
	 * and applies a case-insensitive regex match because HTTP header names are
	 * case-insensitive per RFC 7230.
	 *
	 * @since 3.0.0
	 *
	 * @param string $pattern Regex pattern to match against header names (without delimiters/anchors).
	 *
	 * @return array<string, string> Associative array of matching header name => value pairs.
	 */
	public function getMatchingHeaders( $pattern ) {
		$regex = '#^(?:' . str_replace( '#', '\\#', $pattern ) . ')$#i';

		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- intentional; invalid regex must be silently skipped.
		if ( false === @preg_match( $regex, '' ) ) {
			return array();
		}

		$results = array();

		foreach ( $this->getAllHeaders() as $headerName => $value ) {
			if ( preg_match( $regex, $headerName ) ) {
				$results[ $headerName ] = $value;
			}
		}

		return $results;
	}

	/**
	 * Get all HTTP request headers as an associative array.
	 *
	 * Extracts headers from $_SERVER entries with HTTP_ prefix and
	 * returns them with normalised Title-Case names.
	 *
	 * @since 3.0.0
	 *
	 * @return array<string, string> Header name => value pairs.
	 */
	public function getAllHeaders() {
		$headers = array();

		foreach ( $this->server as $key => $value ) {
			if ( 0 !== strpos( $key, 'HTTP_' ) || ! is_string( $value ) ) {
				continue;
			}
			$headers[ self::headerNameFromKey( substr( $key, 5 ) ) ] = $value;
		}

		// An HTTP_-prefixed spelling wins, matching getHeader().
		foreach ( self::CGI_HEADERS as $key ) {
			$headerName = self::headerNameFromKey( $key );
			if ( ! isset( $headers[ $headerName ] ) && isset( $this->server[ $key ] ) && is_string( $this->server[ $key ] ) ) {
				$headers[ $headerName ] = $this->server[ $key ];
			}
		}

		return $headers;
	}

	/**
	 * Get all parameter key names from GET and POST as a flat list.
	 *
	 * Recursively flattens nested arrays into bracket-path keys matching
	 * PHP's $_POST structure (e.g., param[key][subkey]).
	 *
	 * @since 3.0.2
	 *
	 * @return string[] Unique array of all parameter key names.
	 */
	public function getArgNames() {
		$keys = array();
		$this->collectKeys( $this->get, '', $keys );
		$this->collectKeys( $this->post, '', $keys );
		return array_values( array_unique( $this->withoutInjectedRawBodyKey( $keys ) ) );
	}

	/**
	 * Get the POST parameter names, excluding the synthetic fail-closed sentinel.
	 *
	 * For callers that enumerate or log POST parameter names (e.g. incident
	 * records): the injected RAW_BODY_KEY is not a client-supplied parameter and
	 * must not be recorded as one. Mirrors getArgNames()'s handling.
	 *
	 * @since 4.0.2
	 *
	 * @return string[] POST parameter names with the injected sentinel removed.
	 */
	public function getPostNames() {
		return $this->withoutInjectedRawBodyKey( array_keys( $this->post ) );
	}

	/**
	 * Remove the synthetic fail-closed sentinel (RAW_BODY_KEY) from a name list.
	 *
	 * Stripped only when we injected it, so a real GET parameter that happens to
	 * share the name survives (the synthetic key only ever lives in $this->post).
	 *
	 * @since 4.0.2
	 *
	 * @param string[] $names Parameter names.
	 *
	 * @return string[] Names with the injected sentinel removed.
	 */
	private function withoutInjectedRawBodyKey( array $names ) {
		if ( ! $this->raw_body_fail_closed || isset( $this->get[ self::RAW_BODY_KEY ] ) ) {
			return $names;
		}
		return array_values(
			array_filter(
				$names,
				function ( $k ) {
					return self::RAW_BODY_KEY !== $k;
				}
			)
		);
	}

	/**
	 * Recursively collect all array keys as bracket-path strings.
	 *
	 * @since 3.0.2
	 *
	 * @param array  $arr    Array to walk.
	 * @param string $prefix Current bracket-path prefix.
	 * @param array  $keys   Collected keys (passed by reference).
	 * @param int    $depth  Current recursion depth.
	 */
	private function collectKeys( $arr, $prefix, &$keys, $depth = 0 ) {
		if ( ! is_array( $arr ) || $depth >= self::MAX_KEY_DEPTH || count( $keys ) >= self::MAX_KEY_COUNT ) {
			return;
		}
		foreach ( $arr as $k => $v ) {
			if ( count( $keys ) >= self::MAX_KEY_COUNT ) {
				return;
			}
			$full   = '' === $prefix ? (string) $k : $prefix . '[' . $k . ']';
			$keys[] = $full;
			if ( is_array( $v ) ) {
				$this->collectKeys( $v, $full, $keys, $depth + 1 );
			}
		}
	}

	/**
	 * Extract the physical request method from server variables.
	 *
	 * @param array $server Server variables.
	 *
	 * @return string The HTTP method.
	 */
	private function extractPhysicalMethod( $server ) {
		if ( isset( $server['REQUEST_METHOD'] ) ) {
			return $server['REQUEST_METHOD'];
		}

		// Default to GET if no method is found.
		return 'GET';
	}

	/**
	 * Resolve the method WordPress will dispatch this request as.
	 *
	 * @since 4.1.1
	 *
	 * @return string The HTTP method.
	 */
	private function resolveEffectiveMethod() {
		/*
		 * WordPress checks the query parameter first and consults the header
		 * only when it is absent, so a present-but-unusable _method masks the
		 * header rather than falling through to it.
		 */
		if ( isset( $this->get['_method'] ) ) {
			$override = $this->get['_method'];
		} else {
			$override = $this->getHeader( 'X-HTTP-Method-Override' );
		}

		if ( ! is_string( $override ) ) {
			return $this->method;
		}

		$override = strtoupper( $override );
		if ( ! in_array( $override, self::OVERRIDABLE_METHODS, true ) ) {
			return $this->method;
		}

		// Avoid REST routing work unless a valid override could change enforcement.
		if ( strtoupper( $this->method ) === $override || ! $this->isRestRequest() ) {
			return $this->method;
		}

		return $override;
	}

	/**
	 * Check whether WordPress will serve this request through the REST server.
	 *
	 * Deliberately conservative: the ?rest_route= form and REST rewrites count
	 * only when the server executes the installation's front controller or an
	 * exact standard core script that bootstraps the same WordPress dispatcher.
	 * REST detection controls additional logical-verb enforcement only. The
	 * physical method is always enforced independently.
	 *
	 * @since 4.1.1
	 *
	 * @return bool True when the request targets the REST API.
	 */
	private function isRestRequest() {
		if ( null !== $this->is_rest_request ) {
			return $this->is_rest_request;
		}

		$route_status = $this->restRouteQueryVarStatus();
		if ( null !== $route_status ) {
			$this->is_rest_request = $route_status && $this->isRestDispatchEntryScript();

			return $this->is_rest_request;
		}

		$this->is_rest_request = $this->isRestDispatchEntryScript() && $this->matchesRestRewrite();

		return $this->is_rest_request;
	}

	/**
	 * Check whether the executing script can enter WordPress REST dispatch.
	 *
	 * @since 4.1.1
	 *
	 * @return bool True for the installation index or an exact core entry script.
	 */
	private function isRestDispatchEntryScript() {
		return $this->isInstallationIndexScript() || $this->isAbspathCoreRestEntryScript();
	}

	/**
	 * Mirror WordPress's public-query-variable source and mismatch semantics.
	 *
	 * @since 4.1.1
	 *
	 * @return bool|null True for a usable route, false for an explicit unusable
	 *                   route/mismatch, or null when neither source contains it.
	 */
	private function restRouteQueryVarStatus() {
		$in_get  = isset( $this->get['rest_route'] );
		$in_post = isset( $this->form_post['rest_route'] );
		$route   = null;
		if ( ! $in_get && ! $in_post ) {
			return null;
		}

		if ( $in_get && $in_post && $this->get['rest_route'] !== $this->form_post['rest_route'] ) {
			return false;
		}

		if ( $in_post ) {
			$route = $this->form_post['rest_route'];
		} elseif ( $in_get ) {
			$route = $this->get['rest_route'];
		}

		return is_scalar( $route ) && ! empty( $route );
	}

	/**
	 * Check whether the server executed the installation's front controller.
	 *
	 * @since 4.1.1
	 *
	 * @return bool True when SCRIPT_NAME identifies the public front controller.
	 */
	private function isInstallationIndexScript() {
		if ( ! isset( $this->server['SCRIPT_NAME'] ) || ! is_string( $this->server['SCRIPT_NAME'] ) ) {
			return false;
		}

		$script   = '/' . ltrim( str_replace( '\\', '/', $this->server['SCRIPT_NAME'] ), '/' );
		$home     = $this->homeBasePath();
		$expected = '/' . ( '' === $home ? '' : $home . '/' ) . 'index.php';

		/*
		 * A virtual subsite can use /site/index.php URLs while the web server
		 * still executes the network's root /index.php front controller.
		 */
		return '/index.php' === $script
			|| $expected === $script
			|| $this->isAbspathIndexScript();
	}

	/**
	 * Check the filesystem identity used by ordinary physical subdirectory installs.
	 *
	 * @since 4.1.1
	 *
	 * @return bool True when SCRIPT_FILENAME is ABSPATH/index.php.
	 */
	private function isAbspathIndexScript() {
		if ( ! defined( 'ABSPATH' )
			|| ! isset( $this->server['SCRIPT_FILENAME'] )
			|| ! is_string( $this->server['SCRIPT_FILENAME'] ) ) {
			return false;
		}

		$script   = rtrim( str_replace( '\\', '/', $this->server['SCRIPT_FILENAME'] ), '/' );
		$expected = rtrim( str_replace( '\\', '/', (string) ABSPATH ), '/' ) . '/index.php';

		return $script === $expected;
	}

	/**
	 * Check exact filesystem identity for standard non-index core entry scripts.
	 *
	 * These scripts invoke wp() or wp-blog-header.php and can therefore reach
	 * the same REST rewrites as index.php. Only the canonical ABSPATH identity
	 * is accepted; URL names, basenames, suffixes and alternate paths are not.
	 *
	 * @since 4.1.1
	 *
	 * @return bool True when SCRIPT_FILENAME is an allow-listed ABSPATH script.
	 */
	private function isAbspathCoreRestEntryScript() {
		if ( ! defined( 'ABSPATH' )
			|| ! isset( $this->server['SCRIPT_FILENAME'] )
			|| ! is_string( $this->server['SCRIPT_FILENAME'] ) ) {
			return false;
		}

		$script = rtrim( str_replace( '\\', '/', $this->server['SCRIPT_FILENAME'] ), '/' );
		$base   = rtrim( str_replace( '\\', '/', (string) ABSPATH ), '/' );

		foreach ( self::CORE_REST_ENTRY_SCRIPTS as $entry ) {
			if ( $script === $base . '/' . $entry ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check the ordinary front-controller path against WordPress's rewrite order.
	 *
	 * @since 4.1.1
	 *
	 * @return bool True when the first matching rewrite targets rest_route.
	 */
	private function matchesRestRewrite() {
		$rules                                   = $this->getRewriteRules();
		list( $requested_path, $requested_file ) = $this->rewriteRequestPaths();
		$request_match                           = $requested_path;

		foreach ( $rules as $match => $query ) {
			/*
			 * WP::parse_request() prepends the requested file only for rewrite
			 * patterns anchored to it. Keep request_match outside the loop: core
			 * does not reset it after an index-anchored rule changes it.
			 */
			if ( ! empty( $requested_file )
				&& 0 === strpos( $match, $requested_file )
				&& $requested_file !== $requested_path
			) {
				$request_match = $requested_file . '/' . $requested_path;
			}

			// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Stored third-party rewrite regexes can be malformed; fail closed as non-matching.
			$matched = @preg_match( "#^$match#", $request_match, $matches );
			if ( 1 !== $matched ) {
				// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Mirrors WordPress's one-pass decoded retry without exposing regex warnings.
				$matched = @preg_match( "#^$match#", urldecode( $request_match ), $matches );
			}
			if ( 1 !== $matched ) {
				continue;
			}
			if ( $this->shouldSkipVerbosePageRule( $query, $matches ) ) {
				continue;
			}

			return $this->rewriteTargetsRestRoute( $query, $matches );
		}

		return false;
	}

	/**
	 * Check whether WordPress will reject a matching verbose page rule.
	 *
	 * Defender runs before core constructs WP_Rewrite, so derive the verbose
	 * page flag from the same permalink option and expression as WP_Rewrite::init().
	 * A missing page or a status excluded by WP::parse_request() lets matching
	 * continue to the next persisted rewrite rule.
	 *
	 * @since 4.1.1
	 *
	 * @param mixed $query   Rewrite target.
	 * @param array $matches Regex captures from the matched request path.
	 *
	 * @return bool True when rewrite matching must continue.
	 */
	private function shouldSkipVerbosePageRule( $query, $matches ) {
		if ( ! $this->usesVerbosePageRules() || ! is_string( $query ) ) {
			return false;
		}

		if ( ! preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch ) ) {
			return false;
		}

		$capture = $varmatch[1];
		if ( ! array_key_exists( $capture, $matches )
			|| ! is_scalar( $matches[ $capture ] )
			|| ! function_exists( 'get_page_by_path' )
			|| ! function_exists( 'get_post_status_object' )
		) {
			return false;
		}

		$page = get_page_by_path( $matches[ $capture ] );
		if ( ! $page ) {
			return true;
		}
		if ( ! is_object( $page ) || ! property_exists( $page, 'post_status' ) || ! is_scalar( $page->post_status ) ) {
			return false;
		}

		$post_status_obj = get_post_status_object( $page->post_status );
		if ( ! is_object( $post_status_obj )
			|| ! isset( $post_status_obj->public )
			|| ! isset( $post_status_obj->protected )
			|| ! isset( $post_status_obj->private )
			|| ! isset( $post_status_obj->exclude_from_search )
		) {
			return false;
		}

		if ( ! $post_status_obj->public && ! $post_status_obj->protected
			&& ! $post_status_obj->private && $post_status_obj->exclude_from_search ) {
			return true;
		}

		return false;
	}

	/**
	 * Check the permalink form that enables WP_Rewrite verbose page rules.
	 *
	 * @since 4.1.1
	 *
	 * @return bool True when WordPress will validate verbose page matches.
	 */
	private function usesVerbosePageRules() {
		if ( ! function_exists( 'get_option' ) ) {
			return false;
		}

		$permalink_structure = get_option( 'permalink_structure' );

		return is_string( $permalink_structure )
			&& 1 === preg_match( '/^[^%]*%(?:postname|category|tag|author)%/', $permalink_structure );
	}

	/**
	 * Resolve a matched rewrite target as WP::parse_request() does.
	 *
	 * Capture substitution and parse_str() matter here: query keys such as
	 * rest%5Froute normalize to rest_route before WordPress dispatches them.
	 *
	 * @since 4.1.1
	 *
	 * @param mixed $query   Rewrite target.
	 * @param array $matches Regex captures from the matched request path.
	 *
	 * @return bool True when the resolved target supplies a REST route.
	 */
	private function rewriteTargetsRestRoute( $query, $matches ) {
		if ( ! is_string( $query ) ) {
			return false;
		}

		$query = preg_replace( '!^.+\?!', '', $query );
		$query = preg_replace_callback(
			'/(\$matches\[([1-9][0-9]*)\])/',
			function ( $reference ) use ( $matches ) {
				$index = (int) $reference[2];
				// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode -- Exact WP_MatchesMapRegex capture encoding.
				return isset( $matches[ $index ] ) ? urlencode( $matches[ $index ] ) : '';
			},
			$query
		);

		$vars = array();
		parse_str( addslashes( $query ), $vars );

		return isset( $vars['rest_route'] )
			&& is_scalar( $vars['rest_route'] )
			&& ! empty( $vars['rest_route'] );
	}

	/**
	 * Get persisted rewrite rules, with core REST rules as a no-option fallback.
	 *
	 * Calling WP_Rewrite::wp_rewrite_rules() here could regenerate rules before
	 * all init callbacks register theirs, changing routing. Read only already
	 * established rules; the fallback mirrors rest_api_register_rewrites().
	 *
	 * @since 4.1.1
	 *
	 * @return array Rewrite regex => query mappings.
	 */
	private function getRewriteRules() {
		if ( function_exists( 'get_option' ) ) {
			$rules = get_option( 'rewrite_rules' );
			if ( is_array( $rules ) && ! empty( $rules ) ) {
				return $rules;
			}
			if ( empty( get_option( 'permalink_structure' ) ) ) {
				return array();
			}
		}
		if ( isset( $GLOBALS['wp_rewrite']->rules )
			&& is_array( $GLOBALS['wp_rewrite']->rules )
			&& ! empty( $GLOBALS['wp_rewrite']->rules ) ) {
			return $GLOBALS['wp_rewrite']->rules;
		}

		$prefix = self::DEFAULT_REST_PREFIX;
		if ( function_exists( 'rest_get_url_prefix' ) ) {
			$prefix = trim( (string) rest_get_url_prefix(), '/' );
		}
		if ( '' === $prefix ) {
			return array();
		}

		$index = 'index.php';
		if ( isset( $GLOBALS['wp_rewrite']->index ) && is_string( $GLOBALS['wp_rewrite']->index ) ) {
			$index = trim( $GLOBALS['wp_rewrite']->index, '/' );
		}

		return array(
			'^' . $prefix . '/?$'                   => 'index.php?rest_route=/',
			'^' . $prefix . '/(.*)?'                => 'index.php?rest_route=/$matches[1]',
			'^' . $index . '/' . $prefix . '/?$'    => 'index.php?rest_route=/',
			'^' . $index . '/' . $prefix . '/(.*)?' => 'index.php?rest_route=/$matches[1]',
		);
	}

	/**
	 * Build WP::parse_request()'s requested path and requested file.
	 *
	 * PATH_INFO handling cannot be replaced with slash normalization. WordPress
	 * percent-protects it, removes that exact value from REQUEST_URI, strips the
	 * home path from both values, and then chooses which one rewrite rules see.
	 *
	 * @since 4.1.1
	 *
	 * @return string[] Requested path followed by requested file.
	 */
	private function rewriteRequestPaths() {
		$pathinfo = isset( $this->server['PATH_INFO'] ) && is_string( $this->server['PATH_INFO'] )
			? $this->server['PATH_INFO']
			: '';
		$mark     = strpos( $pathinfo, '?' );
		if ( false !== $mark ) {
			$pathinfo = substr( $pathinfo, 0, $mark );
		}
		$pathinfo = str_replace( '%', '%25', $pathinfo );

		$req_uri = $this->getUri();
		$mark    = strpos( $req_uri, '?' );
		if ( false !== $mark ) {
			$req_uri = substr( $req_uri, 0, $mark );
		}

		$req_uri  = str_replace( $pathinfo, '', $req_uri );
		$req_uri  = trim( $req_uri, '/' );
		$pathinfo = trim( $pathinfo, '/' );
		$home     = $this->homeBasePath();

		if ( '' !== $home ) {
			$home_regex = '|^' . preg_quote( $home, '|' ) . '|i';
			$req_uri    = preg_replace( $home_regex, '', $req_uri );
			$req_uri    = trim( $req_uri, '/' );
			$pathinfo   = preg_replace( $home_regex, '', $pathinfo );
			$pathinfo   = trim( $pathinfo, '/' );
		}

		$index = 'index.php';
		if ( isset( $GLOBALS['wp_rewrite']->index ) && is_string( $GLOBALS['wp_rewrite']->index ) ) {
			$index = $GLOBALS['wp_rewrite']->index;
		}

		if ( ! empty( $pathinfo ) && ! preg_match( '|^.*' . $index . '$|', $pathinfo ) ) {
			$requested_path = $pathinfo;
		} else {
			if ( $req_uri === $index ) {
				$req_uri = '';
			}
			$requested_path = $req_uri;
		}

		$requested_file = $req_uri;

		return array( $requested_path, $requested_file );
	}

	/**
	 * Get the URL path WordPress strips before rewrite matching.
	 *
	 * @since 4.1.1
	 *
	 * @return string Home path without leading or trailing slashes.
	 */
	private function homeBasePath() {
		$home = '';
		if ( function_exists( 'home_url' ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url is not available in the isolated Request unit environment.
			$path = parse_url( home_url(), PHP_URL_PATH );
			if ( is_string( $path ) ) {
				$home = trim( $path, '/' );
			}
		}
		if ( '' !== $home ) {
			return $home;
		}

		if ( ! isset( $this->server['SCRIPT_NAME'] ) || ! $this->isAbspathIndexScript() ) {
			return '';
		}

		$base = str_replace( '\\', '/', dirname( (string) $this->server['SCRIPT_NAME'] ) );

		return '/' === $base ? '' : trim( $base, '/' );
	}
}

Directory Contents

Dirs: 2 × Files: 16
Name Size Perms Modified Actions
Handler DIR
- drwxr-xr-x 2026-11-03 01:06:29
Edit Download
Model DIR
- drwxr-xr-x 2026-11-03 01:06:29
Edit Download
1.97 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
3.44 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
22.33 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
22.10 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
4.28 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
945 B lrw-r--r-- 2026-11-03 01:06:29
Edit Download
6.88 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
17.87 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
3.08 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
62.62 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
4.92 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
18.52 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
4.44 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
14.27 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
9.23 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
28.98 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
If ZipArchive is unavailable, a .tar will be created (no compression).
© 2026 0xNothings — Secure File Manager. All rights reserved. Built with ❤️ & Tailwind x Dark UI