authorize( self::request_filters() ) ); } ); } add_submenu_page( 'logged-ai', __( 'Response', 'logged-ai' ), __( 'Response', 'logged-ai' ), 'logged_ai_manage_response', 'logged-ai-response', array( self::class, 'response' ) ); } /** * Resolve the current required scope or terminate the native administration request. * * @param string $cap Native capability required for the current request administration operation. * * @return mixed Current authorized scope returned by the existing native adapter. */ private static function scope( string $cap ): mixed { $scope = Scope::current( $cap ); if ( is_wp_error( $scope ) ) { wp_die( esc_html__( 'You do not have permission for this site.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 403 ) ); } return $scope; } /** * Return loaded rows or terminate with an escaped access/storage error. * * @param array|\WP_Error $rows Loaded operation rows or an explicit structured error. * * @return array Successfully loaded operation rows. */ private static function loaded( array|\WP_Error $rows ): array { if ( is_wp_error( $rows ) ) { wp_die( esc_html( PresentationTranslations::text( $rows->get_error_message() ) ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => in_array( $rows->get_error_code(), array( 'forbidden', 'logged_ai_forbidden', 'sensitive_forbidden' ), true ) ? 403 : 503 ) ); } return $rows; } /** * Resolve the current site display timezone or terminate on context failure. * * @return TimeDisplay Current validated site timezone formatter. */ private static function time(): TimeDisplay { try { return TimeDisplay::site(); } catch ( \Throwable $error ) { wp_die( esc_html__( 'Site timezone context changed. Reload this view.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 403 ) ); } } /** * Render an escaped request-page heading with scope and selected timezone. * * @param string $title Fixed page title supplied by the current administration renderer. * @param object $scope Current operation scope whose native grants are checked. * @param TimeDisplay|null $time Optional current site timezone formatter to reuse. * * @return TimeDisplay Timezone formatter reused by this page. */ private static function start( string $title, object $scope, ?TimeDisplay $time = null ): TimeDisplay { $time ??= self::time(); echo ( '
' . '

' . ( esc_html( PresentationTranslations::text( $title ) ) ) . '

' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. 3: Value or formatting element shown in this message. */ esc_html__( 'Site %1$s · Network %2$s · Times shown in %3$s', 'logged-ai' ), esc_html( (string) $scope->site_id ), esc_html( (string) $scope->network_id ), esc_html( $time->zone() ) ) . '

' ); return $time; } /** * Render one escaped hidden administration form field. * * @param string $key Fixed form field name to escape for output. * @param string $value Hidden form field string escaped in its HTML attribute context. * * @return void */ private static function field( string $key, string $value ): void { echo ''; } /** * Render an escaped submit control using one supported button style. * * @param string $label Fixed translated or caller-selected submit control label. * @param string $style Requested button style; only primary receives the primary appearance. * * @return void */ private static function submit( string $label, string $style = 'secondary' ): void { echo ''; } /** * Open the fixed request-action POST form with its operation and nonce fields. * * @param string $operation Supported request action encoded into the nonce-bound form. * * @return void */ private static function form( string $operation ): void { echo '
'; self::field( 'action', 'logged_ai_request_action' ); self::field( 'operation', $operation ); wp_nonce_field( 'logged_ai_request_action' ); } /** * Render only an allowlisted fixed request-operation notice. * * @return void */ private static function message(): void { $request_query = \LoggedAI\AdminInput::query(); $messages = array( 'saved' => 'The scoped policy was saved. The next eligible request must independently verify WordPress-layer response.', 'revoked' => 'The policy was revoked. New eligible requests read current database state.', 'disabled' => 'WordPress-layer response is disabled. Observation continues.', 'enabled' => 'WordPress-layer response is enabled for currently valid, unexpired active policies.', ); $key = isset( $request_query['notice'] ) && is_string( $request_query['notice'] ) ? $request_query['notice'] : ''; if ( isset( $messages[ $key ] ) ) { echo '

' . esc_html( PresentationTranslations::text( $messages[ $key ] ) ) . '

'; } } /** * Validate the optional exact request-observation selection without numeric repair. * * Validate before WordPress emits the admin header, and again at the final read. * * @return int Positive selected identifier, or zero when no selection is supplied. */ private static function selected_observation(): int { $request_query = \LoggedAI\AdminInput::query(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only observation selection needs no mutation nonce; absent evidence selects the ordinary list. if ( ! isset( $request_query['evidence'] ) ) { return 0; } if ( ! is_string( $request_query['evidence'] ) || ! preg_match( '/\A[1-9][0-9]{0,17}\z/D', $request_query['evidence'] ) ) { wp_die( esc_html__( 'Select a valid request observation.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Cast only the positive-decimal selector already validated above for this scoped read-only view. return (int) $request_query['evidence']; } /** * Validate read-only selections or nonce-checked export selections. * * @param bool $post Whether the caller already checked the export nonce. * @return array Canonical bounded filter strings. */ private static function request_filters( bool $post = false ): array { $request_query = \LoggedAI\AdminInput::query(); $request_form = \LoggedAI\AdminInput::form(); $input = $post ? $request_form['logged_ai_filters'] ?? array() : $request_query['logged_ai_filters'] ?? array(); if ( ! is_array( $input ) || count( $input ) > count( RequestsBrowse::FILTERS ) ) { wp_die( esc_html__( 'Choose valid request filters.', 'logged-ai' ), '', array( 'response' => 400 ) ); } foreach ( $input as $value ) { if ( ! is_string( $value ) || strlen( $value ) > 64 ) { wp_die( esc_html__( 'Choose valid request filters.', 'logged-ai' ), '', array( 'response' => 400 ) ); } } $filters = RequestsBrowse::normalize( $input ); if ( is_wp_error( $filters ) ) { wp_die( esc_html( $filters->get_error_message() ), '', array( 'response' => 400 ) ); } return $filters; } /** * Render compact native filters, preserving values without saving preferences. * * @param array $filters Validated request filters. * @return void */ private static function filter_form( array $filters ): void { echo '
' . esc_html__( 'Filter requests', 'logged-ai' ) . ''; echo ''; self::field( 'page', 'logged-ai-requests' ); echo '
'; foreach ( array( 'from' => __( 'From date (UTC)', 'logged-ai' ), 'to' => __( 'Through date (UTC)', 'logged-ai' ), 'identity' => __( 'Available source address or mask', 'logged-ai' ) ) as $key => $label ) { echo ''; } $families = array( '' => __( 'All finding families', 'logged-ai' ) ); foreach ( RequestsClassifier::rules() as $key => $rule ) { $families[ $key ] = PresentationTranslations::text( $rule['label'] ); } $families['automatic_response'] = __( 'Automatic response decision', 'logged-ai' ); $families['observer_selftest'] = __( 'Observer self-test', 'logged-ai' ); $selects = array( 'source' => array( __( 'Source', 'logged-ai' ), array( '' => __( 'All sources', 'logged-ai' ), 'wordpress' => __( 'WordPress', 'logged-ai' ), 'server_log' => __( 'Server log', 'logged-ai' ), 'synthetic_test' => __( 'Synthetic test', 'logged-ai' ) ) ), 'family' => array( __( 'Finding family', 'logged-ai' ), $families ), 'response' => array( __( 'Logged action', 'logged-ai' ), array( '' => __( 'All actions', 'logged-ai' ), 'blocked' => __( 'Blocked by Logged', 'logged-ai' ), 'none' => __( 'No Logged response', 'logged-ai' ), 'unconfirmed' => __( 'Unconfirmed local response', 'logged-ai' ) ) ), ); foreach ( $selects as $key => $definition ) { echo ''; } foreach ( array( 'method' => __( 'HTTP method', 'logged-ai' ), 'status' => __( 'HTTP status', 'logged-ai' ) ) as $key => $label ) { echo ''; } echo '
'; foreach ( array( 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'TRACE', 'TRACK', 'CONNECT' ) as $method ) { echo ''; } echo ''; foreach ( array( '2xx', '3xx', '4xx', '5xx', '200', '403', '404', '429', '500' ) as $status ) { echo ''; } echo '

' . esc_html__( 'Leave a field empty for all values. Status accepts unknown, a class such as 4xx, or an exact code. Blocked by Logged requires a recorded local response; an external 403 alone does not qualify. Dates include the whole UTC day.', 'logged-ai' ) . '

' . esc_html__( 'Source matching uses available retained context: a full address needs full privacy mode and sensitive-context permission; masks use IPv4 /24 or IPv6 /48. A shared address does not identify a person. This page always uses the current WordPress site.', 'logged-ai' ) . '

'; self::submit( __( 'Apply filters', 'logged-ai' ), 'primary' ); echo ' ' . esc_html__( 'Clear filters', 'logged-ai' ) . '

'; } /** * Open the current reader after its native operation authorization. * * @param string $capability Required reader capability. * @return RequestsBrowse Authorized current-site reader. */ private static function browser( string $capability = 'logged_ai_view_events' ): RequestsBrowse { $reader = RequestsBrowse::current( $capability ); if ( is_wp_error( $reader ) ) { wp_die( esc_html( $reader->get_error_message() ), '', array( 'response' => 403 ) ); } return $reader; } /** * Render scoped minimized request findings, group context and eligible preview controls. * * @return void */ public static function screen(): void { // Resolve translation callbacks before reading authorized request evidence. $display_text = array( 0 => __( 'Review admitted request findings and the response recorded for each one. A finding does not establish intent or a successful attack.', 'logged-ai' ), 1 => __( 'Collection coverage and address privacy', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 2 => __( '%1$sWordPress layer only.%2$s Bounded configuration, source/diagnostic, traversal-shaped, compound query-shaped, component-metadata and method observations run at %3$s, priority -999. Static files, cache hits, upstream responses and code that bypasses WordPress are not observed.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 3 => __( 'IP mode: %1$s%2$s%3$s. The default source is the direct peer. Forwarding is interpreted only through an explicitly configured trusted-proxy boundary. Source derivation and quality are shown with evidence. Exact-source policies require current full-mode evidence. A shared network address does not identify a person. Bodies, full queries, cookies and authorization headers are never retained. Query policies store only deliberately selected allowlisted numeric conditions.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 4 => __( 'Partial telemetry: at least %1$s observations were not admitted by the resource limits. Concurrent lock contention and failures can cause further uncounted gaps.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 5 => __( 'Request health is degraded or unavailable. Code: %1$s. Absence of records does not prove absence of requests.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 6 => __( 'Request retention: %1$s days · Groups: %2$s days', 'logged-ai' ), 7 => __( 'Cleanup and telemetry limits', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 8 => __( 'Cleanup runs in batches of 500 per hourly WP-Cron run; delayed cron can leave a backlog. Last successful cleanup: %1$s. Admission caps and policy expiry do not depend on cleanup.', 'logged-ai' ), 9 => __( 'not yet observed', 'logged-ai' ), 10 => __( 'Every retained row is an admitted observation, not a count of attackers or all traffic. The separate Logged.ai Pro add-on supplies server-log import. Retained imported findings remain readable here.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 11 => __( '%1$sRequest source and observer self-test%2$s', 'logged-ai' ), 12 => __( 'Patterns over time', 'logged-ai' ), 13 => __( 'Groups combine admitted observations by this site, primary family, source label and UTC minute. They do not group people or exact IPs. Counts are admitted lower bounds, not all traffic; a request with several rule matches appears in one primary group. Samples may be unavailable after observation retention.', 'logged-ai' ), 14 => __( 'No retained groups are available.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 15 => __( '%1$s · %2$s · at least %3$s admitted · %4$s', 'logged-ai' ), 16 => __( 'Unavailable; this is a site/time/family group', 'logged-ai' ), 17 => __( 'Selected observation', 'logged-ai' ), 18 => __( 'Individual observations', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 19 => __( 'Selected request #%1$s. %2$sReturn to recent observations%3$s', 'logged-ai' ), 20 => __( 'No admitted matching observations are available in the selected retention interval. This can mean no matching traffic, unsupported routing, retention, sampling or capture failure.', 'logged-ai' ), 21 => __( 'Request evidence authority changed.', 'logged-ai' ), 22 => __( 'Why it matched', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 23 => __( '%1$s Confidence: %2$s. Intent, identity, vulnerability and exploitation are unknown.', 'logged-ai' ), 24 => __( 'Inspect captured source fields', 'logged-ai' ), 25 => __( 'Client-source derivation', 'logged-ai' ), 26 => __( 'Client-source quality', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 27 => __( 'Client source (%s)', 'logged-ai' ), 28 => __( 'yes; observation only', 'logged-ai' ), 29 => __( 'no within the bounded supported view', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 30 => __( 'WordPress-layer response issued; policy %s', 'logged-ai' ), 31 => __( 'Unconfirmed local response; no verified issuance receipt', 'logged-ai' ), 32 => __( 'No Logged.ai response', 'logged-ai' ), 33 => __( 'Unknown', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 34 => __( 'Quality flags: %1$s', 'logged-ai' ), 35 => __( 'none reported within the supported parser', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 36 => __( 'Scoped response exceptions applied: %1$s. This observation remains visible; unrelated policy responses still apply.', 'logged-ai' ), 37 => __( 'Related activity', 'logged-ai' ), 38 => __( 'No application effect or actor relationship has been established. Matching IP context is not identity. A 200, 403 or 404 does not establish exposure or prevention.', 'logged-ai' ), 39 => __( 'Available response controls', 'logged-ai' ), 40 => __( 'Exact safe path, this site, GET and HEAD only.', 'logged-ai' ), 41 => __( 'Duration ', 'logged-ai' ), 42 => __( 'Preview requires response capability and complete eligible evidence. Ambiguous/encoded input remains observation only.', 'logged-ai' ), 43 => __( 'Interpretation limits', 'logged-ai' ), 44 => __( 'These conservative rules do not provide complete attack coverage. Repeated-resource patterns use only the bounded retained view; arbitrary missing-path sequences are unavailable. The observer self-test checks a controlled WordPress request through the configured source boundary; it does not test all routing or prove the site is secure. Typed manual policies use a separate matcher revision; existing policies keep their original conditions.', 'logged-ai' ), ); $display_labels = PresentationTranslations::snapshot( array( 'Manual policy response decision', 'Manual policy condition', 'A deliberately configured manual condition matched; this is not an attack finding. Check the recorded action to see whether a response was issued.', 'Ordinary benign requests can match a manual condition. A match alone does not prove a denial or an attack.', 'An observation-only manual condition matched. This policy recorded the match without denying the request; no attack classification is inferred.', 'Observation-only policy %1$s matched (revision %2$s). This policy did not deny the request.', 'A deliberately configured manual condition matched; this is a response decision, not an attack finding.', 'Ordinary benign requests can match a manual condition. No attack classification is inferred from this response.', 'Configuration-resource probe pattern', 'Exact reserved configuration filename or recognized backup variant.', 'An authorized scanner or broken integration can request these resources.', 'Source-control or diagnostic discovery', 'Exact source-control metadata or diagnostic resource template.', 'Authorized diagnostics can deliberately request these resources.', 'Traversal-shaped resource request', 'Two parent-directory segments precede a recognized configuration/system-resource suffix.', 'Malformed links and authorized scanners are possible; target application normalization is unknown.', 'SQL-shaped numeric parameter', 'A numeric-identifier parameter contains a compound boolean or UNION projection with a comment delimiter.', 'A test or code example can contain the same structure; no execution or intent is established.', 'Executable-markup-shaped parameter', 'A numeric/navigation parameter contains paired script markup, an event-handler tag, or an executable URL scheme.', 'Authorized testing can contain the same syntax. Editing/search content parameters are not classified.', 'Component metadata enumeration', 'A plugin readme or theme metadata path was requested; enumeration is not exploitation.', 'Documentation tools and legitimate theme stylesheets can use related routes; theme style.css is excluded.', 'Unusual method at a non-API resource', 'TRACE/TRACK/CONNECT or a write method was used on a reserved metadata/configuration resource.', 'Protocol/security tests may intentionally exercise methods; ordinary API writes and OPTIONS are excluded.', 'The scoped policy was saved. The next eligible request must independently verify WordPress-layer response.', 'The policy was revoked. New eligible requests read current database state.', 'WordPress-layer response is disabled. Observation continues.', 'WordPress-layer response is enabled for currently valid, unexpired active policies.', 'Requests, in context.', 'Export latest 500 minimized requests (JSON)', 'Retained request family', 'First / last (selected timezone)', 'Source association', 'Unavailable; this is a site/time/family group', 'Partial analyses', 'Unknown final statuses', 'HTTP 1xx / 2xx / 3xx / 4xx / 5xx', 'Recorded WordPress responses', 'Sample evidence IDs', 'Retained request finding', 'Interpret only the recorded rule and available context.', 'Evidence ID', 'Alert work', 'Local request reference', 'Sensor / source', 'Rule / revision', 'Safe resource template', 'Method', 'Client-context availability', 'Partial analysis', 'yes; observation only', 'no within the bounded supported view', 'Final HTTP status', 'Logged.ai action', 'Unconfirmed local response; no verified issuance receipt', 'No Logged.ai response', 'Application effect', 'Unknown', 'Captured (selected timezone)', 'Ingested (selected timezone)', 'Response observed (selected timezone)', '15 minutes', '1 hour', '24 hours', '7 days', 'Preview temporary policy', 'Exact safe resource path', 'Reserved source-control path prefix', 'Structured query on one exact path', 'Exact retained derived address (full mode required)', 'Preview reviewed conditions', 'Response', 'Disable all local response for this site', 'Enable remaining active policies', 'Policy ID', 'Origin', 'State', 'Revision', 'Methods', 'Scope', 'Expiry (selected timezone)', 'Parser', 'Preview hash', 'Revoke', 'Latest at most 500 admitted observations visible at query time within the configured request retention. Concurrent and later inserts are not a transactionally complete snapshot. Telemetry is admission-limited.', 'Review temporary policy', 'Safe target', 'GET and HEAD', 'Duration after confirmation', 'Enforcement layer', 'WordPress init, priority -999; not edge/server/static', 'Parser revision', '/?dumb-code-to-hack-site', 'Confirm reviewed policy', 'Response authority is required for the selected user.', 'Use --yes to enable still-valid active policies; durations are not extended.', 'LOGGED_AI_DISABLE_RESPONSE is active. Remove it using operator configuration first.', 'The scoped response setting could not be changed. Enabling retained IP policies requires --user with response and sensitive-context permissions.', 'Use revoke POLICY_ID with exactly --dry-run or --yes in the selected site.', 'The policy ID is invalid.', 'The policy ID is invalid, outside this site, unavailable or already revoked.', 'Use status, list, disable, enable --yes, or revoke POLICY_ID --dry-run|--yes.', 'Response authority changed.', 'Use requests prune --dry-run to preview one selected site, or requests prune --yes to apply one bounded batch. Choose exactly one flag and an explicit --url; no all-sites operation is supported.', 'Use requests list, requests export, requests prune --dry-run, or requests prune --yes. The separate Logged.ai Pro add-on supplies reference import with explicit source configuration.', 'Request evidence authority is required for the selected user.', 'Request evidence authority changed.', 'Authorized scanners, diagnostic tools and unrelated visitors can produce this pattern. A direct network peer may represent many people.', 'Repeated distinct reserved-resource requests', 'At least 10 complete admitted GET/HEAD findings cover at least three distinct safe reserved-resource templates in one fixed UTC minute.', 'Known configuration/source/diagnostic templates; complete parser result; this authorized site and source label. Status can be unknown.', 'Repeated distinct reserved resources with observed 404s', 'At least 10 complete admitted GET/HEAD findings have a finalized observed 404 across at least three distinct safe reserved-resource templates in one fixed UTC minute.', 'Request-sequence access is unavailable.', 'Request-sequence authority changed.', 'Current retained admitted findings only. Counts are lower bounds; missing telemetry is not quantifiable. Fixed UTC-minute boundaries can split a sequence. Site and exact-peer views overlap and must not be summed. No attacker identity, historical alert, exposure, prevention or automated-response eligibility is established.', 'Admitted / complete eligible', 'First / last (display timezone)', 'Safe templates', 'Known finalized 404s', 'Recorded local responses', 'Unknown final status', 'Window still open', 'Sample observation IDs', ) ); global $wpdb; $scope = self::scope( 'logged_ai_view_events' ); $focused = self::selected_observation(); $filters = self::request_filters(); $page = $focused ? null : self::loaded( self::browser()->page( $filters ) ); $time = self::start( 'Requests, in context.', $scope ); $privacy = Requests::privacy_settings(); echo ( '

' . esc_html( $display_text[ 0 ] ) . '

' . '
' . '' . esc_html( $display_text[ 1 ] ) . '' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. 3: Value or formatting element shown in this message. */ esc_html( $display_text[ 2 ] ), '', '', 'init' ) . '

' ); echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. 3: Value or formatting element shown in this message. */ esc_html( $display_text[ 3 ] ), '', esc_html( $privacy['ip_mode'] ), '' ) . '

' ); echo '
'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Read current scoped admission-gap totals from custom request-limit storage. $limits = $wpdb->get_row( $wpdb->prepare( 'SELECT dropped,last_drop FROM %i WHERE network_id=%d AND site_id=%d', Requests::table( 'request_limits' ), $scope->network_id, $scope->site_id ), ARRAY_A ); $state = Requests::state( $scope->network_id, $scope->site_id ); if ( $limits && (int) $limits['dropped'] > 0 ) { echo ( '
' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. */ esc_html( $display_text[ 4 ] ), esc_html( $limits['dropped'] ) ) . '

' . '
' ); } if ( null === $state || '' !== $state['degraded'] ) { echo ( '
' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. */ esc_html( $display_text[ 5 ] ), esc_html( $state['degraded'] ?? 'schema_unavailable' ) ) . '

' . '
' ); } echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. */ esc_html( $display_text[ 6 ] ), esc_html( (string) $privacy['request_days'] ), esc_html( (string) $privacy['request_group_days'] ) ) . '

' . '
' . '' . esc_html( $display_text[ 7 ] ) . '' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. */ esc_html( $display_text[ 8 ] ), esc_html( ( ! empty( $state['last_cleanup'] ) ? $time->text( (int) $state['last_cleanup'] ) : $display_text[ 9 ] ) ) ) . '

' . '

' . esc_html( $display_text[ 10 ] ) . '

' . '
' ); if ( Scope::can( 'logged_ai_manage_settings' ) ) { echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. */ esc_html( $display_text[ 11 ] ), '', '' ) . '

' ); } if ( ! $focused ) { self::filter_form( $filters ); if ( current_user_can( 'logged_ai_export_events' ) ) { self::form( 'export' ); foreach ( $filters as $key => $value ) { self::field( 'logged_ai_filters[' . $key . ']', $value ); } self::submit( __( 'Export selection and context (JSON)', 'logged-ai' ), 'secondary' ); echo '

' . esc_html__( 'Up to 500 matching observations, 100 group summaries and 100 policy-history entries. With filters, context is linked to the exported observations; group counts describe whole groups. Without filters, context includes recent site history. Each category declares its limit and whether more records exist.', 'logged-ai' ) . '

'; } } if ( ! $focused && ! array_filter( $filters ) ) { echo ( '

' . esc_html( $display_text[ 12 ] ) . '

' . '

' . esc_html( $display_text[ 13 ] ) . '

' ); $groups = self::loaded( Requests::groups( $scope ) ); DesignActivity::request_chart( $groups ); if ( ! $groups ) { echo ( '

' . esc_html( $display_text[ 14 ] ) . '

' ); } foreach ( $groups as $group ) { $definition = RequestsClassifier::rules()[ $group['family'] ] ?? array( 'label' => 'Retained request family' ); echo ( '
' . '' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. 3: Value or formatting element shown in this message. 4: Value or formatting element shown in this message. */ esc_html( $display_text[ 15 ] ), esc_html( PresentationTranslations::lookup( $display_labels, $definition['label'] ) ), esc_html( $time->text( (int) $group['window_start'] ) ), esc_html( $group['admitted_count'] ), esc_html( $group['source'] ) ) . '' . '
' ); foreach ( array( 'First / last (selected timezone)' => $time->text( $group['first_at'] ) . ' / ' . $time->text( $group['last_at'] ), 'Source association' => $display_text[ 16 ], 'Partial analyses' => $group['partial_count'], 'Unknown final statuses' => $group['unknown_count'], 'HTTP 1xx / 2xx / 3xx / 4xx / 5xx' => implode( ' / ', array( $group['status_1xx'], $group['status_2xx'], $group['status_3xx'], $group['status_4xx'], $group['status_5xx'] ) ), 'Recorded WordPress responses' => $group['own_responses'], 'Sample evidence IDs' => $group['sample_ids'], ) as $key => $value ) { echo '
' . esc_html( PresentationTranslations::lookup( $display_labels, $key ) ) . '
' . esc_html( (string) $value ) . '
'; } echo '
'; } RequestsSequences::render( $scope, $time ); } echo ( '

' . esc_html( $focused ? $display_text[ 17 ] : $display_text[ 18 ] ) . '

' ); $rows = $focused ? self::loaded( Requests::observation( $scope, $focused ) ) : $page['rows']; if ( $focused ) { echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. 3: Value or formatting element shown in this message. */ esc_html( $display_text[ 19 ] ), esc_html( (string) $focused ), '', '' ) . '

' ); } if ( ! $rows ) { echo ( '

' . esc_html( $display_text[ 20 ] ) . '

' ); } foreach ( $rows as $row ) { if ( ! $scope->valid_for( 'logged_ai_view_events' ) ) { wp_die( esc_html( $display_text[ 21 ] ), '', array( 'response' => 403 ) ); } $row_time_label = Design::time_label( $time, $row['captured_at'] ); $row = self::loaded( Requests::for_scope( $scope, array( $row ) ) )[0]; $definition = RequestsClassifier::rules()[ $row['family'] ] ?? array( 'label' => 'Retained request finding', 'overlap' => 'Interpret only the recorded rule and available context.', ); echo '
' . esc_html( PresentationTranslations::lookup( $display_labels, $definition['label'] ) ) . ' · ' . esc_html( $row['safe_path'] ) . ' · ' . esc_html( $row_time_label ) . ' · ' . esc_html( $row['source'] ) . ''; echo ( '

' . esc_html( $display_text[ 22 ] ) . '

' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. */ esc_html( $display_text[ 23 ] ), esc_html( PresentationTranslations::lookup( $display_labels, (string) $row['reason'] ) ), esc_html( $row['confidence'] ) ) . '

' . '

' . ( esc_html( PresentationTranslations::lookup( $display_labels, $definition['overlap'] ) ) ) . '

' ); echo ( '
' . '' . esc_html( $display_text[ 24 ] ) . '' . '
' . '
' . esc_html( $display_text[ 25 ] ) . '
' . '
' . ( esc_html( $row['source_derivation'] ?? 'direct_peer' ) ) . '
' . '
' . esc_html( $display_text[ 26 ] ) . '
' . '
' . ( esc_html( $row['source_quality'] ?? 'legacy_peer' ) ) . '
' ); $facts = array( 'Evidence ID' => $row['id'], 'Alert work' => $row['alert_state'] ?? 'unavailable', 'Local request reference' => $row['request_uid'], 'Sensor / source' => $row['sensor'] . ' / ' . $row['source'], 'Rule / revision' => $row['rule_id'] . ' / ' . $row['rule_revision'], 'Safe resource template' => $row['safe_path'], 'Method' => $row['method'], sprintf( /* translators: %s: current address privacy mode identifier. */ $display_text[ 27 ], $row['ip_mode'] ) => $row['client_mask'] ?? 'unavailable', 'Client-context availability' => $row['client_context_reason'] ?? 'unavailable', 'Partial analysis' => (int) $row['partial'] ? $display_text[ 28 ] : $display_text[ 29 ], 'Final HTTP status' => $row['status'] ?? 'unknown', 'Logged.ai action' => 'wordpress_response_issued' === $row['own_response'] ? sprintf( /* translators: %s: recorded response policy identifier. */ $display_text[ 30 ], $row['policy_id'] ) : ( 'wordpress_response_unconfirmed' === $row['own_response'] ? $display_text[ 31 ] : $display_text[ 32 ] ), 'Application effect' => $display_text[ 33 ], 'Captured (selected timezone)' => $time->text( $row['captured_at'] ), 'Ingested (selected timezone)' => $time->text( $row['ingested_at'] ), 'Response observed (selected timezone)' => $time->text( $row['response_at'] ?? null ), ); foreach ( $facts as $label => $value ) { echo '
' . esc_html( PresentationTranslations::lookup( $display_labels, $label ) ) . '
' . esc_html( (string) $value ) . '
'; } echo '
'; $evidence = json_decode( substr( (string) ( $row['evidence'] ?? '{}' ), 0, 4096 ), true, 16 ); if ( is_array( $evidence ) && is_array( $evidence['observation_policy'] ?? null ) && is_string( $evidence['observation_policy']['policy_id'] ?? null ) && is_int( $evidence['observation_policy']['revision'] ?? null ) ) { echo '

' . esc_html( sprintf( PresentationTranslations::lookup( $display_labels, 'Observation-only policy %1$s matched (revision %2$s). This policy did not deny the request.' ), $evidence['observation_policy']['policy_id'], $evidence['observation_policy']['revision'] ) ) . '

'; } if ( is_array( $evidence ) && isset( $evidence['quality_flags'], $evidence['matches'] ) ) { $quality_text = implode( ', ', array_filter( (array) $evidence['quality_flags'], 'is_string' ) ); echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. */ esc_html( $display_text[ 34 ] ), esc_html( ( $quality_text ? $quality_text : $display_text[ 35 ] ) ) ) . '

' . ''; if ( ! empty( $evidence['response_exceptions'] ) ) { echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. */ esc_html( $display_text[ 36 ] ), '' . ( esc_html( wp_json_encode( $evidence['response_exceptions'] ) ) ) . '' ) . '

' ); } } echo ( '

' . esc_html( $display_text[ 37 ] ) . '

' . '

' . esc_html( $display_text[ 38 ] ) . '

' . '

' . esc_html( $display_text[ 39 ] ) . '

' ); if ( current_user_can( 'logged_ai_manage_response' ) && 0 === (int) $row['partial'] && in_array( $row['safe_path'], Requests::PATHS, true ) ) { self::form( 'preview' ); self::field( 'evidence_id', $row['id'] ); echo ( '

' . esc_html( $display_text[ 40 ] ) . '

' . '

' . '' . '

'; self::submit( 'Preview temporary policy', 'secondary' ); echo ''; } else { echo ( '

' . esc_html( $display_text[ 42 ] ) . '

' ); } if ( current_user_can( 'logged_ai_manage_response' ) && 0 === (int) $row['partial'] ) { self::typed_form( $row ); } echo '

'; } if ( ! $focused && $page['has_more'] ) { $next = $filters; $next['before'] = $page['next_before']; echo '

'; } if ( ! $focused && '' !== $filters['before'] ) { $latest = $filters; $latest['before'] = ''; echo '

' . esc_html__( 'Newest matching requests', 'logged-ai' ) . '

'; } echo ( '
' . '' . esc_html( $display_text[ 43 ] ) . '' . '

' . esc_html( $display_text[ 44 ] ) . '

' . '
' . '
' ); } /** * Render bounded manual-policy condition, exception and duration controls. * * @param array $row Native retained event or request row selected for this operation. * * @return void */ private static function typed_form( array $row ): void { $id = (string) $row['id']; echo ( '
' . '' . esc_html__( 'Create temporary policy with reviewed conditions', 'logged-ai' ) . '' ); self::form( 'preview_typed' ); self::field( 'evidence_id', $id ); echo ( '

' . '' . '' . '

' . '

' . esc_html__( 'Exact path matches one safe resource. Reserved prefix matches an allowed source-control path prefix. Path + query adds a structured condition to one exact path. Exact source uses the retained derived address and requires full source mode.', 'logged-ai' ) . '

' . esc_html__( 'Exact-source controls additionally require sensitive-context permission.', 'logged-ai' ) . '

' . '

' . '' . '' . '

' . '

' . esc_html__( 'Exact paths and prefixes require this observation’s safe resource template. Root is available only with a restrictive query condition. Prefixes end at a segment boundary.', 'logged-ai' ) . '

' . '
' . '' . esc_html__( 'Query condition (used only for query policies)', 'logged-ai' ) . '' . '

' . '' . '' . ' ' . '' . '' . '

' . '

' . esc_html__( 'Equals number matches the exact reviewed number. Key exists matches the selected key with any value.', 'logged-ai' ) . '

' . '

' . '' . '' . '

' . '
' ); echo ( '
' . '' . esc_html__( 'Optional exact-path response exceptions', 'logged-ai' ) . '' . '

' . esc_html__( 'Each exception affects only this policy, GET/HEAD, this site and its expiry. It neither grants WordPress access nor suppresses unrelated policies or observations. Select at most five.', 'logged-ai' ) . '

' ); $exception_paths = RequestsPolicy::EXCEPTION_PATHS; if ( in_array( $row['safe_path'], RequestsClassifier::safe_paths(), true ) ) { $exception_paths[] = $row['safe_path']; } foreach ( array_unique( $exception_paths ) as $path ) { echo '

'; } echo ( '
' . '

' . '' . '' . '

' . '

' . esc_html__( 'Exact-source policies can affect checkout, login and API reads, shared-network users and monitors. They stop when source evidence expires or privacy/source settings change. Review exceptions and OS recovery before activating.', 'logged-ai' ) . '

' ); self::submit( 'Preview reviewed conditions', 'secondary' ); echo '
'; } /** * Render current scoped response state, recovery guidance, policies and action history. * * @return void */ public static function response(): void { // Resolve translation callbacks before reading authorized request evidence. $display_text = array( /* translators: Values or formatting elements shown in this message. */ 0 => __( 'Mode: %1$s%2$s%3$s. Revision: %4$s. Policies are read from the database for each eligible request; expiry is checked without cron.', 'logged-ai' ), 1 => __( 'Disabled by operator recovery constant', 'logged-ai' ), 2 => __( 'Enabled for valid active policies', 'logged-ai' ), 3 => __( 'Observe only / response disabled', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 4 => __( 'Response health: %1$s. Invalid policy state fails open.', 'logged-ai' ), 5 => __( 'Exact-source targets are hidden without sensitive-context permission. Resuming retained active IP policies requires that permission; revocation and emergency disable remain available.', 'logged-ai' ), 6 => __( ' Enable valid active policies for their original remaining duration.', 'logged-ai' ), 7 => __( 'Operator recovery', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 8 => __( 'Set %1$s before WordPress loads, or run %2$s. Recovery does not require reaching this screen. There is no public recovery URL, cookie, header or query parameter.', 'logged-ai' ), 9 => __( 'Manual policies', 'logged-ai' ), 10 => __( 'Saved active state is configuration, not proof of a response. The request evidence must show a subsequent independently observed WordPress-layer response. Automatic temporary response, when configured, has separate controls and eligibility checks.', 'logged-ai' ), 11 => __( 'No policies for this site. Inspect an eligible request to create a preview.', 'logged-ai' ), 12 => __( 'Response authority changed.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 13 => __( 'Compiled conditions: %1$s', 'logged-ai' ), 14 => __( 'Hidden: sensitive-context permission is required. Saved policy state is unchanged.', 'logged-ai' ), 15 => __( 'Personal target erased or unavailable under current privacy settings', 'logged-ai' ), 16 => __( 'Exceptions apply only to this policy and inherit its scope, GET/HEAD methods and expiry. Exact-source policies require a fresh preview after any settings change.', 'logged-ai' ), 17 => __( 'Policy history', 'logged-ai' ), 18 => __( 'Latest 100 scoped actions. A requested action does not imply a successful save; saved actions and errors are separate.', 'logged-ai' ), 19 => __( 'Time (selected timezone)', 'logged-ai' ), 20 => __( 'Action', 'logged-ai' ), 21 => __( 'Actor ID', 'logged-ai' ), 22 => __( 'Policy', 'logged-ai' ), 23 => __( 'OS operator / unknown user', 'logged-ai' ), 24 => __( 'site setting', 'logged-ai' ), /* translators: 1: WordPress site identifier. 2: WordPress network identifier. */ 25 => __( 'site %1$s, network %2$s', 'logged-ai' ), ); $display_labels = PresentationTranslations::snapshot( array( 'Manual policy response decision', 'Manual policy condition', 'A deliberately configured manual condition matched; this is not an attack finding. Check the recorded action to see whether a response was issued.', 'Ordinary benign requests can match a manual condition. A match alone does not prove a denial or an attack.', 'An observation-only manual condition matched. This policy recorded the match without denying the request; no attack classification is inferred.', 'Observation-only policy %1$s matched (revision %2$s). This policy did not deny the request.', 'A deliberately configured manual condition matched; this is a response decision, not an attack finding.', 'Ordinary benign requests can match a manual condition. No attack classification is inferred from this response.', 'Configuration-resource probe pattern', 'Exact reserved configuration filename or recognized backup variant.', 'An authorized scanner or broken integration can request these resources.', 'Source-control or diagnostic discovery', 'Exact source-control metadata or diagnostic resource template.', 'Authorized diagnostics can deliberately request these resources.', 'Traversal-shaped resource request', 'Two parent-directory segments precede a recognized configuration/system-resource suffix.', 'Malformed links and authorized scanners are possible; target application normalization is unknown.', 'SQL-shaped numeric parameter', 'A numeric-identifier parameter contains a compound boolean or UNION projection with a comment delimiter.', 'A test or code example can contain the same structure; no execution or intent is established.', 'Executable-markup-shaped parameter', 'A numeric/navigation parameter contains paired script markup, an event-handler tag, or an executable URL scheme.', 'Authorized testing can contain the same syntax. Editing/search content parameters are not classified.', 'Component metadata enumeration', 'A plugin readme or theme metadata path was requested; enumeration is not exploitation.', 'Documentation tools and legitimate theme stylesheets can use related routes; theme style.css is excluded.', 'Unusual method at a non-API resource', 'TRACE/TRACK/CONNECT or a write method was used on a reserved metadata/configuration resource.', 'Protocol/security tests may intentionally exercise methods; ordinary API writes and OPTIONS are excluded.', 'The scoped policy was saved. The next eligible request must independently verify WordPress-layer response.', 'The policy was revoked. New eligible requests read current database state.', 'WordPress-layer response is disabled. Observation continues.', 'WordPress-layer response is enabled for currently valid, unexpired active policies.', 'Requests, in context.', 'Export latest 500 minimized requests (JSON)', 'Retained request family', 'First / last (selected timezone)', 'Source association', 'Unavailable; this is a site/time/family group', 'Partial analyses', 'Unknown final statuses', 'HTTP 1xx / 2xx / 3xx / 4xx / 5xx', 'Recorded WordPress responses', 'Sample evidence IDs', 'Retained request finding', 'Interpret only the recorded rule and available context.', 'Evidence ID', 'Alert work', 'Local request reference', 'Sensor / source', 'Rule / revision', 'Safe resource template', 'Method', 'Client-context availability', 'Partial analysis', 'yes; observation only', 'no within the bounded supported view', 'Final HTTP status', 'Logged.ai action', 'Unconfirmed local response; no verified issuance receipt', 'No Logged.ai response', 'Application effect', 'Unknown', 'Captured (selected timezone)', 'Ingested (selected timezone)', 'Response observed (selected timezone)', '15 minutes', '1 hour', '24 hours', '7 days', 'Preview temporary policy', 'Exact safe resource path', 'Reserved source-control path prefix', 'Structured query on one exact path', 'Exact retained derived address (full mode required)', 'Preview reviewed conditions', 'Response', 'Disable all local response for this site', 'Enable remaining active policies', 'Policy ID', 'Origin', 'State', 'Revision', 'Methods', 'Scope', 'Expiry (selected timezone)', 'Parser', 'Preview hash', 'Revoke', 'Latest at most 500 admitted observations visible at query time within the configured request retention. Concurrent and later inserts are not a transactionally complete snapshot. Telemetry is admission-limited.', 'Review temporary policy', 'Safe target', 'GET and HEAD', 'Duration after confirmation', 'Enforcement layer', 'WordPress init, priority -999; not edge/server/static', 'Parser revision', '/?dumb-code-to-hack-site', 'Confirm reviewed policy', 'Response authority is required for the selected user.', 'Use --yes to enable still-valid active policies; durations are not extended.', 'LOGGED_AI_DISABLE_RESPONSE is active. Remove it using operator configuration first.', 'The scoped response setting could not be changed. Enabling retained IP policies requires --user with response and sensitive-context permissions.', 'Use revoke POLICY_ID with exactly --dry-run or --yes in the selected site.', 'The policy ID is invalid.', 'The policy ID is invalid, outside this site, unavailable or already revoked.', 'Use status, list, disable, enable --yes, or revoke POLICY_ID --dry-run|--yes.', 'Response authority changed.', 'Use requests prune --dry-run to preview one selected site, or requests prune --yes to apply one bounded batch. Choose exactly one flag and an explicit --url; no all-sites operation is supported.', 'Use requests list, requests export, requests prune --dry-run, or requests prune --yes. The separate Logged.ai Pro add-on supplies reference import with explicit source configuration.', 'Request evidence authority is required for the selected user.', 'Request evidence authority changed.', 'Authorized scanners, diagnostic tools and unrelated visitors can produce this pattern. A direct network peer may represent many people.', 'Repeated distinct reserved-resource requests', 'At least 10 complete admitted GET/HEAD findings cover at least three distinct safe reserved-resource templates in one fixed UTC minute.', 'Known configuration/source/diagnostic templates; complete parser result; this authorized site and source label. Status can be unknown.', 'Repeated distinct reserved resources with observed 404s', 'At least 10 complete admitted GET/HEAD findings have a finalized observed 404 across at least three distinct safe reserved-resource templates in one fixed UTC minute.', 'Request-sequence access is unavailable.', 'Request-sequence authority changed.', 'Current retained admitted findings only. Counts are lower bounds; missing telemetry is not quantifiable. Fixed UTC-minute boundaries can split a sequence. Site and exact-peer views overlap and must not be summed. No attacker identity, historical alert, exposure, prevention or automated-response eligibility is established.', 'Admitted / complete eligible', 'First / last (display timezone)', 'Safe templates', 'Known finalized 404s', 'Recorded local responses', 'Unknown final status', 'Window still open', 'Sample observation IDs', ) ); $scope = self::scope( 'logged_ai_manage_response' ); $time = self::start( 'Response', $scope ); self::message(); $state = Requests::state( $scope->network_id, $scope->site_id ); $enabled = $state && 1 === (int) $state['enabled']; echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. 3: Value or formatting element shown in this message. 4: Value or formatting element shown in this message. */ esc_html( $display_text[ 0 ] ), '', esc_html( ( Requests::recovery_disabled() ? $display_text[ 1 ] : ( $enabled ? $display_text[ 2 ] : $display_text[ 3 ] ) ) ), '', esc_html( (string) ( $state['revision'] ?? 'unavailable' ) ) ) . '

' ); if ( null === $state || '' !== $state['degraded'] ) { echo ( '
' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. */ esc_html( $display_text[ 4 ] ), esc_html( $state['degraded'] ?? 'schema_unavailable' ) ) . '

' . '
' ); } if ( ! Requests::sensitive_allowed( $scope->network_id, $scope->site_id ) ) { echo ( '

' . esc_html( $display_text[ 5 ] ) . '

' ); } self::form( $enabled ? 'disable' : 'enable' ); if ( ! $enabled ) { echo ( '

' . '' . '

' ); } self::submit( $enabled ? 'Disable all local response for this site' : 'Enable remaining active policies', 'secondary' ); echo ( '' . '

' . esc_html( $display_text[ 7 ] ) . '

' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. */ esc_html( $display_text[ 8 ] ), 'define(\'LOGGED_AI_DISABLE_RESPONSE\', true);', 'wp logged-ai response disable --url=YOUR_SITE' ) . '

' ); echo ( '

' . esc_html( $display_text[ 9 ] ) . '

' . '

' . esc_html( $display_text[ 10 ] ) . '

' ); $rows = self::loaded( Requests::policies( $scope->network_id, $scope->site_id ) ); if ( ! $rows ) { echo ( '

' . esc_html( $display_text[ 11 ] ) . '

' ); } foreach ( $rows as $row ) { if ( ! $scope->valid_for( 'logged_ai_manage_response' ) ) { wp_die( esc_html( $display_text[ 12 ] ), '', array( 'response' => 403 ) ); } $row = self::loaded( Requests::policies_for_scope( $scope, array( $row ) ) )[0]; echo '
' . esc_html( $row['safe_path'] ) . ' · ' . esc_html( $row['state'] ) . '
'; foreach ( array( 'Policy ID' => $row['id'], 'Origin' => $row['origin'], 'State' => $row['state'], 'Revision' => $row['revision'], 'Methods' => $row['methods'], 'Scope' => sprintf( $display_text[ 25 ], $row['site_id'], $row['network_id'] ), 'Expiry (selected timezone)' => $time->text( (int) $row['expires_at'] ), 'Parser' => $row['parser_version'], 'Preview hash' => $row['compiled_hash'], ) as $key => $value ) { echo '
' . esc_html( PresentationTranslations::lookup( $display_labels, $key ) ) . '
' . esc_html( (string) $value ) . '
'; } echo '
'; if ( RequestsPolicy::VERSION === $row['parser_version'] ) { echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. */ esc_html( $display_text[ 13 ] ), '' . ( esc_html( $row['target_json'] ?? ( 'sensitive_permission_required' === ( $row['target_context_reason'] ?? '' ) ? $display_text[ 14 ] : $display_text[ 15 ] ) ) ) . '' ) . '

' . '

' . esc_html( $display_text[ 16 ] ) . '

' ); } if ( in_array( $row['state'], array( 'draft', 'active', 'observe', 'failed' ), true ) ) { self::form( 'revoke' ); self::field( 'policy_id', $row['id'] ); self::submit( 'Revoke', 'secondary' ); echo ''; } echo '

'; } echo ( '

' . esc_html( $display_text[ 17 ] ) . '

' . '

' . esc_html( $display_text[ 18 ] ) . '

' . '' . '' . '' . '' . '' . '' . '' . '' . '' . '' ); foreach ( self::loaded( Requests::history( $scope->network_id, $scope->site_id ) ) as $row ) { echo ( '' . '' . '' . '' . '' . '' ); } echo '
' . esc_html( $display_text[ 19 ] ) . '' . esc_html( $display_text[ 20 ] ) . '' . esc_html( $display_text[ 21 ] ) . '' . esc_html( $display_text[ 22 ] ) . '
' . ( esc_html( $time->text( (int) $row['happened_at'] ) ) ) . '' . ( esc_html( $row['action'] ) ) . '' . ( esc_html( ( '0' === $row['actor_id'] ? $display_text[ 23 ] : $row['actor_id'] ) ) ) . '' . ( esc_html( $row['policy_id'] ?? $display_text[ 24 ] ) ) . '
'; } /** * Validate and dispatch nonce-bound request export or deliberate policy operations. * * @return void */ public static function post(): void { $request_form = \LoggedAI\AdminInput::form( array( 'path' => 'url', 'exceptions' => 'url' ) ); if ( 'POST' !== \LoggedAI\AdminInput::method() ) { wp_die( esc_html__( 'POST is required.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 405 ) ); } $operation = isset( $request_form['operation'] ) && is_string( $request_form['operation'] ) ? $request_form['operation'] : ''; $scope = self::scope( 'export' === $operation ? 'logged_ai_export_events' : 'logged_ai_manage_response' ); check_admin_referer( 'logged_ai_request_action' ); if ( 'export' === $operation ) { $reader = self::browser( 'logged_ai_export_events' ); $filters = self::request_filters( true ); nocache_headers(); if ( ! $scope->valid_for( 'logged_ai_export_events' ) ) { wp_die( esc_html__( 'Request export authority changed.', 'logged-ai' ), '', array( 'response' => 403 ) ); } $result = $reader->download( $filters, static function ( string $json ): void { header( 'Content-Type: application/json; charset=UTF-8' ); header( 'Content-Disposition: attachment; filename="logged-ai-requests.json"' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The guarded reader supplies a complete JSON document for this JSON-only download response. echo $json; } ); if ( is_wp_error( $result ) ) { self::loaded( $result ); } exit; } if ( in_array( $operation, array( 'preview', 'preview_typed' ), true ) ) { $evidence = $request_form['evidence_id'] ?? null; $ttl = $request_form['ttl'] ?? null; if ( ! is_string( $evidence ) || ! preg_match( '/\A[1-9][0-9]{0,18}\z/D', $evidence ) || (string) (int) $evidence !== $evidence || ! is_string( $ttl ) || ! in_array( $ttl, array_map( 'strval', Requests::TTLS ), true ) ) { wp_die( esc_html__( 'Select a valid request observation and duration.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } $input = null; if ( 'preview_typed' === $operation ) { $input = array(); foreach ( array( 'kind', 'path', 'key', 'operator', 'value' ) as $field ) { $raw = $request_form[ $field ] ?? ''; if ( ! is_string( $raw ) || strlen( $raw ) > 256 ) { wp_die( esc_html__( 'Invalid response condition.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } $input[ $field ] = $raw; if ( strlen( $input[ $field ] ) > 128 ) { wp_die( esc_html__( 'Invalid response condition.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } } $exceptions = $request_form['exceptions'] ?? array(); if ( ! is_array( $exceptions ) || count( $exceptions ) > 5 ) { wp_die( esc_html__( 'Select at most five valid response exceptions.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } $input['exceptions'] = array(); foreach ( $exceptions as $exception ) { if ( ! is_string( $exception ) || strlen( $exception ) > 256 ) { wp_die( esc_html__( 'Invalid response exception.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } $exception = $exception; if ( strlen( $exception ) > 128 ) { wp_die( esc_html__( 'Invalid response exception.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } $input['exceptions'][] = $exception; } } $display = self::preview_translations(); $preview = Requests::preview( $scope->network_id, $scope->site_id, (int) $evidence, (int) $ttl, $input ); if ( is_wp_error( $preview ) ) { wp_die( esc_html( PresentationTranslations::text( $preview->get_error_message() ) ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 409 ) ); } self::preview_screen( $scope, $preview, $display ); exit; } $notice = ''; if ( 'confirm' === $operation ) { if ( '1' !== ( $request_form['acknowledge'] ?? '' ) ) { wp_die( esc_html__( 'Review and acknowledge the exact scoped effect and recovery procedure.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } $id = $request_form['policy_id'] ?? null; $hash = $request_form['compiled_hash'] ?? null; if ( ! is_string( $id ) || ! preg_match( '/\A[a-f0-9]{32}\z/D', $id ) || ! is_string( $hash ) || ! preg_match( '/\A[a-f0-9]{64}\z/D', $hash ) ) { wp_die( esc_html__( 'Invalid response confirmation. Review a fresh preview.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } $activate = 'active' === ( $request_form['mode'] ?? '' ); if ( ! in_array( $request_form['mode'] ?? '', array( 'active', 'observe' ), true ) ) { wp_die( esc_html__( 'Select an explicit response mode.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } $saved = Requests::confirm( $scope->network_id, $scope->site_id, $id, $hash, $activate ); if ( is_wp_error( $saved ) ) { wp_die( esc_html( PresentationTranslations::text( $saved->get_error_message() ) ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 409 ) ); } $notice = 'saved'; } elseif ( 'revoke' === $operation ) { $id = $request_form['policy_id'] ?? null; if ( ! is_string( $id ) || ! preg_match( '/\A[a-f0-9]{32}\z/D', $id ) ) { wp_die( esc_html__( 'Select a valid response policy.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } if ( ! Requests::revoke( $scope->network_id, $scope->site_id, $id ) ) { wp_die( esc_html__( 'The scoped policy was not changed; it may already be revoked or unavailable.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 409 ) ); } $notice = 'revoked'; } elseif ( in_array( $operation, array( 'disable', 'enable' ), true ) ) { if ( 'enable' === $operation && ( '1' !== ( $request_form['acknowledge'] ?? '' ) || Requests::recovery_disabled() ) ) { wp_die( esc_html__( 'Enable requires acknowledgment and an inactive recovery constant.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } if ( ! Requests::set_enabled( $scope->network_id, $scope->site_id, 'enable' === $operation ) ) { wp_die( esc_html__( 'The response setting could not be saved.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 409 ) ); } $notice = 'enable' === $operation ? 'enabled' : 'disabled'; } else { wp_die( esc_html__( 'Unknown request operation.', 'logged-ai' ), esc_html__( 'Logged.ai', 'logged-ai' ), array( 'response' => 400 ) ); } wp_safe_redirect( add_query_arg( array( 'page' => 'logged-ai-response', 'notice' => $notice, ), admin_url( 'admin.php' ) ) ); exit; } /** * Render the exact saved draft, bounded sample projection and explicit confirmation controls. * * @param object $scope Current operation scope whose native grants are checked. * @param array $policy Saved operator-bound manual-policy draft to present for review. * @param array $display Labels resolved before the authorized draft was created. * * @return void */ /** * Translate fixed preview labels before the authorized draft is created. * * @return array Prepared plain and escaped labels for the private renderer. */ private static function preview_translations(): array { // Resolve translation callbacks before reading authorized request evidence. $display_text = array( 0 => __( 'Policy preview authority changed.', 'logged-ai' ), 1 => __( 'Preview temporary policy · Logged.ai', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 2 => __( '%1$sNo policy has been activated.%2$s This preview expires in five minutes and is bound to this operator, site, parser revision and exact conditions.', 'logged-ai' ), 3 => __( 'GET and HEAD', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 4 => __( '%s seconds', 'logged-ai' ), 5 => __( 'WordPress init, priority -999; not edge/server/static', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 6 => __( 'Exact compiled conditions: %1$s', 'logged-ai' ), 7 => __( 'Exceptions allow only this policy’s matching GET/HEAD requests at the exact listed paths in this site until policy expiry. They do not grant WordPress permissions or override other policies. Changing exceptions requires a fresh policy preview; revoke the old policy separately.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 8 => __( '%1$sShared-source warning:%2$s this source address may represent a proxy, shared household, office, scanner or monitor. Checkout, login, recovery and API GET/HEAD requests from it may be denied. The source target stops at %3$s or after privacy/source settings change, even if the policy duration is longer. Only an authenticated user with response authority can reach this plugin’s Response screen through a narrow recovery exception. Keep OS recovery access available.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 9 => __( 'Projected matches: %1$s of at most 100 recent admitted minimized findings; historical target data unavailable for %2$s inspected rows. Routine benign requests are not retained. Query conditions cannot be replayed because query contents are not retained. This is a structural projection, not a full historical replay or a count of all matching traffic. No requests are resubmitted.', 'logged-ai' ), 10 => __( 'Same versioned matcher: boundary examples', 'logged-ai' ), 11 => __( 'Example', 'logged-ai' ), 12 => __( 'Result', 'logged-ai' ), 13 => __( 'Matches exact condition', 'logged-ai' ), 14 => __( 'No denial', 'logged-ai' ), 15 => __( 'These are matcher examples, not traffic classifications. For exact-source policies the examples assume the selected derived address. Benign traffic can match a deliberate policy; other code can have run before this hook. Method/path ambiguity fails open for typed conditions.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 16 => __( 'Recovery: the OS operator can run %1$s or set %2$s to boolean true before WordPress loads. There is no public bypass.', 'logged-ai' ), 17 => __( 'Policy confirmation authority changed.', 'logged-ai' ), 18 => __( 'Policy mode', 'logged-ai' ), 19 => __( ' Save observation-only policy (never denies)', 'logged-ai' ), 20 => __( ' Activate this temporary policy and enable local response for valid existing policies', 'logged-ai' ), 21 => __( ' I reviewed the exact site, path, methods, finite duration, coverage limits and OS recovery instructions.', 'logged-ai' ), /* translators: Values or formatting elements shown in this message. */ 22 => __( '%1$sCancel and return to Requests%2$s', 'logged-ai' ), ); $display_labels = PresentationTranslations::snapshot( array( 'Manual policy response decision', 'Manual policy condition', 'A deliberately configured manual condition matched; this is not an attack finding. Check the recorded action to see whether a response was issued.', 'Ordinary benign requests can match a manual condition. A match alone does not prove a denial or an attack.', 'An observation-only manual condition matched. This policy recorded the match without denying the request; no attack classification is inferred.', 'Observation-only policy %1$s matched (revision %2$s). This policy did not deny the request.', 'A deliberately configured manual condition matched; this is a response decision, not an attack finding.', 'Ordinary benign requests can match a manual condition. No attack classification is inferred from this response.', 'Configuration-resource probe pattern', 'Exact reserved configuration filename or recognized backup variant.', 'An authorized scanner or broken integration can request these resources.', 'Source-control or diagnostic discovery', 'Exact source-control metadata or diagnostic resource template.', 'Authorized diagnostics can deliberately request these resources.', 'Traversal-shaped resource request', 'Two parent-directory segments precede a recognized configuration/system-resource suffix.', 'Malformed links and authorized scanners are possible; target application normalization is unknown.', 'SQL-shaped numeric parameter', 'A numeric-identifier parameter contains a compound boolean or UNION projection with a comment delimiter.', 'A test or code example can contain the same structure; no execution or intent is established.', 'Executable-markup-shaped parameter', 'A numeric/navigation parameter contains paired script markup, an event-handler tag, or an executable URL scheme.', 'Authorized testing can contain the same syntax. Editing/search content parameters are not classified.', 'Component metadata enumeration', 'A plugin readme or theme metadata path was requested; enumeration is not exploitation.', 'Documentation tools and legitimate theme stylesheets can use related routes; theme style.css is excluded.', 'Unusual method at a non-API resource', 'TRACE/TRACK/CONNECT or a write method was used on a reserved metadata/configuration resource.', 'Protocol/security tests may intentionally exercise methods; ordinary API writes and OPTIONS are excluded.', 'The scoped policy was saved. The next eligible request must independently verify WordPress-layer response.', 'The policy was revoked. New eligible requests read current database state.', 'WordPress-layer response is disabled. Observation continues.', 'WordPress-layer response is enabled for currently valid, unexpired active policies.', 'Requests, in context.', 'Export latest 500 minimized requests (JSON)', 'Retained request family', 'First / last (selected timezone)', 'Source association', 'Unavailable; this is a site/time/family group', 'Partial analyses', 'Unknown final statuses', 'HTTP 1xx / 2xx / 3xx / 4xx / 5xx', 'Recorded WordPress responses', 'Sample evidence IDs', 'Retained request finding', 'Interpret only the recorded rule and available context.', 'Evidence ID', 'Alert work', 'Local request reference', 'Sensor / source', 'Rule / revision', 'Safe resource template', 'Method', 'Client-context availability', 'Partial analysis', 'yes; observation only', 'no within the bounded supported view', 'Final HTTP status', 'Logged.ai action', 'Unconfirmed local response; no verified issuance receipt', 'No Logged.ai response', 'Application effect', 'Unknown', 'Captured (selected timezone)', 'Ingested (selected timezone)', 'Response observed (selected timezone)', '15 minutes', '1 hour', '24 hours', '7 days', 'Preview temporary policy', 'Exact safe resource path', 'Reserved source-control path prefix', 'Structured query on one exact path', 'Exact retained derived address (full mode required)', 'Preview reviewed conditions', 'Response', 'Disable all local response for this site', 'Enable remaining active policies', 'Policy ID', 'Origin', 'State', 'Revision', 'Methods', 'Scope', 'Expiry (selected timezone)', 'Parser', 'Preview hash', 'Revoke', 'Latest at most 500 admitted observations visible at query time within the configured request retention. Concurrent and later inserts are not a transactionally complete snapshot. Telemetry is admission-limited.', 'Review temporary policy', 'Safe target', 'GET and HEAD', 'Duration after confirmation', 'Enforcement layer', 'WordPress init, priority -999; not edge/server/static', 'Parser revision', '/?dumb-code-to-hack-site', 'Confirm reviewed policy', 'Response authority is required for the selected user.', 'Use --yes to enable still-valid active policies; durations are not extended.', 'LOGGED_AI_DISABLE_RESPONSE is active. Remove it using operator configuration first.', 'The scoped response setting could not be changed. Enabling retained IP policies requires --user with response and sensitive-context permissions.', 'Use revoke POLICY_ID with exactly --dry-run or --yes in the selected site.', 'The policy ID is invalid.', 'The policy ID is invalid, outside this site, unavailable or already revoked.', 'Use status, list, disable, enable --yes, or revoke POLICY_ID --dry-run|--yes.', 'Response authority changed.', 'Use requests prune --dry-run to preview one selected site, or requests prune --yes to apply one bounded batch. Choose exactly one flag and an explicit --url; no all-sites operation is supported.', 'Use requests list, requests export, requests prune --dry-run, or requests prune --yes. The separate Logged.ai Pro add-on supplies reference import with explicit source configuration.', 'Request evidence authority is required for the selected user.', 'Request evidence authority changed.', 'Authorized scanners, diagnostic tools and unrelated visitors can produce this pattern. A direct network peer may represent many people.', 'Repeated distinct reserved-resource requests', 'At least 10 complete admitted GET/HEAD findings cover at least three distinct safe reserved-resource templates in one fixed UTC minute.', 'Known configuration/source/diagnostic templates; complete parser result; this authorized site and source label. Status can be unknown.', 'Repeated distinct reserved resources with observed 404s', 'At least 10 complete admitted GET/HEAD findings have a finalized observed 404 across at least three distinct safe reserved-resource templates in one fixed UTC minute.', 'Request-sequence access is unavailable.', 'Request-sequence authority changed.', 'Current retained admitted findings only. Counts are lower bounds; missing telemetry is not quantifiable. Fixed UTC-minute boundaries can split a sequence. Site and exact-peer views overlap and must not be summed. No attacker identity, historical alert, exposure, prevention or automated-response eligibility is established.', 'Admitted / complete eligible', 'First / last (display timezone)', 'Safe templates', 'Known finalized 404s', 'Recorded local responses', 'Unknown final status', 'Window still open', 'Sample observation IDs', ) ); return array( 'text' => $display_text, 'labels' => $display_labels ); } private static function preview_screen( object $scope, array $policy, array $display ): void { $display_text = $display['text']; $display_labels = $display['labels']; global $wpdb; $time = self::time(); if ( ! $scope->valid_for_all( 'ip' === ( $policy['target_kind'] ?? '' ) ? array( 'logged_ai_manage_response', 'logged_ai_view_sensitive_context' ) : array( 'logged_ai_manage_response' ) ) ) { wp_die( esc_html( $display_text[ 0 ] ), '', array( 'response' => 403 ) ); } $samples = self::loaded( Requests::observations( $scope->network_id, $scope->site_id, 100 ) ); $typed = RequestsPolicy::VERSION === $policy['parser_version']; $condition = $typed ? json_decode( $policy['target_json'], true, 8 ) : null; $settings = Requests::privacy_settings(); $matches = 0; $unavailable = 0; foreach ( $samples as $sample ) { if ( $typed ) { if ( 'query' === $condition['kind'] || (int) $sample['partial'] || ! in_array( $sample['safe_path'], RequestsClassifier::safe_paths(), true ) || ( 'ip' === $condition['kind'] && 'full' !== $sample['ip_mode'] ) ) { ++$unavailable; continue; } if ( RequestsPolicy::matches( $policy, RequestsPolicy::view( $sample['safe_path'], $sample['method'], $sample['client_mask'] ), $settings ) ) { ++$matches; } } elseif ( $sample['safe_path'] === $policy['safe_path'] && ! (int) $sample['partial'] && in_array( $sample['method'], array( 'GET', 'HEAD' ), true ) ) { ++$matches; } } if ( ! $scope->valid_for_all( 'ip' === ( $policy['target_kind'] ?? '' ) ? array( 'logged_ai_manage_response', 'logged_ai_view_sensitive_context' ) : array( 'logged_ai_manage_response' ) ) ) { wp_die( esc_html( $display_text[ 0 ] ), '', array( 'response' => 403 ) ); } // Native admin-post response with no executable scripts or remote assets. nocache_headers(); header( 'Content-Type: text/html; charset=UTF-8' ); wp_enqueue_style( 'logged-ai-preview', plugins_url( 'assets/admin.css', LOGGED_AI_FILE ), array(), LOGGED_AI_VERSION ); wp_enqueue_style( 'logged-ai-preview-requests', plugins_url( 'assets/requests.css', LOGGED_AI_FILE ), array( 'logged-ai-preview' ), LOGGED_AI_VERSION ); echo '' . '' . '' . '' . esc_html( $display_text[ 1 ] ) . '' ); wp_print_styles( array( 'logged-ai-preview', 'logged-ai-preview-requests' ) ); echo '
'; self::start( 'Review temporary policy', $scope, $time ); if ( ! $scope->valid_for_all( 'ip' === ( $policy['target_kind'] ?? '' ) ? array( 'logged_ai_manage_response', 'logged_ai_view_sensitive_context' ) : array( 'logged_ai_manage_response' ) ) ) { wp_die( esc_html( $display_text[ 0 ] ), '', array( 'response' => 403 ) ); } echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. */ esc_html( $display_text[ 2 ] ), '', '' ) . '

' . '
' ); foreach ( array( 'Safe target' => $policy['safe_path'], 'Methods' => $display_text[ 3 ], 'Duration after confirmation' => sprintf( /* translators: %s: policy duration in seconds. */ $display_text[ 4 ], $policy['ttl'] ), 'Enforcement layer' => $display_text[ 5 ], 'Parser revision' => $policy['parser_version'], 'Preview hash' => $policy['compiled_hash'], ) as $key => $value ) { echo '
' . esc_html( PresentationTranslations::lookup( $display_labels, $key ) ) . '
' . esc_html( $value ) . '
'; } echo '
'; if ( $typed ) { echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. */ esc_html( $display_text[ 6 ] ), '' . ( esc_html( $policy['target_json'] ) ) . '' ) . '

' . '

' . esc_html( $display_text[ 7 ] ) . '

' ); if ( 'ip' === $condition['kind'] ) { echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. 3: Value or formatting element shown in this message. */ esc_html( $display_text[ 8 ] ), '', '', esc_html( $time->text( $condition['source_expires_at'] ) ) ) . '

' ); } } echo ( '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. */ esc_html( $display_text[ 9 ] ), esc_html( (string) $matches ), esc_html( (string) $unavailable ) ) . '

' ); echo ( '' . '' . '' . '' . '' . '' . '' . '' . '' ); $example_path = $typed ? $condition['path'] ?? '/example/' : $policy['safe_path']; $positive = $typed && 'query' === $condition['kind'] ? $example_path . '?' . $condition['key'] . '=' . ( $condition['value'] ?? '42' ) : $example_path; foreach ( array( array( $positive, 'GET' ), array( $positive, 'HEAD' ), array( $positive, 'POST' ), array( '/checkout/', 'GET' ), array( '/?dumb-code-to-hack-site', 'GET' ), array( str_replace( '.', '%2e', $positive ), 'GET' ), array( strtoupper( $positive ), 'GET' ), array( $example_path . '/extra', 'GET' ), array( $example_path . 'extra', 'GET' ) ) as [$target, $method] ) { $parsed = Requests::parse( $target ); $would_match = $typed ? RequestsPolicy::matches( $policy, RequestsPolicy::view( $target, $method, $condition['ip'] ?? null ), $settings ) : $parsed['eligible'] && $parsed['safe_path'] === $policy['safe_path'] && in_array( $method, array( 'GET', 'HEAD' ), true ); echo ( '' . '' . '' . '' ); } echo ( '' . '
' . esc_html( $display_text[ 10 ] ) . '
' . esc_html( $display_text[ 11 ] ) . '' . esc_html( $display_text[ 12 ] ) . '
' . ( esc_html( $method . ' ' . $target ) ) . '' . ( esc_html( ( $would_match ? $display_text[ 13 ] : $display_text[ 14 ] ) ) ) . '
' . '

' . esc_html( $display_text[ 15 ] ) . '

' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. */ esc_html( $display_text[ 16 ] ), 'wp logged-ai response disable --url=YOUR_SITE', 'LOGGED_AI_DISABLE_RESPONSE' ) . '

' ); if ( ! $scope->valid_for_all( 'ip' === ( $policy['target_kind'] ?? '' ) ? array( 'logged_ai_manage_response', 'logged_ai_view_sensitive_context' ) : array( 'logged_ai_manage_response' ) ) ) { wp_die( esc_html( $display_text[ 17 ] ), '', array( 'response' => 403 ) ); } self::form( 'confirm' ); self::field( 'policy_id', $policy['id'] ); self::field( 'compiled_hash', $policy['compiled_hash'] ); echo ( '
' . '' . esc_html( $display_text[ 18 ] ) . '' . '

' . '' . '

' . '

' . '' . '

' . '
' . '

' . '' . '

' ); self::submit( 'Confirm reviewed policy', 'primary' ); echo ( '' . '

' . sprintf( /* translators: 1: Value or formatting element shown in this message. 2: Value or formatting element shown in this message. */ esc_html( $display_text[ 22 ] ), '', '' ) . '

' . '' . '
' . '' . '' ); } /** * Dispatch scoped OS-operator response inspection, recovery and explicit revocation commands. * * OS-level operator commands. --url chooses one WordPress site, never all sites. * * @param array $args Positional WP-CLI command arguments. * @param array $assoc Named WP-CLI command flags or filters. * * @return void */ public static function response_cli( array $args, array $assoc ): void { [$network, $site] = Requests::scope_ids(); $command = $args[0] ?? 'status'; if ( get_current_user_id() > 0 && ! Scope::can( 'logged_ai_manage_response' ) ) { \WP_CLI::error( __( 'Response authority is required for the selected user.', 'logged-ai' ) ); } $result = null; if ( 'status' === $command ) { $result = array( 'network_id' => (string) $network, 'site_id' => (string) $site, 'recovery_constant' => Requests::recovery_disabled(), 'state' => Requests::state( $network, $site ), 'layer' => 'wordpress_init', 'policy_cache' => 'none', ); } elseif ( 'list' === $command ) { $result = Requests::policies( $network, $site ); } elseif ( 'disable' === $command || 'enable' === $command ) { if ( 'enable' === $command && ! isset( $assoc['yes'] ) ) { \WP_CLI::error( __( 'Use --yes to enable still-valid active policies; durations are not extended.', 'logged-ai' ) ); } if ( 'enable' === $command && Requests::recovery_disabled() ) { \WP_CLI::error( __( 'LOGGED_AI_DISABLE_RESPONSE is active. Remove it using operator configuration first.', 'logged-ai' ) ); } if ( ! Requests::set_enabled( $network, $site, 'enable' === $command ) ) { \WP_CLI::error( __( 'The scoped response setting could not be changed. Enabling retained IP policies requires --user with response and sensitive-context permissions.', 'logged-ai' ) ); } $result = array( 'action' => $command, 'site_id' => (string) $site, 'saved' => true, ); } elseif ( 'revoke' === $command ) { $dry_run = array( 'dry-run' => true ) === $assoc; if ( 2 !== count( $args ) || ( ! $dry_run && array( 'yes' => true ) !== $assoc ) ) { \WP_CLI::error( __( 'Use revoke POLICY_ID with exactly --dry-run or --yes in the selected site.', 'logged-ai' ) ); } $id = $args[1] ?? ''; if ( ! is_string( $id ) || ! preg_match( '/\A[a-f0-9]{32}\z/D', $id ) ) { \WP_CLI::error( __( 'The policy ID is invalid.', 'logged-ai' ) ); } if ( $dry_run ) { $result = Requests::revoke_preview( $network, $site, $id ); } elseif ( ! Requests::revoke( $network, $site, $id ) ) { \WP_CLI::error( __( 'The policy ID is invalid, outside this site, unavailable or already revoked.', 'logged-ai' ) ); } else { $result = array( 'action' => 'revoked', 'policy_id' => $id, 'site_id' => (string) $site, ); } } else { \WP_CLI::error( __( 'Use status, list, disable, enable --yes, or revoke POLICY_ID --dry-run|--yes.', 'logged-ai' ) ); } if ( is_wp_error( $result ) ) { \WP_CLI::error( PresentationTranslations::text( $result->get_error_message() ) ); } if ( 'list' === $command ) { if ( get_current_user_id() > 0 ) { $scope = Scope::current( 'logged_ai_manage_response' ); if ( is_wp_error( $scope ) ) { \WP_CLI::error( __( 'Response authority changed.', 'logged-ai' ) ); } $result = Requests::policies_for_scope( $scope, $result ); if ( is_wp_error( $result ) ) { \WP_CLI::error( PresentationTranslations::text( $result->get_error_message() ) ); } } else { $result = Requests::policies_for_viewer( $result, $network, $site ); } } \WP_CLI::line( wp_json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } /** * Dispatch bounded request listing/export, explicit retention and registered import handling. * * @param array $args Positional WP-CLI command arguments. * @param array $assoc Named WP-CLI command flags or filters. * * @return void */ public static function requests_cli( array $args, array $assoc ): void { if ( 'import' === ( $args[0] ?? '' ) && has_action( 'logged_ai_requests_import_cli' ) ) { if ( ! defined( 'LOGGED_AI_PRO_EDITION_API' ) || 2 !== LOGGED_AI_PRO_EDITION_API ) { \WP_CLI::error( __( 'Install the matching separate Logged.ai Pro add-on before using its importer.', 'logged-ai' ) ); } do_action( 'logged_ai_requests_import_cli', $args, $assoc ); return; } [$network, $site] = Requests::scope_ids(); if ( 'prune' === ( $args[0] ?? '' ) ) { $dry_run = array( 'dry-run' => true ) === $assoc; if ( 1 !== count( $args ) || ( ! $dry_run && array( 'yes' => true ) !== $assoc ) ) { \WP_CLI::error( __( 'Use requests prune --dry-run to preview one selected site, or requests prune --yes to apply one bounded batch. Choose exactly one flag and an explicit --url; no all-sites operation is supported.', 'logged-ai' ) ); } $result = Requests::retention( $dry_run ); if ( is_wp_error( $result ) ) { \WP_CLI::error( PresentationTranslations::text( $result->get_error_message() ) ); } \WP_CLI::line( wp_json_encode( $result, JSON_PRETTY_PRINT ) ); return; } if ( ! in_array( $args[0] ?? 'list', array( 'list', 'export' ), true ) ) { \WP_CLI::error( __( 'Use requests list, requests export, requests prune --dry-run, or requests prune --yes. The separate Logged.ai Pro add-on supplies reference import with explicit source configuration.', 'logged-ai' ) ); } $required = 'export' === ( $args[0] ?? 'list' ) ? 'logged_ai_export_events' : 'logged_ai_view_events'; $limit = $assoc['limit'] ?? ( 'logged_ai_export_events' === $required ? '500' : '50' ); if ( ! is_string( $limit ) || ! preg_match( '/\A[1-9][0-9]{0,2}\z/D', $limit ) || (int) $limit > 500 ) { \WP_CLI::error( __( 'Use a limit from 1 to 500.', 'logged-ai' ) ); } $filters = array(); foreach ( $assoc as $key => $value ) { if ( 'limit' !== $key ) { $filters[ $key ] = $value; } } $reader = RequestsBrowse::current( $required ); if ( is_wp_error( $reader ) ) { \WP_CLI::error( $reader->get_error_message() ); } if ( 'logged_ai_export_events' === $required ) { $result = $reader->download( $filters, static fn( string $json ) => \WP_CLI::line( $json ), (int) $limit ); if ( is_wp_error( $result ) ) { \WP_CLI::error( $result->get_error_message() ); } return; } $result = $reader->page( $filters, (int) $limit ); if ( is_wp_error( $result ) ) { \WP_CLI::error( $result->get_error_message() ); } if ( 'logged_ai_view_events' === $required ) { $result['observations'] = $result['rows']; unset( $result['rows'] ); $result['network_id'] = (string) $network; $result['site_id'] = (string) $site; $result['membership'] = 'Latest matching admitted observations within request retention; dates use UTC. The continuation cursor is exclusive. Not a complete traffic snapshot.'; } \WP_CLI::line( wp_json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } }