Plugin Directory

Changeset 3795216


Ignore:
Timestamp:
11/10/2026 08:35:25 PM (15 hours ago)
Author:
bookfunnel
Message:

Release version 1.1.1

Location:
bookfunnel/trunk
Files:
10 edited

Legend:

Unmodified
Added
Removed
  • bookfunnel/trunk/bookfunnel.php

    r3670548 r3795216  
    33 * Plugin Name: BookFunnel
    44 * Description: Sell ebooks and audiobooks on your WooCommerce store and let BookFunnel handle the delivery.
    5  * Version: 1.1
     5 * Version: 1.1.1
    66 * Author: BookFunnel
    77 * Author URI: https://bookfunnel.com
    … …  
    1212 * Requires PHP: 7.4
    1313 * WC requires at least: 8.0
    14  * WC tested up to: 10.7.0
     14 * WC tested up to: 11.2.2
    1515 *
    1616 * License: GPLv2 or later
    … …  
    2323
    2424// Plugin constants.
    25 define( 'BF_WC_VERSION', '1.1' );
     25define( 'BF_WC_VERSION', '1.1.1' );
     26define( 'BF_WC_DB_VERSION', '1' );
    2627define( 'BF_WC_PLUGIN_FILE', __FILE__ );
    2728define( 'BF_WC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    8889 */
    8990function bookfunnel_woocommerce_activate() {
     91        require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-activator.php';
     92
     93        BF_WC_Activator::activate();
     94
    9095        if ( ! class_exists( 'WooCommerce' ) ) {
    9196                add_action( 'admin_notices', 'bookfunnel_woocommerce_missing_wc_notice' );
    … …  
    9398        }
    9499
    95         require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-activator.php';
    96100        require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-cron.php';
    97101
    98         BF_WC_Activator::activate();
    99102        BF_WC_Cron::activate();
    100103}
    … …  
    179182 */
    180183function bookfunnel_woocommerce_init() {
     184        require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-activator.php';
     185
     186        BF_WC_Activator::maybe_install();
     187
    181188        if ( ! class_exists( 'WooCommerce' ) ) {
    182189                add_action( 'admin_notices', 'bookfunnel_woocommerce_missing_wc_notice' );
    … …  
    184191        }
    185192
    186         require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-activator.php';
    187193        require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-logger.php';
    188194        require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-auth.php';
    … …  
    196202        BookfunnelWoocommerce::instance();
    197203}
     204
     205// phpcs:enable WordPress.Files.FileName
  • bookfunnel/trunk/changelog.txt

    r3670548 r3795216  
    11*** BookFunnel Changelog ***
     2 
     32026-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 
    28 
    392026-08-27 - version 1.1
  • bookfunnel/trunk/includes/class-bf-activator.php

    r3524639 r3795216  
    1818         */
    1919        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() {
    2046                global $wpdb;
     47
     48                require_once BF_WC_PLUGIN_DIR . 'includes/class-bf-logger.php';
    2149
    2250                require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    … …  
    4169
    4270                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 );
    4389        }
    4490}
  • bookfunnel/trunk/includes/class-bf-admin.php

    r3670548 r3795216  
    103103                add_action( 'admin_init', array( $this, 'register_settings' ) );
    104104                add_action( 'admin_init', array( $this, 'handle_page_actions' ) );
     105                add_filter( 'option_page_capability_' . self::SETTINGS_GROUP, array( $this, 'settings_capability' ) );
    105106                add_action( 'admin_notices', array( $this, 'render_admin_notices' ) );
    106107                add_action( 'wp_dashboard_setup', array( $this, 'register_dashboard_widget' ) );
    … …  
    198199
    199200        /**
     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        /**
    200210         * Sanitize the email injection option.
    201211         *
    … …  
    256266                        check_admin_referer( 'bf_wc_dismiss_delivery_notice' );
    257267                        $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();
    258273                }
    259274
    … …  
    634649
    635650                $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() above
     651                        $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- constant query; only $wpdb->prefix is interpolated
    637652
    638653                if ( is_array( $results ) ) {
    … …  
    715730         */
    716731        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() {
    717744                $state = wp_generate_password( 32, false );
    718745                set_transient( 'bf_wc_auth_state_' . get_current_user_id(), $state, 5 * MINUTE_IN_SECONDS );
    719746
    720                 return add_query_arg(
     747                $url = add_query_arg(
    721748                        array(
    722749                                'store_url'    => site_url(),
    … …  
    726753                        'https://dashboard.bookfunnel.com/integrate/woocommerce'
    727754                );
     755
     756                wp_redirect( $url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- fixed BookFunnel dashboard host
     757                exit;
    728758        }
    729759
    … …  
    11091139        private function determine_event_type_from_order( $order ) {
    11101140                switch ( (string) $order->get_status() ) {
     1141                        case 'processing':
    11111142                        case 'completed':
    11121143                                return BF_WC_Notifier::EVENT_COMPLETED;
  • bookfunnel/trunk/includes/class-bf-auth.php

    r3670548 r3795216  
    6868                delete_transient( 'bf_wc_auth_state_' . get_current_user_id() );
    6969
    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 ) ) {
    7171                        BF_WC_Logger::warning( 'BookFunnel authentication callback rejected because the nonce was invalid.' );
    7272                        return;
    … …  
    7878                $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
    7979
    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                ) {
    8185                        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 ),
    8792                                )
     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
    88101                        );
    89102                        return;
    … …  
    124137
    125138        /**
     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        /**
    126157         * Determine whether the plugin is authenticated.
    127158         *
    … …  
    196227                }
    197228
    198                 $response = wp_remote_post(
     229                $response = wp_safe_remote_post(
    199230                        $webhook_url,
    200231                        array(
  • bookfunnel/trunk/includes/class-bf-cron.php

    r3524639 r3795216  
    155155                        $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query is built with $wpdb->prepare() above
    156156
     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
    157166                if ( ! is_array( $results ) ) {
    158167                        return array();
  • bookfunnel/trunk/includes/class-bf-delivery-link.php

    r3670548 r3795216  
    7777                );
    7878
    79                 $response = wp_remote_get( $request_url, array( 'timeout' => 3 ) );
     79                $response = wp_safe_remote_get( $request_url, array( 'timeout' => 3 ) );
    8080                $body     = self::parse_success_body( $response );
    8181
  • bookfunnel/trunk/includes/class-bf-notifier.php

    r3670548 r3795216  
    105105
    106106                if ( ! $notification_id ) {
     107                        global $wpdb;
     108
    107109                        self::maybe_log(
    108110                                $args,
    … …  
    114116                                        'response_code' => 0,
    115117                                        'response_body' => 'Notification insert failed.',
     118                                        'db_error'      => $wpdb->last_error,
    116119                                )
    117120                        );
    … …  
    125128
    126129                $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
    127135                if ( '' === $base_url ) {
    128136                        $failure_result = self::handle_send_failure( $notification_id, 'Webhook URL not configured.' );
    … …  
    176184                }
    177185
    178                 $response = wp_remote_post(
     186                $response = wp_safe_remote_post(
    179187                        $webhook_url,
    180188                        array(
    … …  
    392400                );
    393401
    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;
    395415        }
    396416
    … …  
    491511                );
    492512
    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                }
    494518        }
    495519
    … …  
    528552                        );
    529553
    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
    531560                        BF_WC_Cron::send_abandonment_email( $notification );
    532561
    … …  
    549578                );
    550579
    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                }
    552585
    553586                if ( 4 === $attempt_number ) {
    … …  
    578611
    579612                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
    580617                        return array();
    581618                }
    582619
    583620                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 );
    584636        }
    585637
  • bookfunnel/trunk/readme.txt

    r3670548 r3795216  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.1
     7Stable tag: 1.1.1
    88WC requires at least: 8.0
    9 WC tested up to: 11.0.1
     9WC tested up to: 11.2.2
    1010License: GPLv2 or later
    1111License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    135135== Changelog ==
    136136 
     1372026-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 
    1371432026-08-27 - version 1.1
    138144* 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  
    55 * @package BookFunnelWooCommerce
    66 */
     7
    78defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
    89
    … …  
    2122        'bf_wc_email_injection',
    2223        'bf_wc_legacy_connection_warning',
     24        'bf_wc_revoke_on_partial_refund',
     25        'bf_wc_db_version',
    2326);
    2427
    … …  
    2629        delete_option( $bf_wc_option_name );
    2730}
     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.