�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: ValueResolver.php Size: 28.98 KB
/home2/astrociv/public_html/gotransit.in/wp-content/plugins/imunify-security/inc/App/Defender/ValueResolver.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;

use CloudLinux\Imunify\App\Defender\Model\Condition;
use CloudLinux\Imunify\App\Defender\Model\ConditionSource;

/**
 * Resolves candidate values from a Request based on a parsed condition name.
 *
 * Handles the three resolution modes (field regex, scan-all, single field),
 * bracket-path navigation, URI decoding, and source dispatching.
 *
 * @since 3.0.0
 */
class ValueResolver {

	/**
	 * FILES sub-selectors that resolve to a value.
	 *
	 * Mirrors the cases handled by getFilesSubValue(); any other sub-selector
	 * resolves to null for every request.
	 *
	 * @since 4.1.0
	 *
	 * @var string[]
	 */
	const FILES_SUB_SELECTORS = array( 'name', 'filename', 'type', 'content', 'full_path' );

	/**
	 * Lower bound for the FILES leaf-extraction depth.
	 *
	 * In practice this IS the operative value: the derived term only overtakes
	 * it once ``max_input_nesting_level`` exceeds 126, so on a default host
	 * (64) and on any hardened one the depth in effect is 128. The derivation
	 * exists for the hosts above that, and the floor for the host where
	 * ini_get() is unreadable and casts to 0 — where a derived depth alone
	 * would flatten nothing at all. 128 is double the 63 levels PHP accepts at
	 * the default ini.
	 *
	 * @since 4.1.1
	 *
	 * @var int
	 */
	const FILES_LEAF_DEPTH_FLOOR = 128;

	/**
	 * Upper bound for the FILES leaf-extraction depth.
	 *
	 * Leaf extraction recurses, so the depth must stay finite even if a host sets
	 * an absurd ``max_input_nesting_level``. PHP cannot deliver a $_FILES tree
	 * anywhere near this deep; the ceiling exists so that a userland-mutated
	 * $_FILES (a plugin may rewrite it before the WAF runs) cannot turn a
	 * pathological single chain into unbounded recursion.
	 *
	 * @since 4.1.1
	 *
	 * @var int
	 */
	const FILES_LEAF_DEPTH_CEILING = 1024;

	/**
	 * Lower bound for the number of FILES leaves to collect.
	 *
	 * Derived alongside the depth for the same reason, and stated separately
	 * because the two fail differently: an exceeded depth stops a branch, an
	 * exceeded count stops collection outright and drops leaves silently. PHP
	 * caps a request at ``max_file_uploads`` parts (20 by default), so this is
	 * an order of magnitude above anything a real request can produce.
	 *
	 * @since 4.1.1
	 *
	 * @var int
	 */
	const FILES_LEAF_COUNT_FLOOR = 256;

