�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: IncidentRecorder.php Size: 17.87 KB
/home2/astrociv/public_html/avitours.in/wp-content/plugins/imunify-security/inc/App/Defender/IncidentRecorder.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/
 *
 * phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
 * phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
 */

namespace CloudLinux\Imunify\App\Defender;

use CloudLinux\Imunify\App\Bot\AtomicFileWriter;
use CloudLinux\Imunify\App\Defender\Model\Rule;
use CloudLinux\Imunify\App\Defender\Model\TargetInfo;
use CloudLinux\Imunify\App\Helpers\IpAddress;

/**
 * Incident recorder for security events.
 *
 * Records security incidents to daily files for processing by the Imunify agent.
 *
 * @since 2.1.0
 */
class IncidentRecorder {

	/**
	 * Maximum size for raw POST data in bytes (1 kB).
	 *
	 * @var int
	 */
	const RAW_DATA_MAX_SIZE = 1024;

	/**
	 * Maximum JSON-encoded size for array data fields in bytes (10 kB).
	 *
	 * @var int
	 */
	const ARRAY_DATA_MAX_SIZE = 10240;

	/**
	 * Rate limiter instance.
	 *
	 * @var RateLimiter
	 */
	private $rateLimiter;

	/**
	 * Constructor.
	 *
	 * @param RateLimiter $rateLimiter Rate limiter instance.
	 */
	public function __construct( RateLimiter $rateLimiter ) {
		$this->rateLimiter = $rateLimiter;
	}

	/**
	 * Record a security incident.
	 *
	 * @param Rule       $rule       The rule that was triggered.
	 * @param string     $mode       The rule mode (pass or block).
	 * @param TargetInfo $targetInfo Target information.
	 * @param Request    $request    Request object.
	 * @param string     $version    Ruleset version.
	 *
	 * @return void
	 */
	public function recordIncident( Rule $rule, $mode, TargetInfo $targetInfo, Request $request, $version = '' ) {
		// Extract client IP address for rate limiting.
		$ip_address = IpAddress::getClientIp( $request );

		// Check rate limit before recording incident.
		if ( ! $this->rateLimiter->checkRateLimit( $rule->getId(), $ip_address ) ) {
			// Rate limit exceeded, silently skip recording.
			return;
		}

		$incidentData = $this->buildIncidentData( $rule, $mode, $targetInfo, $request, $version );
		$this->writeIncidentToFile( $incidentData );
	}

	/**
	 * Record a browser-side service worker finding.
	 *
	 * The finding is made in a browser and only reaches PHP when an administrator
	 * is logged in, so the address on this request belongs to that administrator.
	 * Recording it as the attacker would feed our own customers' addresses into
	 * whatever consumes that field, so it is reported empty. The real address is
	 * still used for rate limiting, which never leaves the site.
	 *
	 * @param string     $ruleId     Rule that carried the payload.
	 * @param string     $cve        Rule identifier used in place of a CVE.
	 * @param string     $mode       Deployed rule mode (pass or block).
	 * @param string     $event      One of unregistered, detected, recurrence.
	 * @param array      $details    Validated event details.
	 * @param Request    $request    Request the report arrived on.
	 * @param TargetInfo $targetInfo Target the rule names, resolved on this site.
	 * @param string     $version    Ruleset version.
	 *
	 * @return bool True when the incident was recorded.
	 */
	public function recordServiceWorkerIncident( $ruleId, $cve, $mode, $event, array $details, Request $request, TargetInfo $targetInfo, $version = '' ) {
		$ipAddress = IpAddress::getClientIp( $request );

		if ( ! $this->rateLimiter->checkRateLimit( $ruleId, $ipAddress ) ) {
			return false;
		}

		$message = sprintf(
			'IM WP plugin: %s %s %s %s %s||SW:%s||CTX:%s||SCOPE:%s||SCRIPT:%s',
			$ruleId,
			$cve,
			$targetInfo->getSlug(),
			$targetInfo->getVersion(),
			$mode,
			$event,
			$details['context'],
			self::forMessage( $details['scope'] ),
			self::forMessage( $details['script_url'] )
		);

		if ( ! empty( $details['recurrences'] ) ) {
			$message .= '||RECUR:' . (int) $details['recurrences'];
		}

		$message .= '||PL:' . $details['payload_id'] . '/' . $details['payload_version'];

		if ( ! empty( $version ) ) {
			$message .= '||RSV:' . $version;
		}

		// The agent keeps a fixed set of fields and drops the rest, so the
		// structured detail travels in the one free-form field that survives.
		$rawData = $this->buildServiceWorkerRawData( $event, $details );

		$incidentData = array_merge(
			array(
				'ts'             => time(),
				'rule_id'        => $ruleId,
				'cve'            => $cve,
				'mode'           => $mode,
				'target'         => $targetInfo->getType(),
				'slug'           => $targetInfo->getSlug(),
				'version'        => $targetInfo->getVersion(),
				'user_logged_in' => is_user_logged_in(),
				'message'        => $message,
				'RAW_DATA'       => $rawData,
			),
			$this->getServerData( $request )
		);

		// Every field that can carry the administrator's address or their session.
		// Blanking attacker_ip and REMOTE_ADDR alone is not anonymisation: the
		// client address on a proxied site lives in the forwarded header, which is
		// precisely where IpAddress::getClientIp() looks first, and the referer is
		// the wp-admin URL they were on, which routinely embeds a live nonce.
		$incidentData['attacker_ip']          = '';
		$incidentData['REMOTE_ADDR']          = '';
		$incidentData['HTTP_X_FORWARDED_FOR'] = '';
		$incidentData['HTTP_REFERER']         = '';

		$this->writeIncidentToFile( $incidentData );

		return true;
	}

	/**
	 * Encode the service worker detail so it fits RAW_DATA and stays decodable.
	 *
	 * Cutting the encoded document to length would leave a consumer with JSON it
	 * cannot parse, so the two fields that can be arbitrarily long are shortened
	 * until the whole document fits instead.
	 *
	 * @param string $event   One of unregistered, detected, recurrence.
	 * @param array  $details Validated event details.
	 *
	 * @return string Empty string when even the shortest form cannot be encoded.
	 */
	private function buildServiceWorkerRawData( $event, array $details ) {
		$scope     = (string) $details['scope'];
		$scriptUrl = (string) $details['script_url'];

		for ( $urlLimit = self::RAW_DATA_MAX_SIZE; $urlLimit >= 0; $urlLimit = (int) floor( $urlLimit / 2 ) ) {
			$encoded = wp_json_encode(
				array(
					'sw_event'        => $event,
					'scope'           => self::clampUtf8( $scope, $urlLimit ),
					'script_url'      => self::clampUtf8( $scriptUrl, $urlLimit ),
					'context'         => $details['context'],
					'recurrences'     => (int) $details['recurrences'],
					'payload_id'      => $details['payload_id'],
					'payload_version' => $details['payload_version'],
					'payload_mode'    => $details['payload_mode'],
				)
			);

			if ( false !== $encoded && strlen( $encoded ) <= self::RAW_DATA_MAX_SIZE ) {
				return $encoded;
			}

			if ( 0 === $urlLimit ) {
				break;
			}
		}

		return '';
	}

	/**
	 * Make a value safe to place between the message's `||` markers.
	 *
	 * A worker's URL is chosen by whoever registered it, which on an infected
	 * site is the attacker, and the backend parses this message on `||`. A URL
	 * carrying the delimiter would otherwise write markers of its own, and they
	 * would land before the real ones. Percent-encoding leaves the URL readable
	 * and recoverable, and leaves exactly one of each marker to find.
	 *
	 * @param string $value Value to place in the message.
	 *
	 * @return string
	 */
	private static function forMessage( $value ) {
		// Also dropped to valid UTF-8: the whole incident is JSON-encoded before
		// it is written, so one bad byte here would cost the entire record, not
		// just this field.
		return self::clampUtf8( str_replace( '|', '%7C', (string) $value ), self::RAW_DATA_MAX_SIZE );
	}

	/**
	 * Shorten a string to a byte length without cutting a character in half.
	 *
	 * A cut inside a multi-byte sequence leaves bytes wp_json_encode() refuses,
	 * which would cost the whole field rather than the tail it was meant to trim.
	 *
	 * @param string $value Value to shorten.
	 * @param int    $bytes Maximum length in bytes.
	 *
	 * @return string
	 */
	private static function clampUtf8( $value, $bytes ) {
		$clamped = substr( $value, 0, $bytes );

		// A trailing incomplete sequence is at most three bytes long.
		while ( '' !== $clamped && 1 !== preg_match( '//u', $clamped ) ) {
			$clamped = substr( $clamped, 0, -1 );
		}

		return $clamped;
	}

	/**
	 * Write incident data to the hourly file.
	 *
	 * @param array $incidentData The incident data to write.
	 *
	 * @return void
	 */
	private function writeIncidentToFile( $incidentData ) {
		$filePath = $this->getHourlyFilePath();

		// Ensure directory exists before writing.
		if ( ! $this->ensureDirectoryExists() ) {
			// Failed to ensure directory exists, cannot proceed.
			return;
		}

		// Encode the JSON data as base64 and format with PHP comment prefix.
		$jsonData = wp_json_encode( $incidentData );
		if ( false === $jsonData ) {
			// Writing the '#' with nothing after it would put a line in the file
			// that no consumer can read, which is worse than dropping the record.
			return;
		}
		$encodedData = base64_encode( $jsonData ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		$jsonLine    = '#' . $encodedData . "\n";

		// Check if file exists to determine if we need to write the PHP header.
		$fileExists = file_exists( $filePath );

		// If file doesn't exist, write the PHP header first.
		if ( ! $fileExists ) {
			$phpHeader = "<?php __halt_compiler();\n";
			@file_put_contents( $filePath, $phpHeader );
		}

		// Append to the hourly file.
		@file_put_contents(
			$filePath,
			$jsonLine,
			FILE_APPEND
		);
	}

	/**
	 * Build incident data array.
	 *
	 * @param Rule       $rule       The rule that was triggered.
	 * @param string     $mode       The rule mode (pass or block).
	 * @param TargetInfo $targetInfo Target information.
	 * @param Request    $request    Request object.
	 * @param string     $version    Ruleset version.
	 *
	 * @return array
	 */
	private function buildIncidentData( Rule $rule, $mode, TargetInfo $targetInfo, Request $request, $version = '' ) {
		$result = array(
			'ts'             => time(),
			'rule_id'        => $rule->getId(),
			'cve'            => $rule->getCve(),
			'mode'           => $mode,
			'target'         => $targetInfo->getType(),
			'slug'           => $targetInfo->getSlug(),
			'version'        => $targetInfo->getVersion(),
			'user_logged_in' => is_user_logged_in(),
			'attacker_ip'    => IpAddress::getClientIp( $request ),
			'message'        => $this->buildLogMessage( $rule, $mode, $targetInfo, $version ),
			'FILES'          => $this->getFilesData( $request ),
			'GET_NAMES'      => $this->getGetNames( $request ),
			'POST_NAMES'     => $this->getPostNames( $request ),
			'RAW_DATA'       => $this->getRawPostData(),
		);

		// Merge server data fields at top level.
		return array_merge( $result, $this->getServerData( $request ) );
	}

	/**
	 * Build log message for the incident.
	 *
	 * @param Rule       $rule       The rule that was triggered.
	 * @param string     $mode       The rule mode (pass or block).
	 * @param TargetInfo $targetInfo Target information.
	 * @param string     $version    Ruleset version.
	 *
	 * @return string
	 */
	private function buildLogMessage( Rule $rule, $mode, TargetInfo $targetInfo, $version = '' ) {
		$message = sprintf(
			'IM WP plugin: %s %s %s %s %s',
			$rule->getId(),
			$rule->getCve(),
			$targetInfo->getSlug(),
			$targetInfo->getVersion(),
			$mode
		);

		if ( ! empty( $version ) ) {
			$message .= '||RSV:' . $version;
		}

		return $message;
	}


	/**
	 * Get server data from Request object.
	 *
	 * @param Request $request Request object.
	 *
	 * @return array
	 */
	private function getServerData( Request $request ) {
		$result = array();
		$server = $request->getAllServer();

		$serverFields = array(
			'REMOTE_ADDR',
			'REQUEST_METHOD',
			'SCRIPT_FILENAME',
			'PHP_SELF',
			'PATH_INFO',
			'REQUEST_URI',
			'QUERY_STRING',
			'HTTP_X_FORWARDED_FOR',
			'HTTP_USER_AGENT',
			'HTTP_REFERER',
		);

		foreach ( $serverFields as $field ) {
			$result[ $field ] = isset( $server[ $field ] ) ? $server[ $field ] : '';
		}

		return $result;
	}

	/**
	 * Get raw POST data from php://input.
	 *
	 * Limits the data to first 1 kB and base64 encodes it.
	 *
	 * @return string Base64-encoded string of first 1 kB of raw POST data.
	 */
	public function getRawPostData() {
		$result = @file_get_contents( 'php://input' );

		if ( ! $result ) {
			return '';
		}

		// Limit to first 1 kB.
		$truncated = substr( $result, 0, self::RAW_DATA_MAX_SIZE );

		// Base64 encode the truncated data.
		return base64_encode( $truncated ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
	}

	/**
	 * Get POST argument names as array.
	 *
	 * @param Request $request Request object.
	 *
	 * @return array Array of POST argument names, limited to 10 kB JSON-encoded size.
	 */
	private function getPostNames( Request $request ) {
		// getPostNames() already drops the synthetic fail-closed RAW_BODY_KEY
		// sentinel, so it is not logged as a client-supplied POST parameter.
		$names = $request->getPostNames();

		// Limit the array to 10 kB JSON-encoded size.
		return $this->limitArraySize( $names, self::ARRAY_DATA_MAX_SIZE );
	}

	/**
	 * Get GET argument names as array.
	 *
	 * @param Request $request Request object.
	 *
	 * @return array Array of GET argument names, limited to 10 kB JSON-encoded size.
	 */
	private function getGetNames( Request $request ) {
		$names = array_keys( $request->getAllGet() );

		// Limit the array to 10 kB JSON-encoded size.
		return $this->limitArraySize( $names, self::ARRAY_DATA_MAX_SIZE );
	}

	/**
	 * Get files information.
	 *
	 * Handles both single-file and multi-file upload structures from PHP's
	 * $_FILES superglobal. Multi-file uploads (e.g. name="files[]") produce
	 * arrays for 'name', 'type', 'size' — these are expanded into individual
	 * entries keyed as "{fieldName}_0", "{fieldName}_1", etc.
	 *
	 * @param Request $request Request object.
	 *
	 * @return array Detailed file information, limited to 10 kB JSON-encoded size.
	 */
	private function getFilesData( Request $request ) {
		$result = array();
		$files  = $request->getAllFiles();

		foreach ( $files as $fieldName => $fileInfo ) {
			if ( ! isset( $fileInfo['name'] ) ) {
				continue;
			}

			if ( is_array( $fileInfo['name'] ) ) {
				$names = $fileInfo['name'];
				$types = isset( $fileInfo['type'] ) && is_array( $fileInfo['type'] ) ? $fileInfo['type'] : array();
				$sizes = isset( $fileInfo['size'] ) && is_array( $fileInfo['size'] ) ? $fileInfo['size'] : array();

				foreach ( $names as $index => $name ) {
					$key            = $fieldName . '_' . $index;
					$result[ $key ] = array(
						'name' => $name,
						'type' => isset( $types[ $index ] ) ? $types[ $index ] : '',
						'size' => isset( $sizes[ $index ] ) ? $sizes[ $index ] : 0,
					);
				}
			} else {
				$result[ $fieldName ] = array(
					'name' => $fileInfo['name'],
					'type' => isset( $fileInfo['type'] ) ? $fileInfo['type'] : '',
					'size' => isset( $fileInfo['size'] ) ? $fileInfo['size'] : 0,
				);
			}
		}

		// Limit the array to 10 kB JSON-encoded size.
		return $this->limitArraySize( $result, self::ARRAY_DATA_MAX_SIZE );
	}

	/**
	 * Get the hourly file path for incidents.
	 *
	 * @return string
	 */
	private function getHourlyFilePath() {
		$date   = gmdate( 'Y-m-d-H' );
		$result = WP_CONTENT_DIR . '/imunify-security/incidents';
		return $result . '/' . $date . '.php';
	}

	/**
	 * Ensure the incidents directory exists.
	 *
	 * @return bool
	 */
	public function ensureDirectoryExists() {
		$incidentsDir = WP_CONTENT_DIR . '/imunify-security/incidents';
		if ( ! is_dir( $incidentsDir ) ) {
			$success = wp_mkdir_p( $incidentsDir );
			if ( ! $success ) {
				return false;
			}
		}

		// Ensure directory listing protection is in place.
		$this->ensureDirectoryListingProtection( $incidentsDir );

		return true;
	}

	/**
	 * Ensure directory listing protection files exist.
	 *
	 * Creates .htaccess, index.php, and index.html files to prevent directory listing.
	 *
	 * @param string $directory The directory path to protect.
	 *
	 * @return void
	 */
	public function ensureDirectoryListingProtection( $directory ) {
		AtomicFileWriter::ensureDirectoryProtection( $directory );
	}

	/**
	 * Limit array size based on estimated JSON-encoded size.
	 *
	 * @param array $array The array to limit.
	 * @param int   $max_size Maximum size in bytes for JSON-encoded array.
	 *
	 * @return array The truncated array that fits within the size limit.
	 */
	private function limitArraySize( $array, $max_size ) {
		if ( empty( $array ) ) {
			return $array;
		}

		$is_numeric_array = $this->isNumericArray( $array );
		$result           = array();
		$estimated_size   = 2;

		foreach ( $array as $key => $value ) {
			$elementSize = 0;

			if ( $is_numeric_array ) {
				$elementSize = strlen( $value ) + 2;
			} else {
				$keySize      = strlen( (string) $key ) + 4;
				$fileInfoSize = 2;
				$first_inner  = true;
				foreach ( $value as $k => $v ) {
					$keyLen        = strlen( (string) $k );
					$valLen        = is_array( $v )
						? strlen( (string) wp_json_encode( $v ) )
						: strlen( (string) $v );
					$fileInfoSize += $keyLen + $valLen + 5;
					if ( ! $first_inner ) {
						$fileInfoSize++;
					}
					$first_inner = false;
				}
				$elementSize = $keySize + $fileInfoSize;
			}

			if ( ! empty( $result ) ) {
				$elementSize++;
			}

			if ( $estimated_size + $elementSize > $max_size ) {
				break;
			}

			$result[ $key ]  = $value;
			$estimated_size += $elementSize;
		}

		return $result;
	}

	/**
	 * Check if an array is a numeric array (sequential integer keys starting from 0).
	 *
	 * @param array $array The array to check.
	 *
	 * @return bool True if numeric array, false if associative.
	 */
	private function isNumericArray( $array ) {
		if ( empty( $array ) ) {
			return true;
		}

		$keys         = array_keys( $array );
		$expected_key = 0;

		foreach ( $keys as $key ) {
			if ( ! is_int( $key ) || $key !== $expected_key ) {
				return false;
			}
			++$expected_key;
		}

		return true;
	}
}

Directory Contents

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