feat: add structured file-based logging with admin log viewer

- New WooList_Logger class writes to wp-content/uploads/woolist-logs/woolist.log
  - INFO level: subscription events, test connection results (always recorded)
  - ERROR level: API failures, config problems (always recorded + php error_log fallback)
  - DEBUG level: full request URLs (password redacted), raw responses, step-by-step
    flow (only when "Enable debug logging" is checked in settings)
  - Auto-rotates at 1 MB; log directory protected by .htaccess
- API class: logs every request URL (redacted) and raw response body at DEBUG,
  errors at ERROR; subscribe_email_to_list logs each step (lookup/create/add)
- Hooks class: logs hook fire, skip reasons, and sync intent at DEBUG/INFO/ERROR
- Shortcode class: logs AJAX submissions, coupon generation, and failures
- Admin: new Logging section with "Enable debug logging" checkbox;
  log viewer textarea (last 300 lines, dark theme) + Clear Log button
  both visible at bottom of WooCommerce → Settings → phpList tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 16:25:33 +01:00
parent 0429a282bc
commit f4c9e39493
6 changed files with 309 additions and 28 deletions

View File

@@ -23,6 +23,9 @@ 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.
add_action( 'admin_post_woolist_clear_log', [ $this, 'handle_clear_log' ] );
// Enqueue admin JS/CSS only on the WC settings page. // Enqueue admin JS/CSS only on the WC settings page.
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
} }
@@ -44,6 +47,7 @@ class WooList_Admin {
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();
} }
/** /**
@@ -79,6 +83,48 @@ class WooList_Admin {
. '</a></p>'; . '</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>';
echo '</p>';
}
/** /**
* Handle the "Test Connection" form action. * Handle the "Test Connection" form action.
*/ */
@@ -89,10 +135,13 @@ class WooList_Admin {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'woolist-phplist' ) ); wp_die( esc_html__( 'You do not have permission to perform this action.', 'woolist-phplist' ) );
} }
WooList_Logger::info( 'Test Connection triggered by user ID ' . get_current_user_id() );
$result = $this->api->lists_get(); $result = $this->api->lists_get();
$user_id = get_current_user_id(); $user_id = get_current_user_id();
if ( is_wp_error( $result ) ) { if ( is_wp_error( $result ) ) {
WooList_Logger::error( 'Test Connection failed: ' . $result->get_error_message() );
set_transient( set_transient(
'woolist_test_connection_notice_' . $user_id, 'woolist_test_connection_notice_' . $user_id,
[ [
@@ -103,6 +152,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).' );
set_transient( set_transient(
'woolist_test_connection_notice_' . $user_id, 'woolist_test_connection_notice_' . $user_id,
[ [
@@ -121,6 +171,29 @@ class WooList_Admin {
exit; exit;
} }
/**
* Handle the "Clear Log" admin-post action.
*/
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' ) );
}
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. * Enqueue admin-side assets on WC Settings → phpList tab.
*/ */
@@ -129,7 +202,6 @@ class WooList_Admin {
if ( ! $screen || strpos( $screen->id, 'woocommerce' ) === false ) { if ( ! $screen || strpos( $screen->id, 'woocommerce' ) === false ) {
return; return;
} }
// No custom admin JS/CSS needed yet; WC handles the settings form.
} }
/** /**
@@ -336,6 +408,25 @@ class WooList_Admin {
'type' => 'sectionend', 'type' => 'sectionend',
'id' => 'woolist_section_newsletter', 'id' => 'woolist_section_newsletter',
], ],
// ── Section 6: Logging ───────────────────────────────────────────
[
'title' => __( 'Logging', 'woolist-phplist' ),
'type' => 'title',
'desc' => __( 'Control what gets recorded in the activity log shown below.', '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' ),
'id' => 'woolist_enable_debug_log',
'type' => 'checkbox',
'default' => 'no',
],
[
'type' => 'sectionend',
'id' => 'woolist_section_logging',
],
]; ];
} }
} }

View File

@@ -12,7 +12,7 @@ class WooList_API {
/** /**
* Retrieve a saved plugin option. * Retrieve a saved plugin option.
* *
* @param string $key Option key (without prefix). * @param string $key Option key (without woolist_ prefix).
* @param mixed $default Default value. * @param mixed $default Default value.
* @return mixed * @return mixed
*/ */
@@ -63,38 +63,45 @@ class WooList_API {
$url = $this->build_url( $cmd, $extra ); $url = $this->build_url( $cmd, $extra );
if ( is_wp_error( $url ) ) { if ( is_wp_error( $url ) ) {
WooList_Logger::error( 'Cannot build URL for cmd=' . $cmd . ': ' . $url->get_error_message() );
return $url; return $url;
} }
WooList_Logger::debug( '→ API request cmd=' . $cmd . ' url=' . WooList_Logger::redact_url( $url ) );
$response = wp_remote_post( $url, [ 'timeout' => 15 ] ); $response = wp_remote_post( $url, [ 'timeout' => 15 ] );
if ( is_wp_error( $response ) ) { if ( is_wp_error( $response ) ) {
error_log( '[WooList] API request failed for cmd=' . $cmd . ': ' . $response->get_error_message() ); WooList_Logger::error( 'HTTP request failed cmd=' . $cmd . ' error=' . $response->get_error_message() );
return $response; return $response;
} }
$code = wp_remote_retrieve_response_code( $response ); $code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response ); $body = wp_remote_retrieve_body( $response );
// Log the raw response at debug level (truncated to 2 KB to avoid huge entries).
WooList_Logger::debug( '← API response cmd=' . $cmd . ' http=' . $code . ' body=' . substr( $body, 0, 2048 ) );
if ( $code < 200 || $code >= 300 ) { if ( $code < 200 || $code >= 300 ) {
error_log( '[WooList] API returned HTTP ' . $code . ' for cmd=' . $cmd ); WooList_Logger::error( 'API returned HTTP ' . $code . ' cmd=' . $cmd );
return new WP_Error( 'woolist_http_error', 'HTTP error ' . $code ); return new WP_Error( 'woolist_http_error', 'HTTP error ' . $code );
} }
$data = json_decode( $body, true ); $data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) { if ( json_last_error() !== JSON_ERROR_NONE ) {
error_log( '[WooList] API returned invalid JSON for cmd=' . $cmd . ': ' . $body ); WooList_Logger::error( 'Invalid JSON response cmd=' . $cmd . ' body=' . substr( $body, 0, 500 ) );
return new WP_Error( 'woolist_json_error', 'Invalid JSON response from phpList.' ); return new WP_Error( 'woolist_json_error', 'Invalid JSON response from phpList.' );
} }
// phpList REST API signals errors via a "status" field. // phpList REST API signals errors via a "status" field.
if ( isset( $data['status'] ) && strtolower( $data['status'] ) === 'error' ) { if ( isset( $data['status'] ) && strtolower( $data['status'] ) === 'error' ) {
$message = $data['errormessage'] ?? $data['message'] ?? 'Unknown API error'; $message = $data['errormessage'] ?? $data['message'] ?? 'Unknown API error';
error_log( '[WooList] API error for cmd=' . $cmd . ': ' . $message ); WooList_Logger::error( 'API error cmd=' . $cmd . ' message=' . $message );
return new WP_Error( 'woolist_api_error', $message ); return new WP_Error( 'woolist_api_error', $message );
} }
WooList_Logger::debug( 'API call succeeded cmd=' . $cmd );
return $data; return $data;
} }
@@ -118,9 +125,9 @@ class WooList_API {
return $this->call( return $this->call(
'subscriberAdd', 'subscriberAdd',
[ [
'email' => rawurlencode( $email ), 'email' => rawurlencode( $email ),
'confirmed' => 1, 'confirmed' => 1,
'htmlemail' => 1, 'htmlemail' => 1,
] ]
); );
} }
@@ -152,35 +159,45 @@ class WooList_API {
* @return array{success: bool, subscriber_id: int|null} * @return array{success: bool, subscriber_id: int|null}
*/ */
public function subscribe_email_to_list( string $email, int $list_id ): array { public function subscribe_email_to_list( string $email, int $list_id ): array {
// Try to find existing subscriber. WooList_Logger::debug( 'subscribe_email_to_list email=' . $email . ' list_id=' . $list_id );
// Step 1: look up existing subscriber.
$subscriber_id = null; $subscriber_id = null;
$existing = $this->subscriber_get_by_email( $email ); $existing = $this->subscriber_get_by_email( $email );
if ( ! is_wp_error( $existing ) && ! empty( $existing['id'] ) ) { if ( ! is_wp_error( $existing ) && ! empty( $existing['id'] ) ) {
$subscriber_id = (int) $existing['id']; $subscriber_id = (int) $existing['id'];
WooList_Logger::debug( 'Found existing subscriber id=' . $subscriber_id . ' email=' . $email );
} else { } else {
// Create new subscriber. // Step 2: create a new subscriber.
WooList_Logger::debug( 'Subscriber not found, creating new email=' . $email );
$added = $this->subscriber_add( $email ); $added = $this->subscriber_add( $email );
if ( is_wp_error( $added ) ) { if ( is_wp_error( $added ) ) {
error_log( '[WooList] Could not add subscriber ' . $email . ': ' . $added->get_error_message() ); WooList_Logger::error( 'Could not create subscriber email=' . $email . ' error=' . $added->get_error_message() );
return [ 'success' => false, 'subscriber_id' => null ]; return [ 'success' => false, 'subscriber_id' => null ];
} }
$subscriber_id = isset( $added['id'] ) ? (int) $added['id'] : null; $subscriber_id = isset( $added['id'] ) ? (int) $added['id'] : null;
if ( $subscriber_id ) {
WooList_Logger::info( 'Created new subscriber id=' . $subscriber_id . ' email=' . $email );
} else {
WooList_Logger::error( 'API returned no subscriber ID after add email=' . $email . ' response=' . wp_json_encode( $added ) );
return [ 'success' => false, 'subscriber_id' => null ];
}
} }
if ( ! $subscriber_id ) { // Step 3: add subscriber to the list.
error_log( '[WooList] Could not determine subscriber ID for ' . $email ); WooList_Logger::debug( 'Adding subscriber ' . $subscriber_id . ' to list ' . $list_id );
return [ 'success' => false, 'subscriber_id' => null ];
}
// Add subscriber to the list.
$result = $this->list_subscriber_add( $list_id, $subscriber_id ); $result = $this->list_subscriber_add( $list_id, $subscriber_id );
if ( is_wp_error( $result ) ) { if ( is_wp_error( $result ) ) {
error_log( '[WooList] Could not add subscriber ' . $subscriber_id . ' to list ' . $list_id . ': ' . $result->get_error_message() ); WooList_Logger::error( 'Could not add subscriber to list subscriber_id=' . $subscriber_id . ' list_id=' . $list_id . ' error=' . $result->get_error_message() );
return [ 'success' => false, 'subscriber_id' => $subscriber_id ]; return [ 'success' => false, 'subscriber_id' => $subscriber_id ];
} }
WooList_Logger::info( 'Subscribed email=' . $email . ' subscriber_id=' . $subscriber_id . ' list_id=' . $list_id );
return [ 'success' => true, 'subscriber_id' => $subscriber_id ]; return [ 'success' => true, 'subscriber_id' => $subscriber_id ];
} }

View File

@@ -26,26 +26,32 @@ class WooList_Hooks {
* @param int $order_id WooCommerce order ID. * @param int $order_id WooCommerce order ID.
*/ */
public function on_order_completed( int $order_id ): void { public function on_order_completed( int $order_id ): void {
WooList_Logger::debug( 'Hook fired: order_completed order_id=' . $order_id );
if ( get_option( 'woolist_sync_completed' ) !== 'yes' ) { if ( get_option( 'woolist_sync_completed' ) !== 'yes' ) {
WooList_Logger::debug( 'Completed order sync disabled, skipping.' );
return; return;
} }
$list_id = (int) get_option( 'woolist_completed_list_id', 0 ); $list_id = (int) get_option( 'woolist_completed_list_id', 0 );
if ( $list_id < 1 ) { if ( $list_id < 1 ) {
error_log( '[WooList] Completed order sync enabled but no list ID configured.' ); WooList_Logger::error( 'Completed order sync enabled but no list ID configured order_id=' . $order_id );
return; return;
} }
$order = wc_get_order( $order_id ); $order = wc_get_order( $order_id );
if ( ! $order ) { if ( ! $order ) {
WooList_Logger::error( 'Could not load order order_id=' . $order_id );
return; return;
} }
$email = $order->get_billing_email(); $email = $order->get_billing_email();
if ( ! is_email( $email ) ) { if ( ! is_email( $email ) ) {
WooList_Logger::error( 'Order has no valid billing email order_id=' . $order_id );
return; return;
} }
WooList_Logger::info( 'Syncing completed order order_id=' . $order_id . ' email=' . $email . ' list_id=' . $list_id );
$this->api->subscribe_email_to_list( $email, $list_id ); $this->api->subscribe_email_to_list( $email, $list_id );
} }
@@ -55,26 +61,32 @@ class WooList_Hooks {
* @param int $order_id WooCommerce order ID. * @param int $order_id WooCommerce order ID.
*/ */
public function on_order_cancelled( int $order_id ): void { public function on_order_cancelled( int $order_id ): void {
WooList_Logger::debug( 'Hook fired: order_cancelled order_id=' . $order_id );
if ( get_option( 'woolist_sync_cancelled' ) !== 'yes' ) { if ( get_option( 'woolist_sync_cancelled' ) !== 'yes' ) {
WooList_Logger::debug( 'Cancelled order sync disabled, skipping.' );
return; return;
} }
$list_id = (int) get_option( 'woolist_cancelled_list_id', 0 ); $list_id = (int) get_option( 'woolist_cancelled_list_id', 0 );
if ( $list_id < 1 ) { if ( $list_id < 1 ) {
error_log( '[WooList] Cancelled order sync enabled but no list ID configured.' ); WooList_Logger::error( 'Cancelled order sync enabled but no list ID configured order_id=' . $order_id );
return; return;
} }
$order = wc_get_order( $order_id ); $order = wc_get_order( $order_id );
if ( ! $order ) { if ( ! $order ) {
WooList_Logger::error( 'Could not load order order_id=' . $order_id );
return; return;
} }
$email = $order->get_billing_email(); $email = $order->get_billing_email();
if ( ! is_email( $email ) ) { if ( ! is_email( $email ) ) {
WooList_Logger::error( 'Order has no valid billing email order_id=' . $order_id );
return; return;
} }
WooList_Logger::info( 'Syncing cancelled order order_id=' . $order_id . ' email=' . $email . ' list_id=' . $list_id );
$this->api->subscribe_email_to_list( $email, $list_id ); $this->api->subscribe_email_to_list( $email, $list_id );
} }
@@ -84,21 +96,26 @@ class WooList_Hooks {
* @param int $user_id Newly registered user ID. * @param int $user_id Newly registered user ID.
*/ */
public function on_user_register( int $user_id ): void { public function on_user_register( int $user_id ): void {
WooList_Logger::debug( 'Hook fired: user_register user_id=' . $user_id );
if ( get_option( 'woolist_sync_signup' ) !== 'yes' ) { if ( get_option( 'woolist_sync_signup' ) !== 'yes' ) {
WooList_Logger::debug( 'Account signup sync disabled, skipping.' );
return; return;
} }
$list_id = (int) get_option( 'woolist_signup_list_id', 0 ); $list_id = (int) get_option( 'woolist_signup_list_id', 0 );
if ( $list_id < 1 ) { if ( $list_id < 1 ) {
error_log( '[WooList] Account signup sync enabled but no list ID configured.' ); WooList_Logger::error( 'Account signup sync enabled but no list ID configured user_id=' . $user_id );
return; return;
} }
$user = get_userdata( $user_id ); $user = get_userdata( $user_id );
if ( ! $user || ! is_email( $user->user_email ) ) { if ( ! $user || ! is_email( $user->user_email ) ) {
WooList_Logger::error( 'Could not load user or invalid email user_id=' . $user_id );
return; return;
} }
WooList_Logger::info( 'Syncing new account user_id=' . $user_id . ' email=' . $user->user_email . ' list_id=' . $list_id );
$this->api->subscribe_email_to_list( $user->user_email, $list_id ); $this->api->subscribe_email_to_list( $user->user_email, $list_id );
} }
} }

View File

@@ -0,0 +1,141 @@
<?php
/**
* Simple file-based logger for WooList.
*
* Writes to wp-content/uploads/woolist-logs/woolist.log.
* The directory is protected from direct HTTP access via .htaccess.
*
* Levels:
* INFO — always written (subscription events, connection test results)
* ERROR — always written (API failures, config problems)
* DEBUG — written only when "Enable debug logging" is on (full request/response)
*
* @package WooList
*/
defined( 'ABSPATH' ) || exit;
class WooList_Logger {
/** Absolute path to the log file. */
private static string $log_file = '';
/** Whether verbose debug lines are recorded. */
private static bool $debug_enabled = false;
/** Maximum log file size in bytes before rotation (1 MB). */
private const MAX_SIZE = 1048576;
/**
* Initialise the logger: create the log directory and read the debug setting.
* Must be called after WordPress options are available.
*/
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' );
}
// ── Public logging methods ───────────────────────────────────────────────
/** Always logged. Use for normal subscription / connection events. */
public static function info( string $message ): void {
self::write( 'INFO', $message );
}
/** Always logged. Also echoes to php error_log as a fallback. */
public static function error( string $message ): void {
self::write( 'ERROR', $message );
error_log( '[WooList ERROR] ' . $message );
}
/**
* Logged only when debug mode is enabled.
* Use for full request URLs, raw response bodies, step-by-step flow.
*/
public static function debug( string $message ): void {
if ( self::$debug_enabled ) {
self::write( 'DEBUG', $message );
}
}
// ── Utility ─────────────────────────────────────────────────────────────
/**
* Strip the password parameter from a phpList URL before logging it.
*
* @param string $url Full API URL.
* @return string URL with password value replaced by ***.
*/
public static function redact_url( string $url ): string {
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;
}
// 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 );
}
}

View File

@@ -101,24 +101,30 @@ class WooList_Shortcode {
public function handle_ajax(): void { public function handle_ajax(): void {
// 1. Verify nonce. // 1. Verify nonce.
if ( ! check_ajax_referer( 'woolist_newsletter_nonce', 'nonce', false ) ) { if ( ! check_ajax_referer( 'woolist_newsletter_nonce', 'nonce', false ) ) {
WooList_Logger::error( 'Newsletter AJAX: nonce verification failed' );
wp_send_json_error( [ 'message' => __( 'Security check failed. Please refresh and try again.', 'woolist-phplist' ) ], 403 ); wp_send_json_error( [ 'message' => __( 'Security check failed. Please refresh and try again.', 'woolist-phplist' ) ], 403 );
} }
// 2. Validate email. // 2. Validate email.
$email = isset( $_POST['woolist_email'] ) ? sanitize_email( wp_unslash( $_POST['woolist_email'] ) ) : ''; $email = isset( $_POST['woolist_email'] ) ? sanitize_email( wp_unslash( $_POST['woolist_email'] ) ) : '';
if ( ! is_email( $email ) ) { if ( ! is_email( $email ) ) {
WooList_Logger::debug( 'Newsletter AJAX: invalid email submitted' );
wp_send_json_error( [ 'message' => __( 'Please enter a valid email address.', 'woolist-phplist' ) ], 400 ); wp_send_json_error( [ 'message' => __( 'Please enter a valid email address.', 'woolist-phplist' ) ], 400 );
} }
WooList_Logger::debug( 'Newsletter AJAX: submission received email=' . $email );
// 3. Get list ID. // 3. Get list ID.
$list_id = (int) get_option( 'woolist_newsletter_list_id', 0 ); $list_id = (int) get_option( 'woolist_newsletter_list_id', 0 );
if ( $list_id < 1 ) { if ( $list_id < 1 ) {
WooList_Logger::error( 'Newsletter AJAX: no list ID configured' );
wp_send_json_error( [ 'message' => __( 'Newsletter is not configured. Please contact the site administrator.', 'woolist-phplist' ) ], 500 ); wp_send_json_error( [ 'message' => __( 'Newsletter is not configured. Please contact the site administrator.', 'woolist-phplist' ) ], 500 );
} }
// 4. Subscribe email to phpList. // 4. Subscribe email to phpList.
$result = $this->api->subscribe_email_to_list( $email, $list_id ); $result = $this->api->subscribe_email_to_list( $email, $list_id );
if ( ! $result['success'] ) { if ( ! $result['success'] ) {
WooList_Logger::error( 'Newsletter AJAX: subscription failed email=' . $email );
wp_send_json_error( [ 'message' => __( 'Could not subscribe your email. Please try again later.', 'woolist-phplist' ) ], 500 ); wp_send_json_error( [ 'message' => __( 'Could not subscribe your email. Please try again later.', 'woolist-phplist' ) ], 500 );
} }
@@ -131,14 +137,18 @@ class WooList_Shortcode {
if ( $coupon_mode === 'fixed' ) { if ( $coupon_mode === 'fixed' ) {
$coupon_code = sanitize_text_field( get_option( 'woolist_coupon_fixed_code', '' ) ); $coupon_code = sanitize_text_field( get_option( 'woolist_coupon_fixed_code', '' ) );
WooList_Logger::debug( 'Newsletter AJAX: using fixed coupon code=' . $coupon_code );
} else { } else {
$coupon_code = $this->generate_coupon( $email ); $coupon_code = $this->generate_coupon( $email );
WooList_Logger::info( 'Newsletter AJAX: generated coupon code=' . $coupon_code . ' email=' . $email );
} }
} }
// Replace {coupon} placeholder in the thank-you message. // Replace {coupon} placeholder in the thank-you message.
$thankyou_msg = str_replace( '{coupon}', esc_html( $coupon_code ), $thankyou_msg ); $thankyou_msg = str_replace( '{coupon}', esc_html( $coupon_code ), $thankyou_msg );
WooList_Logger::info( 'Newsletter AJAX: success email=' . $email );
wp_send_json_success( wp_send_json_success(
[ [
'message' => wp_kses_post( $thankyou_msg ), 'message' => wp_kses_post( $thankyou_msg ),
@@ -155,13 +165,15 @@ class WooList_Shortcode {
*/ */
private function generate_coupon( string $email ): string { private function generate_coupon( string $email ): string {
if ( ! class_exists( 'WC_Coupon' ) ) { if ( ! class_exists( 'WC_Coupon' ) ) {
error_log( '[WooList] WC_Coupon class not available; cannot generate coupon.' ); WooList_Logger::error( 'WC_Coupon class not available; cannot generate coupon.' );
return ''; return '';
} }
$discount_pct = (int) get_option( 'woolist_coupon_discount_pct', 10 ); $discount_pct = (int) get_option( 'woolist_coupon_discount_pct', 10 );
$expiry_days = (int) get_option( 'woolist_coupon_expiry_days', 30 ); $expiry_days = (int) get_option( 'woolist_coupon_expiry_days', 30 );
$coupon_code = 'WOOLIST-' . strtoupper( substr( md5( $email . time() ), 0, 8 ) ); $coupon_code = 'WOOLIST-' . strtoupper( substr( md5( $email . time() ), 0, 8 ) );
WooList_Logger::debug( 'Generating coupon code=' . $coupon_code . ' discount=' . $discount_pct . '% expiry_days=' . $expiry_days );
$expiry_date = ''; $expiry_date = '';
if ( $expiry_days > 0 ) { if ( $expiry_days > 0 ) {
@@ -180,11 +192,10 @@ class WooList_Shortcode {
); );
if ( is_wp_error( $post_id ) ) { if ( is_wp_error( $post_id ) ) {
error_log( '[WooList] Failed to create coupon post: ' . $post_id->get_error_message() ); WooList_Logger::error( 'Failed to create coupon post error=' . $post_id->get_error_message() );
return ''; return '';
} }
// Set coupon meta via WC functions.
update_post_meta( $post_id, 'discount_type', 'percent' ); update_post_meta( $post_id, 'discount_type', 'percent' );
update_post_meta( $post_id, 'coupon_amount', (string) $discount_pct ); update_post_meta( $post_id, 'coupon_amount', (string) $discount_pct );
update_post_meta( $post_id, 'usage_limit', '1' ); update_post_meta( $post_id, 'usage_limit', '1' );
@@ -196,6 +207,7 @@ class WooList_Shortcode {
update_post_meta( $post_id, 'date_expires', strtotime( $expiry_date ) ); update_post_meta( $post_id, 'date_expires', strtotime( $expiry_date ) );
} }
WooList_Logger::debug( 'Coupon created post_id=' . $post_id . ' code=' . $coupon_code . ' expires=' . ( $expiry_date ?: 'never' ) );
return $coupon_code; return $coupon_code;
} }
} }

View File

@@ -42,6 +42,9 @@ function woolist_check_woocommerce() {
function woolist_init() { function woolist_init() {
load_plugin_textdomain( 'woolist-phplist', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); load_plugin_textdomain( 'woolist-phplist', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
require_once WOOLIST_PATH . 'includes/class-woolist-logger.php';
WooList_Logger::init();
if ( ! woolist_check_woocommerce() ) { if ( ! woolist_check_woocommerce() ) {
return; return;
} }