�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: SiteAllowlistValidator.php Size: 30.46 KB
/home2/astrociv/public_html/asquarebuildcon.in/wp-content/plugins/imunify-security/inc/App/Bot/SiteAllowlistValidator.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\Bot;

/**
 * Validates and normalizes a submitted site-owner allowlist, all or nothing.
 *
 * Duplicates (same normalized value, ignoring case) are dropped and do not count towards the caps.
 *
 * @since 4.1.4
 */
class SiteAllowlistValidator {

	const CAPS = array(
		SiteAllowlist::LIST_IP         => 250,
		SiteAllowlist::LIST_REST_API   => 50,
		SiteAllowlist::LIST_PATH       => 50,
		SiteAllowlist::LIST_USER_AGENT => 50,
	);

	const NOTE_MAX_LENGTH       = 60;
	const VALUE_MAX_LENGTH      = 255;
	const INPUT_MAX_BYTES       = 2048;
	const USER_AGENT_MIN_LENGTH = 4;

	/**
	 * Characters of a value quoted in a message before it is shortened.
	 */
	const MESSAGE_VALUE_LENGTH = 60;

	/**
	 * Path segments that get a warning wherever they appear, so WordPress in its own folder
	 * (`/wp/wp-admin`) is covered too: login, XML-RPC, admin, core code and the root scripts.
	 */
	const SENSITIVE_PATH_SEGMENTS = array(
		'wp-login.php',
		'xmlrpc.php',
		'wp-admin',
		'wp-includes',
		'wp-comments-post.php',
		'wp-activate.php',
		'wp-blog-header.php',
		'wp-config.php',
		'wp-cron.php',
		'wp-links-opml.php',
		'wp-load.php',
		'wp-mail.php',
		'wp-settings.php',
		'wp-signup.php',
		'wp-trackback.php',
	);

	/**
	 * Folders below wp-content that hold code; an entry for them or for wp-content itself gets a warning.
	 */
	const SENSITIVE_CONTENT_FOLDERS = array( 'plugins', 'themes', 'mu-plugins' );

	/**
	 * First REST segments of the core API; `batch` can call any route, `wp/` ones included.
	 */
	const CORE_REST_NAMESPACES = array( 'wp', 'batch' );

	/**
	 * First path segments that get a warning only at the start: below the home path it is the
	 * front controller, deeper down an ordinary page. A first `wp-json` gets path_rest_prefix.
	 */
	const SENSITIVE_FIRST_SEGMENTS = array( 'index.php' );

	/**
	 * Typical browser user agents. User-agent entries match as substrings, so an entry
	 * found in one of these would let ordinary visitors through. Digits are folded
	 * before the comparison, so a version number in the entry changes nothing.
	 */
	const BROWSER_USER_AGENTS = array(
		'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0',
		'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0',
		'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15',
		'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0',
		'Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/26.0 Chrome/130.0.0.0 Mobile Safari/537.36',
		'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
		'Mozilla/5.0 (iPad; CPU OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/130.0.0.0 Mobile/15E148 Safari/604.1',
		'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36',
		'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
		'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
		'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0',
		'Mozilla/5.0 (Android 14; Mobile; rv:131.0) Gecko/131.0 Firefox/131.0',
		'Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
		'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko',
		'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0)',
	);

	const IPV4_MAPPED_PREFIX = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff";

	/**
	 * Lower-case segments of the home path, cut from pasted full URLs.
	 *
	 * @var string[]
	 */
	private $home_segments;

	/**
	 * Lower-case segments of the folder WordPress lives in below the home path, empty when
	 * WordPress lives at the home path or the site URL path is unknown.
	 *
	 * @var string[]
	 */
	private $site_segments = array();

	/**
	 * Returns which of the given site paths ('/site2/') belong to other sites of the network.
	 *
	 * @var callable|null
	 */
	private $site_lookup;

	/**
	 * The other site the last checked path starts with, for its warning message.
	 *
	 * @var array|null
	 */
	private $other_site = null;

	/**
	 * Site paths already looked up: path => whether it belongs to another site.
	 *
	 * @var bool[]
	 */
	private $site_cache = array();

	/**
	 * Issues that block the save.
	 *
	 * @var array
	 */
	private $errors = array();

	/**
	 * Issues that do not block the save.
	 *
	 * @var array
	 */
	private $warnings = array();

