diff --git a/woolist-phplist/includes/class-woolist-admin.php b/woolist-phplist/includes/class-woolist-admin.php index 8cf4038..7cdaedd 100644 --- a/woolist-phplist/includes/class-woolist-admin.php +++ b/woolist-phplist/includes/class-woolist-admin.php @@ -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 { . '
'; } + /** + * 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 esc_html__( 'Log file: ', 'woolist-phplist' );
+ echo '' . esc_html( $log_path ) . '';
+ echo '
' . esc_html__( 'Log is empty.', 'woolist-phplist' ) . '
'; + } else { + echo ''; + echo '' + . esc_html__( 'Clear Log', 'woolist-phplist' ) + . ''; + echo '
'; + } + /** * 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', + ], ]; } } diff --git a/woolist-phplist/includes/class-woolist-api.php b/woolist-phplist/includes/class-woolist-api.php index cf42a38..42862da 100644 --- a/woolist-phplist/includes/class-woolist-api.php +++ b/woolist-phplist/includes/class-woolist-api.php @@ -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 ]; } diff --git a/woolist-phplist/includes/class-woolist-hooks.php b/woolist-phplist/includes/class-woolist-hooks.php index b845a7f..ffee203 100644 --- a/woolist-phplist/includes/class-woolist-hooks.php +++ b/woolist-phplist/includes/class-woolist-hooks.php @@ -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 ); } } diff --git a/woolist-phplist/includes/class-woolist-logger.php b/woolist-phplist/includes/class-woolist-logger.php new file mode 100644 index 0000000..23e3480 --- /dev/null +++ b/woolist-phplist/includes/class-woolist-logger.php @@ -0,0 +1,141 @@ += 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 ); + } +} diff --git a/woolist-phplist/includes/class-woolist-shortcode.php b/woolist-phplist/includes/class-woolist-shortcode.php index bfd5817..fbbdca5 100644 --- a/woolist-phplist/includes/class-woolist-shortcode.php +++ b/woolist-phplist/includes/class-woolist-shortcode.php @@ -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; } } diff --git a/woolist-phplist/woolist-phplist.php b/woolist-phplist/woolist-phplist.php index 3776040..3524ae3 100644 --- a/woolist-phplist/woolist-phplist.php +++ b/woolist-phplist/woolist-phplist.php @@ -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; }