	/**
	 * Resolve candidate values for a condition from the request.
	 *
	 * Returns a mixed[] of values (strings or arrays) that should each be
	 * tested by the caller's matcher. The caller is responsible for type
	 * checking and leaf extraction on array values.
	 *
	 * @param Condition $condition The condition to resolve values for.
	 * @param Request   $request   Request object.
	 *
	 * @return array Candidate values to test.
	 */
	public function resolveValues( Condition $condition, Request $request ) {
		if ( ! $condition->hasRequiredFields() ) {
			return array();
		}

		$parsed = $condition->parseName();
		$source = $parsed['source'];
		$values = array();

		if ( null !== $parsed['field_regex'] ) {
			$regexValues = $this->getFieldValuesByRegex( $request, $source, $parsed['field_regex'] );

			if ( null !== $parsed['bracket_path'] ) {
				$values = $this->navigateBracketPathIntoValues( array_values( $regexValues ), $parsed['bracket_path'] );
			} else {
				$values = array_values( $regexValues );
			}
		} elseif ( null === $parsed['field'] && $this->isCollectionSource( $source ) ) {
			$values = array_values( $this->getAllSourceValues( $request, $source ) );
		} elseif ( ConditionSource::FILES === $source && self::isFilesScanAll( $parsed ) ) {
			$filesParsed = self::parseFilesField( $parsed['field'] );
			$values      = self::getAllFilesSubValues( $request, $filesParsed['sub'] );
		} elseif ( ConditionSource::REQUEST_URI === $source ) {
			$values = $this->getUriMatchCandidates( $request );
		} elseif ( null !== $parsed['bracket_path'] && self::bracketPathHasRegex( $parsed['bracket_path'] ) ) {
			$values = $this->resolveFieldWithRegexBrackets( $request, $parsed );
		} elseif ( ConditionSource::ARGS === $source && null !== $parsed['field'] ) {
			$values = $this->collectArgOccurrences( $request, $parsed );
		} else {
			$value = $this->getFieldValue( $request, $parsed );

			if ( null !== $value ) {
				// FILES is already reduced to leaf strings by getFieldValue(), so
				// a multi or nested input contributes one candidate per part —
				// the same flat shape scan-all produces. Keeping FILES flat here
				// means no FILES value ever relies on the matcher's own leaf walk,
				// whose depth is tuned for ARGS.
				$values = ConditionSource::FILES === $source && is_array( $value )
					? $value
					: array( $value );
			}
		}

		// ARGS_NAMES is a field-less source — always returns all key names regardless of parsed field.
		if ( ConditionSource::ARGS_NAMES === $source ) {
			return $request->getArgNames();
		}

		if ( ConditionSource::ARGS === $source && ! empty( $values ) ) {
			return self::decodeArgValues( $values );
		}

		return $values;
	}

	/**
	 * Get field value from request based on parsed condition name.
	 *
	 * Supports bracket-notation for nested PHP arrays (e.g., ARGS:param[key]).
	 * Resolution order: nested array traversal first, literal key fallback.
	 *
	 * @param Request $request Request object.
	 * @param array   $parsed  Parsed condition name from Condition::parseNameString().
	 *
	 * @return string|array<string, mixed>|null Field value or null if not found.
	 */
	public function getFieldValue( $request, $parsed ) {
		$source      = $parsed['source'];
		$field       = $parsed['field'];
		$bracketPath = isset( $parsed['bracket_path'] ) ? $parsed['bracket_path'] : null;
		$rawField    = isset( $parsed['raw_field'] ) ? $parsed['raw_field'] : null;

		switch ( $source ) {
			case ConditionSource::ARGS:
				if ( null === $field ) {
					return null;
				}
				if ( null !== $bracketPath ) {
					// POST-wins, as for a plain field below.
					$value = $request->resolveNestedPost( $field, $bracketPath );
					if ( null === $value ) {
						$value = $request->resolveNestedGet( $field, $bracketPath );
					}
					if ( null !== $value ) {
						return $value;
					}
					$value = $request->post( $rawField );
					if ( null === $value ) {
						$value = $request->get( $rawField );
					}
					return $value;
				}
				$fieldValue = $request->post( $field );
				if ( null === $fieldValue ) {
					$fieldValue = $request->get( $field );
				}
				return $fieldValue;
			case ConditionSource::REST_ROUTE:
				return null === $field && null === $parsed['field_regex'] && null === $bracketPath
					? $request->getRestRoute() : null;
			case ConditionSource::REQUEST_URI:
				return $this->getDecodedUri( $request );
			case ConditionSource::FILES:
				if ( null === $field ) {
					return null;
				}
				if ( self::hasIndexedBracketPath( $parsed ) ) {
					// A FILES condition cannot navigate into a sub-value, so an
					// indexed path is a request the engine cannot honour. Resolve
					// to nothing rather than silently testing the whole value —
					// see flattenFilesValue()'s note on failing closed.
					return null;
				}
				$filesParsed = self::parseFilesField( $field );
				if ( null !== $filesParsed['sub'] ) {
					return self::flattenFilesValue(
						self::getFilesSubValue( $request, $filesParsed['field'], $filesParsed['sub'] )
					);
				}
				// The bare-entry form resolves to the entry's leaf values, not the
				// raw $_FILES entry: a rule matching FILES:<field> is asking about
				// the strings a client supplied, and the associative entry is not
				// something a matcher can test. Drops the non-string sub-keys
				// (error, size) as a side effect — see the CHANGELOG entry.
				return self::flattenFilesValue(
					self::withoutServerSubKeys( $request->getFile( $filesParsed['field'] ) )
				);
			case ConditionSource::REQUEST_COOKIES:
				if ( null === $field ) {
					return null;
				}
				return $request->cookie( $field );
			case ConditionSource::REQUEST_HEADERS:
				if ( null === $field ) {
					return null;
				}
				return $request->getHeader( $field );
			default:
				return null;
		}
	}

