feat: WC-native logging + Add to phpList button on order page
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 <noreply@anthropic.com>
This commit is contained in:
40
woolist-phplist/assets/js/woolist-admin.js
Normal file
40
woolist-phplist/assets/js/woolist-admin.js
Normal file
@@ -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 );
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* WooCommerce Settings tab for WooList — phpList Integration.
|
* WooCommerce Settings tab and order-page meta box for WooList.
|
||||||
*
|
*
|
||||||
* @package WooList
|
* @package WooList
|
||||||
*/
|
*/
|
||||||
@@ -15,7 +15,7 @@ class WooList_Admin {
|
|||||||
public function __construct( WooList_API $api ) {
|
public function __construct( WooList_API $api ) {
|
||||||
$this->api = $api;
|
$this->api = $api;
|
||||||
|
|
||||||
// Register the custom WooCommerce settings tab.
|
// WooCommerce settings tab.
|
||||||
add_filter( 'woocommerce_settings_tabs_array', [ $this, 'add_settings_tab' ], 50 );
|
add_filter( 'woocommerce_settings_tabs_array', [ $this, 'add_settings_tab' ], 50 );
|
||||||
add_action( 'woocommerce_settings_tabs_woolist', [ $this, 'render_settings' ] );
|
add_action( 'woocommerce_settings_tabs_woolist', [ $this, 'render_settings' ] );
|
||||||
add_action( 'woocommerce_update_options_woolist', [ $this, 'save_settings' ] );
|
add_action( 'woocommerce_update_options_woolist', [ $this, 'save_settings' ] );
|
||||||
@@ -23,50 +23,38 @@ class WooList_Admin {
|
|||||||
// Test connection admin-post handler.
|
// Test connection admin-post handler.
|
||||||
add_action( 'admin_post_woolist_test_connection', [ $this, 'handle_test_connection' ] );
|
add_action( 'admin_post_woolist_test_connection', [ $this, 'handle_test_connection' ] );
|
||||||
|
|
||||||
// Clear log admin-post handler.
|
// Order page meta box (classic + HPOS).
|
||||||
add_action( 'admin_post_woolist_clear_log', [ $this, 'handle_clear_log' ] );
|
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_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ── Settings tab ─────────────────────────────────────────────────────────
|
||||||
* Add "phpList" tab to WooCommerce → Settings.
|
|
||||||
*
|
|
||||||
* @param array $tabs Existing tabs.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function add_settings_tab( array $tabs ): array {
|
public function add_settings_tab( array $tabs ): array {
|
||||||
$tabs['woolist'] = __( 'phpList', 'woolist-phplist' );
|
$tabs['woolist'] = __( 'phpList', 'woolist-phplist' );
|
||||||
return $tabs;
|
return $tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the settings tab content.
|
|
||||||
*/
|
|
||||||
public function render_settings(): void {
|
public function render_settings(): void {
|
||||||
woocommerce_admin_fields( $this->get_settings() );
|
woocommerce_admin_fields( $this->get_settings() );
|
||||||
$this->render_test_connection_button();
|
$this->render_test_connection_button();
|
||||||
$this->render_log_viewer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Save settings when the form is submitted.
|
|
||||||
*/
|
|
||||||
public function save_settings(): void {
|
public function save_settings(): void {
|
||||||
woocommerce_update_options( $this->get_settings() );
|
woocommerce_update_options( $this->get_settings() );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Output the "Test Connection" button below the settings form.
|
|
||||||
*/
|
|
||||||
private function render_test_connection_button(): void {
|
private function render_test_connection_button(): void {
|
||||||
$action_url = wp_nonce_url(
|
$action_url = wp_nonce_url(
|
||||||
admin_url( 'admin-post.php?action=woolist_test_connection' ),
|
admin_url( 'admin-post.php?action=woolist_test_connection' ),
|
||||||
'woolist_test_connection'
|
'woolist_test_connection'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Display any stored transient notice.
|
|
||||||
$notice = get_transient( 'woolist_test_connection_notice_' . get_current_user_id() );
|
$notice = get_transient( 'woolist_test_connection_notice_' . get_current_user_id() );
|
||||||
if ( $notice ) {
|
if ( $notice ) {
|
||||||
delete_transient( 'woolist_test_connection_notice_' . get_current_user_id() );
|
delete_transient( 'woolist_test_connection_notice_' . get_current_user_id() );
|
||||||
@@ -80,54 +68,12 @@ class WooList_Admin {
|
|||||||
|
|
||||||
echo '<p><a href="' . esc_url( $action_url ) . '" class="button button-secondary">'
|
echo '<p><a href="' . esc_url( $action_url ) . '" class="button button-secondary">'
|
||||||
. esc_html__( 'Test Connection', 'woolist-phplist' )
|
. esc_html__( 'Test Connection', 'woolist-phplist' )
|
||||||
. '</a></p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 '<div class="notice notice-success inline"><p>' . esc_html( $notice ) . '</p></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '<hr style="margin:2em 0;">';
|
|
||||||
echo '<h2>' . esc_html__( 'Activity Log', 'woolist-phplist' ) . '</h2>';
|
|
||||||
echo '<p style="color:#646970;">';
|
|
||||||
echo esc_html__( 'Log file: ', 'woolist-phplist' );
|
|
||||||
echo '<code>' . esc_html( $log_path ) . '</code>';
|
|
||||||
echo '</p>';
|
|
||||||
|
|
||||||
if ( $log_content === '' ) {
|
|
||||||
echo '<p><em>' . esc_html__( 'Log is empty.', 'woolist-phplist' ) . '</em></p>';
|
|
||||||
} else {
|
|
||||||
echo '<div style="position:relative;">';
|
|
||||||
echo '<textarea readonly rows="20" style="width:100%;font-family:monospace;font-size:12px;background:#1e1e1e;color:#d4d4d4;padding:12px;border:1px solid #ccc;border-radius:4px;resize:vertical;white-space:pre;">'
|
|
||||||
. esc_textarea( $log_content )
|
|
||||||
. '</textarea>';
|
|
||||||
echo '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '<p>';
|
|
||||||
echo '<a href="' . esc_url( $clear_url ) . '" class="button button-secondary" '
|
|
||||||
. 'onclick="return confirm(\'' . esc_js( __( 'Clear the entire log file?', 'woolist-phplist' ) ) . '\')">'
|
|
||||||
. esc_html__( 'Clear Log', 'woolist-phplist' )
|
|
||||||
. '</a>';
|
. '</a>';
|
||||||
echo '</p>';
|
echo ' <span style="color:#646970;font-size:13px;margin-left:8px;">'
|
||||||
|
. esc_html__( 'Check WooCommerce → Status → Logs (source: woolist-phplist) to see results.', 'woolist-phplist' )
|
||||||
|
. '</span></p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the "Test Connection" form action.
|
|
||||||
*/
|
|
||||||
public function handle_test_connection(): void {
|
public function handle_test_connection(): void {
|
||||||
check_admin_referer( 'woolist_test_connection' );
|
check_admin_referer( 'woolist_test_connection' );
|
||||||
|
|
||||||
@@ -152,7 +98,7 @@ class WooList_Admin {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$count = is_array( $result ) ? count( $result ) : '?';
|
$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(
|
set_transient(
|
||||||
'woolist_test_connection_notice_' . $user_id,
|
'woolist_test_connection_notice_' . $user_id,
|
||||||
[
|
[
|
||||||
@@ -171,44 +117,193 @@ class WooList_Admin {
|
|||||||
exit;
|
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 {
|
public function add_order_meta_box(): void {
|
||||||
check_admin_referer( 'woolist_clear_log' );
|
foreach ( [ 'shop_order', 'woocommerce_page_wc-orders' ] as $screen ) {
|
||||||
|
add_meta_box(
|
||||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
'woolist_order_metabox',
|
||||||
wp_die( esc_html__( 'You do not have permission to perform this action.', 'woolist-phplist' ) );
|
__( '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 {
|
public function render_order_meta_box( $post_or_order ): void {
|
||||||
$screen = get_current_screen();
|
// Normalise to a WC order object regardless of storage mode.
|
||||||
if ( ! $screen || strpos( $screen->id, 'woocommerce' ) === false ) {
|
if ( $post_or_order instanceof WC_Abstract_Order ) {
|
||||||
|
$order = $post_or_order;
|
||||||
|
} else {
|
||||||
|
$order = wc_get_order( $post_or_order->ID );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $order ) {
|
||||||
|
echo '<p>' . esc_html__( 'Could not load order.', 'woolist-phplist' ) . '</p>';
|
||||||
return;
|
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 '<p style="color:#d63638;">'
|
||||||
|
. esc_html__( 'No phpList lists are configured yet. Add list IDs under WooCommerce → Settings → phpList.', 'woolist-phplist' )
|
||||||
|
. '</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nonce = wp_create_nonce( 'woolist_manual_subscribe_' . $order_id );
|
||||||
|
?>
|
||||||
|
<div class="woolist-metabox-wrap">
|
||||||
|
<p style="margin:0 0 8px;">
|
||||||
|
<strong><?php esc_html_e( 'Email:', 'woolist-phplist' ); ?></strong>
|
||||||
|
<?php echo esc_html( $email ?: __( '(none)', 'woolist-phplist' ) ); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin:0 0 6px;">
|
||||||
|
<label for="woolist-list-select-<?php echo esc_attr( $order_id ); ?>" style="display:block;margin-bottom:4px;font-weight:600;">
|
||||||
|
<?php esc_html_e( 'Target list:', 'woolist-phplist' ); ?>
|
||||||
|
</label>
|
||||||
|
<select id="woolist-list-select-<?php echo esc_attr( $order_id ); ?>"
|
||||||
|
class="woolist-list-select"
|
||||||
|
style="width:100%;">
|
||||||
|
<?php foreach ( $lists as $list_id => $label ) : ?>
|
||||||
|
<option value="<?php echo esc_attr( $list_id ); ?>">
|
||||||
|
<?php echo esc_html( $label . ' (ID: ' . $list_id . ')' ); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="button woolist-manual-subscribe-btn"
|
||||||
|
style="width:100%;margin-top:4px;"
|
||||||
|
data-order-id="<?php echo esc_attr( $order_id ); ?>"
|
||||||
|
data-nonce="<?php echo esc_attr( $nonce ); ?>">
|
||||||
|
<?php esc_html_e( 'Add to phpList', 'woolist-phplist' ); ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="woolist-metabox-response" style="display:none;margin-top:8px;padding:6px 8px;border-radius:3px;font-size:13px;"></div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build and return the WooCommerce settings field definitions.
|
* Build an associative array of [ list_id => human label ] for all lists
|
||||||
|
* that have an ID configured in settings, regardless of whether sync is enabled.
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array<int, string>
|
||||||
*/
|
*/
|
||||||
|
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 {
|
private function get_settings(): array {
|
||||||
return [
|
return [
|
||||||
// ── Section 1: Connection ────────────────────────────────────────
|
// ── Section 1: Connection ────────────────────────────────────────
|
||||||
@@ -413,12 +508,12 @@ class WooList_Admin {
|
|||||||
[
|
[
|
||||||
'title' => __( 'Logging', 'woolist-phplist' ),
|
'title' => __( 'Logging', 'woolist-phplist' ),
|
||||||
'type' => 'title',
|
'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',
|
'id' => 'woolist_section_logging',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'title' => __( 'Enable debug logging', 'woolist-phplist' ),
|
'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',
|
'id' => 'woolist_enable_debug_log',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'default' => 'no',
|
'default' => 'no',
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Simple file-based logger for WooList.
|
* Logger for WooList — delegates to WooCommerce's built-in logging system.
|
||||||
*
|
*
|
||||||
* Writes to wp-content/uploads/woolist-logs/woolist.log.
|
* Logs are visible at WooCommerce → Status → Logs, source: woolist-phplist.
|
||||||
* The directory is protected from direct HTTP access via .htaccess.
|
|
||||||
*
|
*
|
||||||
* Levels:
|
* Levels:
|
||||||
* INFO — always written (subscription events, connection test results)
|
* INFO — always written (subscription events, connection test results)
|
||||||
* ERROR — always written (API failures, config problems)
|
* ERROR — always written (API failures, config problems)
|
||||||
* DEBUG — written only when "Enable debug logging" is on (full request/response)
|
* DEBUG — written only when "Enable debug logging" is checked in settings
|
||||||
*
|
*
|
||||||
* @package WooList
|
* @package WooList
|
||||||
*/
|
*/
|
||||||
@@ -17,38 +16,19 @@ defined( 'ABSPATH' ) || exit;
|
|||||||
|
|
||||||
class WooList_Logger {
|
class WooList_Logger {
|
||||||
|
|
||||||
/** Absolute path to the log file. */
|
private const SOURCE = 'woolist-phplist';
|
||||||
private static string $log_file = '';
|
|
||||||
|
|
||||||
/** Whether verbose debug lines are recorded. */
|
/** Whether verbose debug lines are recorded. */
|
||||||
private static bool $debug_enabled = false;
|
private static bool $debug_enabled = false;
|
||||||
|
|
||||||
/** Maximum log file size in bytes before rotation (1 MB). */
|
/** Lazy-loaded WC_Logger instance. */
|
||||||
private const MAX_SIZE = 1048576;
|
private static ?WC_Logger_Interface $wc_logger = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise the logger: create the log directory and read the debug setting.
|
* Initialise: read the debug setting.
|
||||||
* Must be called after WordPress options are available.
|
* Called on plugins_loaded after options are available.
|
||||||
*/
|
*/
|
||||||
public static function init(): void {
|
public static function init(): void {
|
||||||
$upload = wp_upload_dir();
|
|
||||||
$log_dir = $upload['basedir'] . '/woolist-logs';
|
|
||||||
|
|
||||||
if ( ! is_dir( $log_dir ) ) {
|
|
||||||
wp_mkdir_p( $log_dir );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block direct HTTP access and directory listing.
|
|
||||||
$htaccess = $log_dir . '/.htaccess';
|
|
||||||
if ( ! file_exists( $htaccess ) ) {
|
|
||||||
file_put_contents( $htaccess, "Require all denied\ndeny from all\n" );
|
|
||||||
}
|
|
||||||
$index = $log_dir . '/index.php';
|
|
||||||
if ( ! file_exists( $index ) ) {
|
|
||||||
file_put_contents( $index, "<?php // Silence is golden.\n" );
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$log_file = $log_dir . '/woolist.log';
|
|
||||||
self::$debug_enabled = ( get_option( 'woolist_enable_debug_log' ) === 'yes' );
|
self::$debug_enabled = ( get_option( 'woolist_enable_debug_log' ) === 'yes' );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,22 +36,21 @@ class WooList_Logger {
|
|||||||
|
|
||||||
/** Always logged. Use for normal subscription / connection events. */
|
/** Always logged. Use for normal subscription / connection events. */
|
||||||
public static function info( string $message ): void {
|
public static function info( string $message ): void {
|
||||||
self::write( 'INFO', $message );
|
self::wc()->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 {
|
public static function error( string $message ): void {
|
||||||
self::write( 'ERROR', $message );
|
self::wc()->error( $message, [ 'source' => self::SOURCE ] );
|
||||||
error_log( '[WooList ERROR] ' . $message );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logged only when debug mode is enabled.
|
* 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 {
|
public static function debug( string $message ): void {
|
||||||
if ( self::$debug_enabled ) {
|
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 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 {
|
public static function is_debug_enabled(): bool {
|
||||||
return self::$debug_enabled;
|
return self::$debug_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Internal ─────────────────────────────────────────────────────────────
|
// ── Internal ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static function write( string $level, string $message ): void {
|
/** Return (and lazy-load) the WC_Logger instance. */
|
||||||
if ( empty( self::$log_file ) ) {
|
private static function wc(): WC_Logger_Interface {
|
||||||
return;
|
if ( self::$wc_logger === null ) {
|
||||||
|
self::$wc_logger = wc_get_logger();
|
||||||
}
|
}
|
||||||
|
return self::$wc_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 );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user