ification( $notification ); if ( ! $success ) { wp_send_json_error( array( 'message' => __( 'Failed to save notification.', 'wp-smushit' ) ) ); } wp_send_json_success(); } /** * AJAX handler for fetching the latest notifications for the UI. * Called after a long-running background process completes or dies on-page, * so the frontend can replace optimistic entries with server-written ones. * * @return void */ public function ajax_get_notifications() { check_ajax_referer( 'wp-smush-ajax' ); if ( ! Helper::is_user_allowed( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions', 'wp-smushit' ) ), 403 ); } wp_send_json_success( array( 'notifications' => $this->get_notifications_for_ui(), ) ); } private function get_notifications_for_ui() { $this->enforce_notification_limit(); $notifications = $this->get_notifications(); if ( empty( $notifications ) ) { $this->add_notification( array( 'module' => 'smush', 'content' => $this->string_utils->get_raw_string( 'Welcome to Smush', $this->whitelabel->replace_branding_terms( __( 'Welcome to Smush', 'wp-smushit' ) ) ), ) ); $notifications = $this->get_notifications(); } return array_map( array( $this, 'sanitize_notification_for_ui' ), $notifications ); } /** * Track the completion of a scan process. * * @return void */ public function log_scan_completed() { $this->add_notification( array( 'module' => 'scan', 'content' => $this->string_utils->get_raw_string( 'Scan completed.', __( 'Scan completed.', 'wp-smushit' ) ), ) ); } /** * Track the death of a scan process. * * @return void */ public function log_scan_process_death() { $this->add_notification( array( 'module' => 'scan', 'content' => $this->string_utils->get_raw_string( 'Scan failed.', __( 'Scan failed.', 'wp-smushit' ) ), ) ); } /** * Track the completion of a bulk smush process. * * @return void */ public function log_bulk_smush_completed() { $this->add_notification( array( 'module' => 'bulk_smush', 'content' => $this->string_utils->get_raw_string( 'Bulk Optimization completed.', __( 'Bulk Optimization completed.', 'wp-smushit' ) ), ) ); } /** * Track the death of a bulk smush process. * * @return void */ public function log_bulk_smush_process_death() { $this->add_notification( array( 'module' => 'bulk_smush', 'content' => $this->string_utils->get_raw_string( 'Bulk Optimization failed.', __( 'Bulk Optimization failed.', 'wp-smushit' ) ), ) ); } /** * Track CDN going fully active after provisioning completes. * * @return void */ public function log_cdn_activated() { $this->add_notification( array( 'module' => 'cdn', 'content' => $this->string_utils->get_raw_string( 'CDN setup complete.', __( 'CDN setup complete.', 'wp-smushit' ) ), ) ); } /** * Get the notifications. * * Reads directly from the database (bypasses object cache) to ensure * writes from other background processes are visible. * * @return array */ public function get_notifications() { $notifications = $this->thread_safe_options->get_option( self::$notification_data_key, array() ); return is_array( $notifications ) ? $notifications : array(); } /** * Add a notification atomically. * * Uses a single JSON_ARRAY_APPEND database query so concurrent background * processes never overwrite each other's entries. * * @param mixed $notification Notification data. * * @return bool True if the notification was successfully stored, false otherwise. */ public function add_notification( $notification ) { $sanitized_notification = $this->sanitize_notification( $notification ); if ( empty( $sanitized_notification ) ) { return false; } $result = $this->thread_safe_options->append_object_to_array( self::$notification_data_key, $sanitized_notification ); return $result !== false && $result > 0; } /** * Sanitize a notification for display in the UI. * Translates the notification content. * * @param array $notification Notification data. * * @return array Sanitized notification data. */ private function sanitize_notification_for_ui( $notification ) { $sanitized = $this->sanitize_notification( $notification ); if ( empty( $sanitized['content'] ) ) { return array(); } $sanitized['content'] = $this->string_utils->get_translated_string( $sanitized['content'] ); return $sanitized; } /** * Sanitize a notification. * * @param mixed $notification Notification data. * * @return array{id: string, timestamp: string|int, type: string, content: string, url: string} */ private function sanitize_notification( $notification ) { if ( empty( $notification['content'] ) ) { return array(); } // Sanitize and ensure all expected fields exist. $sanitized = array(); $sanitized['id'] = isset( $notification['id'] ) ? sanitize_text_field( $notification['id'] ) : uniqid( 'smush_notification_' ); $sanitized['timestamp'] = isset( $notification['timestamp'] ) ? sanitize_text_field( $notification['timestamp'] ) : microtime( true ); $sanitized['type'] = isset( $notification['type'] ) ? sanitize_text_field( $notification['type'] ) : 'info'; $sanitized['module'] = isset( $notification['module'] ) ? sanitize_text_field( $notification['module'] ) : ''; $sanitized['content'] = isset( $notification['content'] ) ? sanitize_text_field( $notification['content'] ) : ''; $sanitized['url'] = isset( $notification['url'] ) ? sanitize_text_field( $notification['url'] ) : ''; return $sanitized; } /** * Trim the stored list to the maximum allowed size, keeping the most recent entries. * * Called once per UI read (localize / poll), so background processes can append * atomically without any per-write overhead. The worst-case outcome of deferring * this to read-time is storing a few extra entries between writes. * * @return void */ private function enforce_notification_limit() { $notifications = $this->get_notifications(); if ( count( $notifications ) <= self::$max_notification ) { return; } // Sort newest-first, then keep only the allowed maximum. usort( $notifications, function ( $a, $b ) { return $b['timestamp'] <=> $a['timestamp']; } ); $notifications = array_slice( $notifications, 0, self::$max_notification ); // Overwrite the option with the trimmed, sorted list. // Uses replace_array() so the value is stored as JSON, keeping it // consistent with the JSON-based read in get_notifications(). $this->thread_safe_options->replace_object_array( self::$notification_data_key, $notifications ); } }