array( 'invalid_report' ) ); } $health = get_option( 'kconv_retention_health', array() ); if ( ! is_array( $health ) ) { $health = array(); } $health['finished'] = time(); $health['deleted'] = max( 0, (int) ( $report['deleted'] ?? 0 ) ); $health['batches'] = max( 0, (int) ( $report['batches'] ?? 0 ) ); // Do not retain exception/SQL text supplied by an extension or database driver. $health['errors'] = isset( $report['errors'] ) ? ( is_array( $report['errors'] ) ? count( $report['errors'] ) : (int) (bool) $report['errors'] ) : 0; $health['remaining'] = isset( $report['remaining'] ) ? max( 0, (int) $report['remaining'] ) : null; if ( ! $health['errors'] ) { $health['last_success'] = $health['finished']; } update_option( 'kconv_retention_health', $health, false ); } public static function health() { global $wpdb; $saved = get_option( 'kconv_retention_health', array() ); if ( ! is_array( $saved ) ) { $saved = array(); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The local retention health count is a fresh aggregate over the fixed plugin entries table. It is administrator diagnostic data; caching would misstate current overdue cleanup. $rows = $wpdb->get_row( $wpdb->prepare( 'SELECT COUNT(*) AS expired,MIN(expires_at) AS oldest FROM %i WHERE expires_at < %s', KCONV_Core::table( 'entries' ), gmdate( 'Y-m-d H:i:s' ) ), ARRAY_A ); $storage_ok = ! $wpdb->last_error && is_array( $rows ); $daily = wp_next_scheduled( 'kconv_cleanup' ); $continuation = wp_next_scheduled( 'kconv_cleanup_continue' ); $next = $daily && $continuation ? min( $daily, $continuation ) : ( $daily ? $daily : $continuation ); $overdue = $storage_ok && ! empty( $rows['oldest'] ) && $rows['oldest'] < gmdate( 'Y-m-d H:i:s', time() - 2 * DAY_IN_SECONDS ); $continuation_overdue = $storage_ok && ! empty( $rows['expired'] ) && $continuation && $continuation < time() - 900; $unfinished = ! empty( $saved['started'] ) && (int) $saved['started'] > (int) ( $saved['finished'] ?? 0 ); return array( 'saved' => $saved, 'next' => $next, 'continuation' => $continuation, 'continuation_overdue' => $continuation_overdue, 'storage_ok' => $storage_ok, 'expired' => $storage_ok ? (int) $rows['expired'] : null, 'oldest' => $storage_ok ? $rows['oldest'] : null, 'unfinished' => $unfinished, 'attention' => ! $storage_ok || ! $daily || ! empty( $saved['errors'] ) || $overdue || $continuation_overdue || ( $unfinished && $saved['started'] < time() - 900 ) ); } public static function panel() { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( get_option( 'kconv_schema_problem' ) ) { echo '

' . esc_html__( 'Form storage setup needs attention. Some required database tables or columns could not be verified. Repair storage before relying on new submissions.', 'killerconversions' ) . '

'; } $followup = get_option( 'kconv_postcommit_problem', array() ); if ( is_array( $followup ) && ! empty( $followup['entry_id'] ) ) { echo '

' . esc_html__( 'An entry was saved, but a follow-up action did not finish. Review the entry and its delivery history before taking another action.', 'killerconversions' ) . ' ' . esc_html__( 'Review saved entry', 'killerconversions' ) . '

'; } $health = self::health(); $saved = $health['saved']; // Only a recorded successful, completed run with no pending expired rows earns the compact presentation. // Unknown/never-run state keeps the ordinary panel; the underlying health rules and cleanup actions are unchanged. $compact = ! $health['attention'] && ! $health['unfinished'] && 0 === $health['expired'] && ! empty( $saved['finished'] ) && ! empty( $saved['last_success'] ) && (int) $saved['finished'] === (int) $saved['last_success']; $status = $health['attention'] ? __( 'Needs attention', 'killerconversions' ) : ( $compact ? __( 'Last cleanup completed', 'killerconversions' ) : __( 'Review cleanup status', 'killerconversions' ) ); echo '
' . esc_html__( 'Retention health', 'killerconversions' ) . ' — ' . esc_html( $status ) . '
'; if ( ! $health['storage_ok'] ) { echo '

' . esc_html__( 'Retention storage could not be inspected. Counts are unavailable; repair storage before retrying.', 'killerconversions' ) . '

'; } else { /* translators: %s: number of expired entries. */ echo '

' . esc_html( sprintf( _n( '%s entry is past its retention time.', '%s entries are past their retention time.', $health['expired'], 'killerconversions' ), number_format_i18n( $health['expired'] ) ) ) . '

'; if ( $health['oldest'] ) { echo '

' . esc_html__( 'Oldest expiry:', 'killerconversions' ) . ' ' . esc_html( KCONV_Entries::display_time( $health['oldest'] ) ) . '

'; } } echo '
' . esc_html__( 'Last completed run', 'killerconversions' ) . '
' . esc_html( ! empty( $saved['finished'] ) ? KCONV_Entries::display_time( gmdate( 'Y-m-d H:i:s', (int) $saved['finished'] ) ) : __( 'No completed run recorded yet', 'killerconversions' ) ) . '
' . esc_html__( 'Next scheduled run', 'killerconversions' ) . '
' . esc_html( $health['next'] ? KCONV_Entries::display_time( gmdate( 'Y-m-d H:i:s', $health['next'] ) ) : __( 'Not scheduled', 'killerconversions' ) ) . '
'; if ( $health['continuation'] && $health['expired'] > 0 ) { echo '

' . esc_html( $health['continuation_overdue'] ? __( 'The next cleanup batch is overdue. Check that WordPress cron is running, or run cleanup now.', 'killerconversions' ) : __( 'More expired entries remain. Another cleanup batch is scheduled; it will run when WordPress cron runs.', 'killerconversions' ) ) . '

'; } /* translators: 1: number of deleted entries, 2: cleanup batch count. */ if ( ! empty( $saved['finished'] ) ) { echo '

' . esc_html( sprintf( __( 'Last run deleted %1$s entries in %2$s batches.', 'killerconversions' ), number_format_i18n( $saved['deleted'] ?? 0 ), number_format_i18n( $saved['batches'] ?? 0 ) ) ) . '

'; } if ( ! empty( $saved['errors'] ) ) { echo '

' . esc_html__( 'The last cleanup reported a storage, processing or scheduling failure. Remaining records were not reported as deleted. Repair the cause and run cleanup again.', 'killerconversions' ) . '

'; } if ( $health['unfinished'] ) { echo '

' . esc_html__( 'The latest cleanup started but has not recorded completion. It may still be running or may have been interrupted.', 'killerconversions' ) . '

'; } echo '

' . esc_html__( 'Cleanup permanently removes expired entries and their related local files, delivery and measurement records. It does not change retention periods or remove data already received by another service. A schedule alone does not prove the server runs WordPress cron.', 'killerconversions' ) . '

'; wp_nonce_field( 'kconv_retention' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only panel() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards. $filters = KCONV_Entries::filters( wp_unslash( $_GET ) ); if ( ! is_wp_error( $filters ) ) { foreach ( $filters as $key => $value ) { echo ''; } } echo '
'; } public static function handle() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Administrator access required.', 'killerconversions' ), '', array( 'response' => 403 ) ); } if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) ) { wp_nonce_ays( 'kconv_retention' ); } $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); if ( ! wp_verify_nonce( $nonce, 'kconv_retention' ) ) { wp_nonce_ays( 'kconv_retention' ); } $operation = isset( $_POST['operation'] ) && is_string( $_POST['operation'] ) ? sanitize_key( $_POST['operation'] ) : ''; if ( 'schedule' === $operation ) { if ( ! wp_next_scheduled( 'kconv_cleanup' ) ) { $scheduled = wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'kconv_cleanup', array(), true ); if ( is_wp_error( $scheduled ) || ! $scheduled ) { wp_die( esc_html__( 'Cleanup could not be scheduled. Check WordPress cron storage and retry.', 'killerconversions' ), '', array( 'response' => 503 ) ); } } } elseif ( 'run' === $operation ) { KCONV_Core::cleanup(); } else { wp_die( esc_html__( 'Choose a supported retention action.', 'killerconversions' ), '', array( 'response' => 400 ) ); } $context = KCONV_Entries::return_context( wp_unslash( $_POST ) ); wp_safe_redirect( add_query_arg( array_merge( $context, array( 'page' => 'kconv-entries' ) ), admin_url( 'admin.php' ) ) ); exit; } }