�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: BotAllowlistSettings.php Size: 46.32 KB
/home2/astrociv/public_html/avitours.in/wp-content/plugins/imunify-security/inc/App/Views/BotAllowlistSettings.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\Views;

use CloudLinux\Imunify\App\Bot\AllowlistCheck;
use CloudLinux\Imunify\App\Bot\AllowlistMatch;
use CloudLinux\Imunify\App\Bot\AllowlistType;
use CloudLinux\Imunify\App\Bot\BundledData;
use CloudLinux\Imunify\App\Bot\IpWhitelist;
use CloudLinux\Imunify\App\Bot\MonitoringUserAgents;
use CloudLinux\Imunify\App\Bot\MuLoader;
use CloudLinux\Imunify\App\Bot\RestNamespaceMatcher;
use CloudLinux\Imunify\App\Bot\SiteAllowlist;
use CloudLinux\Imunify\App\Bot\SiteAllowlistStore;
use CloudLinux\Imunify\App\Bot\SiteAllowlistValidator;

/**
 * The allowlist settings on the Bot Traffic page: the "Allowlist" status card, the settings
 * section with one tab per list, the "Check a request" form, and the AJAX endpoint that
 * validates and saves the site owner's entries and checks requests. The read-only rules are
 * shown as they are in effect, so the counts match what requests see.
 *
 * @since 4.1.4
 */
class BotAllowlistSettings {

	const AJAX_ACTION     = 'imunify_security_bot_allowlist';
	const NONCE_ACTION    = 'imunify_security_bot_allowlist';
	const OP_VALIDATE     = 'validate';
	const OP_SAVE         = 'save';
	const OP_CHECK        = 'check';
	const SECTION_ID      = 'imunify-bot-allowlist';
	const DATA_ELEMENT_ID = 'imunify-bot-allowlist-data';

	/**
	 * Rules shown only as a note below the lists: requests the server sends to itself.
	 */
	const SAME_SERVER_RULES = 1;

	/**
	 * Absolute path to wp-content.
	 *
	 * @var string
	 */
	private $wpContentDir;

	/**
	 * Where the site owner's entries are kept.
	 *
	 * @var SiteAllowlistStore
	 */
	private $store;

	/**
	 * Bundled bot data, under the refreshed copy in wp-content.
	 *
	 * @var string
	 */
	private $bundledDir;

	/**
	 * The folder WordPress is installed in, where "Check a request" finds the script a URL runs.
	 *
	 * @var string
	 */
	private $wordpressDir;

	/**
	 * Set up for a site.
	 *
	 * @param string                  $wp_content_dir Absolute path to wp-content.
	 * @param SiteAllowlistStore|null $store          Store of the entries; defaults to the one of wp-content.
	 * @param string|null             $bundled_dir    Bundled bot data; defaults to the plugin's own.
	 * @param string|null             $wordpress_dir  The folder WordPress is installed in; defaults to ABSPATH.
	 */
	public function __construct( $wp_content_dir, $store = null, $bundled_dir = null, $wordpress_dir = null ) {
		$this->wpContentDir = rtrim( (string) $wp_content_dir, '/' );
		$this->store        = null !== $store ? $store : SiteAllowlistStore::forContentDir( $this->wpContentDir );
		$this->bundledDir   = null !== $bundled_dir ? (string) $bundled_dir : dirname( __DIR__ ) . '/Bot/data';
		$this->wordpressDir = null !== $wordpress_dir ? (string) $wordpress_dir : ABSPATH;
	}

	/**
	 * Hook the AJAX endpoint.
	 *
	 * @return void
	 */
	public function register() {
		add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'handleAjax' ) );
	}

	/**
	 * The capability that may change the allowlist. On multisite one allowlist covers every
	 * site of the network, so only a super admin may change it.
	 *
	 * @return string
	 */
	public static function requiredCapability() {
		return is_multisite() ? 'manage_network_options' : 'manage_options';
	}

	/**
	 * Whether the current user may change the allowlist.
	 *
	 * @return bool
	 */
	public static function currentUserCanEdit() {
		return function_exists( 'current_user_can' ) && current_user_can( self::requiredCapability() );
	}

	/**
	 * Everything the card and the section show.
	 *
	 * @param int|null $now Unix timestamp for the expiry of hosting-provider entries; defaults to time().
	 * @return array
	 */
	public function computeState( $now = null ) {
		$lists   = $this->store->load()->toArray();
		$builtin = $this->builtinRules();
		$hosting = $this->hostingIps( $now );

		$tab_counts  = array(
			SiteAllowlist::LIST_IP         => count( $lists[ SiteAllowlist::LIST_IP ] ) + count( $hosting ),
			SiteAllowlist::LIST_REST_API   => count( $lists[ SiteAllowlist::LIST_REST_API ] ) + count( $builtin[ SiteAllowlist::LIST_REST_API ] ),
			SiteAllowlist::LIST_PATH       => count( $lists[ SiteAllowlist::LIST_PATH ] ) + count( $builtin[ SiteAllowlist::LIST_PATH ] ),
			SiteAllowlist::LIST_USER_AGENT => count( $lists[ SiteAllowlist::LIST_USER_AGENT ] ) + count( $builtin[ SiteAllowlist::LIST_USER_AGENT ] ),
		);
		$owner_count = 0;
		foreach ( $lists as $entries ) {
			$owner_count += count( $entries );
		}

		return array(
			'can_edit'    => self::currentUserCanEdit(),
			'multisite'   => is_multisite(),
			'home_path'   => self::urlPath( home_url() ),
			'lists'       => $lists,
			'hosting_ips' => $hosting,
			'builtin'     => $builtin,
			'owner_count' => $owner_count,
			'tab_counts'  => $tab_counts,
			'rule_count'  => array_sum( $tab_counts ) + self::SAME_SERVER_RULES,
		);
	}

	/**
	 * Number of rules in effect, as the card and the "Manage allowlist" link show it.
	 *
	 * @param array $state Output of computeState().
	 * @return int
	 */
	public static function ruleCount( $state ) {
		return isset( $state['rule_count'] ) ? (int) $state['rule_count'] : 0;
	}

	/**
	 * The fourth status card. It opens and closes the settings section.
	 *
	 * @param array $state Output of computeState().
	 * @return string
	 */
	public function renderStatusCard( $state ) {
		$texts = self::cardTexts( $state );
		return '<button type="button" class="imunify-security__bot-traffic-status-card imunify-security__bot-traffic-status-card--allowlist js-bot-allowlist-toggle"'
			. ' aria-expanded="false" aria-controls="' . esc_attr( self::SECTION_ID ) . '">'
			. '<span class="imunify-security__bot-traffic-status-icon imunify-security__bot-traffic-status-icon--allowlist">' . self::iconShield() . '</span>'
			. '<span class="imunify-security__bot-traffic-status-body">'
			. '<span class="imunify-security__bot-traffic-status-label">' . esc_html__( 'Allowlist', 'imunify-security' ) . '</span>'
			. '<span class="imunify-security__bot-traffic-status-value js-bot-allowlist-card-total">' . esc_html( $texts['total'] ) . '</span>'
			. '<span class="imunify-security__bot-traffic-status-sub">'
			. '<span class="js-bot-allowlist-card-owner">' . esc_html( $texts['owner'] ) . '</span></span>'
			. '<span class="imunify-security__bot-traffic-status-sub">'
			. '<span class="imunify-security__bot-traffic-status-go">' . esc_html__( 'Manage ›', 'imunify-security' ) . '</span>'
			. '</span></span></button>';
	}

	/**
	 * The "Manage allowlist" link under the allowlisted breakdown.
	 *
	 * @param array $state Output of computeState().
	 * @return string
	 */
	public function renderManageLink( $state ) {
		$texts = self::cardTexts( $state );
		return '<button type="button" class="button-link js-bot-allowlist-open" aria-controls="' . esc_attr( self::SECTION_ID ) . '">'
			. esc_html( $texts['link'] )
			. '</button>';
	}

	/**
	 * The settings section, hidden until the card or the link opens it.
	 *
	 * @param array $state Output of computeState().
	 * @return string
	 */
	public function renderSection( $state ) {
		$can_edit = ! empty( $state['can_edit'] );
		$title_id = self::SECTION_ID . '-title';

		$html  = '<section id="' . esc_attr( self::SECTION_ID ) . '" class="imunify-security__bot-allowlist" aria-labelledby="' . esc_attr( $title_id ) . '" hidden>';
		$html .= '<div class="imunify-security__bot-allowlist-head"><div class="imunify-security__bot-allowlist-head-text">';
		$html .= '<h2 id="' . esc_attr( $title_id ) . '" tabindex="-1">' . esc_html__( 'Allowlist', 'imunify-security' ) . '</h2>';
		$html .= '<p class="imunify-security__bot-allowlist-intro">'
			. esc_html__( 'Requests that match a rule skip bot checks and rate limits. Your entries are added to the rules your hosting provider set in Imunify360 and the rules Imunify Security maintains for you. You cannot change those rules here. They update automatically.', 'imunify-security' )
			. '</p>';
		if ( ! $can_edit ) {
			$html .= '<p class="imunify-security__bot-allowlist-intro">'
				. esc_html__( 'Only a network administrator can change the allowlist. It applies to every site in this network.', 'imunify-security' )
				. '</p>';
		} elseif ( ! empty( $state['multisite'] ) ) {
			$html .= '<p class="imunify-security__bot-allowlist-intro">'
				. esc_html__( 'The entries apply to every site in this network.', 'imunify-security' )
				. '</p>';
		}
		$html .= '</div><button type="button" class="button js-bot-allowlist-close">' . esc_html__( 'Close', 'imunify-security' ) . '</button></div>';

		$html .= '<div class="imunify-security__bot-allowlist-tabs" role="tablist" aria-label="' . esc_attr__( 'Allowlist lists', 'imunify-security' ) . '">';
		$first = true;
		foreach ( SiteAllowlist::listNames() as $list ) {
			$html .= '<button type="button" role="tab" class="imunify-security__bot-allowlist-tab js-bot-allowlist-tab"'
				. ' id="' . esc_attr( self::SECTION_ID . '-tab-' . $list ) . '"'
				. ' aria-controls="' . esc_attr( self::SECTION_ID . '-panel-' . $list ) . '"'
				. ' aria-selected="' . ( $first ? 'true' : 'false' ) . '"'
				. ( $first ? '' : ' tabindex="-1"' )
				. ' data-list="' . esc_attr( $list ) . '">'
				. esc_html( self::listTitle( $list ) )
				. ' <span class="imunify-security__bot-allowlist-count js-bot-allowlist-tab-count">'
				. esc_html( number_format_i18n( $state['tab_counts'][ $list ] ) ) . '</span></button>';
			$first = false;
		}
		$html .= '</div>';

		$first = true;
		foreach ( SiteAllowlist::listNames() as $list ) {
			$html .= $this->renderPanel( $state, $list, $first );
			$first = false;
		}

		$html .= '<p class="imunify-security__bot-allowlist-foot-note">'
			. esc_html(
				sprintf(
					/* translators: %d: number of rules for requests the server sends to itself. */
					_n( 'Also always allowed: requests that this server sends to itself (%d rule).', 'Also always allowed: requests that this server sends to itself (%d rules).', self::SAME_SERVER_RULES, 'imunify-security' ),
					self::SAME_SERVER_RULES
				)
			)
			. '</p>';

		if ( $can_edit ) {
			$html .= self::renderCheck( isset( $state['home_path'] ) ? (string) $state['home_path'] : '' );
			$html .= '<div class="imunify-security__bot-allowlist-savebar js-bot-allowlist-savebar">'
				. '<span class="imunify-security__bot-allowlist-save-text js-bot-allowlist-save-text" role="status" aria-live="polite">'
				. esc_html__( 'No unsaved changes', 'imunify-security' ) . '</span>'
				. '<span class="imunify-security__bot-allowlist-save-actions">'
				. '<button type="button" class="button js-bot-allowlist-discard" disabled>' . esc_html__( 'Discard', 'imunify-security' ) . '</button>'
				. '<button type="button" class="button button-primary js-bot-allowlist-save" disabled>' . esc_html__( 'Save changes', 'imunify-security' ) . '</button>'
				. '</span></div>';
		}

		$html .= $this->renderDataBlock( $state );
		$html .= '</section>';
		return $html;
	}

	/**
	 * The texts the settings script shows, for wp_localize_script(). %s placeholders are filled in by the script.
	 *
	 * @return array
	 */
	public static function scriptStrings() {
		return array(
			/* translators: %s: the entry that was added. */
			'added'         => __( '%s was added. Save the changes to apply it.', 'imunify-security' ),
			/* translators: %s: the entry. */
			'duplicate'     => __( '%s is already in your list.', 'imunify-security' ),
			/* translators: %s: maximum number of entries in the list. */
			'full'          => __( 'This list is full (%s entries). Remove an entry first.', 'imunify-security' ),
			'addAnyway'     => __( 'Add anyway', 'imunify-security' ),
			'cancel'        => __( 'Cancel', 'imunify-security' ),
			'remove'        => __( 'Remove', 'imunify-security' ),
			/* translators: %s: the entry to remove. */
			'removeEntry'   => __( 'Remove %s', 'imunify-security' ),
			'undo'          => __( 'Undo', 'imunify-security' ),
			/* translators: %s: the removed entry. */
			'undoEntry'     => __( 'Undo removing %s', 'imunify-security' ),
			'newTag'        => __( 'new', 'imunify-security' ),
			/* translators: 1: formatted number of entries in the list, 2: formatted maximum number of entries. */
			'capText'       => __( '%1$s of %2$s', 'imunify-security' ),
			/* translators: %s: number of entries added or removed and not saved yet. */
			'unsaved'       => __( 'Unsaved changes: %s', 'imunify-security' ),
			'noChanges'     => __( 'No unsaved changes', 'imunify-security' ),
			'checking'      => __( 'Checking…', 'imunify-security' ),
			'saving'        => __( 'Saving…', 'imunify-security' ),
			'wait'          => __( 'Wait until the changes are saved, then add the entry again.', 'imunify-security' ),
			'requestFailed' => __( 'The request failed. Check your connection and try again.', 'imunify-security' ),
			'expired'       => __( 'Your session has expired. Reload the page and try again.', 'imunify-security' ),
			/* translators: 1: kind of rule, such as "Path", 2: the rule, 3: who manages it, such as "added by you". */
			'checkHit'      => __( 'Allowlisted, matched by %1$s rule %2$s (%3$s).', 'imunify-security' ),
			'checkSkips'    => __( 'The request skips bot checks and rate limits.', 'imunify-security' ),
			'checkMiss'     => __( 'Not allowlisted.', 'imunify-security' ),
			'checkNoRule'   => __( 'No rule matches, so the request goes through bot checks and rate limits as usual.', 'imunify-security' ),
			/* translators: %s: the note the site owner wrote for the entry. */
			'checkNote'     => __( 'Your note: %s', 'imunify-security' ),
			/* translators: %s: path and query of the request that was checked. */
			'checkRequest'  => __( 'Checked request: %s', 'imunify-security' ),
			'checkOff'      => __( 'Bot protection is off on this site, so no request is checked right now.', 'imunify-security' ),
		);
	}

	/**
	 * Handle an AJAX request: validate one entry before it is added, save every list, or check a request.
	 *
	 * @return void
	 */
	public function handleAjax() {
		if ( ! self::currentUserCanEdit() ) {
			wp_send_json_error(
				array( 'message' => __( 'You are not allowed to change the allowlist.', 'imunify-security' ) ),
				403
			);
			return; // @phpstan-ignore deadCode.unreachable (wp_die handler may be overridden)
		}
		check_ajax_referer( self::NONCE_ACTION, '_ajax_nonce' );

		// The validator checks and normalizes the raw input; a sanitizer would change what it checks.
		// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$op = isset( $_POST['op'] ) ? wp_unslash( $_POST['op'] ) : '';
		if ( self::OP_VALIDATE === $op ) {
			$this->sendValidation(
				isset( $_POST['list'] ) ? wp_unslash( $_POST['list'] ) : '',
				isset( $_POST['value'] ) ? wp_unslash( $_POST['value'] ) : '',
				isset( $_POST['note'] ) ? wp_unslash( $_POST['note'] ) : ''
			);
			return;
		}
		if ( self::OP_SAVE === $op ) {
			$this->sendSave( isset( $_POST['lists'] ) ? wp_unslash( $_POST['lists'] ) : null );
			return;
		}
		if ( self::OP_CHECK === $op ) {
			$this->sendCheck(
				isset( $_POST['url'] ) ? wp_unslash( $_POST['url'] ) : '',
				isset( $_POST['ip'] ) ? wp_unslash( $_POST['ip'] ) : '',
				isset( $_POST['user_agent'] ) ? wp_unslash( $_POST['user_agent'] ) : '',
				isset( $_POST['lists'] ) ? wp_unslash( $_POST['lists'] ) : null
			);
			return;
		}
		// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		wp_send_json_error( array( 'message' => __( 'Invalid request.', 'imunify-security' ) ), 400 );
	}

	/**
	 * Validate one entry for the "Add" form; nothing is saved.
	 *
	 * @param mixed $list  List name.
	 * @param mixed $value Submitted value.
	 * @param mixed $note  Submitted note.
	 * @return void
	 */
	private function sendValidation( $list, $value, $note ) {
		if ( ! is_string( $list ) || ! in_array( $list, SiteAllowlist::listNames(), true ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid request.', 'imunify-security' ) ), 400 );
			return; // @phpstan-ignore deadCode.unreachable (wp_die handler may be overridden)
		}
		$validation = $this->validator()->validate(
			array(
				$list => array(
					array(
						'value' => $value,
						'note'  => $note,
					),
				),
			)
		);
		$entry      = null;
		if ( $validation->isValid() ) {
			$entries = $validation->allowlist()->entries( $list );
			$entry   = isset( $entries[0] ) ? $entries[0] : null;
		}
		wp_send_json_success(
			array(
				'entry'    => $entry,
				'errors'   => $validation->errors(),
				'warnings' => $validation->warnings(),
			)
		);
	}

	/**
	 * Save every list, or nothing when any entry is invalid.
	 *
	 * @param mixed $raw JSON object of the four lists.
	 * @return void
	 */
	private function sendSave( $raw ) {
		$input      = is_string( $raw ) ? json_decode( $raw, true ) : null;
		$validation = $this->validator()->validate( $input );
		if ( ! $validation->isValid() ) {
			wp_send_json_error(
				array(
					'message'  => __( 'Nothing was saved. Fix the entries marked in red and save again.', 'imunify-security' ),
					'errors'   => $validation->errors(),
					'warnings' => $validation->warnings(),
				),
				400
			);
			return; // @phpstan-ignore deadCode.unreachable (wp_die handler may be overridden)
		}

		$allowlist = $validation->allowlist();
		$result    = $this->store->save( $allowlist );
		if ( SiteAllowlistStore::FAILED === $result ) {
			wp_send_json_error( array( 'message' => __( 'Not saved. Please try again.', 'imunify-security' ) ), 500 );
			return; // @phpstan-ignore deadCode.unreachable (wp_die handler may be overridden)
		}

		wp_send_json_success(
			array(
				'status'   => $result,
				'message'  => SiteAllowlistStore::PENDING === $result
					? __( 'Saved. The new rules take effect within an hour.', 'imunify-security' )
					: __( 'Saved. The new rules apply from the next request.', 'imunify-security' ),
				'lists'    => $allowlist->toArray(),
				'warnings' => $validation->warnings(),
				'card'     => self::cardTexts( $this->computeState() ),
			)
		);
	}

	/**
	 * Check whether a request would be allowlisted, with the unsaved lists when given; nothing is saved or sent.
	 *
	 * @param mixed $url       URL or path.
	 * @param mixed $ip        Client IP address.
	 * @param mixed $ua        User agent.
	 * @param mixed $raw_lists JSON object of the four lists as edited, null for the saved ones.
	 * @return void
	 */
	private function sendCheck( $url, $ip, $ua, $raw_lists ) {
		$field = AllowlistCheck::invalidField( $url, $ip, $ua );
		if ( null !== $field ) {
			wp_send_json_error(
				array(
					'field'   => $field,
					'message' => self::checkFieldMessage( $field ),
				),
				400
			);
			return; // @phpstan-ignore deadCode.unreachable (wp_die handler may be overridden)
		}

		$saved     = $this->store->load();
		$allowlist = $saved;
		if ( null !== $raw_lists ) {
			$validation = $this->validator()->validate( is_string( $raw_lists ) ? json_decode( $raw_lists, true ) : null );
			if ( ! $validation->isValid() ) {
				wp_send_json_error(
					array(
						'message' => __( 'Some of your unsaved entries are not valid. Fix them, then check again.', 'imunify-security' ),
						'errors'  => $validation->errors(),
					),
					400
				);
				return; // @phpstan-ignore deadCode.unreachable (wp_die handler may be overridden)
			}
			$allowlist = $validation->allowlist();
		}

		// On a network a typed path may belong to another site, so it is looked up as a URL.
		$home = rtrim( home_url(), '/' );
		if ( is_multisite() && 1 === preg_match( AllowlistCheck::AUTHORITY_PATTERN, $home ) && 1 !== preg_match( AllowlistCheck::AUTHORITY_PATTERN, trim( $url ) ) ) {
			$url = $home . trim( $url );
		}
		$server_addr = isset( $_SERVER['SERVER_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_ADDR'] ) ) : '';
		$routing     = self::routingFor( $url );
		$check       = new AllowlistCheck( $this->wpContentDir, $routing, $server_addr, $this->wordpressFolder( $routing ) );
		$server      = $check->server( $url, $ip, $ua );
		$match       = $check->match( $url, $ip, $ua, $allowlist );
		$protection  = MuLoader::isProtectionActive( $this->wpContentDir );
		if ( null === $match ) {
			wp_send_json_success(
				array(
					'allowlisted' => false,
					'request'     => $server['REQUEST_URI'],
					'protection'  => $protection,
				)
			);
			return; // @phpstan-ignore deadCode.unreachable (wp_die handler may be overridden)
		}
		wp_send_json_success(
			array(
				'allowlisted'  => true,
				'request'      => $server['REQUEST_URI'],
				'type'         => $match->type(),
				'type_label'   => AllowlistType::label( $match->type() ),
				'entry'        => $match->entry(),
				'source'       => $match->source(),
				'source_label' => self::checkSourceLabel( $match, $saved ),
				'note'         => $match->note(),
				'protection'   => $protection,
			)
		);
	}

	/**
	 * Where WordPress lives, so the check can see which script a URL runs: on a network the
	 * folder is served at the network's path, on a single site at the site URL's path.
	 *
	 * @param array $routing Routing of the site that serves the URL.
	 * @return array|null Null when the check cannot map URLs to files.
	 */
	private function wordpressFolder( $routing ) {
		if ( ! is_multisite() ) {
			return array(
				'dir'               => $this->wordpressDir,
				'path'              => isset( $routing['site_path'] ) ? (string) $routing['site_path'] : '',
				'subfolder_network' => false,
			);
		}
		$path = rtrim( (string) get_current_site()->path, '/' );
		$main = self::urlPath( get_site_url( get_main_site_id() ) );
		// The main site's URL names the folder WordPress lives in; it lies below the network path or nowhere the check can map.
		if ( $main !== $path && 0 !== strpos( $main, $path . '/' ) ) {
			return null;
		}
		return array(
			'dir'               => $this->wordpressDir,
			'path'              => $path,
			'subfolder_network' => ! is_subdomain_install(),
			'folder'            => (string) substr( $main, strlen( $path ) ),
		);
	}

	/**
	 * Routing of the site that serves a URL. One allowlist covers the whole network, so a URL of
	 * another site in it is checked with that site's paths, as requests to it are.
	 *
	 * @param string $url URL or path; a path is on the current site.
	 * @return array
	 */
	private static function routingFor( $url ) {
		if ( ! is_multisite() || 1 !== preg_match( '#^(?:[a-z][a-z0-9+.-]*:)?//([^/?\#:]*)(?::(\d*))?([^?\#]*)#i', trim( $url ), $parts ) ) {
			return MuLoader::siteRouting();
		}
		// As ms_load_current_site_and_network() does: the default ports are dropped, and on a
		// subfolder network one segment below the network path names the site.
		$domain   = strtolower( $parts[1] ) . ( '' === $parts[2] || '80' === $parts[2] || '443' === $parts[2] ? '' : ':' . $parts[2] );
		$segments = is_subdomain_install() ? 1 : substr_count( get_current_site()->path, '/' );
		$site     = get_site_by_path( $domain, '' === $parts[3] ? '/' : $parts[3], $segments );
		if ( ! $site || get_current_blog_id() === (int) $site->blog_id ) {
			return MuLoader::siteRouting();
		}
		switch_to_blog( (int) $site->blog_id );
		$routing = MuLoader::siteRouting();
		restore_current_blog();
		return $routing;
	}

	/**
	 * Why a field of the check form cannot be checked.
	 *
	 * @param string $field One of the AllowlistCheck::FIELD_* values.
	 * @return string
	 */
	private static function checkFieldMessage( $field ) {
		if ( AllowlistCheck::FIELD_URL === $field ) {
			return __( 'Enter a full URL, such as https://example.com/feed/, or a path that starts with /.', 'imunify-security' );
		}
		if ( AllowlistCheck::FIELD_IP === $field ) {
			return __( 'Enter one IPv4 or IPv6 address.', 'imunify-security' );
		}
		return __( 'The user agent must be one line of text, up to 2,048 bytes.', 'imunify-security' );
	}

	/**
	 * Who manages the rule that matched; an owner's entry the saved lists lack is not saved yet.
	 *
	 * @param AllowlistMatch $match The match.
	 * @param SiteAllowlist  $saved The saved entries.
	 * @return string
	 */
	private static function checkSourceLabel( AllowlistMatch $match, SiteAllowlist $saved ) {
		if ( AllowlistMatch::SOURCE_HOSTING_PROVIDER === $match->source() ) {
			return __( 'set by your hosting provider in Imunify360', 'imunify-security' );
		}
		if ( AllowlistMatch::SOURCE_SITE_OWNER !== $match->source() ) {
			return __( 'maintained by Imunify Security', 'imunify-security' );
		}
		// The site owner's match types are the names of their lists.
		$values = in_array( $match->type(), SiteAllowlist::listNames(), true )
			? array_column( $saved->entries( $match->type() ), 'value' )
			: array();
		return in_array( $match->entry(), $values, true )
			? __( 'added by you', 'imunify-security' )
			: __( 'added by you, not saved yet', 'imunify-security' );
	}

	/**
	 * A validator for this site: the home path cuts pasted URLs, the site path finds the WordPress folder.
	 *
	 * @return SiteAllowlistValidator
	 */
	private function validator() {
		return new SiteAllowlistValidator( self::urlPath( home_url() ), self::urlPath( site_url() ), self::otherSiteLookup() );
	}

	/**
	 * On a network whose sites live in subfolders, a lookup of the given paths among the other
	 * sites: an entry that starts with one of them covers that site's pages twice over.
	 *
	 * @return callable|null
	 */
	private static function otherSiteLookup() {
		if ( ! is_multisite() || is_subdomain_install() ) {
			return null;
		}
		$current = get_current_blog_id();
		$network = get_current_network_id();
		return function ( $paths ) use ( $current, $network ) {
			$sites = get_sites(
				array(
					'network_id'             => $network,
					'path__in'               => $paths,
					'site__not_in'           => array( $current ),
					'number'                 => count( $paths ),
					'update_site_meta_cache' => false,
				)
			);
			$found = array();
			foreach ( is_array( $sites ) ? $sites : array() as $site ) {
				$found[] = (string) $site->path;
			}
			return $found;
		};
	}

	/**
	 * The path of a URL without its trailing slash, '' at the root.
	 *
	 * @param string $url URL.
	 * @return string
	 */
	private static function urlPath( $url ) {
		$path = wp_parse_url( (string) $url, PHP_URL_PATH );
		return is_string( $path ) ? rtrim( $path, '/' ) : '';
	}

	/**
	 * The card's two lines.
	 *
	 * @param array $state Output of computeState().
	 * @return array array( 'total' => ..., 'owner' => ..., 'link' => ... )
	 */
	private static function cardTexts( $state ) {
		$count = self::ruleCount( $state );
		$owner = isset( $state['owner_count'] ) ? (int) $state['owner_count'] : 0;
		return array(
			'total' => sprintf(
				/* translators: %s: formatted number of allowlist rules in effect. */
				_n( '%s rule in effect', '%s rules in effect', $count, 'imunify-security' ),
				number_format_i18n( $count )
			),
			'owner' => sprintf(
				/* translators: %s: formatted number of allowlist entries the site owner added. */
				_n( '%s added by you', '%s added by you', $owner, 'imunify-security' ),
				number_format_i18n( $owner )
			),
			'link'  => sprintf(
				/* translators: %s: formatted number of allowlist rules in effect. */
				_n( 'Manage allowlist (%s rule) ›', 'Manage allowlist (%s rules) ›', $count, 'imunify-security' ),
				number_format_i18n( $count )
			),
		);
	}

	/**
	 * The Imunify360 whitelist entries in effect, a single address shown without its prefix length.
	 *
	 * @param int|null $now Unix timestamp; defaults to time().
	 * @return array List of array( 'value' => ..., 'expires' => Unix timestamp, 0 for never ).
	 */
	private function hostingIps( $now ) {
		$out = array();
		foreach ( IpWhitelist::forContentDir( $this->wpContentDir )->entries( $now ) as $entry ) {
			$out[] = array(
				'value'   => preg_replace( false === strpos( $entry[0], ':' ) ? '#/32$#' : '#/128$#', '', $entry[0] ),
				'expires' => $entry[1],
			);
		}
		return $out;
	}

	/**
	 * The rules Imunify Security maintains, one line per rule as the site owner sees it.
	 *
	 * @return array List name => list of array( 'value' => ..., 'label' => ... ).
	 */
	private function builtinRules() {
		$data       = new BundledData( $this->bundledDir, $this->wpContentDir . '/' . IpWhitelist::DATA_DIR . '/bot-data' );
		$namespaces = BundledData::loadArrayFile( $data->pathFor( 'signatures/rest-namespaces.php' ), 'signatures' );
		$matcher    = new RestNamespaceMatcher( $namespaces, true );
		$rest       = array();
		foreach ( $matcher->namespaces() as $namespace ) {
			$rest[] = array(
				'value' => $namespace,
				'label' => self::restNamespaceLabel( $namespace ),
			);
		}

		$user_agents = array();
		foreach ( MonitoringUserAgents::tokens() as $token ) {
			$user_agents[] = array(
				'value' => $token,
				'label' => 'NewRelicPinger' === $token ? 'New Relic' : $token,
			);
		}

		return array(
			SiteAllowlist::LIST_REST_API   => $rest,
			SiteAllowlist::LIST_PATH       => array(
				array(
					'value' => '/wp-cron.php',
					'label' => __( 'WordPress cron', 'imunify-security' ),
				),
				array(
					'value' => '/wp-json',
					'label' => __( 'REST API discovery (the root only)', 'imunify-security' ),
				),
				array(
					'value' => '/wp-admin/admin-ajax.php',
					'label' => __( 'Site Health checks only', 'imunify-security' ),
				),
				array(
					'value' => '/cart',
					'label' => __( 'WooCommerce cart', 'imunify-security' ),
				),
				array(
					'value' => '/checkout',
					'label' => __( 'WooCommerce checkout', 'imunify-security' ),
				),
				array(
					'value' => '/?add-to-cart, wc-ajax, wc-api',
					'label' => __( 'WooCommerce store requests', 'imunify-security' ),
				),
			),
			SiteAllowlist::LIST_USER_AGENT => $user_agents,
		);
	}

	/**
	 * The product a feed REST namespace belongs to, '' when unknown.
	 *
	 * @param string $namespace Namespace with a trailing slash.
	 * @return string
	 */
	private static function restNamespaceLabel( $namespace ) {
		$labels = array(
			'wc/'            => 'WooCommerce',
			'atarim/'        => 'Atarim',
			'elementor/'     => 'Elementor',
			'elementor/mcp/' => 'Elementor MCP',
		);
		return isset( $labels[ $namespace ] ) ? $labels[ $namespace ] : '';
	}

	/**
	 * One tab panel: help, the add form, the owner's entries and the read-only rules.
	 *
	 * @param array  $state   Output of computeState().
	 * @param string $list    List name.
	 * @param bool   $visible Whether the panel is the selected one.
	 * @return string
	 */
	private function renderPanel( $state, $list, $visible ) {
		$can_edit = ! empty( $state['can_edit'] );
		$texts    = self::listTexts( $list, isset( $state['home_path'] ) ? (string) $state['home_path'] : '' );
		$caps     = SiteAllowlistValidator::CAPS;
		$entries  = $state['lists'][ $list ];

		$html  = '<div role="tabpanel" class="imunify-security__bot-allowlist-panel js-bot-allowlist-panel"'
			. ' id="' . esc_attr( self::SECTION_ID . '-panel-' . $list ) . '"'
			. ' aria-labelledby="' . esc_attr( self::SECTION_ID . '-tab-' . $list ) . '"'
			. ' data-list="' . esc_attr( $list ) . '"' . ( $visible ? '' : ' hidden' ) . '>';
		$html .= '<p class="imunify-security__bot-allowlist-help">' . $texts['help'] . '</p>';
		if ( SiteAllowlist::LIST_USER_AGENT === $list ) {
			$html .= '<p class="imunify-security__bot-allowlist-warn-note">'
				. esc_html__( 'User agents are easy to fake. Any bot can send the same text and skip all checks. Use an IP address instead when the service publishes one.', 'imunify-security' )
				. '</p>';
		}
		if ( $can_edit ) {
			$html .= '<form class="imunify-security__bot-allowlist-add js-bot-allowlist-add" data-list="' . esc_attr( $list ) . '" novalidate>'
				. '<label class="imunify-security__bot-allowlist-field imunify-security__bot-allowlist-field--entry"><span>' . esc_html( $texts['label'] ) . '</span>'
				. '<input type="text" name="value" autocomplete="off" spellcheck="false" placeholder="' . esc_attr( $texts['placeholder'] ) . '"></label>'
				. '<label class="imunify-security__bot-allowlist-field imunify-security__bot-allowlist-field--note"><span>' . esc_html__( 'Note (optional)', 'imunify-security' ) . '</span>'
				. '<input type="text" name="note" maxlength="' . (int) SiteAllowlistValidator::NOTE_MAX_LENGTH . '" autocomplete="off" placeholder="' . esc_attr__( 'What is it for?', 'imunify-security' ) . '"></label>'
				. '<button type="submit" class="button button-primary">' . esc_html__( 'Add', 'imunify-security' ) . '</button>'
				. '</form>';
		}
		$html .= '<div class="imunify-security__bot-allowlist-msg js-bot-allowlist-msg" role="status" aria-live="polite"></div>';

		$html .= '<div class="imunify-security__bot-allowlist-sec-head"><h3>' . esc_html__( 'Your entries', 'imunify-security' ) . '</h3>'
			. '<span class="imunify-security__bot-allowlist-cap js-bot-allowlist-cap">'
			. esc_html(
				sprintf(
					/* translators: 1: formatted number of entries in the list, 2: formatted maximum number of entries. */
					__( '%1$s of %2$s', 'imunify-security' ),
					number_format_i18n( count( $entries ) ),
					number_format_i18n( $caps[ $list ] )
				)
			)
			. '</span></div>';
		$html .= '<div class="imunify-security__bot-allowlist-table-box"><table class="imunify-security__bot-allowlist-table">'
			. '<thead><tr><th scope="col">' . esc_html__( 'Entry', 'imunify-security' ) . '</th>'
			. '<th scope="col">' . esc_html__( 'Note', 'imunify-security' ) . '</th>'
			. ( $can_edit ? '<th scope="col"><span class="screen-reader-text">' . esc_html__( 'Remove', 'imunify-security' ) . '</span></th>' : '' )
			. '</tr></thead>'
			. '<tbody class="js-bot-allowlist-rows" data-empty="' . esc_attr( $texts['empty'] ) . '">';
		foreach ( $entries as $entry ) {
			$html .= '<tr><td><code>' . esc_html( $entry['value'] ) . '</code></td><td>' . esc_html( $entry['note'] ) . '</td>'
				. ( $can_edit ? '<td class="imunify-security__bot-allowlist-actions"><button type="button" class="button-link imunify-security__bot-allowlist-remove js-bot-allowlist-remove">' . esc_html__( 'Remove', 'imunify-security' ) . '</button></td>' : '' )
				. '</tr>';
		}
		if ( empty( $entries ) ) {
			$html .= '<tr class="imunify-security__bot-allowlist-empty"><td colspan="' . ( $can_edit ? 3 : 2 ) . '">' . esc_html( $texts['empty'] ) . '</td></tr>';
		}
		$html .= '</tbody></table></div>';

		$html .= SiteAllowlist::LIST_IP === $list
			? $this->renderHostingIps( $state['hosting_ips'] )
			: $this->renderBuiltin( $state['builtin'][ $list ], $texts['source_note'] );

		$html .= '</div>';
		return $html;
	}

	/**
	 * The Imunify360 whitelist, folded, searchable and with the expiry of each entry.
	 *
	 * @param array $entries Output of hostingIps().
	 * @return string
	 */
	private function renderHostingIps( $entries ) {
		$date_format = (string) get_option( 'date_format', 'Y-m-d' );
		$rows        = '';
		foreach ( $entries as $entry ) {
			$expires = 0 === $entry['expires']
				? __( 'Never', 'imunify-security' )
				: self::localDate( $date_format, $entry['expires'] );
			$rows   .= '<tr class="js-bot-allowlist-ro-row"><td><code>' . esc_html( $entry['value'] ) . '</code></td><td>' . esc_html( $expires ) . '</td></tr>';
		}
		if ( empty( $entries ) ) {
			$rows .= '<tr><td colspan="2">' . esc_html__( 'Your hosting provider has not allowlisted any IP addresses.', 'imunify-security' ) . '</td></tr>';
		} else {
			$rows .= '<tr class="js-bot-allowlist-nomatch" hidden><td colspan="2">' . esc_html__( 'No entry matches your search.', 'imunify-security' ) . '</td></tr>';
		}

		$search = empty( $entries ) ? '' : '<label class="imunify-security__bot-allowlist-field imunify-security__bot-allowlist-field--search"><span>' . esc_html__( 'Search', 'imunify-security' ) . '</span>'
			. '<input type="search" class="js-bot-allowlist-search" autocomplete="off" spellcheck="false" placeholder="' . esc_attr__( 'Filter by IP address', 'imunify-security' ) . '"></label>';

		return $this->renderReadOnly(
			__( 'Set by your hosting provider in Imunify360', 'imunify-security' ),
			__( 'Ask your hosting provider to change these.', 'imunify-security' ),
			count( $entries ),
			$search,
			__( 'Expires', 'imunify-security' ),
			$rows
		);
	}

	/**
	 * A UTC timestamp as a date in the site's time zone, with the offset of that date.
	 *
	 * @param string $format    Date format.
	 * @param int    $timestamp Unix timestamp.
	 * @return string
	 */
	private static function localDate( $format, $timestamp ) {
		if ( function_exists( 'wp_date' ) ) {
			$date = wp_date( $format, $timestamp );
			if ( is_string( $date ) ) {
				return $date;
			}
		}
		// WordPress before 5.3 has no wp_date(); today's offset may be an hour off across a DST change.
		return date_i18n( $format, $timestamp + (int) ( (float) get_option( 'gmt_offset', 0 ) * HOUR_IN_SECONDS ) );
	}

	/**
	 * The rules Imunify Security maintains for one list, folded.
	 *
	 * @param array  $rules       Rules of the list.
	 * @param string $source_note How the rules are updated.
	 * @return string
	 */
	private function renderBuiltin( $rules, $source_note ) {
		$rows = '';
		foreach ( $rules as $rule ) {
			$rows .= '<tr><td><code>' . esc_html( $rule['value'] ) . '</code></td><td>' . esc_html( $rule['label'] ) . '</td></tr>';
		}
		return $this->renderReadOnly(
			__( 'Maintained by Imunify Security', 'imunify-security' ),
			$source_note,
			count( $rules ),
			'',
			__( 'For', 'imunify-security' ),
			$rows
		);
	}

	/**
	 * A folded group of read-only rules.
	 *
	 * @param string $source      Who manages the rules.
	 * @param string $note        How they change.
	 * @param int    $count       Number of rules.
	 * @param string $search_html Search field markup, '' for none.
	 * @param string $column      Heading of the second column.
	 * @param string $rows_html   Escaped table rows.
	 * @return string
	 */
	private function renderReadOnly( $source, $note, $count, $search_html, $column, $rows_html ) {
		return '<details class="imunify-security__bot-allowlist-ro">'
			. '<summary><span class="imunify-security__bot-allowlist-lock" aria-hidden="true">' . self::iconLock() . '</span>'
			. '<b>' . esc_html( $source ) . ' · ' . esc_html( number_format_i18n( $count ) ) . '</b>'
			. '<span class="imunify-security__bot-allowlist-ro-note">' . esc_html( $note ) . '</span></summary>'
			. '<div class="imunify-security__bot-allowlist-ro-body">' . $search_html
			. '<div class="imunify-security__bot-allowlist-table-box"><table class="imunify-security__bot-allowlist-table">'
			. '<thead><tr><th scope="col">' . esc_html__( 'Entry', 'imunify-security' ) . '</th><th scope="col">' . esc_html( $column ) . '</th></tr></thead>'
			. '<tbody>' . $rows_html . '</tbody></table></div></div></details>';
	}

	/**
	 * The "Check a request" form. The script sends it with the unsaved lists.
	 *
	 * @param string $home_path Path of the home URL, '' at the root.
	 * @return string
	 */
	private static function renderCheck( $home_path ) {
		$title_id = self::SECTION_ID . '-check-title';
		$help     = esc_html__( 'See whether a request would be allowlisted, and by which rule. Your unsaved changes are included. No request is sent to your site.', 'imunify-security' );
		if ( '' !== $home_path ) {
			$help .= ' ' . self::helpWithCode(
				/* translators: %s: path of the site's home URL, such as /blog. */
				__( 'Paths are relative to your site\'s path, %s.', 'imunify-security' ),
				array( $home_path )
			);
		}
		return '<div class="imunify-security__bot-allowlist-check" role="group" aria-labelledby="' . esc_attr( $title_id ) . '">'
			. '<h3 id="' . esc_attr( $title_id ) . '">' . esc_html__( 'Check a request', 'imunify-security' ) . '</h3>'
			. '<p class="imunify-security__bot-allowlist-help">' . $help . '</p>'
			. '<form class="imunify-security__bot-allowlist-check-form js-bot-allowlist-check" novalidate>'
			. '<label class="imunify-security__bot-allowlist-field imunify-security__bot-allowlist-field--url"><span>' . esc_html__( 'URL', 'imunify-security' ) . '</span>'
			. '<input type="text" name="url" autocomplete="off" spellcheck="false" placeholder="' . esc_attr__( 'https://example.com/feed/', 'imunify-security' ) . '"></label>'
			. '<label class="imunify-security__bot-allowlist-field imunify-security__bot-allowlist-field--ip"><span>' . esc_html__( 'IP address', 'imunify-security' ) . '</span>'
			. '<input type="text" name="ip" autocomplete="off" spellcheck="false" placeholder="' . esc_attr__( '203.0.113.24', 'imunify-security' ) . '"></label>'
			. '<label class="imunify-security__bot-allowlist-field imunify-security__bot-allowlist-field--ua"><span>' . esc_html__( 'User agent', 'imunify-security' ) . '</span>'
			. '<input type="text" name="user_agent" autocomplete="off" spellcheck="false" placeholder="' . esc_attr__( 'MyMonitor/1.0', 'imunify-security' ) . '"></label>'
			. '<button type="submit" class="button">' . esc_html__( 'Check', 'imunify-security' ) . '</button>'
			. '</form>'
			. '<div class="imunify-security__bot-allowlist-check-result js-bot-allowlist-check-result" role="status" aria-live="polite"></div>'
			. '</div>';
	}

	/**
	 * The JSON the script starts from: the saved entries and the caps.
	 *
	 * @param array $state Output of computeState().
	 * @return string
	 */
	private function renderDataBlock( $state ) {
		$readonly = array();
		foreach ( SiteAllowlist::listNames() as $list ) {
			$readonly[ $list ] = $state['tab_counts'][ $list ] - count( $state['lists'][ $list ] );
		}
		$json = wp_json_encode(
			array(
				'lists'    => $state['lists'],
				'caps'     => SiteAllowlistValidator::CAPS,
				'readonly' => $readonly,
			),
			JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
		);
		if ( false === $json ) {
			$json = '{}';
		}
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON_HEX_TAG makes the payload safe inside a script element.
		return '<script type="application/json" id="' . esc_attr( self::DATA_ELEMENT_ID ) . '">' . $json . '</script>';
	}

	/**
	 * A list's tab title.
	 *
	 * @param string $list List name.
	 * @return string
	 */
	private static function listTitle( $list ) {
		$titles = array(
			SiteAllowlist::LIST_IP         => __( 'IP addresses', 'imunify-security' ),
			SiteAllowlist::LIST_REST_API   => __( 'REST API routes', 'imunify-security' ),
			SiteAllowlist::LIST_PATH       => __( 'Paths', 'imunify-security' ),
			SiteAllowlist::LIST_USER_AGENT => __( 'User agents', 'imunify-security' ),
		);
		return $titles[ $list ];
	}

	/**
	 * A list's help (escaped markup), field label, placeholder, empty-list text and source note.
	 *
	 * @param string $list      List name.
	 * @param string $home_path Path of the home URL, '' at the root.
	 * @return array
	 */
	private static function listTexts( $list, $home_path ) {
		switch ( $list ) {
			case SiteAllowlist::LIST_IP:
				return array(
					'help'        => esc_html__( 'One IPv4 or IPv6 address, or a CIDR range. Requests from these addresses are always allowlisted.', 'imunify-security' ),
					'label'       => __( 'IP address or range', 'imunify-security' ),
					'placeholder' => __( '203.0.113.24 or 198.51.100.0/24', 'imunify-security' ),
					'empty'       => __( 'You have not added any IP addresses.', 'imunify-security' ),
					'source_note' => '',
				);
			case SiteAllowlist::LIST_REST_API:
				return array(
					'help'        => self::helpWithCode(
						/* translators: 1: example route prefix, 2: REST URL form, 3: query-string form, 4: the wildcard character, 5: example prefix with a wildcard. */
						__( 'A route prefix such as %1$s. It covers every route below it, sent as %2$s or %3$s. %4$s stands for one whole segment, as in %5$s. You can paste a full URL.', 'imunify-security' ),
						array( 'myplugin/v1/', '/wp-json/…', '?rest_route=…', '*', 'myplugin/*/webhook/' )
					),
					'label'       => __( 'REST API route', 'imunify-security' ),
					'placeholder' => __( 'myplugin/v1/ or a full /wp-json/… URL', 'imunify-security' ),
					'empty'       => __( 'You have not added any REST API routes.', 'imunify-security' ),
					'source_note' => __( 'Updated automatically.', 'imunify-security' ),
				);
			case SiteAllowlist::LIST_PATH:
				$help = self::helpWithCode(
					/* translators: 1: example path, 2: a path below it, 3: a path that is not below it, 4: the wildcard character, 5: example path with a wildcard. */
					__( 'A path such as %1$s. It covers the path and everything below it (%2$s, not %3$s). %4$s stands for one whole segment, as in %5$s. The query string is not matched.', 'imunify-security' ),
					array( '/feed', '/feed/rss2', '/feedback', '*', '/*/feed' )
				);
				if ( '' !== $home_path ) {
					$help .= ' ' . self::helpWithCode(
						/* translators: %s: path of the site's home URL, such as /blog. */
						__( 'Paths are relative to your site\'s path, %s.', 'imunify-security' ),
						array( $home_path )
					);
				}
				return array(
					'help'        => $help,
					'label'       => __( 'Path', 'imunify-security' ),
					'placeholder' => __( '/my-path or a full URL', 'imunify-security' ),
					'empty'       => __( 'You have not added any paths.', 'imunify-security' ),
					'source_note' => __( 'Updated with the plugin.', 'imunify-security' ),
				);
			default:
				return array(
					'help'        => self::helpWithCode(
						/* translators: %s: example user agent text. */
						__( 'Text that the user agent contains, at least 4 characters, in any letter case. Use the part that names the tool, for example %s.', 'imunify-security' ),
						array( 'MyMonitor' )
					),
					'label'       => __( 'User agent text', 'imunify-security' ),
					'placeholder' => __( 'MyMonitor', 'imunify-security' ),
					'empty'       => __( 'You have not added any user agents.', 'imunify-security' ),
					'source_note' => __( 'Updated with the plugin.', 'imunify-security' ),
				);
		}
	}

	/**
	 * Escape a translated help text and fill its placeholders with code samples.
	 *
	 * @param string   $format  Translated printf format.
	 * @param string[] $samples Code samples, in placeholder order.
	 * @return string Escaped markup.
	 */
	private static function helpWithCode( $format, $samples ) {
		$codes = array();
		foreach ( $samples as $sample ) {
			$codes[] = '<code>' . esc_html( $sample ) . '</code>';
		}
		return vsprintf( esc_html( $format ), $codes );
	}

	/**
	 * Shield glyph for the Allowlist card.
	 *
	 * @return string
	 */
	private static function iconShield() {
		return '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z"/><path d="M9 12l2 2 4-4"/></svg>';
	}

	/**
	 * Lock glyph for the read-only groups.
	 *
	 * @return string
	 */
	private static function iconLock() {
		return '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>';
	}
}

Directory Contents

Dirs: 1 × Files: 5
Name Size Perms Modified Actions
Display DIR
- drwxr-xr-x 2026-11-03 01:06:25
Edit Download
11.02 KB lrwxr-xr-x 2026-11-03 01:06:25
Edit Download
46.32 KB lrwxr-xr-x 2026-11-03 01:06:25
Edit Download
45.53 KB lrwxr-xr-x 2026-11-03 01:06:25
Edit Download
52.39 KB lrwxr-xr-x 2026-11-03 01:06:25
Edit Download
12.48 KB lrwxr-xr-x 2026-11-03 01:06:25
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