	/**
	 * Set up for a site.
	 *
	 * @param string|null   $home_path   Path of the home URL, '' on a root site.
	 * @param string|null   $site_path   Path of the site URL (where WordPress lives), null when unknown.
	 * @param callable|null $site_lookup On a network whose sites live in subfolders: given site paths
	 *                                   such as '/site2/', returns those of other sites.
	 */
	public function __construct( $home_path = '', $site_path = null, $site_lookup = null ) {
		$this->site_lookup   = is_callable( $site_lookup ) ? $site_lookup : null;
		$this->home_segments = self::splitSegments( strtolower( (string) $home_path ) );
		$site                = self::splitSegments( strtolower( (string) $site_path ) );
		$depth               = count( $this->home_segments );
		if ( array_slice( $site, 0, $depth ) === $this->home_segments ) {
			$this->site_segments = array_slice( $site, $depth );
		}
	}

	/**
	 * Validate a submitted allowlist.
	 *
	 * @param mixed $input array( 'ip' => array( array( 'value' => ..., 'note' => ... ) ), ... );
	 *                     a missing list is an empty list.
	 * @return SiteAllowlistValidation
	 */
	public function validate( $input ) {
		$this->errors   = array();
		$this->warnings = array();

		if ( ! is_array( $input ) ) {
			$this->addIssue( $this->errors, '', -1, '', 'invalid_list' );
			return new SiteAllowlistValidation( null, $this->errors, $this->warnings );
		}

		$lists = array();
		foreach ( SiteAllowlist::listNames() as $name ) {
			$lists[ $name ] = array();
			if ( ! isset( $input[ $name ] ) ) {
				continue;
			}
			if ( ! is_array( $input[ $name ] ) ) {
				$this->addIssue( $this->errors, $name, -1, '', 'invalid_list' );
				continue;
			}
			$lists[ $name ] = $this->validateList( $name, array_values( $input[ $name ] ) );
		}

		$allowlist = empty( $this->errors ) ? SiteAllowlist::fromArray( $lists ) : null;
		return new SiteAllowlistValidation( $allowlist, $this->errors, $this->warnings );
	}

	/**
	 * Validate the entries of one list.
	 *
	 * @param string $name    List name.
	 * @param array  $entries Submitted entries.
	 * @return array Accepted entries.
	 */
	private function validateList( $name, $entries ) {
		$accepted = array();
		$seen     = array();
		$over_cap = false;
		$caps     = self::CAPS;
		$cap      = $caps[ $name ];
		foreach ( $entries as $index => $entry ) {
			$raw = is_array( $entry ) && isset( $entry['value'] ) ? $entry['value'] : null;
			if ( ! is_string( $raw ) || ( isset( $entry['note'] ) && ! is_string( $entry['note'] ) ) ) {
				$this->addIssue( $this->errors, $name, $index, $raw, 'invalid_entry' );
				continue;
			}

			$checked = $this->checkEntry( $name, $raw, isset( $entry['note'] ) ? $entry['note'] : '' );
			if ( isset( $checked['error'] ) ) {
				$this->addIssue( $this->errors, $name, $index, $raw, $checked['error'] );
				continue;
			}

			$key = strtolower( $checked['value'] );
			if ( isset( $seen[ $key ] ) ) {
				continue;
			}
			$seen[ $key ] = true;

			if ( count( $accepted ) >= $cap ) {
				if ( ! $over_cap ) {
					$this->addIssue( $this->errors, $name, $index, $raw, 'over_cap' );
					$over_cap = true;
				}
				continue;
			}

			foreach ( $checked['warnings'] as $code ) {
				$this->addIssue( $this->warnings, $name, $index, $raw, $code );
			}
			// Looked up only for accepted entries, so a save costs at most one lookup per entry under the cap.
			if ( SiteAllowlist::LIST_PATH === $name ) {
				$this->other_site = $this->otherSite( self::splitSegments( $checked['value'] ) );
				if ( null !== $this->other_site ) {
					$this->addIssue( $this->warnings, $name, $index, $raw, 'path_other_site' );
				}
			}
			$accepted[] = array(
				'value' => $checked['value'],
				'note'  => $checked['note'],
			);
		}
		return $accepted;
	}