	/**
	 * Check whether a FILES field selects every uploaded file.
	 *
	 * Scan-all is written with an empty field before the sub-selector —
	 * ``FILES::name`` — mirroring ``ARGS``'s field-less scan-all. FILES cannot
	 * reuse the bare-source form (``FILES``) because a file has no single
	 * "value": the sub-selector is what says which part of each upload to test.
	 *
	 * Only the sub-selectors getFilesSubValue() understands qualify, so a
	 * typo such as ``FILES::nmae`` resolves to nothing instead of silently
	 * scanning every file with the wrong selector. An indexed bracket path
	 * disqualifies it for the same reason: FILES cannot navigate into a
	 * sub-value, so ``FILES::name[0]`` must not quietly scan every upload.
	 *
	 * @since 4.1.1
	 *
	 * @param array $parsed Parsed condition name from Condition::parseNameString().
	 *
	 * @return bool True if the field selects all uploaded files.
	 */
	public static function isFilesScanAll( array $parsed ) {
		$field = $parsed['field'];

		if ( ! is_string( $field ) || '' === $field ) {
			return false;
		}

		if ( self::hasIndexedBracketPath( $parsed ) ) {
			return false;
		}

		$filesParsed = self::parseFilesField( $field );

		return '' === $filesParsed['field']
			&& null !== $filesParsed['sub']
			&& in_array( $filesParsed['sub'], self::FILES_SUB_SELECTORS, true );
	}

	/**
	 * Check whether a bracket path asks to navigate into a value.
	 *
	 * An empty pair is not navigation: ``FILES:wc-upload-file[]`` is how a rule
	 * author writes the HTML name of a multi-file input, and PHP drops the
	 * brackets when it builds the $_FILES key, so the base field is what the
	 * request actually carries. A path with any non-empty segment
	 * (``FILES::name[0]``) IS navigation, and FILES has no resolver for it.
	 *
	 * @since 4.1.1
	 *
	 * @param array $parsed Parsed condition name from Condition::parseNameString().
	 *
	 * @return bool True if at least one segment names an index or key.
	 */
	public static function hasIndexedBracketPath( array $parsed ) {
		$bracketPath = isset( $parsed['bracket_path'] ) ? $parsed['bracket_path'] : null;

		if ( is_array( $bracketPath ) ) {
			foreach ( $bracketPath as $segment ) {
				if ( '' !== $segment ) {
					return true;
				}
			}
		}

		// The parser only lifts a TRAILING group into bracket_path, and skips
		// extraction entirely past MAX_FIELD_LENGTH_FOR_BRACKET. Either way a
		// non-empty group can still be sitting in the field text — mid-string in
		// FILES:doc[0]:name, or unextracted in a very long name — where it would
		// slip past the check above and flip not_exists from "false" to "true
		// for every request". Read the text rather than trust the split.
		$field = isset( $parsed['raw_field'] ) && is_string( $parsed['raw_field'] )
			? $parsed['raw_field']
			: $parsed['field'];

		if ( ! is_string( $field ) ) {
			return false;
		}

		// Non-empty group only: an empty pair is the HTML name of a multi-file
		// input, which normalizeFilesFieldKey() resolves to the base field.
		return (bool) preg_match( '/\[[^\[\]]+\]/', $field );
	}

