From 9b1d653789fba893f04fa4bcf6f0b9d65383eb67 Mon Sep 17 00:00:00 2001 From: Malin Date: Fri, 20 Feb 2026 08:00:05 +0100 Subject: [PATCH] feat: WC-native logging + Add to phpList button on order page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logging: - Replace custom file logger with wc_get_logger() (source: woolist-phplist) - Logs now appear in WooCommerce → Status → Logs, no filesystem access needed - Remove log viewer / Clear Log from settings page (WC UI handles this) - Keep "Enable debug logging" checkbox to control DEBUG-level verbosity Order page meta box: - New "phpList Sync" side meta box on every order edit page - Works with both classic (shop_order) and HPOS (woocommerce_page_wc-orders) - Shows billing email + dropdown of all configured lists - "Add to phpList" button triggers AJAX subscribe, shows inline result - Result and full API trace logged to WC logs under woolist-phplist source - woolist-admin.js handles button state and response display Co-Authored-By: Claude Sonnet 4.6 --- woolist-phplist/assets/js/woolist-admin.js | 40 +++ .../includes/class-woolist-admin.php | 283 ++++++++++++------ .../includes/class-woolist-logger.php | 95 ++---- 3 files changed, 247 insertions(+), 171 deletions(-) create mode 100644 woolist-phplist/assets/js/woolist-admin.js diff --git a/woolist-phplist/assets/js/woolist-admin.js b/woolist-phplist/assets/js/woolist-admin.js new file mode 100644 index 0000000..975d003 --- /dev/null +++ b/woolist-phplist/assets/js/woolist-admin.js @@ -0,0 +1,40 @@ +/* global ajaxurl, jQuery */ +( function ( $ ) { + 'use strict'; + + $( document ).on( 'click', '.woolist-manual-subscribe-btn', function () { + var $btn = $( this ); + var $box = $btn.closest( '.woolist-metabox-wrap' ); + var $resp = $box.find( '.woolist-metabox-response' ); + var listId = $box.find( '.woolist-list-select' ).val(); + + $resp.hide().removeClass( 'woolist-mb-success woolist-mb-error' ).html( '' ); + $btn.prop( 'disabled', true ).text( 'Subscribing\u2026' ); + + $.ajax( { + url: ajaxurl, + method: 'POST', + data: { + action: 'woolist_manual_subscribe', + nonce: $btn.data( 'nonce' ), + order_id: $btn.data( 'order-id' ), + list_id: listId, + }, + } ) + .done( function ( response ) { + if ( response.success ) { + $resp.addClass( 'woolist-mb-success' ).html( '✓ ' + response.data.message ); + } else { + $resp.addClass( 'woolist-mb-error' ).html( '✗ ' + response.data.message ); + } + $resp.show(); + } ) + .fail( function () { + $resp.addClass( 'woolist-mb-error' ).html( '✗ Request failed.' ).show(); + } ) + .always( function () { + $btn.prop( 'disabled', false ).text( 'Add to phpList' ); + } ); + } ); + +} )( jQuery ); diff --git a/woolist-phplist/includes/class-woolist-admin.php b/woolist-phplist/includes/class-woolist-admin.php index 7cdaedd..19a5446 100644 --- a/woolist-phplist/includes/class-woolist-admin.php +++ b/woolist-phplist/includes/class-woolist-admin.php @@ -1,6 +1,6 @@ api = $api; - // Register the custom WooCommerce settings tab. + // WooCommerce settings tab. add_filter( 'woocommerce_settings_tabs_array', [ $this, 'add_settings_tab' ], 50 ); add_action( 'woocommerce_settings_tabs_woolist', [ $this, 'render_settings' ] ); add_action( 'woocommerce_update_options_woolist', [ $this, 'save_settings' ] ); @@ -23,50 +23,38 @@ class WooList_Admin { // Test connection admin-post handler. add_action( 'admin_post_woolist_test_connection', [ $this, 'handle_test_connection' ] ); - // Clear log admin-post handler. - add_action( 'admin_post_woolist_clear_log', [ $this, 'handle_clear_log' ] ); + // Order page meta box (classic + HPOS). + add_action( 'add_meta_boxes', [ $this, 'add_order_meta_box' ] ); - // Enqueue admin JS/CSS only on the WC settings page. + // AJAX handler for the manual-subscribe button. + add_action( 'wp_ajax_woolist_manual_subscribe', [ $this, 'handle_manual_subscribe' ] ); + + // Admin assets. add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); } - /** - * Add "phpList" tab to WooCommerce → Settings. - * - * @param array $tabs Existing tabs. - * @return array - */ + // ── Settings tab ───────────────────────────────────────────────────────── + public function add_settings_tab( array $tabs ): array { $tabs['woolist'] = __( 'phpList', 'woolist-phplist' ); return $tabs; } - /** - * Render the settings tab content. - */ public function render_settings(): void { woocommerce_admin_fields( $this->get_settings() ); $this->render_test_connection_button(); - $this->render_log_viewer(); } - /** - * Save settings when the form is submitted. - */ public function save_settings(): void { woocommerce_update_options( $this->get_settings() ); } - /** - * Output the "Test Connection" button below the settings form. - */ private function render_test_connection_button(): void { $action_url = wp_nonce_url( admin_url( 'admin-post.php?action=woolist_test_connection' ), 'woolist_test_connection' ); - // Display any stored transient notice. $notice = get_transient( 'woolist_test_connection_notice_' . get_current_user_id() ); if ( $notice ) { delete_transient( 'woolist_test_connection_notice_' . get_current_user_id() ); @@ -80,54 +68,12 @@ class WooList_Admin { echo '

' . esc_html__( 'Test Connection', 'woolist-phplist' ) - . '

'; - } - - /** - * Render the log viewer panel below all settings. - */ - private function render_log_viewer(): void { - $log_content = WooList_Logger::read_recent( 300 ); - $log_path = WooList_Logger::get_log_path(); - $clear_url = wp_nonce_url( - admin_url( 'admin-post.php?action=woolist_clear_log' ), - 'woolist_clear_log' - ); - - $notice = get_transient( 'woolist_clear_log_notice_' . get_current_user_id() ); - if ( $notice ) { - delete_transient( 'woolist_clear_log_notice_' . get_current_user_id() ); - echo '

' . esc_html( $notice ) . '

'; - } - - echo '
'; - echo '

' . esc_html__( 'Activity Log', 'woolist-phplist' ) . '

'; - echo '

'; - echo esc_html__( 'Log file: ', 'woolist-phplist' ); - echo '' . esc_html( $log_path ) . ''; - echo '

'; - - if ( $log_content === '' ) { - echo '

' . esc_html__( 'Log is empty.', 'woolist-phplist' ) . '

'; - } else { - echo '
'; - echo ''; - echo '
'; - } - - echo '

'; - echo '' - . esc_html__( 'Clear Log', 'woolist-phplist' ) . ''; - echo '

'; + echo ' ' + . esc_html__( 'Check WooCommerce → Status → Logs (source: woolist-phplist) to see results.', 'woolist-phplist' ) + . '