	/**
	 * Check and normalize one entry.
	 *
	 * @param string $name List name.
	 * @param string $raw  Submitted value.
	 * @param string $note Submitted note.
	 * @return array Either array( 'error' => code ) or array( 'value', 'note', 'warnings' ).
	 */
	private function checkEntry( $name, $raw, $note ) {
		$value = trim( $raw, " \t\n\r" );
		if ( '' === $value ) {
			return array( 'error' => 'empty' );
		}
		if ( strlen( $value ) > self::INPUT_MAX_BYTES ) {
			return array( 'error' => 'too_long' );
		}
		if ( ! self::isPlainText( $value ) || ! self::isPlainText( $note, true ) ) {
			return array( 'error' => 'invalid_text' );
		}

		switch ( $name ) {
			case SiteAllowlist::LIST_IP:
				$checked = self::checkIp( $value );
				break;
			case SiteAllowlist::LIST_REST_API:
				$checked = self::checkRestRoute( $value );
				break;
			case SiteAllowlist::LIST_PATH:
				$checked = $this->checkPath( $value );
				break;
			default:
				$checked = self::checkUserAgent( $value );
		}
		if ( isset( $checked['error'] ) ) {
			return $checked;
		}
		if ( self::length( $checked['value'] ) > self::VALUE_MAX_LENGTH ) {
			return array( 'error' => 'too_long' );
		}
		$checked['note'] = self::cleanNote( $note );
		return $checked;
	}

	/**
	 * Whether a string is valid UTF-8 without control characters.
	 *
	 * @param string $text            Text to check.
	 * @param bool   $allow_controls  Whether control characters are allowed (they are stripped later).
	 * @return bool
	 */
	private static function isPlainText( $text, $allow_controls = false ) {
		if ( 1 !== preg_match( '//u', $text ) ) {
			return false;
		}
		return $allow_controls || 0 === preg_match( '/\p{Cc}/u', $text );
	}

	/**
	 * Strip control and invisible format characters (text direction, zero-width), trim, and cut the note to its maximum length.
	 *
	 * @param string $note Valid UTF-8 note.
	 * @return string
	 */
	private static function cleanNote( $note ) {
		$note = trim( preg_replace( '/[\p{Cc}\p{Cf}]/u', '', $note ) );
		preg_match( '/^.{0,' . self::NOTE_MAX_LENGTH . '}/su', $note, $match );
		return rtrim( $match[0] );
	}

	/**
	 * Normalize an IP address or CIDR range.
	 *
	 * @param string $value Trimmed value.
	 * @return array
	 */
	private static function checkIp( $value ) {
		$parts = explode( '/', $value );
		if ( count( $parts ) > 2 || false === filter_var( $parts[0], FILTER_VALIDATE_IP ) ) {
			return array( 'error' => 'ip_invalid' );
		}
		$binary = inet_pton( $parts[0] );
		$bits   = strlen( $binary ) * 8;
		$prefix = $bits;
		if ( 2 === count( $parts ) ) {
			if ( 1 !== preg_match( '/^[0-9]{1,3}$/', $parts[1] ) || (int) $parts[1] > $bits ) {
				return array( 'error' => 'ip_invalid' );
			}
			$prefix = (int) $parts[1];
		}
		if ( 128 === $bits && $prefix >= 96 && self::IPV4_MAPPED_PREFIX === substr( $binary, 0, 12 ) ) {
			$binary  = substr( $binary, 12 );
			$bits    = 32;
			$prefix -= 96;
		}
		if ( 0 === $prefix ) {
			return array( 'error' => 'ip_everything' );
		}

		$value = inet_ntop( self::maskAddress( $binary, $prefix ) );
		if ( $prefix < $bits ) {
			$value .= '/' . $prefix;
		}
		$broad = 32 === $bits ? $prefix < 16 : $prefix < 32;
		return array(
			'value'    => $value,
			'warnings' => $broad ? array( 'ip_broad' ) : array(),
		);
	}

	/**
	 * Zero the host bits of an address.
	 *
	 * @param string $binary Packed address.
	 * @param int    $prefix Network prefix length.
	 * @return string
	 */
	private static function maskAddress( $binary, $prefix ) {
		$masked = '';
		$length = strlen( $binary );
		for ( $i = 0; $i < $length; $i++ ) {
			$keep    = max( 0, min( 8, $prefix - $i * 8 ) );
			$masked .= chr( ord( $binary[ $i ] ) & ( 0xFF << ( 8 - $keep ) ) & 0xFF );
		}
		return $masked;
	}