	/**
	 * Depth limit for flattening a $_FILES sub-value.
	 *
	 * Derived rather than hard-coded. PHP mirrors an input's bracket shape into
	 * every $_FILES sub-key, so a file part named ``zz[a][b][c][d][e][f]`` puts
	 * the filename six levels down; anything shallower than PHP's own ceiling
	 * is therefore a rename away from being a bypass, and a fixed number has
	 * already produced two different off-by-one boundaries in this area.
	 *
	 * Measured on live multipart: $_FILES is exactly one level stricter than
	 * $_POST because the name/type/... sub-key costs a level, so PHP accepts
	 * ``max_input_nesting_level - 1`` bracket groups (63 at the default 64,
	 * 9 when the ini is lowered to 10) and drops deeper variables silently.
	 * The ``+ 2`` is calibrated, not slop. The sub-selector paths need to reach
	 * PHP's own ceiling of ini - 1; the bare-entry path walks one level further,
	 * because it starts at the entry and must step past the name/type/... key
	 * before it reaches the same leaves. So the worst internal consumer needs
	 * ini, and ``+ 2`` clears it with one level to spare.
	 *
	 * Deriving from the ini means no realistic host setting can open a gap, and
	 * the floor and ceiling keep the recursion finite. The ceiling is a genuine
	 * trade-off rather than a free win: a host setting max_input_nesting_level
	 * above the ceiling would once again be able to nest past the walk. Nothing
	 * plausible sets it there — it is 16x the default — and bounded recursion is
	 * worth more than covering that case, but it is a limit, not an absolute.
	 *
	 * @since 4.1.1
	 *
	 * @return int Maximum depth to walk.
	 */
	private static function filesLeafMaxDepth() {
		$iniDepth = (int) ini_get( 'max_input_nesting_level' );

		return min(
			self::FILES_LEAF_DEPTH_CEILING,
			max( self::FILES_LEAF_DEPTH_FLOOR, $iniDepth + 2 )
		);
	}

	/**
	 * Reduce a $_FILES value of any shape to what a matcher can test.
	 *
	 * A file input can be single (``zz``), multi (``zz[]``) or nested
	 * (``zz[x][y]``), and PHP mirrors that shape into every sub-key. Matchers
	 * take only a value — they cannot know which source it came from, so they
	 * cannot pick a per-source depth — which is why FILES is flattened here and
	 * ARGS keeps its deliberate 5/100 limits at the matcher.
	 *
	 * @since 4.1.1
	 *
	 * @param mixed $value A $_FILES sub-value or entry.
	 *
	 * @return mixed A string is returned as-is; an array becomes a flat list of
	 *               its leaf strings.
	 */
	public static function flattenFilesValue( $value ) {
		if ( ! is_array( $value ) ) {
			return $value;
		}

		return Request::extractLeafValues( $value, self::filesLeafMaxDepth(), self::filesLeafMaxCount() );
	}

	/**
	 * Leaf-count limit for flattening a $_FILES value.
	 *
	 * Leaf extraction otherwise applies the ARGS default of 100, which is a
	 * cost control for a tree PHP does not bound. PHP does bound $_FILES: it
	 * keeps at most ``max_file_uploads`` parts per request. Deriving from that
	 * keeps the limit above anything reachable over HTTP while staying finite
	 * for a $_FILES a plugin rewrote.
	 *
	 * @since 4.1.1
	 *
	 * @return int Maximum leaves to collect.
	 */
	private static function filesLeafMaxCount() {
		$maxUploads = (int) ini_get( 'max_file_uploads' );

		return max( self::FILES_LEAF_COUNT_FLOOR, $maxUploads * 4 );
	}

