woocommerce-gateway-paypal-express-checkout icon indicating copy to clipboard operation
woocommerce-gateway-paypal-express-checkout copied to clipboard

Caching Plugin and Nonce Lifespan causing "secure browser" popup to fail

Open devonto opened this issue 6 years ago • 13 comments

Describe the bug When using WP Rocket, a page-caching plugin (www.wp-rocket.me), caching of individual product pages cause the "buy now" button to fail - opening the "Secure Browser", but closing with a JS error; due to the nonce in use expiring very quickly.

To Reproduce

  1. Enable a caching plugin (I use WP Rocket, I'm sure others will show the same issue)
  2. Wait for cache to expire (not sure on timings, see below)
  3. Try to click "Buy Now" button on a product page
  4. "Secure browser" window pops up to log in to PayPal with spinner graphic
  5. "Secure browser" window closes and javascript error present in console
  6. XHR response from ? is "Cheatin' huh"?

Expected behavior The secure browser window should open as expected to permit a standard checkout flow.

Console Log

logger.js:63 ppxo_unhandled_error {stack: "Error: TypeError: Cannot read property 'messages' …paypalobjects.com/api/checkout.4.0.286.js:4327:9)", errtype: "[object Error]", timestamp: 1569327768192, windowID: "08aba31bbc", pageID: "bff1009b6b", …}
print @ logger.js:63
log @ logger.js:181
error @ logger.js:234
(anonymous) @ setup.js:36
(anonymous) @ exceptions.js:27
(anonymous) @ promise.js:122
setTimeout (async)
_proto.reject @ promise.js:120
_loop @ promise.js:171
_proto.dispatch @ promise.js:153
_proto.reject @ promise.js:127
_loop @ promise.js:171
_proto.dispatch @ promise.js:153
_proto.reject @ promise.js:127
_loop @ promise.js:178
_proto.dispatch @ promise.js:153
_proto.reject @ promise.js:127
(anonymous) @ promise.js:207
_loop @ promise.js:176
_proto.dispatch @ promise.js:153
_proto.reject @ promise.js:127
(anonymous) @ promise.js:51
respond @ client.js:147
_RECEIVE_MESSAGE_TYPE.<computed> @ types.js:126
receiveMessage @ index.js:114
messageListener @ index.js:140
types.js:121 Uncaught Error: TypeError: Cannot read property 'messages' of undefined
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:625
    at _loop (5be91ae8aef9b3b73997b1f9cbcc7375.js:62)
    at ZalgoPromise._proto.dispatch (5be91ae8aef9b3b73997b1f9cbcc7375.js:63)
    at ZalgoPromise._proto.resolve (5be91ae8aef9b3b73997b1f9cbcc7375.js:61)
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:59
    at XMLHttpRequest.<anonymous> (5be91ae8aef9b3b73997b1f9cbcc7375.js:78)
    at Object._RECEIVE_MESSAGE_TYPE.<computed> [as postrobot_message_response] (types.js:121)
    at receiveMessage (index.js:114)
    at messageListener (index.js:140)
_RECEIVE_MESSAGE_TYPE.<computed> @ types.js:121
receiveMessage @ index.js:114
messageListener @ index.js:140
setTimeout (async)
(anonymous) @ exceptions.js:16
(anonymous) @ promise.js:122
setTimeout (async)
_proto.reject @ promise.js:120
_loop @ promise.js:171
_proto.dispatch @ promise.js:153
_proto.reject @ promise.js:127
_loop @ promise.js:171
_proto.dispatch @ promise.js:153
_proto.reject @ promise.js:127
_loop @ promise.js:178
_proto.dispatch @ promise.js:153
_proto.reject @ promise.js:127
(anonymous) @ promise.js:207
_loop @ promise.js:176
_proto.dispatch @ promise.js:153
_proto.reject @ promise.js:127
(anonymous) @ promise.js:51
respond @ client.js:147
_RECEIVE_MESSAGE_TYPE.<computed> @ types.js:126
receiveMessage @ index.js:114
messageListener @ index.js:140
5be91ae8aef9b3b73997b1f9cbcc7375.js:350 Uncaught Error: Error: TypeError: Cannot read property 'messages' of undefined
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:625
    at _loop (5be91ae8aef9b3b73997b1f9cbcc7375.js:62)
    at ZalgoPromise._proto.dispatch (5be91ae8aef9b3b73997b1f9cbcc7375.js:63)
    at ZalgoPromise._proto.resolve (5be91ae8aef9b3b73997b1f9cbcc7375.js:61)
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:59
    at XMLHttpRequest.<anonymous> (5be91ae8aef9b3b73997b1f9cbcc7375.js:78)
    at Object._RECEIVE_MESSAGE_TYPE.<computed> [as postrobot_message_response] (checkout.4.0.286.js:4277)
    at receiveMessage (checkout.4.0.286.js:4308)
    at messageListener (checkout.4.0.286.js:4327)
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:350
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:350
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:335
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:335
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:335
    at replaceObject (5be91ae8aef9b3b73997b1f9cbcc7375.js:335)
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:335
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:335
    at 5be91ae8aef9b3b73997b1f9cbcc7375.js:335
    at replaceObject (5be91ae8aef9b3b73997b1f9cbcc7375.js:335)

Environment

  • WordPress Version: 5.2.3
  • WooCommerce Version: 3.7.0
  • PayPal Express Checkout Plugin Version: 1.6.17
  • Browser [e.g. chrome, safari] and Version: Chrome 76.0.3809.132
  • Any other plugins installed

WooCommerce Cardstream: by Cardstream – 1.3 Capability Manager Enhanced: by PublishPress – 1.7.5 Classic Editor: by WordPress Contributors – 1.5 Codisto LINQ by Codisto: by Codisto – 1.3.32 WPBakery Page Builder: by Michael M - WPBakery.com – 6.0.5 Porto Theme - Functionality: by P-Themes – 1.4 Regenerate Thumbnails: by Alex Mills (Viper007Bond) – 3.1.1 Uber Login Logo: by UberWeb – 1.5.1 WooCommerce Admin: by WooCommerce – 0.19.0 WooCommerce Conversion Tracking: by Tareq Hasan – 2.0.4 WooCommerce PayPal Checkout Gateway: by WooCommerce – 1.6.17 WooCommerce Google Analytics Integration: by WooCommerce – 1.4.14 WooCommerce: by Automattic – 3.7.0 Yoast SEO: by Team Yoast – 12.1 WP Rocket: by WP Media – 3.3.7 YITH Google Product Feed for WooCommerce Premium: by YITH – 1.1.6 YITH WooCommerce Brands Add-on Premium: by YITH – 1.3.5 YITH WooCommerce Bulk Product Editing Premium: by YITH – 1.2.16 YITH WooCommerce Dynamic Pricing and Discounts Premium: by YITH – 1.5.4 YITH WooCommerce Order Tracking Premium: by YITH – 1.5.9 YITH WooCommerce PDF Invoice and Shipping List Premium: by YITH – 2.0.6 YITH WooCommerce Tab Manager Premium: by YITH – 1.2.16

Additional context WP Rocket has a cache lifespan of 10 hours, two less than the standard WP Nonce lifespan of 12 hours. Is this plugin doing something differently which is shortening the lifespan of the nonce? Still testing, but a cache with a lifespan of as short as two hours was showing this symptom. A reasonable expiry should be expected to allow caching to work.

The specific nonce check is failing at woocommerce-gateway-paypal-express-checkout/includes/class-wc-gateway-ppec-cart-handler.php line 138

devonto avatar Sep 24 '19 12:09 devonto

2390954-zd

Same issue. @mikkamp mentioned that from his observations, it seems that the plugin would need to say that the cart fragments would expire earlier in the case where a nonce was renewed. That way it can get a new cart fragment once a nonce expires.

FreshPhil avatar Oct 10 '19 16:10 FreshPhil

Follow up with user once bug has been resolved.

DustinHartzler avatar Oct 14 '19 18:10 DustinHartzler

Same Problem here on different Shops. Installed is wp-rocket as caching plugin. Chache expire time is set to 4h. Sometimes it works. I mentioned if you put the product in the cart and than use the express button it always works.

Maveric2005 avatar Oct 15 '19 11:10 Maveric2005

The cart fragment is saved in localstorage in the browser and has an expiry time of 1 day. The cart fragment includes the full cart including the payment buttons with the nonce. So even if the caching plugin isn't returning a cached page, it still wouldn't get a refreshed cart fragment until either:

  • the cart in localstorage is expired
  • the cart contents is changed

Since by default a nonce will have a lifetime of 12 - 24 hours, there is a chance that expires before the cart fragment does. So in theory, even if you take WP Rocket out of the picture, you could still end up with a scenario where the cart fragment has an expired nonce.

I don't see any filter available to make the cart fragment expire any earlier. But another workaround would be to make the nonce valid for longer as is shown here: https://codex.wordpress.org/WordPress_Nonces#Modifying_the_nonce_lifetime

The snippet would need to be modified to return 2 * DAY_IN_SECONDS which will make the nonce valid for 24 - 48 hours.

I mentioned if you put the product in the cart and than use the express button it always works.

That would be expected, because changing the cart contents forces the cart fragment to be refreshed.

Still testing, but a cache with a lifespan of as short as two hours was showing this symptom.

In that scenario how long was it since the cart contents were changed?

mikkamp avatar Oct 15 '19 15:10 mikkamp

We had the same problem with wp super cache. WP Super Cache supports dynamics content to replace Placeholders in cache with real content. So we hooked into the cacheactions and wp_print_footer_scripts to change the nonce values.

Enable late Init, create a folder wp-super-cache-plugins in your plugins folder and create a php file with the following content:

<?php

class Wpsc_PaypalExpress
{
    /**
     * @var Wpsc_PaypalExpress
     */
    protected static $instance;

    protected $localizedFooterScriptHandles = [
        'wc-gateway-ppec-generate-cart' => [
            'objectName' => 'wc_ppec_generate_cart_context',
            'placeHolder' => 'PPEC_GENERATE_CART_NONCE_PLACEHOLDER',
            'nonce' => '_wc_ppec_generate_cart_nonce',
        ],
        'wc-gateway-ppec-smart-payment-buttons' => [
            'objectName' => 'wc_ppec_context',
            'placeHolder' => 'PPEC_SMART_PAYMENT_BUTTON_NONCE_PLACEHOLDER',
            'nonce' => '_wc_ppec_start_checkout_nonce',
        ],
    ];

    public static function instance()
    {
        if (static::$instance === null) {
            static::$instance = new static();
        }

        return static::$instance;
    }

    public function __construct()
    {
        if (function_exists('add_cacheaction')) {
            add_cacheaction('add_cacheaction', [$this, 'addCachifyActions']);
            add_cacheaction('wpsc_cachedata_safety', static function() { return 1; });
            add_cacheaction( 'wpsc_cachedata', [$this, 'replacePlaceholders']);
        }
    }

    public function addCachifyActions()
    {
        add_action('wp_print_footer_scripts', [$this, 'cachifyFooterLocations'], -1);
    }

    public function cachifyFooterLocations()
    {
        global $wp_scripts;
        if (!($wp_scripts instanceof \WP_Dependencies)) {
            return;
        }

        foreach($this->localizedFooterScriptHandles as $handle => $settings) {
            $this->replaceNonce($handle, $settings);
        }
    }

    protected function replaceNonce($handle, array $settings)
    {
        global $wp_scripts;

        if (!wp_script_is($handle)) {
            return;
        }

        $dep = $wp_scripts->registered[$handle];
        /* @var $dep \_WP_Dependency */
        $data = isset($dep->extra['data']) ? $dep->extra['data'] : null;
        //"var $object_name = " . wp_json_encode( $l10n ) . ';';
        //$object_name = wc_ppec_generate_cart_context
        $data = str_replace('var ' . $settings['objectName'] . ' = ', '', $data);
        $data = rtrim($data, ';');
        $data = json_decode($data, true);

        $nonceKey = str_replace('_wc_ppec_', '', $settings['nonce']);
        $data[$nonceKey] = $settings['placeHolder'];
        $dep->extra['data'] = '';
        wp_localize_script($handle, $settings['objectName'], $data);
    }

    public function replacePlaceholders($cachedata)
    {
        foreach($this->localizedFooterScriptHandles as $handle => $settings) {
            $cachedata = str_replace($settings['placeHolder'], wp_create_nonce( $settings['nonce'] ), $cachedata);
        }

        return $cachedata;
    }
}

Wpsc_PaypalExpress::instance();
`

DalbertHab avatar Dec 19 '19 10:12 DalbertHab

@DalbertHab can you give a quick explanation of what your plugin does?

unhammer avatar Jan 30 '20 10:01 unhammer

@unhammer If WP Super Cache caches a page the nonce is cached, too. My plugin replaces the nonce values of _wc_ppec_generate_cart_nonce and _wc_ppec_start_checkout_nonce with placeholders after the page is rendered and before it is saved to cache (Method addCachifyActions). When a cached page is served, wp super cached calls another part of the plugin that replaces the placeholders with newly generated nonce-values (Method replacePlaceholders).

DalbertHab avatar Jan 30 '20 11:01 DalbertHab

thanks, will try :)

unhammer avatar Jan 30 '20 12:01 unhammer

@DalbertHab do you have a solution if i use the wp_rocket cache plugin? or is there an other solution?

Maveric2005 avatar Apr 05 '20 04:04 Maveric2005

Anyone help me with the solution because I need to speed up my website. Wp-rocket with PayPal, PayPal can't pop up when someone purchase

ihabalfaqeh avatar Apr 18 '20 15:04 ihabalfaqeh

@devonto how do you solve the problem

ihabalfaqeh avatar Apr 18 '20 15:04 ihabalfaqeh

@ihabalfaqeh

To be honest, I switched plugins! https://yithemes.com/themes/plugins/yith-paypal-express-checkout-for-woocommerce/

devonto avatar Apr 18 '20 16:04 devonto

Thank you @devonto I ll try it

ihabalfaqeh avatar Apr 18 '20 17:04 ihabalfaqeh