	/**
	 * Normalize a REST route prefix, reducing a pasted URL or wp-json path to its route.
	 *
	 * @param string $value Trimmed value.
	 * @return array
	 */
	private static function checkRestRoute( $value ) {
		$value = strtolower( $value );
		$value = substr( $value, 0, strcspn( $value, '#' ) );
		if ( 1 === preg_match( '#^[a-z][a-z0-9+.-]*://[^/?]*#', $value, $authority ) ) {
			$value = substr( $value, strlen( $authority[0] ) );
		}
		$query_at = strpos( $value, '?' );
		$path     = false === $query_at ? $value : substr( $value, 0, $query_at );
		$args     = array();
		if ( false !== $query_at ) {
			parse_str( substr( $value, $query_at + 1 ), $args );
		}

		if ( isset( $args['rest_route'] ) && is_string( $args['rest_route'] ) ) {
			$route = $args['rest_route'];
		} elseif ( 1 === preg_match( '#(?:^|/)wp-json(?:/(.*))?$#', $path, $match ) ) {
			$route = isset( $match[1] ) ? $match[1] : '';
		} else {
			$route = $path;
		}

		$segments = self::splitSegments( $route );
		if ( empty( $segments ) ) {
			return array( 'error' => 'rest_invalid_segment' );
		}
		foreach ( $segments as $segment ) {
			if ( '*' !== $segment && 1 !== preg_match( '/^[a-z0-9][a-z0-9._-]*$/', $segment ) ) {
				return array( 'error' => 'rest_invalid_segment' );
			}
		}
		if ( '*' === $segments[0] ) {
			return array( 'error' => 'rest_wildcard_namespace' );
		}
		return array(
			'value'    => implode( '/', $segments ) . '/',
			'warnings' => in_array( $segments[0], self::CORE_REST_NAMESPACES, true ) ? array( 'rest_core' ) : array(),
		);
	}

	/**
	 * Normalize a site path, reducing a pasted URL to its path below the home path.
	 *
	 * @param string $value Trimmed value.
	 * @return array
	 */
	private function checkPath( $value ) {
		if ( strlen( $value ) !== strcspn( $value, '?#' ) ) {
			return array( 'error' => 'path_query' );
		}
		$from_url = 1 === preg_match( '#^[a-z][a-z0-9+.-]*://[^/]*#i', $value, $authority );
		$path     = $from_url ? substr( $value, strlen( $authority[0] ) ) : $value;
		if ( $from_url && '' === $path ) {
			$path = '/';
		}
		// Decoded once, as the request path is before matching.
		$path = rawurldecode( $path );
		// A stored value goes through here again on the next save, so it must not decode any further.
		if ( ! self::isPlainText( $path ) || rawurldecode( $path ) !== $path ) {
			return array( 'error' => 'invalid_text' );
		}
		if ( strlen( $path ) !== strcspn( $path, '?#' ) ) {
			return array( 'error' => 'path_query' );
		}
		if ( '/' !== $path[0] ) {
			return array( 'error' => 'path_no_slash' );
		}

		$segments = self::splitSegments( $path );
		$depth    = count( $this->home_segments );
		$in_home  = $depth > 0 && array_map( 'strtolower', array_slice( $segments, 0, $depth ) ) === $this->home_segments;
		if ( $from_url && $in_home ) {
			$segments = array_slice( $segments, $depth );
		}
		// The front controller alone serves every page, so it is the whole site too; MultiViews runs it as /index.
		$lower = array_map( 'strtolower', $segments );
		if ( empty( $segments ) || array( 'index.php' ) === $lower || array( 'index' ) === $lower ) {
			return array( 'error' => 'path_root' );
		}

		$only_wildcards = true;
		foreach ( $segments as $segment ) {
			if ( '.' === $segment || '..' === $segment ) {
				return array( 'error' => 'path_dot_segment' );
			}
			if ( '*' !== $segment && false !== strpos( $segment, '*' ) ) {
				return array( 'error' => 'path_partial_wildcard' );
			}
			$only_wildcards = $only_wildcards && '*' === $segment;
		}
		if ( $only_wildcards ) {
			return array( 'error' => 'path_only_wildcards' );
		}

		$warnings  = array();
		$sensitive = self::sensitivePathWarning( $lower );
		// An entry for the WordPress folder (or a parent of it) covers wp-login.php and wp-admin.
		if ( null === $sensitive && $this->coversWordPressFolder( $lower ) ) {
			$sensitive = 'path_sensitive';
		}
		if ( null !== $sensitive ) {
			$warnings[] = $sensitive;
		}
		// Paths are matched below the home path: a typed one that repeats it never matches,
		// and a pasted URL from outside it would cover a different page below it.
		if ( $depth > 0 && ! $from_url && $in_home ) {
			$warnings[] = 'path_home_prefix';
		}
		if ( $depth > 0 && $from_url && ! $in_home ) {
			$warnings[] = 'path_outside_home';
		}
		return array(
			'value'    => '/' . implode( '/', $segments ),
			'warnings' => $warnings,
		);
	}