	/**
	 * Collect one sub-value from every uploaded file.
	 *
	 * Delegates to getFilesSubValue() per field so scan-all inherits the same
	 * null-byte stripping and is_uploaded_file() guard a single-field lookup
	 * gets. Files whose sub-value is unavailable are skipped rather than
	 * contributing a null the matchers would have to defend against.
	 *
	 * @since 4.1.1
	 *
	 * @param Request $request Request object.
	 * @param string  $sub     The sub-selector (name, filename, type, content).
	 *
	 * @return array Sub-values across every uploaded file.
	 */
	private static function getAllFilesSubValues( $request, $sub ) {
		$values = array();

		foreach ( array_keys( $request->getAllFiles() ) as $field ) {
			$value = self::getFilesSubValue( $request, $field, $sub );
			if ( null === $value ) {
				continue;
			}

			$value = self::flattenFilesValue( $value );

			if ( is_array( $value ) ) {
				// A multi or nested input contributes one candidate per part,
				// so a rule tests each filename rather than the array holding
				// them. Merging keeps every field's parts in one flat list.
				$values = array_merge( $values, $value );
				continue;
			}

			$values[] = $value;
		}

		return $values;
	}

	/**
	 * Drop the $_FILES sub-keys the server generated, not the client.
	 *
	 * The tmp_name sub-key is PHP's own temp path — every one of them holds the
	 * literal string "php" (``/tmp/phpA1b2C3``), so leaving it in the bare-entry
	 * value makes ``FILES:<field> contains "php"`` fire on an innocent photo.
	 * A rule author writing that means the filename; they can say so exactly
	 * with FILES:<field>:name. Only client-supplied strings belong here.
	 *
	 * @since 4.1.1
	 *
	 * @param mixed $entry One $_FILES entry.
	 *
	 * @return mixed The entry without its server-generated sub-keys.
	 */
	private static function withoutServerSubKeys( $entry ) {
		if ( ! is_array( $entry ) ) {
			return $entry;
		}

		unset( $entry['tmp_name'] );

		return $entry;
	}

	/**
	 * Parse FILES sub-selector from field name.
	 *
	 * FILES:async-upload:name  => field=async-upload, sub=name
	 * FILES:file:type          => field=file, sub=type
	 * FILES:file:content       => field=file, sub=content
	 * FILES:file               => field=file, sub=null (legacy exists-only)
	 *
	 * @since 3.0.2
	 *
	 * @param string $field The field string (part after first colon in condition name).
	 *
	 * @return array Array with 'field' and 'sub' keys.
	 */
	public static function parseFilesField( $field ) {
		$segments = explode( ':', $field, 2 );
		if ( 2 === count( $segments ) ) {
			return array(
				'field' => self::normalizeFilesFieldKey( $segments[0] ),
				'sub'   => $segments[1],
			);
		}
		return array(
			'field' => self::normalizeFilesFieldKey( $field ),
			'sub'   => null,
		);
	}

	/**
	 * Drop the trailing ``[]`` a rule author copies from a multi-file input.
	 *
	 * ``<input name="doc[]" multiple>`` arrives as $_FILES['doc'] — PHP strips
	 * the brackets when it builds the key. A rule written ``FILES:doc[]``
	 * already resolved, because the trailing pair is peeled off into the
	 * condition's bracket path before FILES ever sees the field; but
	 * ``FILES:doc[]:name`` is not trailing, so the pair stayed glued to the
	 * field and the lookup missed a key that can never exist. Normalising here
	 * makes the two forms agree.
	 *
	 * Only an EMPTY pair is notation. ``doc[0]`` names an index, which FILES
	 * cannot navigate, and is left intact so it resolves to nothing.
	 *
	 * @since 4.1.1
	 *
	 * @param string $field The FILES field segment.
	 *
	 * @return string The $_FILES key the request would actually carry.
	 */
	private static function normalizeFilesFieldKey( $field ) {
		if ( ! is_string( $field ) ) {
			return $field;
		}

		while ( '[]' === substr( $field, -2 ) ) {
			$field = substr( $field, 0, -2 );
		}

		return $field;
	}

