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;
}
}