'; } - /** - * Handle the "Test Connection" form action. - */ public function handle_test_connection(): void { check_admin_referer( 'woolist_test_connection' ); @@ -152,7 +98,7 @@ class WooList_Admin { ); } else { $count = is_array( $result ) ? count( $result ) : '?'; - WooList_Logger::info( 'Test Connection succeeded, found ' . $count . ' list(s).' ); + WooList_Logger::info( 'Test Connection succeeded. Found ' . $count . ' list(s).' ); set_transient( 'woolist_test_connection_notice_' . $user_id, [ @@ -171,44 +117,193 @@ class WooList_Admin { exit; } + // ── Order page meta box ─────────────────────────────────────────────────── + /** - * Handle the "Clear Log" admin-post action. + * Register the meta box on both classic (shop_order) and + * HPOS (woocommerce_page_wc-orders) order screens. */ - public function handle_clear_log(): void { - check_admin_referer( 'woolist_clear_log' ); - - if ( ! current_user_can( 'manage_woocommerce' ) ) { - wp_die( esc_html__( 'You do not have permission to perform this action.', 'woolist-phplist' ) ); + public function add_order_meta_box(): void { + foreach ( [ 'shop_order', 'woocommerce_page_wc-orders' ] as $screen ) { + add_meta_box( + 'woolist_order_metabox', + __( 'phpList Sync', 'woolist-phplist' ), + [ $this, 'render_order_meta_box' ], + $screen, + 'side', + 'default' + ); } - - WooList_Logger::clear(); - WooList_Logger::info( 'Log cleared by user ID ' . get_current_user_id() ); - - set_transient( - 'woolist_clear_log_notice_' . get_current_user_id(), - __( 'Log file cleared.', 'woolist-phplist' ), - 60 - ); - - wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=woolist' ) ); - exit; } /** - * Enqueue admin-side assets on WC Settings → phpList tab. + * Render the "Add to phpList" meta box on the order edit page. + * + * @param WP_Post|WC_Abstract_Order $post_or_order Classic WP_Post or HPOS order object. */ - public function enqueue_admin_assets(): void { - $screen = get_current_screen(); - if ( ! $screen || strpos( $screen->id, 'woocommerce' ) === false ) { + public function render_order_meta_box( $post_or_order ): void { + // Normalise to a WC order object regardless of storage mode. + if ( $post_or_order instanceof WC_Abstract_Order ) { + $order = $post_or_order; + } else { + $order = wc_get_order( $post_or_order->ID ); + } + + if ( ! $order ) { + echo '

' . esc_html__( 'Could not load order.', 'woolist-phplist' ) . '

'; return; } + + $order_id = $order->get_id(); + $email = $order->get_billing_email(); + + // Build list of configured lists so the admin can pick one. + $lists = $this->get_configured_lists(); + + if ( empty( $lists ) ) { + echo '

' + . esc_html__( 'No phpList lists are configured yet. Add list IDs under WooCommerce → Settings → phpList.', 'woolist-phplist' ) + . '

'; + return; + } + + $nonce = wp_create_nonce( 'woolist_manual_subscribe_' . $order_id ); + ?> +
+

+ + +

+ +

+ + +

+ + + + +
+ human label ] for all lists + * that have an ID configured in settings, regardless of whether sync is enabled. * - * @return array + * @return array */ + private function get_configured_lists(): array { + $map = [ + (int) get_option( 'woolist_completed_list_id', 0 ) => __( 'Completed Orders', 'woolist-phplist' ), + (int) get_option( 'woolist_cancelled_list_id', 0 ) => __( 'Cancelled Orders', 'woolist-phplist' ), + (int) get_option( 'woolist_signup_list_id', 0 ) => __( 'Account Signup', 'woolist-phplist' ), + (int) get_option( 'woolist_newsletter_list_id', 0 ) => __( 'Newsletter', 'woolist-phplist' ), + ]; + + // Remove entries where no list ID has been set. + unset( $map[0] ); + + return $map; + } + + /** + * AJAX handler for the "Add to phpList" order meta box button. + */ + public function handle_manual_subscribe(): void { + $order_id = (int) ( $_POST['order_id'] ?? 0 ); + + if ( ! check_ajax_referer( 'woolist_manual_subscribe_' . $order_id, 'nonce', false ) ) { + wp_send_json_error( [ 'message' => __( 'Security check failed.', 'woolist-phplist' ) ], 403 ); + } + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( [ 'message' => __( 'Permission denied.', 'woolist-phplist' ) ], 403 ); + } + + $list_id = (int) ( $_POST['list_id'] ?? 0 ); + if ( $list_id < 1 ) { + wp_send_json_error( [ 'message' => __( 'No list selected.', 'woolist-phplist' ) ] ); + } + + $order = wc_get_order( $order_id ); + if ( ! $order ) { + wp_send_json_error( [ 'message' => __( 'Order not found.', 'woolist-phplist' ) ] ); + } + + $email = $order->get_billing_email(); + if ( ! is_email( $email ) ) { + wp_send_json_error( [ 'message' => __( 'Order has no valid billing email.', 'woolist-phplist' ) ] ); + } + + WooList_Logger::info( + 'Manual subscribe: order #' . $order_id . ' email=' . $email . ' list_id=' . $list_id + . ' triggered_by=user#' . get_current_user_id() + ); + + $result = $this->api->subscribe_email_to_list( $email, $list_id ); + + if ( ! $result['success'] ) { + wp_send_json_error( [ + 'message' => __( 'Subscription failed. Check WooCommerce → Status → Logs for details.', 'woolist-phplist' ), + ] ); + } + + wp_send_json_success( [ + 'message' => sprintf( + /* translators: 1: email address, 2: list ID */ + __( '%1$s added to list %2$d.', 'woolist-phplist' ), + esc_html( $email ), + $list_id + ), + ] ); + } + + // ── Admin assets ───────────────────────────────────────────────────────── + + public function enqueue_admin_assets(): void { + $screen = get_current_screen(); + if ( ! $screen ) { + return; + } + + // Enqueue button JS on both classic and HPOS order edit screens. + if ( in_array( $screen->id, [ 'shop_order', 'woocommerce_page_wc-orders' ], true ) ) { + wp_enqueue_script( + 'woolist-admin', + WOOLIST_URL . 'assets/js/woolist-admin.js', + [ 'jquery' ], + WOOLIST_VERSION, + true + ); + + // Inline styles for the meta box response area. + wp_add_inline_style( + 'woocommerce_admin_styles', + '.woolist-mb-success{background:#edfaf1;color:#2d6a0f;border:1px solid #52c41a;}' + . '.woolist-mb-error{background:#fff1f0;color:#a8071a;border:1px solid #ff4d4f;}' + ); + } + } + + // ── Settings definitions ───────────────────────────────────────────────── + private function get_settings(): array { return [ // ── Section 1: Connection ──────────────────────────────────────── @@ -413,12 +508,12 @@ class WooList_Admin { [ 'title' => __( 'Logging', 'woolist-phplist' ), 'type' => 'title', - 'desc' => __( 'Control what gets recorded in the activity log shown below.', 'woolist-phplist' ), + 'desc' => __( 'Logs are written to WooCommerce → Status → Logs (source: woolist-phplist).', 'woolist-phplist' ), 'id' => 'woolist_section_logging', ], [ 'title' => __( 'Enable debug logging', 'woolist-phplist' ), - 'desc' => __( 'Log full API request URLs (password redacted) and raw responses. Disable on production once everything works.', 'woolist-phplist' ), + 'desc' => __( 'Log full API request URLs (password redacted) and raw responses. Useful for diagnosing API issues; disable on production once confirmed working.', 'woolist-phplist' ), 'id' => 'woolist_enable_debug_log', 'type' => 'checkbox', 'default' => 'no', diff --git a/woolist-phplist/includes/class-woolist-logger.php b/woolist-phplist/includes/class-woolist-logger.php index 23e3480..e6752dd 100644 --- a/woolist-phplist/includes/class-woolist-logger.php +++ b/woolist-phplist/includes/class-woolist-logger.php @@ -1,14 +1,13 @@ info( $message, [ 'source' => self::SOURCE ] ); } - /** Always logged. Also echoes to php error_log as a fallback. */ + /** Always logged. */ public static function error( string $message ): void { - self::write( 'ERROR', $message ); - error_log( '[WooList ERROR] ' . $message ); + self::wc()->error( $message, [ 'source' => self::SOURCE ] ); } /** * Logged only when debug mode is enabled. - * Use for full request URLs, raw response bodies, step-by-step flow. + * Use for full request URLs (password redacted), raw responses, flow steps. */ public static function debug( string $message ): void { if ( self::$debug_enabled ) { - self::write( 'DEBUG', $message ); + self::wc()->debug( $message, [ 'source' => self::SOURCE ] ); } } @@ -87,55 +66,17 @@ class WooList_Logger { return preg_replace( '/(\bpassword=)[^&]+/', '$1***', $url ); } - /** - * Return the last $lines lines of the log as a string. - * - * @param int $lines Number of lines to return. - * @return string Log tail, or an empty string when the log doesn't exist yet. - */ - public static function read_recent( int $lines = 300 ): string { - if ( empty( self::$log_file ) || ! file_exists( self::$log_file ) ) { - return ''; - } - - $content = file_get_contents( self::$log_file ); - if ( $content === false || $content === '' ) { - return ''; - } - - $all = explode( "\n", rtrim( $content ) ); - $recent = array_slice( $all, -$lines ); - return implode( "\n", $recent ); - } - - /** Truncate the log file without deleting it. */ - public static function clear(): void { - if ( ! empty( self::$log_file ) ) { - file_put_contents( self::$log_file, '' ); - } - } - - public static function get_log_path(): string { - return self::$log_file; - } - public static function is_debug_enabled(): bool { return self::$debug_enabled; } // ── Internal ───────────────────────────────────────────────────────────── - private static function write( string $level, string $message ): void { - if ( empty( self::$log_file ) ) { - return; + /** Return (and lazy-load) the WC_Logger instance. */ + private static function wc(): WC_Logger_Interface { + if ( self::$wc_logger === null ) { + self::$wc_logger = wc_get_logger(); } - - // Rotate if the file exceeds MAX_SIZE. - if ( file_exists( self::$log_file ) && filesize( self::$log_file ) >= self::MAX_SIZE ) { - rename( self::$log_file, self::$log_file . '.old' ); - } - - $line = '[' . gmdate( 'Y-m-d H:i:s' ) . ' UTC] [' . $level . '] ' . $message . "\n"; - file_put_contents( self::$log_file, $line, FILE_APPEND | LOCK_EX ); + return self::$wc_logger; } }