	/**
	 * Get a FILES sub-value (name, filename, type, or content).
	 *
	 * @since 3.0.2
	 *
	 * @param Request $request Request object.
	 * @param string  $field   The file field key (e.g. 'async-upload').
	 * @param string  $sub     The sub-selector (name, filename, type, content).
	 *
	 * @return string|null The sub-value or null if not available.
	 */
	public static function getFilesSubValue( $request, $field, $sub ) {
		switch ( $sub ) {
			case 'name':
			case 'filename':
				return $request->getFileName( $field );
			case 'type':
				return $request->getFileType( $field );
			case 'content':
				return $request->getFileContent( $field );
			case 'full_path':
				return $request->getFileFullPath( $field );
			default:
				return null;
		}
	}

	/**
	 * Get all field values from request whose names match a regex pattern.
	 *
	 * @param Request $request    Request object.
	 * @param string  $source     Field source (e.g., ARGS, REQUEST_COOKIES).
	 * @param string  $fieldRegex Regex pattern for field names (without delimiters).
	 *
	 * @return array Matching values; for ARGS, a name sent in GET and POST gives both.
	 */
	private function getFieldValuesByRegex( $request, $source, $fieldRegex ) {
		switch ( $source ) {
			case ConditionSource::ARGS:
				return $request->getMatchingArgValues( $fieldRegex );
			case ConditionSource::REQUEST_COOKIES:
				return $request->getMatchingCookies( $fieldRegex );
			case ConditionSource::REQUEST_HEADERS:
				return $request->getMatchingHeaders( $fieldRegex );
			default:
				return array();
		}
	}

	/**
	 * Check whether a source represents a collection of named values.
	 *
	 * Collection sources (ARGS, REQUEST_COOKIES, REQUEST_HEADERS) contain
	 * multiple named fields and support scan-all semantics when no specific
	 * field is given.
	 *
	 * @param string $source Condition source constant.
	 *
	 * @return bool True if the source is a collection.
	 */
	private function isCollectionSource( $source ) {
		return in_array(
			$source,
			array( ConditionSource::ARGS, ConditionSource::REQUEST_COOKIES, ConditionSource::REQUEST_HEADERS ),
			true
		);
	}

	/**
	 * Get all values for a source (scan-all mode).
	 *
	 * @param Request $request Request object.
	 * @param string  $source  Condition source constant.
	 *
	 * @return array Field values; for ARGS, a name sent in GET and POST gives both.
	 */
	private function getAllSourceValues( $request, $source ) {
		switch ( $source ) {
			case ConditionSource::ARGS:
				return $request->getAllArgValues();
			case ConditionSource::REQUEST_COOKIES:
				return $request->getAllCookies();
			case ConditionSource::REQUEST_HEADERS:
				return $request->getAllHeaders();
			default:
				return array();
		}
	}

	/**
	 * Check whether a bracket path contains any regex segments.
	 *
	 * @since 3.0.0
	 *
	 * @param array $bracketPath Array of bracket-path segments.
	 *
	 * @return bool True if at least one segment is a /regex/ pattern.
	 */
	private static function bracketPathHasRegex( array $bracketPath ) {
		foreach ( $bracketPath as $segment ) {
			if ( preg_match( '#^/(.+)/$#', $segment ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Resolve a literal field value then navigate regex-aware bracket path.
	 *
	 * Used when the field name is literal but bracket segments contain /regex/.
	 *
	 * @since 3.0.0
	 *
	 * @param Request $request Request object.
	 * @param array   $parsed  Parsed condition name from Condition::parseNameString().
	 *
	 * @return array Resolved leaf values.
	 */
	private function resolveFieldWithRegexBrackets( Request $request, array $parsed ) {
		$source = $parsed['source'];
		$field  = $parsed['field'];

		if ( null === $field ) {
			return array();
		}

		$rootValue = null;
		switch ( $source ) {
			case ConditionSource::ARGS:
				// Navigate into the GET and the POST value alike, so a decoy in
				// one source cannot hide a payload in the other.
				$roots = array();
				foreach ( array( $request->get( $field ), $request->post( $field ) ) as $root ) {
					if ( null !== $root ) {
						$roots[] = $root;
					}
				}
				return $this->navigateBracketPathIntoValues( $roots, $parsed['bracket_path'] );
			case ConditionSource::REQUEST_COOKIES:
				$rootValue = $request->cookie( $field );
				break;
			case ConditionSource::REQUEST_HEADERS:
				$rootValue = $request->getHeader( $field );
				break;
			default:
				return array();
		}

		if ( null === $rootValue ) {
			return array();
		}

		return self::navigateBracketPath( $rootValue, $parsed['bracket_path'] );
	}

	/**
	 * Navigate a bracket path into each value from a regex-matched set.
	 *
	 * Each regex-matched value is expected to be an array. The bracket path
	 * segments are traversed into each value. Segments wrapped in /regex/
	 * are treated as regex patterns that match multiple keys at that level.
	 *
	 * @since 3.0.0
	 *
	 * @param array $values      Flat array of matched values.
	 * @param array $bracketPath Array of bracket-path segments.
	 *
	 * @return array Resolved leaf values after bracket navigation.
	 */
	private function navigateBracketPathIntoValues( array $values, array $bracketPath ) {
		$results = array();

		foreach ( $values as $value ) {
			$navigated = self::navigateBracketPath( $value, $bracketPath );
			foreach ( $navigated as $leaf ) {
				$results[] = $leaf;
			}
		}

		return $results;
	}

	/**
	 * Navigate a single value through bracket-path segments.
	 *
	 * Literal segments perform a direct array key lookup. Segments matching
	 * the /regex/ convention iterate over keys at that level.
	 *
	 * @since 3.0.0
	 *
	 * @param mixed $value       The value to navigate into.
	 * @param array $bracketPath Array of bracket-path segments.
	 *
	 * @return array Resolved leaf values (may contain strings or arrays).
	 */
	private static function navigateBracketPath( $value, array $bracketPath ) {
		$current = array( $value );

		foreach ( $bracketPath as $segment ) {
			$next = array();

			if ( preg_match( '#^/(.+)/$#', $segment, $m ) ) {
				$regex = '#^(?:' . str_replace( '#', '\\#', $m[1] ) . ')$#';
				// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- invalid regex silently skipped.
				if ( false === @preg_match( $regex, '' ) ) {
					return array();
				}

				foreach ( $current as $item ) {
					if ( ! is_array( $item ) ) {
						continue;
					}
					foreach ( $item as $key => $val ) {
						if ( preg_match( $regex, (string) $key ) ) {
							$next[] = $val;
						}
					}
				}
			} else {
				foreach ( $current as $item ) {
					if ( is_array( $item ) && isset( $item[ $segment ] ) ) {
						$next[] = $item[ $segment ];
					}
				}
			}

			if ( empty( $next ) ) {
				return array();
			}

			$current = $next;
		}

		return $current;
	}

	/**
	 * Apply an extra URL-decode pass to resolved ARGS values.
	 *
	 * PHP's $_GET/$_POST parsing performs a single urldecode. This method
	 * applies one additional urldecode to match the double-decode already
	 * used for REQUEST_URI, closing the double-encoding evasion gap.
	 *
	 * @since 3.0.2
	 *
	 * @param array $values Resolved ARGS values (strings or nested arrays).
	 *
	 * @return array Values with one additional URL-decode applied.
	 */
	private static function decodeArgValues( array $values ) {
		return array_map( array( __CLASS__, 'decodeArgValue' ), $values );
	}

	/**
	 * URL-decode a single ARGS value, recursing into arrays.
	 *
	 * Non-string scalars (int, float, bool) are cast to their string form so
	 * the downstream match operators (matchEquals / matchContains / matchRegex
	 * / detectXSS / detectSQLi) see a consistent string type. Without this
	 * cast, JSON-bodied REST requests such as `{"user_id": 1, "role": 5}`
	 * would produce PHP-int leaves that Request::extractLeafValues()
	 * silently drops (the walker collects only is_string() values), so a rule
	 * like `detectSQLi on ARGS:role` would evaluate to false regardless of
	 * payload content. null is coerced to empty string for the same reason.
	 *
	 * @since 3.0.2
	 *
	 * @param mixed $value The value to decode.
	 *
	 * @return mixed Decoded value (string, array, or unchanged object).
	 */
	private static function decodeArgValue( $value ) {
		if ( is_string( $value ) ) {
			return urldecode( $value );
		}

		if ( is_array( $value ) ) {
			return array_map( array( __CLASS__, 'decodeArgValue' ), $value );
		}

		if ( is_bool( $value ) ) {
			return $value ? '1' : '0';
		}

		if ( null === $value ) {
			return '';
		}

		if ( is_int( $value ) || is_float( $value ) ) {
			return (string) $value;
		}

		return $value;
	}

	/**
	 * Get the decoded request URI for condition evaluation.
	 *
	 * Applies urldecode() twice to defend against both single-encoded
	 * and double-encoded URI bypass attempts (e.g., %2F and %252F).
	 *
	 * @param Request $request Request object.
	 *
	 * @return string The decoded URI.
	 */
	private function getDecodedUri( $request ) {
		return urldecode( urldecode( $request->getUri() ) );
	}

	/**
	 * Values of a named ARGS field, one candidate per source: GET, then POST.
	 * A bracket path is resolved in each source, falling back to the literal
	 * "x[y]" key. Cookies are excluded.
	 *
	 * @param Request $request Request object.
	 * @param array   $parsed  Parsed condition name from Condition::parseNameString().
	 *
	 * @return array<int, mixed> Raw values present (0, 1 or 2 entries).
	 */
	public function collectArgOccurrences( $request, array $parsed ) {
		$field       = $parsed['field'];
		$bracketPath = $parsed['bracket_path'];
		$rawField    = isset( $parsed['raw_field'] ) ? $parsed['raw_field'] : null;
		$values      = array();

		if ( null === $bracketPath ) {
			$candidates = array( $request->get( $field ), $request->post( $field ) );
		} else {
			$fromGet = $request->resolveNestedGet( $field, $bracketPath );
			if ( null === $fromGet && null !== $rawField ) {
				$fromGet = $request->get( $rawField );
			}
			$fromPost = $request->resolveNestedPost( $field, $bracketPath );
			if ( null === $fromPost && null !== $rawField ) {
				$fromPost = $request->post( $rawField );
			}
			$candidates = array( $fromGet, $fromPost );
		}

		foreach ( $candidates as $value ) {
			if ( null !== $value ) {
				$values[] = $value;
			}
		}
		return $values;
	}

	/**
	 * Candidates for REQUEST_URI matching: the raw double-decoded URI, which keeps
	 * ../, // and encoded forms so traversal rules still fire, followed by the
	 * normalised forms of the path (Request::getNormalizedUriCandidates()), so a
	 * root-anchored rule sees the path the web server maps to a file.
	 *
	 * @param Request $request Request object.
	 *
	 * @return string[] Distinct candidates, raw URI first.
	 */
	private function getUriMatchCandidates( $request ) {
		$candidates = array_merge( array( $this->getDecodedUri( $request ) ), $request->getNormalizedUriCandidates() );
		return array_values( array_unique( $candidates ) );
	}
}

Directory Contents

Dirs: 2 × Files: 16
Name Size Perms Modified Actions
Handler DIR
- drwxr-xr-x 2026-11-03 01:06:42
Edit Download
Model DIR
- drwxr-xr-x 2026-11-03 01:06:42
Edit Download
1.97 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
3.44 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
22.33 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
22.10 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
4.28 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
945 B lrw-r--r-- 2026-11-03 01:06:42
Edit Download
6.88 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
17.87 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
3.08 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
62.62 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
4.92 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
18.52 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
4.44 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
14.27 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
9.23 KB lrw-r--r-- 2026-11-03 01:06:42
Edit Download
28.98 KB lrw-r--r-- 2026-11-03 01:06:42
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