	/**
	 * The other site of the network an entry points into: paths match below each site's own
	 * path, so such an entry covers a page below that site's path twice. The entry is looked up
	 * below this site's home path and from the domain root, since the list covers every site.
	 *
	 * @param string[] $segments Segments of the entry, below the home path.
	 * @return array|null array( 'site' => '/site2', 'covers' => what the entry covers there,
	 *                    'rest' => the entry below that site's path, '' when no valid entry is left ).
	 */
	private function otherSite( $segments ) {
		if ( null === $this->site_lookup ) {
			return null;
		}
		$home       = '/' === $this->homePrefix() ? '' : $this->homePrefix();
		$candidates = array();
		foreach ( $segments as $i => $segment ) {
			if ( '*' === $segment ) {
				break;
			}
			$prefix = '/' . strtolower( implode( '/', array_slice( $segments, 0, $i + 1 ) ) ) . '/';
			if ( '' !== $home ) {
				$candidates[ $home . $prefix ] = $i + 1;
			}
			$candidates[ $prefix ] = $i + 1;
		}
		if ( empty( $candidates ) ) {
			return null;
		}
		$unknown = array_values( array_diff( array_keys( $candidates ), array_keys( $this->site_cache ) ) );
		if ( ! empty( $unknown ) ) {
			$found = call_user_func( $this->site_lookup, $unknown );
			$found = is_array( $found ) ? array_map( 'strtolower', array_map( 'strval', $found ) ) : array();
			foreach ( $unknown as $path ) {
				$this->site_cache[ $path ] = in_array( $path, $found, true );
			}
		}
		$best = null;
		foreach ( array_keys( $candidates ) as $path ) {
			if ( $this->site_cache[ $path ] && ( null === $best || strlen( $path ) > strlen( $best ) ) ) {
				$best = $path;
			}
		}
		if ( null === $best ) {
			return null;
		}
		$rest  = array_slice( $segments, $candidates[ $best ] );
		$clean = array();
		if ( ! empty( $rest ) ) {
			$clean = $this->checkPath( '/' . implode( '/', $rest ) );
		}
		return array(
			'site'   => rtrim( $best, '/' ),
			'covers' => rtrim( $best, '/' ) . '/' . implode( '/', $segments ),
			// Suggest the rest only when it is an entry without errors or warnings of its own, nor
			// another site's path again; the rest is shorter, so this ends.
			'rest'   => isset( $clean['value'] ) && empty( $clean['warnings'] )
				&& null === $this->otherSite( self::splitSegments( $clean['value'] ) ) ? $clean['value'] : '',
		);
	}

	/**
	 * The home path as '/blog', '/' on a root site.
	 *
	 * @return string
	 */
	private function homePrefix() {
		return '/' . implode( '/', $this->home_segments );
	}

	/**
	 * Whether an entry covers the folder WordPress lives in below the home path.
	 *
	 * @param string[] $lower Lower-case path segments.
	 * @return bool
	 */
	private function coversWordPressFolder( $lower ) {
		if ( empty( $this->site_segments ) || count( $lower ) > count( $this->site_segments ) ) {
			return false;
		}
		foreach ( $lower as $position => $segment ) {
			if ( '*' !== $segment && $segment !== $this->site_segments[ $position ] ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * The warning for an entry that covers a sensitive WordPress path, most specific first.
	 *
	 * A segment also counts without `.php`, which Apache MultiViews serves the same way.
	 *
	 * @param string[] $lower Lower-case path segments.
	 * @return string|null
	 */
	private static function sensitivePathWarning( $lower ) {
		foreach ( $lower as $segment ) {
			if ( 'admin-ajax.php' === $segment || 'admin-ajax' === $segment ) {
				return 'path_admin_ajax';
			}
		}
		$first = $lower[0];
		if ( 'wp-json' === $first || ( isset( $lower[1] ) && 'wp-json' === $lower[1] && in_array( $first, array( 'index.php', 'index' ), true ) ) ) {
			return 'path_rest_prefix';
		}
		foreach ( $lower as $position => $segment ) {
			if ( in_array( $segment, self::SENSITIVE_PATH_SEGMENTS, true ) || in_array( $segment . '.php', self::SENSITIVE_PATH_SEGMENTS, true ) ) {
				return 'path_sensitive';
			}
			if ( 'wp-content' === $segment && ( ! isset( $lower[ $position + 1 ] ) || in_array( $lower[ $position + 1 ], self::SENSITIVE_CONTENT_FOLDERS, true ) ) ) {
				return 'path_sensitive';
			}
		}
		if ( in_array( $first, self::SENSITIVE_FIRST_SEGMENTS, true ) || in_array( $first . '.php', self::SENSITIVE_FIRST_SEGMENTS, true ) ) {
			return 'path_sensitive';
		}
		return null;
	}

	/**
	 * Check a user-agent substring.
	 *
	 * @param string $value Trimmed value.
	 * @return array
	 */
	private static function checkUserAgent( $value ) {
		if ( self::length( $value ) < self::USER_AGENT_MIN_LENGTH ) {
			return array( 'error' => 'ua_too_short' );
		}
		$folded = self::foldUserAgent( $value );
		foreach ( SignatureRefresher::SIGNATURE_DENYLIST as $token ) {
			if ( false !== strpos( $token, $folded ) ) {
				return array( 'error' => 'ua_browser' );
			}
		}
		foreach ( self::BROWSER_USER_AGENTS as $browser ) {
			if ( false !== strpos( self::foldUserAgent( $browser ), $folded ) ) {
				return array( 'error' => 'ua_browser' );
			}
		}
		return array(
			'value'    => $value,
			'warnings' => array( 'ua_spoofable' ),
		);
	}

	/**
	 * Lower-case a user agent and turn every run of digits into a single 0.
	 *
	 * @param string $ua User agent or entry.
	 * @return string
	 */
	private static function foldUserAgent( $ua ) {
		return preg_replace( '/[0-9]+/', '0', strtolower( $ua ) );
	}

	/**
	 * Number of characters in a valid UTF-8 string.
	 *
	 * @param string $text Text.
	 * @return int
	 */
	private static function length( $text ) {
		return (int) preg_match_all( '/./su', $text );
	}

	/**
	 * Split on slashes, dropping empty segments.
	 *
	 * @param string $path Path or route.
	 * @return string[]
	 */
	private static function splitSegments( $path ) {
		return array_values(
			array_filter(
				explode( '/', $path ),
				function ( $segment ) {
					return '' !== $segment;
				}
			)
		);
	}

	/**
	 * Record an issue.
	 *
	 * @param array  $issues Issue list to add to.
	 * @param string $name   List name, '' for the whole input.
	 * @param int    $index  Position in the submitted list, -1 for the whole list.
	 * @param mixed  $raw    Submitted value.
	 * @param string $code   Issue code.
	 * @return void
	 */
	private function addIssue( &$issues, $name, $index, $raw, $code ) {
		$value    = is_string( $raw ) && 1 === preg_match( '//u', $raw ) ? $raw : '';
		$issues[] = array(
			'list'    => $name,
			'index'   => $index,
			'value'   => $value,
			'code'    => $code,
			'message' => 'path_other_site' === $code
				? self::otherSiteMessage( self::shorten( $value ), $this->other_site )
				: self::message( $code, $value, $index, $name, $this->homePrefix() ),
		);
	}

	/**
	 * The warning for an entry that starts with another site's path.
	 *
	 * @param string $shown The entry, shortened.
	 * @param array  $other Output of otherSite().
	 * @return string
	 */
	private static function otherSiteMessage( $shown, $other ) {
		if ( '' === $other['rest'] ) {
			/* translators: 1: the entry, 2: path of another site in the network, such as /site2, 3: the address the entry covers on that site. */
			return sprintf( __( '"%1$s" points into %2$s, another site in this network. Paths are relative to each site\'s address, so on that site this entry covers %3$s.', 'imunify-security' ), $shown, $other['site'], $other['covers'] );
		}
		/* translators: 1: the entry, 2: path of another site in the network, such as /site2, 3: the address the entry covers on that site, 4: the path to enter instead. */
		return sprintf( __( '"%1$s" points into %2$s, another site in this network. Paths are relative to each site\'s address, so on that site this entry covers %3$s. To cover %4$s on every site, enter %4$s.', 'imunify-security' ), $shown, $other['site'], $other['covers'], $other['rest'] );
	}

	/**
	 * The translated message for an issue, naming the entry.
	 *
	 * @param string $code  Issue code.
	 * @param string $value Submitted value, '' when it cannot be shown.
	 * @param int    $index Position in the submitted list.
	 * @param string $name  List name.
	 * @param string $home  Home path, lower-case.
	 * @return string
	 */
	private static function message( $code, $value, $index, $name, $home ) {
		$position = $index + 1;
		$shown    = self::shorten( $value );
		switch ( $code ) {
			case 'invalid_list':
				return __( 'The allowlist was not sent in the expected format. Nothing was saved.', 'imunify-security' );
			case 'invalid_entry':
				/* translators: %d: position of the entry in its list, starting at 1. */
				return sprintf( __( 'Entry %d is not in the expected format.', 'imunify-security' ), $position );
			case 'empty':
				/* translators: %d: position of the entry in its list, starting at 1. */
				return sprintf( __( 'Entry %d is empty.', 'imunify-security' ), $position );
			case 'invalid_text':
				/* translators: %d: position of the entry in its list, starting at 1. */
				return sprintf( __( 'Entry %d contains characters that are not allowed.', 'imunify-security' ), $position );
			case 'too_long':
				/* translators: 1: the entry, shortened, 2: maximum number of characters. */
				return sprintf( __( '"%1$s" is longer than %2$d characters.', 'imunify-security' ), $shown, self::VALUE_MAX_LENGTH );
			case 'over_cap':
				$caps = self::CAPS;
				/* translators: 1: the first entry over the limit, 2: maximum number of entries in the list. */
				return sprintf( __( '"%1$s" is over the limit of %2$d entries for this list.', 'imunify-security' ), $shown, $caps[ $name ] );
			case 'path_home_prefix':
				/* translators: 1: the entry, 2: path of the site's home URL, such as /blog. */
				return sprintf( __( '"%1$s" starts with your site\'s path, %2$s. Paths here are relative to it, so leave that part out.', 'imunify-security' ), $shown, $home );
			case 'path_outside_home':
				/* translators: 1: the entry, 2: path of the site's home URL, such as /blog. */
				return sprintf( __( '"%1$s" is outside your site\'s path, %2$s. Paths here are relative to it, so this entry covers a different page.', 'imunify-security' ), $shown, $home );
			case 'ua_too_short':
				/* translators: 1: the entry, 2: minimum number of characters. */
				return sprintf( __( '"%1$s" is shorter than %2$d characters.', 'imunify-security' ), $shown, self::USER_AGENT_MIN_LENGTH );
		}
		return sprintf( self::valueMessage( $code ), $shown );
	}

	/**
	 * The translated message format for an issue whose only placeholder is the entry.
	 *
	 * @param string $code Issue code.
	 * @return string
	 */
	private static function valueMessage( $code ) {
		switch ( $code ) {
			case 'ip_invalid':
				/* translators: %s: the entry. */
				return __( '"%s" is not a valid IPv4 or IPv6 address or CIDR range.', 'imunify-security' );
			case 'ip_everything':
				/* translators: %s: the entry. */
				return __( '"%s" covers every IP address. To turn bot protection off, use its on/off switch.', 'imunify-security' );
			case 'ip_broad':
				/* translators: %s: the entry. */
				return __( '"%s" is a very large range. Every address in it skips bot protection.', 'imunify-security' );
			case 'rest_invalid_segment':
				/* translators: %s: the entry. */
				return __( '"%s" is not a valid REST API route prefix. Use letters, digits, dots, dashes and underscores, and * for one whole segment.', 'imunify-security' );
			case 'rest_wildcard_namespace':
				/* translators: %s: the entry. */
				return __( '"%s" starts with *. The first segment must be a fixed namespace, such as myplugin/.', 'imunify-security' );
			case 'rest_core':
				/* translators: %s: the entry. */
				return __( '"%s" is part of the WordPress core REST API. Bots can use it to read your content.', 'imunify-security' );
			case 'path_no_slash':
				/* translators: %s: the entry. */
				return __( '"%s" must start with /.', 'imunify-security' );
			case 'path_root':
				/* translators: %s: the entry. */
				return __( '"%s" covers the whole site. To turn bot protection off, use its on/off switch.', 'imunify-security' );
			case 'path_only_wildcards':
				/* translators: %s: the entry. */
				return __( '"%s" is made only of * segments, so it covers almost every page.', 'imunify-security' );
			case 'path_dot_segment':
				/* translators: %s: the entry. */
				return __( '"%s" contains a . or .. segment.', 'imunify-security' );
			case 'path_query':
				/* translators: %s: the entry. */
				return __( '"%s" contains a query string or fragment (? or #). They are ignored when matching, so remove them.', 'imunify-security' );
			case 'path_partial_wildcard':
				/* translators: %s: the entry. */
				return __( '"%s" uses * inside a segment. * can only stand for one whole segment, as in /downloads/*.', 'imunify-security' );
			case 'path_sensitive':
				/* translators: %s: the entry. */
				return __( '"%s" covers a sensitive WordPress path. Bots that reach it skip bot protection.', 'imunify-security' );
			case 'path_rest_prefix':
				/* translators: %s: the entry. */
				return __( '"%s" is a REST API path. Use the REST API routes list instead: it also matches ?rest_route= requests, and a path entry does not.', 'imunify-security' );
			case 'path_admin_ajax':
				/* translators: %s: the entry. */
				return __( '"%s" allows every AJAX action of every plugin. To allow one service, add its IP address instead.', 'imunify-security' );
			case 'ua_browser':
				/* translators: %s: the entry. */
				return __( '"%s" is part of normal web browser user agents, so it would allow almost every visitor.', 'imunify-security' );
		}
		/* translators: %s: the entry. */
		return __( 'Anyone can send "%s" as their user agent. Use it only for tools you cannot allowlist by IP address.', 'imunify-security' );
	}

	/**
	 * A value fit to quote in a message: no control or invisible format characters, at most MESSAGE_VALUE_LENGTH characters.
	 *
	 * @param string $value Valid UTF-8 value.
	 * @return string
	 */
	private static function shorten( $value ) {
		$value = trim( preg_replace( '/[\p{Cc}\p{Cf}]/u', '', $value ) );
		preg_match( '/^.{0,' . self::MESSAGE_VALUE_LENGTH . '}/su', $value, $match );
		return $match[0] === $value ? $value : $match[0] . '…';
	}
}

Directory Contents

Dirs: 1 × Files: 77
Name Size Perms Modified Actions
data DIR
- drwxr-xr-x 2026-11-03 01:06:28
Edit Download
11.48 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.43 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.47 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.38 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
4.45 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.17 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.01 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
21.21 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
7.34 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
17.81 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
3.38 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.79 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
13.17 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
8.77 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
5.67 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
14.53 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
4.25 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.72 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
7.58 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.55 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
14.00 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.58 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
5.99 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.88 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.41 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.53 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.61 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
4.73 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
7.49 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
17.86 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
14.97 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.47 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
4.76 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.06 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
5.26 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
5.64 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.68 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
10.47 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.49 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
17.09 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
8.51 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
12.60 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
3.46 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
835 B lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.71 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
22.44 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
5.65 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
7.33 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
892 B lrw-r--r-- 2026-11-03 01:06:29
Edit Download
924 B lrw-r--r-- 2026-11-03 01:06:29
Edit Download
4.69 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
18.57 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
6.92 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
3.87 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
7.94 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
18.17 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
8.48 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.80 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
9.49 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.84 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
7.45 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.28 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.27 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
9.90 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
15.82 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
20.41 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
7.79 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
5.61 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.80 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
30.46 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
2.79 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
1.41 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
3.72 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
9.60 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
5.05 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
6.56 KB lrw-r--r-- 2026-11-03 01:06:29
Edit Download
5.16 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