Changeset 3795216
- Timestamp:
- 11/10/2026 08:35:25 PM (15 hours ago)
- Location:
- bookfunnel/trunk
- Files:
-
- 10 edited
-
bookfunnel.php (modified) (8 diffs)
-
changelog.txt (modified) (1 diff)
-
includes/class-bf-activator.php (modified) (2 diffs)
-
includes/class-bf-admin.php (modified) (7 diffs)
-
includes/class-bf-auth.php (modified) (4 diffs)
-
includes/class-bf-cron.php (modified) (1 diff)
-
includes/class-bf-delivery-link.php (modified) (1 diff)
-
includes/class-bf-notifier.php (modified) (9 diffs)
-
readme.txt (modified) (2 diffs)
-
uninstall.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
bookfunnel/trunk/bookfunnel.php
r3670548 r3795216 3 3 * Plugin Name: BookFunnel 4 4 * Description: Sell ebooks and audiobooks on your WooCommerce store and let BookFunnel handle the delivery. 5 * Version: 1.1 5 * Version: 1.1.1 6 6 * Author: BookFunnel 7 7 * Author URI: https://bookfunnel.com … … 12 12 * Requires PHP: 7.4 13 13 * WC requires at least: 8.0 14 * WC tested up to: 1 0.7.014 * WC tested up to: 11.2.2 15 15 * 16 16 * License: GPLv2 or later … … 23 23 24 24 // Plugin constants. 25 define( 'BF_WC_VERSION', '1.1' ); 25 define( 'BF_WC_VERSION', '1.1.1' ); 26 define( 'BF_WC_DB_VERSION', '1' ); 26 27 define( 'BF_WC_PLUGIN_FILE', __FILE__ ); 27 28 define( 'BF_WC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 88 89 */ 89 90 function bookfunnel_woocommerce_activate() { 91 require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-activator.php'; 92 93 BF_WC_Activator::activate(); 94 90 95 if ( ! class_exists( 'WooCommerce' ) ) { 91 96 add_action( 'admin_notices', 'bookfunnel_woocommerce_missing_wc_notice' ); … … 93 98 } 94 99 95 require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-activator.php';96 100 require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-cron.php'; 97 101 98 BF_WC_Activator::activate();99 102 BF_WC_Cron::activate(); 100 103 } … … 179 182 */ 180 183 function bookfunnel_woocommerce_init() { 184 require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-activator.php'; 185 186 BF_WC_Activator::maybe_install(); 187 181 188 if ( ! class_exists( 'WooCommerce' ) ) { 182 189 add_action( 'admin_notices', 'bookfunnel_woocommerce_missing_wc_notice' ); … … 184 191 } 185 192 186 require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-activator.php';187 193 require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-logger.php'; 188 194 require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-auth.php'; … … 196 202 BookfunnelWoocommerce::instance(); 197 203 } 204 205 // phpcs:enable WordPress.Files.FileName -
bookfunnel/trunk/changelog.txt
r3670548 r3795216 1 1 *** BookFunnel Changelog *** 2 3 2026-11-10 - version 1.1.1 4 * Add - Automatic setup check keeps the delivery queue ready, regardless of plugin activation order 5 * Add - "Resend to BookFunnel" now supports orders in Processing status 6 * Add - Shop managers can now manage BookFunnel settings 7 2 8 3 9 2026-08-27 - version 1.1 -
bookfunnel/trunk/includes/class-bf-activator.php
r3524639 r3795216 18 18 */ 19 19 public static function activate() { 20 self::install(); 21 } 22 23 /** 24 * Run the install routine when the stored database version is missing or outdated. 25 * 26 * @return void 27 */ 28 public static function maybe_install() { 29 if ( get_option( 'bf_wc_db_version' ) === BF_WC_DB_VERSION ) { 30 return; 31 } 32 33 if ( get_transient( 'bf_wc_db_install_retry' ) ) { 34 return; 35 } 36 37 self::install(); 38 } 39 40 /** 41 * Create or update the notifications table and record the database version. 42 * 43 * @return void 44 */ 45 private static function install() { 20 46 global $wpdb; 47 48 require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-logger.php'; 21 49 22 50 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; … … 41 69 42 70 dbDelta( $sql ); 71 72 $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name; 73 74 if ( $table_exists ) { 75 update_option( 'bf_wc_db_version', BF_WC_DB_VERSION ); 76 delete_transient( 'bf_wc_db_install_retry' ); 77 return; 78 } 79 80 BF_WC_Logger::error( 81 'BookFunnel notification table could not be created.', 82 array( 83 'table' => $table_name, 84 'db_error' => $wpdb->last_error, 85 ) 86 ); 87 88 set_transient( 'bf_wc_db_install_retry', 1, HOUR_IN_SECONDS ); 43 89 } 44 90 } -
bookfunnel/trunk/includes/class-bf-admin.php
r3670548 r3795216 103 103 add_action( 'admin_init', array( $this, 'register_settings' ) ); 104 104 add_action( 'admin_init', array( $this, 'handle_page_actions' ) ); 105 add_filter( 'option_page_capability_' . self::SETTINGS_GROUP, array( $this, 'settings_capability' ) ); 105 106 add_action( 'admin_notices', array( $this, 'render_admin_notices' ) ); 106 107 add_action( 'wp_dashboard_setup', array( $this, 'register_dashboard_widget' ) ); … … 198 199 199 200 /** 201 * Capability required to save the Settings tab. 202 * 203 * @return string 204 */ 205 public function settings_capability() { 206 return 'manage_woocommerce'; 207 } 208 209 /** 200 210 * Sanitize the email injection option. 201 211 * … … 256 266 check_admin_referer( 'bf_wc_dismiss_delivery_notice' ); 257 267 $this->dismiss_delivery_health_notice(); 268 } 269 270 if ( isset( $_GET['bf_wc_connect'] ) ) { 271 check_admin_referer( 'bf_wc_connect' ); 272 $this->redirect_to_connect(); 258 273 } 259 274 … … 634 649 635 650 $query = "SELECT status, COUNT(*) AS total FROM {$wpdb->prefix}bf_wc_notifications GROUP BY status"; 636 $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above651 $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant query; only $wpdb->prefix is interpolated 637 652 638 653 if ( is_array( $results ) ) { … … 715 730 */ 716 731 private function get_connect_url() { 732 return wp_nonce_url( 733 add_query_arg( 'bf_wc_connect', '1', $this->get_admin_page_url( 'settings' ) ), 734 'bf_wc_connect' 735 ); 736 } 737 738 /** 739 * Generate the connect state and send the user to BookFunnel. 740 * 741 * @return void 742 */ 743 private function redirect_to_connect() { 717 744 $state = wp_generate_password( 32, false ); 718 745 set_transient( 'bf_wc_auth_state_' . get_current_user_id(), $state, 5 * MINUTE_IN_SECONDS ); 719 746 720 returnadd_query_arg(747 $url = add_query_arg( 721 748 array( 722 749 'store_url' => site_url(), … … 726 753 'https://dashboard.bookfunnel.com/integrate/woocommerce' 727 754 ); 755 756 wp_redirect( $url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- fixed BookFunnel dashboard host 757 exit; 728 758 } 729 759 … … 1109 1139 private function determine_event_type_from_order( $order ) { 1110 1140 switch ( (string) $order->get_status() ) { 1141 case 'processing': 1111 1142 case 'completed': 1112 1143 return BF_WC_Notifier::EVENT_COMPLETED; -
bookfunnel/trunk/includes/class-bf-auth.php
r3670548 r3795216 68 68 delete_transient( 'bf_wc_auth_state_' . get_current_user_id() ); 69 69 70 if ( empty( $stored_state ) || empty( $incoming_state ) || $stored_state !== $incoming_state) {70 if ( empty( $stored_state ) || empty( $incoming_state ) || ! hash_equals( (string) $stored_state, $incoming_state ) ) { 71 71 BF_WC_Logger::warning( 'BookFunnel authentication callback rejected because the nonce was invalid.' ); 72 72 return; … … 78 78 $purchase_uid = isset( $_GET['purchase_uid'] ) ? sanitize_text_field( wp_unslash( $_GET['purchase_uid'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- state token verified above via transient 79 79 80 if ( '' === $token || '' === $webhook_uid || '' === $webhook_url ) { 80 if ( ! self::is_valid_webhook_url( $webhook_url ) 81 || ! preg_match( '/^[a-z0-9]{10}$/', $webhook_uid ) 82 || ! preg_match( '/^[a-z0-9]{10}$/', $purchase_uid ) 83 || ! preg_match( '/^[A-Za-z0-9]{16,128}$/', $token ) 84 ) { 81 85 BF_WC_Logger::warning( 82 'BookFunnel authentication callback rejected because one or more required values were missing or invalid.', 83 array( 84 'has_token' => '' !== $token, 85 'has_webhook_uid' => '' !== $webhook_uid, 86 'has_webhook_url' => '' !== $webhook_url, 86 'BookFunnel authentication callback rejected because one or more values were missing or invalid.', 87 array( 88 'webhook_url' => $webhook_url, 89 'webhook_uid_ok' => (bool) preg_match( '/^[a-z0-9]{10}$/', $webhook_uid ), 90 'purchase_uid_ok' => (bool) preg_match( '/^[a-z0-9]{10}$/', $purchase_uid ), 91 'token_ok' => (bool) preg_match( '/^[A-Za-z0-9]{16,128}$/', $token ), 87 92 ) 93 ); 94 set_transient( 95 BF_WC_Admin::NOTICE_TRANSIENT_PREFIX . get_current_user_id(), 96 array( 97 'type' => 'error', 98 'message' => __( 'The BookFunnel connection response was invalid and was ignored. Please try connecting again.', 'bookfunnel' ), 99 ), 100 MINUTE_IN_SECONDS 88 101 ); 89 102 return; … … 124 137 125 138 /** 139 * Determine whether a URL is an https BookFunnel (bookfunnel.com or subdomain) URL. 140 * 141 * @param string $url URL to check. 142 * @return bool 143 */ 144 public static function is_valid_webhook_url( $url ) { 145 $parts = wp_parse_url( (string) $url ); 146 147 if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) || isset( $parts['user'] ) || isset( $parts['pass'] ) ) { 148 return false; 149 } 150 151 $host = strtolower( $parts['host'] ); 152 153 return 'https' === strtolower( $parts['scheme'] ) && ( 'bookfunnel.com' === $host || '.bookfunnel.com' === substr( $host, -15 ) ); 154 } 155 156 /** 126 157 * Determine whether the plugin is authenticated. 127 158 * … … 196 227 } 197 228 198 $response = wp_ remote_post(229 $response = wp_safe_remote_post( 199 230 $webhook_url, 200 231 array( -
bookfunnel/trunk/includes/class-bf-cron.php
r3524639 r3795216 155 155 $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 156 156 157 if ( '' !== $wpdb->last_error ) { 158 BF_WC_Logger::error( 159 'BookFunnel could not load due notifications from the retry queue.', 160 array( 161 'db_error' => $wpdb->last_error, 162 ) 163 ); 164 } 165 157 166 if ( ! is_array( $results ) ) { 158 167 return array(); -
bookfunnel/trunk/includes/class-bf-delivery-link.php
r3670548 r3795216 77 77 ); 78 78 79 $response = wp_ remote_get( $request_url, array( 'timeout' => 3 ) );79 $response = wp_safe_remote_get( $request_url, array( 'timeout' => 3 ) ); 80 80 $body = self::parse_success_body( $response ); 81 81 -
bookfunnel/trunk/includes/class-bf-notifier.php
r3670548 r3795216 105 105 106 106 if ( ! $notification_id ) { 107 global $wpdb; 108 107 109 self::maybe_log( 108 110 $args, … … 114 116 'response_code' => 0, 115 117 'response_body' => 'Notification insert failed.', 118 'db_error' => $wpdb->last_error, 116 119 ) 117 120 ); … … 125 128 126 129 $base_url = trim( (string) get_option( 'bf_wc_webhook_url', '' ) ); 130 if ( '' !== $base_url && ! BF_WC_Auth::is_valid_webhook_url( $base_url ) ) { 131 self::maybe_log( $args, 'warning', 'BookFunnel webhook URL rejected because it is not an https bookfunnel.com URL.', array( 'webhook_url' => $base_url ) ); 132 $base_url = ''; 133 } 134 127 135 if ( '' === $base_url ) { 128 136 $failure_result = self::handle_send_failure( $notification_id, 'Webhook URL not configured.' ); … … 176 184 } 177 185 178 $response = wp_ remote_post(186 $response = wp_safe_remote_post( 179 187 $webhook_url, 180 188 array( … … 392 400 ); 393 401 394 return (int) $wpdb->get_var( $query ) > 0; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 402 $count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 403 404 if ( null === $count ) { 405 self::log_db_error( 406 'BookFunnel could not check for an existing notification record.', 407 array( 408 'order_id' => $order_id, 409 'event_type' => $event_type, 410 ) 411 ); 412 } 413 414 return (int) $count > 0; 395 415 } 396 416 … … 491 511 ); 492 512 493 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 513 $result = $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 514 515 if ( false === $result ) { 516 self::log_db_error( 'BookFunnel could not mark the notification record as done.', array( 'notification_id' => $notification_id ) ); 517 } 494 518 } 495 519 … … 528 552 ); 529 553 530 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 554 $result = $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 555 556 if ( false === $result ) { 557 self::log_db_error( 'BookFunnel could not mark the notification record as abandoned.', array( 'notification_id' => $notification_id ) ); 558 } 559 531 560 BF_WC_Cron::send_abandonment_email( $notification ); 532 561 … … 549 578 ); 550 579 551 $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 580 $result = $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above 581 582 if ( false === $result ) { 583 self::log_db_error( 'BookFunnel could not schedule the notification record for retry.', array( 'notification_id' => $notification_id ) ); 584 } 552 585 553 586 if ( 4 === $attempt_number ) { … … 578 611 579 612 if ( ! is_array( $result ) ) { 613 if ( '' !== $wpdb->last_error ) { 614 self::log_db_error( 'BookFunnel could not load the notification record.', array( 'notification_id' => $notification_id ) ); 615 } 616 580 617 return array(); 581 618 } 582 619 583 620 return $result; 621 } 622 623 /** 624 * Log a failed notification table query with the database error. 625 * 626 * @param string $message Log message. 627 * @param array $context Log context. 628 * @return void 629 */ 630 private static function log_db_error( $message, array $context ) { 631 global $wpdb; 632 633 $context['db_error'] = $wpdb->last_error; 634 635 BF_WC_Logger::error( $message, $context ); 584 636 } 585 637 -
bookfunnel/trunk/readme.txt
r3670548 r3795216 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1.1 7 Stable tag: 1.1.1 8 8 WC requires at least: 8.0 9 WC tested up to: 11. 0.19 WC tested up to: 11.2.2 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 135 135 == Changelog == 136 136 137 2026-11-10 - version 1.1.1 138 * Add - Automatic setup check keeps the delivery queue ready, regardless of plugin activation order 139 * Add - "Resend to BookFunnel" now supports orders in Processing status 140 * Add - Shop managers can now manage BookFunnel settings 141 142 137 143 2026-08-27 - version 1.1 138 144 * Add - Order-specific delivery link block on the thank-you page and order confirmation email, replacing the generic purchase link -
bookfunnel/trunk/uninstall.php
r3670548 r3795216 5 5 * @package BookFunnelWooCommerce 6 6 */ 7 7 8 defined( 'WP_UNINSTALL_PLUGIN' ) || exit; 8 9 … … 21 22 'bf_wc_email_injection', 22 23 'bf_wc_legacy_connection_warning', 24 'bf_wc_revoke_on_partial_refund', 25 'bf_wc_db_version', 23 26 ); 24 27 … … 26 29 delete_option( $bf_wc_option_name ); 27 30 } 31 32 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- removes this plugin's transients, including per-user keys 33 $wpdb->query( 34 $wpdb->prepare( 35 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", 36 $wpdb->esc_like( '_transient_bf_wc_' ) . '%', 37 $wpdb->esc_like( '_transient_timeout_bf_wc_' ) . '%' 38 ) 39 );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)