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:
@@ -23,6 +23,9 @@ 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' ] );
|
||||
|
||||
// Enqueue admin JS/CSS only on the WC settings page.
|
||||
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
|
||||
}
|
||||
@@ -44,6 +47,7 @@ class WooList_Admin {
|
||||
public function render_settings(): void {
|
||||
woocommerce_admin_fields( $this->get_settings() );
|
||||
$this->render_test_connection_button();
|
||||
$this->render_log_viewer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,6 +83,48 @@ class WooList_Admin {
|
||||
. '</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.
|
||||
*/
|
||||
@@ -89,10 +135,13 @@ class WooList_Admin {
|
||||
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();
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
WooList_Logger::error( 'Test Connection failed: ' . $result->get_error_message() );
|
||||
set_transient(
|
||||
'woolist_test_connection_notice_' . $user_id,
|
||||
[
|
||||
@@ -103,6 +152,7 @@ class WooList_Admin {
|
||||
);
|
||||
} else {
|
||||
$count = is_array( $result ) ? count( $result ) : '?';
|
||||
WooList_Logger::info( 'Test Connection succeeded, found ' . $count . ' list(s).' );
|
||||
set_transient(
|
||||
'woolist_test_connection_notice_' . $user_id,
|
||||
[
|
||||
@@ -121,6 +171,29 @@ class WooList_Admin {
|
||||
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.
|
||||
*/
|
||||
@@ -129,7 +202,6 @@ class WooList_Admin {
|
||||
if ( ! $screen || strpos( $screen->id, 'woocommerce' ) === false ) {
|
||||
return;
|
||||
}
|
||||
// No custom admin JS/CSS needed yet; WC handles the settings form.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,6 +408,25 @@ class WooList_Admin {
|
||||
'type' => 'sectionend',
|
||||
'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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class WooList_API {
|
||||
/**
|
||||
* 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.
|
||||
* @return mixed
|
||||
*/
|
||||
@@ -63,38 +63,45 @@ class WooList_API {
|
||||
$url = $this->build_url( $cmd, $extra );
|
||||
|
||||
if ( is_wp_error( $url ) ) {
|
||||
WooList_Logger::error( 'Cannot build URL for cmd=' . $cmd . ': ' . $url->get_error_message() );
|
||||
return $url;
|
||||
}
|
||||
|
||||
WooList_Logger::debug( '→ API request cmd=' . $cmd . ' url=' . WooList_Logger::redact_url( $url ) );
|
||||
|
||||
$response = wp_remote_post( $url, [ 'timeout' => 15 ] );
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $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 ) {
|
||||
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 );
|
||||
}
|
||||
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
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.' );
|
||||
}
|
||||
|
||||
// phpList REST API signals errors via a "status" field.
|
||||
if ( isset( $data['status'] ) && strtolower( $data['status'] ) === '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 );
|
||||
}
|
||||
|
||||
WooList_Logger::debug( 'API call succeeded cmd=' . $cmd );
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -118,9 +125,9 @@ class WooList_API {
|
||||
return $this->call(
|
||||
'subscriberAdd',
|
||||
[
|
||||
'email' => rawurlencode( $email ),
|
||||
'confirmed' => 1,
|
||||
'htmlemail' => 1,
|
||||
'email' => rawurlencode( $email ),
|
||||
'confirmed' => 1,
|
||||
'htmlemail' => 1,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -152,35 +159,45 @@ class WooList_API {
|
||||
* @return array{success: bool, subscriber_id: int|null}
|
||||
*/
|
||||
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;
|
||||
$existing = $this->subscriber_get_by_email( $email );
|
||||
|
||||
if ( ! is_wp_error( $existing ) && ! empty( $existing['id'] ) ) {
|
||||
$subscriber_id = (int) $existing['id'];
|
||||
WooList_Logger::debug( 'Found existing subscriber id=' . $subscriber_id . ' email=' . $email );
|
||||
} 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 );
|
||||
|
||||
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 ];
|
||||
}
|
||||
|
||||
$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 ) {
|
||||
error_log( '[WooList] Could not determine subscriber ID for ' . $email );
|
||||
return [ 'success' => false, 'subscriber_id' => null ];
|
||||
}
|
||||
|
||||
// Add subscriber to the list.
|
||||
// Step 3: add subscriber to the list.
|
||||
WooList_Logger::debug( 'Adding subscriber ' . $subscriber_id . ' to list ' . $list_id );
|
||||
$result = $this->list_subscriber_add( $list_id, $subscriber_id );
|
||||
|
||||
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 ];
|
||||
}
|
||||
|
||||
WooList_Logger::info( 'Subscribed email=' . $email . ' subscriber_id=' . $subscriber_id . ' list_id=' . $list_id );
|
||||
return [ 'success' => true, 'subscriber_id' => $subscriber_id ];
|
||||
}
|
||||
|
||||
|
||||
@@ -26,26 +26,32 @@ class WooList_Hooks {
|
||||
* @param int $order_id WooCommerce order ID.
|
||||
*/
|
||||
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' ) {
|
||||
WooList_Logger::debug( 'Completed order sync disabled, skipping.' );
|
||||
return;
|
||||
}
|
||||
|
||||
$list_id = (int) get_option( 'woolist_completed_list_id', 0 );
|
||||
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;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
WooList_Logger::error( 'Could not load order order_id=' . $order_id );
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $order->get_billing_email();
|
||||
if ( ! is_email( $email ) ) {
|
||||
WooList_Logger::error( 'Order has no valid billing email order_id=' . $order_id );
|
||||
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 );
|
||||
}
|
||||
|
||||
@@ -55,26 +61,32 @@ class WooList_Hooks {
|
||||
* @param int $order_id WooCommerce order ID.
|
||||
*/
|
||||
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' ) {
|
||||
WooList_Logger::debug( 'Cancelled order sync disabled, skipping.' );
|
||||
return;
|
||||
}
|
||||
|
||||
$list_id = (int) get_option( 'woolist_cancelled_list_id', 0 );
|
||||
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;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
WooList_Logger::error( 'Could not load order order_id=' . $order_id );
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $order->get_billing_email();
|
||||
if ( ! is_email( $email ) ) {
|
||||
WooList_Logger::error( 'Order has no valid billing email order_id=' . $order_id );
|
||||
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 );
|
||||
}
|
||||
|
||||
@@ -84,21 +96,26 @@ class WooList_Hooks {
|
||||
* @param int $user_id Newly registered user ID.
|
||||
*/
|
||||
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' ) {
|
||||
WooList_Logger::debug( 'Account signup sync disabled, skipping.' );
|
||||
return;
|
||||
}
|
||||
|
||||
$list_id = (int) get_option( 'woolist_signup_list_id', 0 );
|
||||
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;
|
||||
}
|
||||
|
||||
$user = get_userdata( $user_id );
|
||||
if ( ! $user || ! is_email( $user->user_email ) ) {
|
||||
WooList_Logger::error( 'Could not load user or invalid email user_id=' . $user_id );
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
||||
141
woolist-phplist/includes/class-woolist-logger.php
Normal file
141
woolist-phplist/includes/class-woolist-logger.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
@@ -101,24 +101,30 @@ class WooList_Shortcode {
|
||||
public function handle_ajax(): void {
|
||||
// 1. Verify nonce.
|
||||
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 );
|
||||
}
|
||||
|
||||
// 2. Validate email.
|
||||
$email = isset( $_POST['woolist_email'] ) ? sanitize_email( wp_unslash( $_POST['woolist_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 );
|
||||
}
|
||||
|
||||
WooList_Logger::debug( 'Newsletter AJAX: submission received email=' . $email );
|
||||
|
||||
// 3. Get list ID.
|
||||
$list_id = (int) get_option( 'woolist_newsletter_list_id', 0 );
|
||||
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 );
|
||||
}
|
||||
|
||||
// 4. Subscribe email to phpList.
|
||||
$result = $this->api->subscribe_email_to_list( $email, $list_id );
|
||||
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 );
|
||||
}
|
||||
|
||||
@@ -131,14 +137,18 @@ class WooList_Shortcode {
|
||||
|
||||
if ( $coupon_mode === 'fixed' ) {
|
||||
$coupon_code = sanitize_text_field( get_option( 'woolist_coupon_fixed_code', '' ) );
|
||||
WooList_Logger::debug( 'Newsletter AJAX: using fixed coupon code=' . $coupon_code );
|
||||
} else {
|
||||
$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.
|
||||
$thankyou_msg = str_replace( '{coupon}', esc_html( $coupon_code ), $thankyou_msg );
|
||||
|
||||
WooList_Logger::info( 'Newsletter AJAX: success email=' . $email );
|
||||
|
||||
wp_send_json_success(
|
||||
[
|
||||
'message' => wp_kses_post( $thankyou_msg ),
|
||||
@@ -155,13 +165,15 @@ class WooList_Shortcode {
|
||||
*/
|
||||
private function generate_coupon( string $email ): string {
|
||||
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 '';
|
||||
}
|
||||
|
||||
$discount_pct = (int) get_option( 'woolist_coupon_discount_pct', 10 );
|
||||
$expiry_days = (int) get_option( 'woolist_coupon_expiry_days', 30 );
|
||||
$coupon_code = 'WOOLIST-' . strtoupper( substr( md5( $email . time() ), 0, 8 ) );
|
||||
$discount_pct = (int) get_option( 'woolist_coupon_discount_pct', 10 );
|
||||
$expiry_days = (int) get_option( 'woolist_coupon_expiry_days', 30 );
|
||||
$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 = '';
|
||||
if ( $expiry_days > 0 ) {
|
||||
@@ -180,11 +192,10 @@ class WooList_Shortcode {
|
||||
);
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
// Set coupon meta via WC functions.
|
||||
update_post_meta( $post_id, 'discount_type', 'percent' );
|
||||
update_post_meta( $post_id, 'coupon_amount', (string) $discount_pct );
|
||||
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 ) );
|
||||
}
|
||||
|
||||
WooList_Logger::debug( 'Coupon created post_id=' . $post_id . ' code=' . $coupon_code . ' expires=' . ( $expiry_date ?: 'never' ) );
|
||||
return $coupon_code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ function woolist_check_woocommerce() {
|
||||
function woolist_init() {
|
||||
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() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user