Plugin Directory

Changeset 3740915


Ignore:
Timestamp:
10/12/2026 09:05:05 AM (13 days ago)
Author:
michaelbengtsson
Message:

Update to version 4.13.4 from GitHub

Location:
klarna-payments-for-woocommerce
Files:
12 added
48 edited
1 copied

Legend:

Unmodified
Added
Removed
  • klarna-payments-for-woocommerce/tags/4.13.4/changelog.txt

    r3728366 r3740915  
    11*** Klarna for WooCommerce Changelog ***
     22026-10-12      - version 4.13.4
     3* Enhancement   - Masked customer personal, and other sensitive data and credentials in the plugin log.
     4* Fix           - Order details are now only added to the pay for order page when WooCommerce would show the payment form: the request carries the order's key, the customer may pay for the order, the order still needs payment, and a guest has confirmed the billing email.
     5
    262026-10-05      - version 4.13.3
    37* Enhancement   - Improved handling of Klarna authorization callbacks and background processing.
  • klarna-payments-for-woocommerce/tags/4.13.4/classes/class-kp-ajax.php

    r3728366 r3740915  
    100100                                                wp_send_json_success( $order->get_checkout_order_received_url() );
    101101                                        } else {
    102                                                 KP_Logger::log( sprintf( '[AJAX]: Order ID: %s. Auth token: %s. %s', $order->get_id(), $auth_token, $recurring_token->get_error_message() ) );
     102                                                KP_Logger::log( sprintf( '[AJAX]: Order ID: %s. Failed to create a recurring token: %s', $order->get_id(), $recurring_token->get_error_message() ) );
    103103                                                wp_send_json_error( 'customer_token_failed' );
    104104                                        }
  • klarna-payments-for-woocommerce/tags/4.13.4/classes/class-kp-assets.php

    r3716655 r3740915  
    66 */
    77
     8use Automattic\WooCommerce\Internal\Utilities\Users;
    89use Krokedil\Klarna\Features;
    910use Krokedil\Klarna\PluginFeatures;
    … …  
    188189
    189190        /**
     191         * Whether WooCommerce asks the guest to confirm the billing email before showing the order-pay form.
     192         *
     193         * @param WC_Order $order The order being paid for.
     194         * @return bool
     195         */
     196        private function guest_should_verify_email( $order ) {
     197                // WooCommerce versions without the check never ask for verification.
     198                if ( ! is_callable( array( Users::class, 'should_user_verify_order_email' ) ) ) {
     199                        return false;
     200                }
     201
     202                // Mirrors WC_Shortcode_Checkout::guest_should_verify_email(), which accepts the email posted by its verify form.
     203                $supplied_email = null;
     204                if ( wp_verify_nonce( (string) filter_input( INPUT_POST, 'check_submission' ), 'wc_verify_email' ) ) {
     205                        $supplied_email = sanitize_email( wp_unslash( (string) filter_input( INPUT_POST, 'email' ) ) );
     206                }
     207
     208                return Users::should_user_verify_order_email( $order->get_id(), $supplied_email, 'order-pay' );
     209        }
     210
     211        /**
    190212         * Get the params for the Klarna Payments checkout script.
    191213         *
    … …  
    200222
    201223                if ( ! empty( $order_id ) ) {
    202                         $order = wc_get_order( $order_id );
    203                         if ( ! empty( $order ) ) {
     224                        $order       = wc_get_order( $order_id );
     225                        $request_key = (string) filter_input( INPUT_GET, 'key', FILTER_SANITIZE_SPECIAL_CHARS );
     226
     227                        // Only expose the order when WooCommerce would render the pay form, so the same checks in the same order.
     228                        if (
     229                                ! empty( $order )
     230                                && hash_equals( $order->get_order_key(), $request_key )
     231                                && current_user_can( 'pay_for_order', $order_id )
     232                                && $order->needs_payment()
     233                                && ! $this->guest_should_verify_email( $order )
     234                        ) {
    204235                                $order_key = $order->get_order_key();
     236                        } else {
     237                                $order_id = null;
    205238                        }
    206239                }
    … …  
    240273                        'order_key'               => $order_key,
    241274                        'submit_button_selectors' => array( 'input#place_order', 'button#place_order' ),
    242                         'addresses'               => $pay_for_order ? array(
     275                        'addresses'               => null !== $order_key ? array(
    243276                                'billing'  => $customer['billing'],
    244277                                'shipping' => $customer['shipping'],
  • klarna-payments-for-woocommerce/tags/4.13.4/classes/class-kp-callbacks.php

    r3728366 r3740915  
    241241                                        $order->add_order_note( __( 'Failed to create a recurring token when returning from the hosted payment page.', 'klarna-payments-for-woocommerce' ) . $recurring_token->get_error_message() );
    242242
    243                                         KP_Logger::log( sprintf( '[AJAX]: Order ID: %s. Auth token: %s. %s', $order->get_id(), $auth_token, $recurring_token->get_error_message() ) );
     243                                        KP_Logger::log( sprintf( '[AJAX]: Order ID: %s. Failed to create a recurring token: %s', $order->get_id(), $recurring_token->get_error_message() ) );
    244244                                }
    245245
  • klarna-payments-for-woocommerce/tags/4.13.4/classes/class-kp-logger.php

    r3716655 r3740915  
    99        exit; // Exit if accessed directly.
    1010}
     11
     12use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\KeyMasker;
    1113
    1214/**
    … …  
    2830        public static function log( $data ) {
    2931                $kp_settings = get_option( 'woocommerce_klarna_payments_settings', array() );
    30                 if ( 'no' !== ( $kp_settings['logging'] ?? 'no' ) ) {
    31                         $message = self::format_data( $data );
    32                         KP_WC()->logger()->info( wp_json_encode( $message ) );
     32                if ( 'no' === ( $kp_settings['logging'] ?? 'no' ) ) {
     33                        return;
    3334                }
     35
     36                $message = self::format_data( $data );
     37
     38                // A failure here costs the entry, it never lets an unmasked one through.
     39                try {
     40                        $message = KeyMasker::mask( self::format_data( $data ) );
     41                } catch ( \Throwable $e ) {
     42                        $message = array( 'error' => KeyMasker::FAILED );
     43                }
     44
     45                KP_WC()->logger()->info( wp_json_encode( $message ) );
    3446        }
    3547
    … …  
    4153         */
    4254        public static function format_data( $data ) {
    43                 if ( isset( $data['request']['body'] ) ) {
     55                if ( isset( $data['request']['body'] ) && is_string( $data['request']['body'] ) ) {
    4456                        $request_body            = json_decode( $data['request']['body'], true );
    4557                        $data['request']['body'] = $request_body;
  • klarna-payments-for-woocommerce/tags/4.13.4/classes/requests/class-kp-requests.php

    r3664907 r3740915  
    99
    1010use Krokedil\Klarna\Utilities\ApiCredentialsUtility;
     11use Krokedil\Klarna\Logging\LogMasking;
     12use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\KeyMasker;
    1113use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Request;
    12 use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Logger;
    1314
    1415/**
    … …  
    8788                );
    8889
    89                 parent::__construct( $config, $settings, $arguments );
     90                parent::__construct( $config, $settings, $arguments, self::get_masked_fields() );
    9091                $this->set_credentials();
    9192                $this->iframe_options = new KP_IFrame( $settings );
    … …  
    285286
    286287        /**
    287          * Logs the response from the request.
    288          *
    289          * @param array|\WP_Error $response The response from the request.
    290          * @param array           $request_args The request args.
    291          * @param string          $request_url The request URL.
    292          *
    293          * @return void
    294          */
    295         protected function log_response( $response, $request_args, $request_url ) {
    296                 $code = wp_remote_retrieve_response_code( $response );
    297 
    298                 // Get the response body if its not a WP_Error.
    299                 $response_body = is_wp_error( $response ) ? array() : json_decode( wp_remote_retrieve_body( $response ), true );
    300 
    301                 // Parse the Request body into an array if its json format.
    302                 $request_body         = $request_args['body'] ?? '';
    303                 $decoded_body         = json_decode( $request_body );
    304                 $request_args['body'] = $decoded_body ?? $request_args['body'] ?? null;
    305                 $request_args         = $this->sanitize_request_args( $request_args );
    306 
    307                 $arguments = $this->arguments;
    308                 if ( isset( $arguments['username'] ) ) {
    309                         $arguments['username'] = '[REDACTED]';
    310                 }
    311                 if ( isset( $arguments['password'] ) ) {
    312                         $arguments['password'] = '[REDACTED]';
    313                 }
    314 
    315                 KP_WC()->logger()->info(
    316                         wp_json_encode(
    317                                 array(
    318                                         'type'        => $this->method,
    319                                         'title'       => $this->log_title,
    320                                         'arguments'   => $arguments,
    321                                         'request'     => $request_args,
    322                                         'request_url' => $request_url,
    323                                         'response'    => array(
    324                                         'body' => $response_body,
    325                                         'code' => $code,
    326                                         ),
    327                                         'timestamp'   => date( 'Y-m-d H:i:s' ),    // phpcs:ignore WordPress.DateTime.RestrictedFunctions -- Date is not used for display.
    328                                 'stack'           => Logger::get_stack( $this->config['extended_debugging'] ),
    329                                 'plugin_version'  => $this->config['plugin_version'],
    330                                 )
    331                         )
     288         * The fields masked out of the log, merged into what the package already masks.
     289         *
     290         * @return array
     291         */
     292        protected static function get_masked_fields() {
     293                return array(
     294                        'request'   => LogMasking::request_fields(),
     295                        'response'  => LogMasking::response_fields(),
     296                        'arguments' => array( 'api_password', 'auth_token', 'recurring_token' ),
     297                );
     298        }
     299
     300        /**
     301         * Mask the tokens Klarna addresses a resource by in the path. A session or order id is
     302         * left readable, since neither authorises anything and both are what a log is searched by.
     303         *
     304         * @param string $request_url The request URL.
     305         * @return string
     306         */
     307        protected function mask_request_url( $request_url ) {
     308                return preg_replace(
     309                        array( '#/authorizations/[^/?]+#', '#/tokens/[^/?]+#' ),
     310                        array( '/authorizations/' . KeyMasker::REDACTED, '/tokens/' . KeyMasker::REDACTED ),
     311                        $request_url
    332312                );
    333313        }
  • klarna-payments-for-woocommerce/tags/4.13.4/classes/requests/post/class-kp-unavailable-features.php

    r3664907 r3740915  
    127127                return new WP_Error( $code, $error_message );
    128128        }
    129 
    130         /**
    131          * Logs the response from the request.
    132          *
    133          * @param array|\WP_Error $response The response from the request.
    134          * @param array           $request_args The request args.
    135          * @param string          $request_url The request URL.
    136          * @return void
    137          */
    138         protected function log_response( $response, $request_args, $request_url ) {
    139                 $this->arguments['api_password'] = '[REDACTED]';
    140                 parent::log_response( $response, $request_args, $request_url );
    141         }
    142129}
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/autoload.php

    r3728366 r3740915  
    2020require_once __DIR__ . '/composer/autoload_real.php';
    2121
    22 return ComposerAutoloaderInitc3911f91ac7a4070427b96bb312df1e3::getLoader();
     22return ComposerAutoloaderInit82df93fcb7d0af6c2c95e9a438100386::getLoader();
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/composer/autoload_classmap.php

    r3728366 r3740915  
    5858    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WooCommerce\\StoreApi\\StoreApiLineShipping' => $vendorDir . '/krokedil/woocommerce/src/StoreApi/StoreApiLineShipping.php',
    5959    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WooCommerce\\Subscription' => $vendorDir . '/krokedil/woocommerce/src/Subscription.php',
     60    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\FieldMasker' => $vendorDir . '/krokedil/wp-api/src/FieldMasker.php',
     61    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\KeyMasker' => $vendorDir . '/krokedil/wp-api/src/KeyMasker.php',
    6062    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Logger' => $vendorDir . '/krokedil/wp-api/src/Logger.php',
     63    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Masking' => $vendorDir . '/krokedil/wp-api/src/Masking.php',
    6164    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Request' => $vendorDir . '/krokedil/wp-api/src/Request.php',
    6265);
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/composer/autoload_real.php

    r3728366 r3740915  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInitc3911f91ac7a4070427b96bb312df1e3
     5class ComposerAutoloaderInit82df93fcb7d0af6c2c95e9a438100386
    66{
    77    private static $loader;
    … …  
    2323        }
    2424
    25         spl_autoload_register(array('ComposerAutoloaderInitc3911f91ac7a4070427b96bb312df1e3', 'loadClassLoader'), true, true);
     25        spl_autoload_register(array('ComposerAutoloaderInit82df93fcb7d0af6c2c95e9a438100386', 'loadClassLoader'), true, true);
    2626        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
    27         spl_autoload_unregister(array('ComposerAutoloaderInitc3911f91ac7a4070427b96bb312df1e3', 'loadClassLoader'));
     27        spl_autoload_unregister(array('ComposerAutoloaderInit82df93fcb7d0af6c2c95e9a438100386', 'loadClassLoader'));
    2828
    2929        require __DIR__ . '/autoload_static.php';
    30         call_user_func(\Composer\Autoload\ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3::getInitializer($loader));
     30        call_user_func(\Composer\Autoload\ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386::getInitializer($loader));
    3131
    3232        $loader->register(true);
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/composer/autoload_static.php

    r3728366 r3740915  
    55namespace Composer\Autoload;
    66
    7 class ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3
     7class ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386
    88{
    99    public static $prefixLengthsPsr4 = array (
    … …  
    8888        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WooCommerce\\StoreApi\\StoreApiLineShipping' => __DIR__ . '/..' . '/krokedil/woocommerce/src/StoreApi/StoreApiLineShipping.php',
    8989        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WooCommerce\\Subscription' => __DIR__ . '/..' . '/krokedil/woocommerce/src/Subscription.php',
     90        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\FieldMasker' => __DIR__ . '/..' . '/krokedil/wp-api/src/FieldMasker.php',
     91        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\KeyMasker' => __DIR__ . '/..' . '/krokedil/wp-api/src/KeyMasker.php',
    9092        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Logger' => __DIR__ . '/..' . '/krokedil/wp-api/src/Logger.php',
     93        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Masking' => __DIR__ . '/..' . '/krokedil/wp-api/src/Masking.php',
    9194        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Request' => __DIR__ . '/..' . '/krokedil/wp-api/src/Request.php',
    9295    );
    … …  
    9598    {
    9699        return \Closure::bind(function () use ($loader) {
    97             $loader->prefixLengthsPsr4 = ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3::$prefixLengthsPsr4;
    98             $loader->prefixDirsPsr4 = ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3::$prefixDirsPsr4;
    99             $loader->classMap = ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3::$classMap;
     100            $loader->prefixLengthsPsr4 = ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386::$prefixLengthsPsr4;
     101            $loader->prefixDirsPsr4 = ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386::$prefixDirsPsr4;
     102            $loader->classMap = ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386::$classMap;
    100103
    101104        }, null, ClassLoader::class);
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/composer/installed.json

    r3728366 r3740915  
    199199        {
    200200            "name": "krokedil\/wp-api",
    201             "version": "1.1.1",
    202             "version_normalized": "1.1.1.0",
     201            "version": "1.2.0",
     202            "version_normalized": "1.2.0.0",
    203203            "source": {
    204204                "type": "git",
    205205                "url": "https:\/\/github.com\/krokedil\/wp-api.git",
    206                 "reference": "a3ba33f7ac640456dad36a33d711d20091c71aea"
    207             },
    208             "dist": {
    209                 "type": "zip",
    210                 "url": "https:\/\/api.github.com\/repos\/krokedil\/wp-api\/zipball\/a3ba33f7ac640456dad36a33d711d20091c71aea",
    211                 "reference": "a3ba33f7ac640456dad36a33d711d20091c71aea",
    212                 "shasum": ""
    213             },
    214             "require-dev": {
     206                "reference": "3473dd2fb6b31ed5060ecd69877c98b5287ebe48"
     207            },
     208            "dist": {
     209                "type": "zip",
     210                "url": "https:\/\/api.github.com\/repos\/krokedil\/wp-api\/zipball\/3473dd2fb6b31ed5060ecd69877c98b5287ebe48",
     211                "reference": "3473dd2fb6b31ed5060ecd69877c98b5287ebe48",
     212                "shasum": ""
     213            },
     214            "require-dev": {
     215                "dealerdirect\/phpcodesniffer-composer-installer": "^1.0",
    215216                "php": "~7.1 || ~8.0",
    216217                "php-stubs\/woocommerce-stubs": "^7.2",
    217                 "php-stubs\/wordpress-stubs": "^6.1"
    218             },
    219             "time": "2024-04-22T09:03:39+00:00",
     218                "php-stubs\/wordpress-stubs": "^6.1",
     219                "phpstan\/phpstan": "^2.0",
     220                "phpunit\/phpunit": "^9.6",
     221                "squizlabs\/php_codesniffer": "^3.13",
     222                "szepeviktor\/phpstan-wordpress": "^2.0",
     223                "wp-coding-standards\/wpcs": "^3.1"
     224            },
     225            "time": "2026-10-07T08:46:15+00:00",
    220226            "type": "library",
    221227            "installation-source": "dist",
    … …  
    225231                }
    226232            },
     233            "scripts": {
     234                "test": [
     235                    "phpunit"
     236                ],
     237                "phpcs": [
     238                    "phpcs"
     239                ],
     240                "phpcbf": [
     241                    "phpcbf"
     242                ],
     243                "phpstan": [
     244                    "phpstan analyse --memory-limit=1G"
     245                ]
     246            },
    227247            "license": [
    228248                "GPL-V2"
    … …  
    236256            "description": "A composer library for Krokedil WordPress plugins that contains classes for working with wp_remote_request to setup request classes. Also uses the WooCommerce logger to log request reponses.",
    237257            "support": {
    238                 "source": "https:\/\/github.com\/krokedil\/wp-api\/tree\/1.1.1",
     258                "source": "https:\/\/github.com\/krokedil\/wp-api\/tree\/1.2.0",
    239259                "issues": "https:\/\/github.com\/krokedil\/wp-api\/issues"
    240260            },
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/composer/installed.php

    r3728366 r3740915  
    33namespace KrokedilKlarnaPaymentsDeps;
    44
    5 return array('root' => array('name' => '__root__', 'pretty_version' => '4.13.3', 'version' => '4.13.3.0', 'reference' => '0d6c8537e224d077da2c9b85736448423cc02baf', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \true), 'versions' => array('__root__' => array('pretty_version' => '4.13.3', 'version' => '4.13.3.0', 'reference' => '0d6c8537e224d077da2c9b85736448423cc02baf', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/settings-page' => array('pretty_version' => '1.3.7', 'version' => '1.3.7.0', 'reference' => 'ef40906dd574b8c61d5831d247661a909c0463e6', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/settings-page', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/support' => array('pretty_version' => '1.0.3', 'version' => '1.0.3.0', 'reference' => 'e4ee49d1c862e45fb284247c84d7c1f5583cb6ba', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/support', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/woocommerce' => array('pretty_version' => '1.9.2', 'version' => '1.9.2.0', 'reference' => '6c38cf569df5eeb3598f206d449eca86ae9ac402', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/woocommerce', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/wp-api' => array('pretty_version' => '1.1.1', 'version' => '1.1.1.0', 'reference' => 'a3ba33f7ac640456dad36a33d711d20091c71aea', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/wp-api', 'aliases' => array(), 'dev_requirement' => \false)));
     5return array('root' => array('name' => '__root__', 'pretty_version' => '4.13.4', 'version' => '4.13.4.0', 'reference' => '5eda0fcd3d6d0288827dc8b76adf3ae1e2d5133e', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \true), 'versions' => array('__root__' => array('pretty_version' => '4.13.4', 'version' => '4.13.4.0', 'reference' => '5eda0fcd3d6d0288827dc8b76adf3ae1e2d5133e', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/settings-page' => array('pretty_version' => '1.3.7', 'version' => '1.3.7.0', 'reference' => 'ef40906dd574b8c61d5831d247661a909c0463e6', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/settings-page', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/support' => array('pretty_version' => '1.0.3', 'version' => '1.0.3.0', 'reference' => 'e4ee49d1c862e45fb284247c84d7c1f5583cb6ba', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/support', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/woocommerce' => array('pretty_version' => '1.9.2', 'version' => '1.9.2.0', 'reference' => '6c38cf569df5eeb3598f206d449eca86ae9ac402', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/woocommerce', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/wp-api' => array('pretty_version' => '1.2.0', 'version' => '1.2.0.0', 'reference' => '3473dd2fb6b31ed5060ecd69877c98b5287ebe48', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/wp-api', 'aliases' => array(), 'dev_requirement' => \false)));
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/krokedil/wp-api/CHANGELOG.md

    r3150014 r3740915  
    77
    88## [Unreleased]
     9
     10------------------
     11## [1.2.0] - 2026-10-07
     12### Added
     13* Added configurable masking for the request, the response and the request arguments, replacing the hardcoded Authorization header and `username`/`password` handling. Rules are declared as `$request_fields_to_mask`, `$response_fields_to_mask` and `$argument_fields_to_mask`, or passed as the `$masked_fields` constructor argument, where they merge into what the class already declares.
     14* Added an allow list to the masking configuration. The reserved `keep` key masks every key in a container that it does not name, so a field the provider adds later is masked by default.
     15* Added `Krokedil\WpApi\FieldMasker`, so a plugin that logs from outside a `Request` subclass can apply the same configured rules.
     16* Added `mask_request_url()` for APIs that address a resource by a token in the path. It returns the URL unchanged by default, and an override that throws costs the log line and not the API call.
     17* Added PHPUnit, PHPCS and PHPStan with `composer test`, `composer phpcs` and `composer phpstan`, and GitHub Actions workflows for pull requests.
     18* Added XML support for response bodies. `get_response_body()` parses the body by its content type: JSON is decoded, XML is parsed into an array, HTML is returned as a string. A body with no or an unknown content type is decoded as JSON when it is valid JSON, so the response masking rules still reach into it.
     19* Added log levels. `Logger::log()` writes with the WooCommerce logger at the `log_level` of the entry, `error` for a non 2xx response and for a body `status` of `error`, `warning` for a body `status` of `warning`, otherwise `info`.
     20* `WP_Error` responses from `wp_remote_request()` are now logged.
     21* Added the `krokedil_wp_api_mask_log_data` filter. Return `false` to turn the log masking off while debugging. It gets the plugin slug as its second argument. There is deliberately no setting for it.
     22* Added `Krokedil\WpApi\Masking`, with `mask_fields()`, `mask_keys()` and `is_enabled()`, so a plugin that masks data outside a `Request` subclass honours the filter too.
     23
     24### Changed
     25* `[MISSING]` now tells an empty value from a sent one on every masked field, rather than only on the Authorization header.
     26* Masking no longer decides by length whether a header holds a token. Any value a rule names is replaced, whatever its length.
     27* Data nested deeper than twelve levels is masked rather than walked.
     28* `request()` returns the body parsed by its content type rather than always through `json_decode()`. A 2xx HTML body, or a body with no or an unknown content type that is not JSON, is now returned as a string where it used to be `null`, and a body labelled JSON or XML that cannot be parsed returns a `WP_Error`.
     29* Request logs are written at the `info` level rather than the `notice` default of `WC_Logger::add()`, so a store with its log threshold at `notice` or higher no longer records successful requests.
     30
     31### Deprecated
     32* `sanitize_request_args()` is deprecated and delegates to the configured masking, keeping its behaviour.
     33
     34### Security
     35* The response body is now masked. It was logged in full, so every field the provider returned, customer details included, reached the log.
     36* Stack trace arguments are now masked. With extended debugging on, every value passed to every caller was json encoded into the log as it was. Frame arguments are positional, so a bare token with no key name still survives unless its shape gives it away.
     37* Added a key name pass over the finished log entry. `Krokedil\WpApi\KeyMasker` masks by key name wherever it appears, and by shape for an Authorization value, a JWT and a standalone base64 blob, catching what no rule describes. Consuming plugins widen the list with `KeyMasker::add_keys()`.
     38* Masking fails closed. A section that cannot be masked is logged as `[MASKING FAILED]` rather than in the clear.
    939
    1040------------------
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/krokedil/wp-api/src/Logger.php

    r3150014 r3740915  
    88namespace KrokedilKlarnaPaymentsDeps\Krokedil\WpApi;
    99
     10use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\KeyMasker;
     11use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Masking;
    1012/**
    1113 * Logger class for the api requests. Used the WooCommerce logger.
    … …  
    1416{
    1517    /**
     18     * The log levels the WooCommerce logger accepts.
     19     */
     20    const LOG_LEVELS = array('emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug');
     21    /**
    1622     * WC Logger instance.
    1723     *
    18      * @var \WC_Logger $log
     24     * @var \WC_Logger_Interface|null $log
    1925     */
    2026    public static $log;
    … …  
    2834    public static function log($slug, $log_data = array())
    2935    {
    30         // Log the data.
    3136        if (empty(self::$log)) {
    32             self::$log = new \WC_Logger();
     37            self::$log = wc_get_logger();
    3338        }
    34         self::$log->add($slug, wp_json_encode($log_data));
     39        // Read the level before masking, a failed pass replaces the whole entry.
     40        $log_level = $log_data['log_level'] ?? 'info';
     41        if (!\in_array($log_level, self::LOG_LEVELS, \true)) {
     42            $log_level = 'info';
     43        }
     44        // A failure here costs the entry, it never lets an unmasked one through.
     45        if (Masking::is_enabled($slug)) {
     46            try {
     47                $log_data = static::mask_log_data($log_data);
     48            } catch (\Throwable $e) {
     49                $log_data = array('error' => KeyMasker::FAILED);
     50            }
     51        }
     52        self::$log->log($log_level, wp_json_encode($log_data), array('source' => $slug));
     53    }
     54    /**
     55     * The last pass over the finished entry, catching anything no rule described.
     56     *
     57     * @param array $log_data The data to log.
     58     * @return mixed
     59     */
     60    protected static function mask_log_data($log_data)
     61    {
     62        return KeyMasker::mask($log_data);
    3563    }
    3664    /**
    … …  
    3866     *
    3967     * @param bool $extended_debugging Whether to include the arguments in the stack trace. This should never be turned on by default, but rather only used when extra information is needed.
     68     * @param bool $mask               Whether to mask the arguments in the stack trace.
    4069     * @return array
    4170     */
    42     public static function get_stack($extended_debugging = \false)
     71    public static function get_stack($extended_debugging = \false, $mask = \true)
    4372    {
    4473        $debug_data = \debug_backtrace();
    … …  
    4877        $count = \count($debug_data);
    4978        for ($i = 5; $i < $count; $i++) {
    50             self::process_debug_line($stack, $debug_data[$i], $extended_debugging);
     79            self::process_debug_line($stack, $debug_data[$i], $extended_debugging, $mask);
    5180        }
    5281        return $stack;
    … …  
    5887     * @param array $debug_line The debug info from the raw stack trace.
    5988     * @param bool  $extended_debugging Whether to include the arguments in the stack trace.
     89     * @param bool  $mask Whether to mask the arguments.
    6090     * @return void
    6191     */
    62     private static function process_debug_line(&$stack, $debug_line, $extended_debugging)
     92    private static function process_debug_line(&$stack, $debug_line, $extended_debugging, $mask = \true)
    6393    {
    6494        $class = $debug_line['class'] ?? '';
    … …  
    6898        self::handle_wp_hook($class, $function, $args, $debug_line);
    6999        // Construct a caller string.
    70         $caller = self::get_caller_string($class, $type, $function, $args, $extended_debugging);
     100        $caller = self::get_caller_string($class, $type, $function, $args, $extended_debugging, $mask);
    71101        $row = array('file' => $debug_line['file'] ?? '', 'line' => $debug_line['line'] ?? '', 'function' => $caller);
    72102        $stack[] = $row;
    73103    }
    74104    /**
    75      * Get the caller string from the stack trace line.
     105     * Get the caller string from the stack trace line. Argument values are masked first, but
     106     * frame arguments are positional, so there is no key name to match on: a bare token passed
     107     * as an argument survives unless its own shape gives it away. Treat these logs as sensitive.
    76108     *
    77109     * @param string $class The class name.
    … …  
    80112     * @param array  $args The arguments passed to the caller.
    81113     * @param bool   $extended_debugging Whether to include the arguments in the stack trace.
     114     * @param bool   $mask Whether to mask the arguments.
    82115     * @return string
    83116     */
    84     private static function get_caller_string($class, $type, $function, $args, $extended_debugging)
     117    private static function get_caller_string($class, $type, $function, $args, $extended_debugging, $mask = \true)
    85118    {
    86119        // Construct a caller string.
    … …  
    88121        $caller .= '(';
    89122        // Only add data if we are doing extended debugging.
    90         $caller .= $extended_debugging ? \implode(', ', \array_map(function ($value) {
     123        $caller .= $extended_debugging ? \implode(', ', \array_map(function ($value) use($mask) {
     124            try {
     125                $value = $mask ? KeyMasker::mask($value) : $value;
     126            } catch (\Throwable $e) {
     127                $value = KeyMasker::FAILED;
     128            }
    91129            // Json encode all values so that we can see what objects and arrays are passed. Dont escape anything, partial output on errors, and ignore slashes and line terminators.
    92130            return wp_json_encode($value, \JSON_UNESCAPED_UNICODE | \JSON_PARTIAL_OUTPUT_ON_ERROR | \JSON_UNESCAPED_LINE_TERMINATORS | \JSON_UNESCAPED_SLASHES);
  • klarna-payments-for-woocommerce/tags/4.13.4/dependencies/krokedil/wp-api/src/Request.php

    r3150014 r3740915  
    99
    1010use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Logger;
     11use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\KeyMasker;
     12use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Masking;
     13use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\FieldMasker;
    1114/**
    1215 * Base request class for the package.
    … …  
    1518{
    1619    /**
     20     * The reserved rule key that turns a rule into an allow list.
     21     */
     22    const MASK_KEEP = FieldMasker::KEEP;
     23    /**
    1724     * Config values.
    1825     *
    … …  
    5764    public $method = 'GET';
    5865    /**
     66     * Fields to mask in the request args before logging. Keys map to a list of field names or
     67     * to nested arrays, matched case insensitively wherever the name appears below that level.
     68     * The reserved 'keep' key turns a rule into an allow list, masking every key it does not name.
     69     *
     70     * Example:
     71     * array(
     72     *     'headers' => array( 'Authorization' ),
     73     *     'body'    => array(
     74     *         'billing_address' => array( 'email', 'phone' ),
     75     *         'customer'        => array( 'keep' => array( 'type' ) ),
     76     *         'attachment'      => 'mask',
     77     *     ),
     78     * )
     79     *
     80     * @var array
     81     */
     82    protected $request_fields_to_mask = array('headers' => array('Authorization'));
     83    /**
     84     * Fields to mask in the response body before logging. Uses the same structure as the
     85     * request fields, including the 'keep' allow list.
     86     *
     87     * Example:
     88     * array(
     89     *     'client_token',
     90     *     'billing_address' => array( 'email', 'phone' ),
     91     * )
     92     *
     93     * @var array
     94     */
     95    protected $response_fields_to_mask = array();
     96    /**
     97     * Fields to mask in the request arguments before logging, same structure as above.
     98     *
     99     * @var array
     100     */
     101    protected $argument_fields_to_mask = array('username', 'password');
     102    /**
    59103     * Constructor.
    60104     *
    61      * @param array $config Configuration array.
    62      */
    63     public function __construct($config = array(), $settings = array(), $arguments = array())
     105     * @param array $config       Configuration array.
     106     * @param array $settings     Plugin settings.
     107     * @param array $arguments    Request arguments.
     108     * @param array $masked_fields Optional extra fields to mask, with keys 'request', 'response' and/or 'arguments'.
     109     *                             Merged into the configured fields, so an allow list can be passed here as well.
     110     */
     111    public function __construct($config = array(), $settings = array(), $arguments = array(), $masked_fields = array())
    64112    {
    65113        $this->config = wp_parse_args($config, $this->defaults);
    66114        $this->settings = $settings;
    67115        $this->arguments = $arguments;
     116        if (!empty($masked_fields['request'])) {
     117            $this->request_fields_to_mask = FieldMasker::merge_config($this->request_fields_to_mask, $masked_fields['request']);
     118        }
     119        if (!empty($masked_fields['response'])) {
     120            $this->response_fields_to_mask = FieldMasker::merge_config($this->response_fields_to_mask, $masked_fields['response']);
     121        }
     122        if (!empty($masked_fields['arguments'])) {
     123            $this->argument_fields_to_mask = FieldMasker::merge_config($this->argument_fields_to_mask, $masked_fields['arguments']);
     124        }
    68125    }
    69126    /**
    … …  
    125182    {
    126183        if (is_wp_error($response)) {
     184            $this->log_response($response, $request_args, $request_url);
    127185            return $response;
    128186        }
    … …  
    131189            $return = $this->get_error_message($response);
    132190        } else {
    133             $return = \json_decode(wp_remote_retrieve_body($response), \true);
     191            $return = $this->get_response_body($response);
    134192        }
    135193        $this->log_response($response, $request_args, $request_url);
    … …  
    150208        }
    151209        // Get the response body if its not a WP_Error.
    152         $response_body = !is_wp_error($response) ? \json_decode(wp_remote_retrieve_body($response), \true) : array();
     210        $response_body = !is_wp_error($response) ? $this->get_response_body($response) : array();
    153211        $code = wp_remote_retrieve_response_code($response);
    154         // Parse the Request body into an array if its json format.
    155         $request_body = $request_args['body'] ?? '';
    156         $decoded_body = \json_decode($request_body);
    157         $request_args['body'] = $decoded_body ?? $request_args['body'] ?? null;
    158         $request_args = $this->sanitize_request_args($request_args);
    159         $arguments = $this->arguments;
    160         if (isset($arguments['username'])) {
    161             $arguments['username'] = '[REDACTED]';
    162         }
    163         if (isset($arguments['password'])) {
    164             $arguments['password'] = '[REDACTED]';
     212        // Set log level. Read from the body before it is masked. A body that failed to parse is a WP_Error.
     213        $body_status = \is_array($response_body) ? $response_body['status'] ?? null : null;
     214        if ($code < 200 || $code > 299 || is_wp_error($response_body)) {
     215            $log_level = 'error';
     216        } elseif ('error' === $body_status) {
     217            // Some APIs (e.g. Fraktjakt) return a successful HTTP status while reporting an
     218            // application-level error in the response body. Treat those as errors too.
     219            $log_level = 'error';
     220        } elseif ('warning' === $body_status) {
     221            $log_level = 'warning';
     222        } else {
     223            $log_level = 'info';
     224        }
     225        $mask = Masking::is_enabled($this->config['slug']);
     226        if ($mask) {
     227            $response_body = $this->mask_response($response_body);
     228            $arguments = $this->mask_arguments($this->arguments);
     229            $request_url = $this->get_masked_request_url($request_url);
     230            $request_args = $this->mask_request_args($request_args);
     231        } else {
     232            $arguments = $this->arguments;
     233            if (isset($request_args['body']) && \is_string($request_args['body'])) {
     234                $request_args['body'] = $this->decode_body($request_args['body']);
     235            }
    165236        }
    166237        // Log the response.
    … …  
    172243            'request_url' => $request_url,
    173244            'response' => array('body' => $response_body, 'code' => $code),
     245            'log_level' => $log_level,
    174246            'timestamp' => \date('Y-m-d H:i:s'),
    175247            // phpcs:ignore WordPress.DateTime.RestrictedFunctions -- Date is not used for display.
    176             'stack' => Logger::get_stack($this->config['extended_debugging']),
     248            'stack' => Logger::get_stack($this->config['extended_debugging'], $mask),
    177249            'plugin_version' => $this->config['plugin_version'],
    178250        ));
    179251    }
    180252    /**
     253     * Returns the body of the response based on the content type.
     254     *
     255     * @param array|\WP_Error $response The response from wp_remote_get() or wp_remote_post().
     256     * @return array|object|string|\WP_Error Parsed response body or WP_Error if an error occurs.
     257     */
     258    public function get_response_body($response)
     259    {
     260        if (is_wp_error($response)) {
     261            return $response;
     262        }
     263        $content_type = wp_remote_retrieve_header($response, 'content-type');
     264        $content_type = \is_array($content_type) ? \implode(', ', $content_type) : (string) $content_type;
     265        $body = wp_remote_retrieve_body($response);
     266        if (\strpos($content_type, 'application/json') !== \false) {
     267            $parsed_body = \json_decode($body, \true);
     268            if (\json_last_error() === \JSON_ERROR_NONE) {
     269                return $parsed_body;
     270            }
     271            return new \WP_Error('json_decode_error', 'Failed to decode JSON');
     272        } elseif (\strpos($content_type, 'text/html') !== \false) {
     273            return $body;
     274        } elseif (\strpos($content_type, 'application/xml') !== \false || \strpos($content_type, 'text/xml') !== \false) {
     275            // Collect parse errors rather than emitting a PHP warning per malformed node.
     276            $use_errors = \libxml_use_internal_errors(\true);
     277            $parsed_body = \simplexml_load_string($body);
     278            \libxml_clear_errors();
     279            \libxml_use_internal_errors($use_errors);
     280            if (\false !== $parsed_body) {
     281                return \json_decode(wp_json_encode($parsed_body), \true);
     282            }
     283            return new \WP_Error('xml_parse_error', 'Failed to parse XML');
     284        } else {
     285            // No or an unknown content type. Decode json anyway, so a body the API did not label
     286            // is still returned as an array and the response masking rules can reach into it.
     287            $parsed_body = \json_decode($body, \true);
     288            return \is_array($parsed_body) ? $parsed_body : $body;
     289        }
     290    }
     291    /**
     292     * Mask sensitive fields in the request args before logging. Reading the configuration
     293     * happens inside the guard, so a broken config costs the section and not the request.
     294     *
     295     * @param array $request_args The request args.
     296     * @return array|string The masked request args, or the failure marker.
     297     */
     298    protected function mask_request_args($request_args)
     299    {
     300        try {
     301            // Decode the body that was really sent, so the rules can reach into it and the log
     302            // holds a readable structure rather than an escaped string. A body no rule can reach
     303            // into is masked whole by a configured body rule, see FieldMasker.
     304            if (isset($request_args['body']) && \is_string($request_args['body'])) {
     305                $request_args['body'] = $this->decode_body($request_args['body']);
     306            }
     307            $fields = $this->request_fields_to_mask;
     308            return empty($fields) ? $request_args : $this->sanitize_field($request_args, $fields);
     309        } catch (\Throwable $e) {
     310            return KeyMasker::FAILED;
     311        }
     312    }
     313    /**
     314     * Decode a json or form encoded body string into an array, or return it as it was.
     315     *
     316     * @param string $body The body string.
     317     * @return array|string
     318     */
     319    private function decode_body($body)
     320    {
     321        $decoded = \json_decode($body, \true);
     322        if (\is_array($decoded)) {
     323            return $decoded;
     324        }
     325        // What http_build_query() produces: key=value pairs joined by '&', nothing else.
     326        if (\preg_match('/^[^=&\\s]+=[^&\\s]*(?:&[^=&\\s]+=[^&\\s]*)*$/', $body)) {
     327            \parse_str($body, $decoded);
     328            return $decoded;
     329        }
     330        return $body;
     331    }
     332    /**
     333     * Mask sensitive fields in the response body before logging.
     334     *
     335     * @param array|\WP_Error $response The decoded response body.
     336     * @return array|string|\WP_Error The masked response body, or the failure marker.
     337     */
     338    protected function mask_response($response)
     339    {
     340        if (is_wp_error($response) || empty($response)) {
     341            return $response;
     342        }
     343        try {
     344            $fields = $this->response_fields_to_mask;
     345            return empty($fields) ? $response : $this->sanitize_field($response, $fields);
     346        } catch (\Throwable $e) {
     347            return KeyMasker::FAILED;
     348        }
     349    }
     350    /**
     351     * Mask sensitive fields in the request arguments before logging.
     352     *
     353     * @param array $arguments The request arguments.
     354     * @return array|string The masked arguments, or the failure marker.
     355     */
     356    protected function mask_arguments($arguments)
     357    {
     358        try {
     359            $fields = $this->argument_fields_to_mask;
     360            return empty($fields) ? $arguments : $this->sanitize_field($arguments, $fields);
     361        } catch (\Throwable $e) {
     362            return KeyMasker::FAILED;
     363        }
     364    }
     365    /**
     366     * Mask the request URL before logging, unchanged by default. Override it when the API
     367     * addresses a resource by a token in the path, such as /authorizations/{token}/order.
     368     *
     369     * @param string $request_url The request URL.
     370     * @return string
     371     */
     372    protected function mask_request_url($request_url)
     373    {
     374        return $request_url;
     375    }
     376    /**
     377     * Call mask_request_url() without letting an override that throws kill the API call.
     378     * The fallback is a redaction, never the raw URL.
     379     *
     380     * @param string $request_url The request URL.
     381     * @return string
     382     */
     383    private function get_masked_request_url($request_url)
     384    {
     385        try {
     386            $masked = $this->mask_request_url($request_url);
     387            return \is_string($masked) ? $masked : KeyMasker::FAILED;
     388        } catch (\Throwable $e) {
     389            return KeyMasker::FAILED;
     390        }
     391    }
     392    /**
     393     * Recursively mask a set of fields within a data array. A rule keeps matching below the
     394     * level it was declared on, so a container nested in a list is treated like one at the root.
     395     *
     396     * @param array $data               The data array to sanitize.
     397     * @param array $fields_to_sanitize The fields to sanitize.
     398     * @return array
     399     */
     400    protected function sanitize_field($data, $fields_to_sanitize)
     401    {
     402        return FieldMasker::mask($data, $fields_to_sanitize);
     403    }
     404    /**
    181405     * Remove sensitive data from the log.
    182406     *
     407     * @deprecated Use mask_request_args() instead. Kept for backward compatibility.
    183408     * @param array $request_args The request data to sanitize.
    184      * @return array The request data sanitized.
     409     * @return array|string The request data sanitized, or the failure marker.
    185410     */
    186411    protected function sanitize_request_args($request_args)
    187412    {
    188         // Do not log the authorization token.
    189         foreach ($request_args['headers'] as $header => $value) {
    190             if ('authorization' === \strtolower($header)) {
    191                 // If it is longer than 15 char., it most likely has a token. This is an assumption that is safe even if it is wrong.
    192                 $request_args['headers'][$header] = \strlen($value) > 15 ? '[REDACTED]' : '[MISSING]';
    193                 break;
    194             }
    195         }
    196         return $request_args;
     413        return $this->mask_request_args($request_args);
    197414    }
    198415    /**
  • klarna-payments-for-woocommerce/tags/4.13.4/klarna-payments-for-woocommerce.php

    r3728366 r3740915  
    66 * Author: klarna
    77 * Author URI: https://www.klarna.com/
    8  * Version: 4.13.3
     8 * Version: 4.13.4
    99 * Text Domain: klarna-payments-for-woocommerce
    1010 * Domain Path: /languages
    1111 *
    1212 * WC requires at least: 5.6.0
    13  * WC tested up to: 11.1.2
     13 * WC tested up to: 11.2.1
    1414 * Requires Plugins: woocommerce
    1515 *
    … …  
    3838use Krokedil\Klarna\ExpressCheckout;
    3939use Krokedil\Klarna\Utilities\ApiCredentialsUtility;
     40use Krokedil\Klarna\Logging\LogMasking;
     41use Krokedil\Klarna\Logging\LogWriter;
    4042
    4143if ( ! defined( 'ABSPATH' ) ) {
    … …  
    5355 * Required minimums and constants
    5456 */
    55 define( 'WC_KLARNA_PAYMENTS_VERSION', '4.13.3' );
     57define( 'WC_KLARNA_PAYMENTS_VERSION', '4.13.4' );
    5658define( 'WC_KLARNA_PAYMENTS_MIN_PHP_VER', '7.4.0' );
    5759define( 'WC_KLARNA_PAYMENTS_MIN_WC_VER', '5.6.0' );
    … …  
    331333                        $this->order_management        = new OrderManagement();
    332334                        $this->logger                  = new Logger( 'klarna_payments', 'no' !== ( $settings['logging'] ?? 'no' ) );
     335
     336                        // Masking has to be in place before anything can log.
     337                        LogMasking::register();
     338                        LogWriter::register();
    333339                        Compatibility::register();
    334340
  • klarna-payments-for-woocommerce/tags/4.13.4/languages/klarna-payments-for-woocommerce.pot

    r3728366 r3740915  
    66"Report-Msgid-Bugs-To: "
    77"https://wordpress.org/support/plugin/klarna-payments-for-woocommerce\n"
    8 "POT-Creation-Date: 2026-10-05 08:10:20+00:00\n"
     8"POT-Creation-Date: 2026-10-12 09:04:01+00:00\n"
    99"MIME-Version: 1.0\n"
    1010"Content-Type: text/plain; charset=utf-8\n"
    … …  
    513513msgstr ""
    514514
    515 #: classes/class-kp-assets.php:250
     515#: classes/class-kp-assets.php:283
    516516#. translators: [customer-facing].
    517517msgid "Please read and accept the terms and conditions to proceed with your order."
    518518msgstr ""
    519519
    520 #: classes/class-kp-assets.php:301
     520#: classes/class-kp-assets.php:334
    521521#. translators: [merchant-facing].
    522522msgid "Select all"
    … …  
    728728#: dependencies/krokedil/settings-page/src/Navigation.php:54
    729729#: dependencies/krokedil/settings-page/src/Support.php:36
    730 #: klarna-payments-for-woocommerce.php:449
     730#: klarna-payments-for-woocommerce.php:455
    731731#. translators: [merchant-facing].
    732732msgid "Support"
    … …  
    879879msgstr ""
    880880
    881 #: includes/kp-functions.php:738 klarna-payments-for-woocommerce.php:665
     881#: includes/kp-functions.php:738 klarna-payments-for-woocommerce.php:671
    882882#. translators: [customer-facing].
    883883msgid "Pay with Klarna"
    884884msgstr ""
    885885
    886 #: klarna-payments-for-woocommerce.php:445
     886#: klarna-payments-for-woocommerce.php:451
    887887#. translators: [merchant-facing].
    888888msgid "Settings"
    889889msgstr ""
    890890
    891 #: klarna-payments-for-woocommerce.php:447
     891#: klarna-payments-for-woocommerce.php:453
    892892#. translators: [merchant-facing].
    893893msgid "Docs"
    894894msgstr ""
    895895
    896 #: klarna-payments-for-woocommerce.php:491
     896#: klarna-payments-for-woocommerce.php:497
    897897#. translators: [merchant-facing].
    898898msgid "The WooCommerce plugin must be active for Klarna Payments to work."
    899899msgstr ""
    900900
    901 #: klarna-payments-for-woocommerce.php:510
     901#: klarna-payments-for-woocommerce.php:516
    902902#. translators: [merchant-facing].
    903903msgid "Dismiss"
    904904msgstr ""
    905905
    906 #: klarna-payments-for-woocommerce.php:517
     906#: klarna-payments-for-woocommerce.php:523
    907907#. translators: [merchant-facing]. URL to docs.
    908908msgid ""
    … …  
    912912msgstr ""
    913913
    914 #: klarna-payments-for-woocommerce.php:611
    915 #: klarna-payments-for-woocommerce.php:620
     914#: klarna-payments-for-woocommerce.php:617
     915#: klarna-payments-for-woocommerce.php:626
    916916msgid ""
    917917"Your installation of Klarna Payments is not complete. If you installed this "
    … …  
    920920msgstr ""
    921921
    922 #: klarna-payments-for-woocommerce.php:655
     922#: klarna-payments-for-woocommerce.php:661
    923923#. translators: [customer-facing].
    924924msgid "Pay with Klarna (free)"
    … …  
    942942
    943943#: src/ExpressCheckout/Api/Notifications/PaymentStateExpired.php:43
    944 #: src/ExpressCheckout/KECOneStepIntegration.php:101
     944#: src/ExpressCheckout/KECOneStepIntegration.php:102
    945945#. translators: [merchant-facing].
    946946msgid "Order cancelled due to expired payment request."
    … …  
    14781478msgstr ""
    14791479
    1480 #: src/OrderManagement/Request/Request.php:164
     1480#: src/OrderManagement/Request/Request.php:165
    14811481#. translators: [merchant-facing].
    14821482msgid "Invalid JSON response from the server."
  • klarna-payments-for-woocommerce/tags/4.13.4/readme.txt

    r3728366 r3740915  
    77Requires PHP: 8.0
    88WC requires at least: 5.6.0
    9 WC tested up to: 11.1.2
    10 Stable tag: 4.13.3
     9WC tested up to: 11.2.1
     10Stable tag: 4.13.4
    1111License: GPLv3 or later
    1212License URI: http://www.gnu.org/licenses/gpl-3.0.html
    … …  
    7070
    7171== Changelog ==
     72= 2026-10-12    - version 4.13.4 =
     73* Enhancement   - Masked customer personal, and other sensitive data and credentials in the plugin log.
     74* Fix           - Order details are now only added to the pay for order page when WooCommerce would show the payment form: the request carries the order's key, the customer may pay for the order, the order still needs payment, and a guest has confirmed the billing email.
     75
    7276= 2026-10-05    - version 4.13.3 =
    7377* Enhancement   - Improved handling of Klarna authorization callbacks and background processing.
  • klarna-payments-for-woocommerce/tags/4.13.4/src/ExpressCheckout/KECOneStepIntegration.php

    r3716655 r3740915  
    9494
    9595                if ( ! $payment_request_id || ! $interoperability_token ) {
    96                         KP_WC()->logger()->error( '[KEC One Step] Missing required fields in payload for expired payment: ' . wp_json_encode( $payload ) );
     96                        $missing = $payment_request_id ? 'interoperability_token' : 'payment_request_id';
     97                        KP_WC()->logger()->error( "[KEC One Step] Missing {$missing} in the payload for expired payment on order {$order->get_id()}." );
    9798                        return;
    9899                }
  • klarna-payments-for-woocommerce/tags/4.13.4/src/OrderManagement/Request/Request.php

    r3664907 r3740915  
    33
    44use Krokedil\Klarna\OrderManagement;
     5use Krokedil\Klarna\Logging\LogMasking;
    56use Krokedil\Klarna\Utilities\ApiCredentialsUtility;
    67
    … …  
    355356         */
    356357        protected function log_response( $response, $request_args, $request_url, $code ) {
    357                 foreach ( $request_args['headers'] as $header => $value ) {
    358                         if ( 'authorization' === strtolower( $header ) ) {
    359                                 // If it is longer than 15 char., it most likely has a token. This is an assumption that is safe even if it is wrong.
    360                                 $request_args['headers'][ $header ] = strlen( $value ) > 15 ? '[REDACTED]' : '[MISSING]';
    361                                 break;
    362                         }
    363                 }
    364                 $log = \KP_Logger::format_log( $this->klarna_order_id, $this->method, $this->log_title, $request_args, $response, $code, $request_url );
     358                // Decode the body so the masking can reach into it, and drop the rest of the HTTP
     359                // response, which holds nothing a log needs and hides the payload inside a string.
     360                $body = is_wp_error( $response ) ? array() : json_decode( wp_remote_retrieve_body( $response ), true );
     361
     362                $request_args = LogMasking::mask_request( $request_args );
     363                $body         = LogMasking::mask_response( $body );
     364
     365                $log = \KP_Logger::format_log( $this->klarna_order_id, $this->method, $this->log_title, $request_args, $body, $code, $request_url );
    365366                \KP_Logger::log( $log );
    366367        }
  • klarna-payments-for-woocommerce/tags/4.13.4/vendor/composer/autoload_classmap.php

    r3664907 r3740915  
    4444    'Krokedil\\Klarna\\ExpressCheckout\\WebhookSetup' => $baseDir . '/src/ExpressCheckout/WebhookSetup.php',
    4545    'Krokedil\\Klarna\\Features' => $baseDir . '/src/Features.php',
     46    'Krokedil\\Klarna\\Logging\\LogMasking' => $baseDir . '/src/Logging/LogMasking.php',
     47    'Krokedil\\Klarna\\Logging\\LogWriter' => $baseDir . '/src/Logging/LogWriter.php',
    4648    'Krokedil\\Klarna\\OnsiteMessaging\\Blocks\\CartBlockIntegration' => $baseDir . '/src/OnsiteMessaging/Blocks/CartBlockIntegration.php',
    4749    'Krokedil\\Klarna\\OnsiteMessaging\\KlarnaOnsiteMessaging' => $baseDir . '/src/OnsiteMessaging/KlarnaOnsiteMessaging.php',
  • klarna-payments-for-woocommerce/tags/4.13.4/vendor/composer/autoload_static.php

    r3696456 r3740915  
    6767        'Krokedil\\Klarna\\ExpressCheckout\\WebhookSetup' => __DIR__ . '/../..' . '/src/ExpressCheckout/WebhookSetup.php',
    6868        'Krokedil\\Klarna\\Features' => __DIR__ . '/../..' . '/src/Features.php',
     69        'Krokedil\\Klarna\\Logging\\LogMasking' => __DIR__ . '/../..' . '/src/Logging/LogMasking.php',
     70        'Krokedil\\Klarna\\Logging\\LogWriter' => __DIR__ . '/../..' . '/src/Logging/LogWriter.php',
    6971        'Krokedil\\Klarna\\OnsiteMessaging\\Blocks\\CartBlockIntegration' => __DIR__ . '/../..' . '/src/OnsiteMessaging/Blocks/CartBlockIntegration.php',
    7072        'Krokedil\\Klarna\\OnsiteMessaging\\KlarnaOnsiteMessaging' => __DIR__ . '/../..' . '/src/OnsiteMessaging/KlarnaOnsiteMessaging.php',
  • klarna-payments-for-woocommerce/tags/4.13.4/vendor/composer/installed.php

    r3728366 r3740915  
    22    'root' => array(
    33        'name' => '__root__',
    4         'pretty_version' => '4.13.3',
    5         'version' => '4.13.3.0',
    6         'reference' => '0d6c8537e224d077da2c9b85736448423cc02baf',
     4        'pretty_version' => '4.13.4',
     5        'version' => '4.13.4.0',
     6        'reference' => '5eda0fcd3d6d0288827dc8b76adf3ae1e2d5133e',
    77        'type' => 'library',
    88        'install_path' => __DIR__ . '/../../',
    … …  
    1212    'versions' => array(
    1313        '__root__' => array(
    14             'pretty_version' => '4.13.3',
    15             'version' => '4.13.3.0',
    16             'reference' => '0d6c8537e224d077da2c9b85736448423cc02baf',
     14            'pretty_version' => '4.13.4',
     15            'version' => '4.13.4.0',
     16            'reference' => '5eda0fcd3d6d0288827dc8b76adf3ae1e2d5133e',
    1717            'type' => 'library',
    1818            'install_path' => __DIR__ . '/../../',
  • klarna-payments-for-woocommerce/trunk/changelog.txt

    r3728366 r3740915  
    11*** Klarna for WooCommerce Changelog ***
     22026-10-12      - version 4.13.4
     3* Enhancement   - Masked customer personal, and other sensitive data and credentials in the plugin log.
     4* Fix           - Order details are now only added to the pay for order page when WooCommerce would show the payment form: the request carries the order's key, the customer may pay for the order, the order still needs payment, and a guest has confirmed the billing email.
     5
    262026-10-05      - version 4.13.3
    37* Enhancement   - Improved handling of Klarna authorization callbacks and background processing.
  • klarna-payments-for-woocommerce/trunk/classes/class-kp-ajax.php

    r3728366 r3740915  
    100100                                                wp_send_json_success( $order->get_checkout_order_received_url() );
    101101                                        } else {
    102                                                 KP_Logger::log( sprintf( '[AJAX]: Order ID: %s. Auth token: %s. %s', $order->get_id(), $auth_token, $recurring_token->get_error_message() ) );
     102                                                KP_Logger::log( sprintf( '[AJAX]: Order ID: %s. Failed to create a recurring token: %s', $order->get_id(), $recurring_token->get_error_message() ) );
    103103                                                wp_send_json_error( 'customer_token_failed' );
    104104                                        }
  • klarna-payments-for-woocommerce/trunk/classes/class-kp-assets.php

    r3716655 r3740915  
    66 */
    77
     8use Automattic\WooCommerce\Internal\Utilities\Users;
    89use Krokedil\Klarna\Features;
    910use Krokedil\Klarna\PluginFeatures;
    … …  
    188189
    189190        /**
     191         * Whether WooCommerce asks the guest to confirm the billing email before showing the order-pay form.
     192         *
     193         * @param WC_Order $order The order being paid for.
     194         * @return bool
     195         */
     196        private function guest_should_verify_email( $order ) {
     197                // WooCommerce versions without the check never ask for verification.
     198                if ( ! is_callable( array( Users::class, 'should_user_verify_order_email' ) ) ) {
     199                        return false;
     200                }
     201
     202                // Mirrors WC_Shortcode_Checkout::guest_should_verify_email(), which accepts the email posted by its verify form.
     203                $supplied_email = null;
     204                if ( wp_verify_nonce( (string) filter_input( INPUT_POST, 'check_submission' ), 'wc_verify_email' ) ) {
     205                        $supplied_email = sanitize_email( wp_unslash( (string) filter_input( INPUT_POST, 'email' ) ) );
     206                }
     207
     208                return Users::should_user_verify_order_email( $order->get_id(), $supplied_email, 'order-pay' );
     209        }
     210
     211        /**
    190212         * Get the params for the Klarna Payments checkout script.
    191213         *
    … …  
    200222
    201223                if ( ! empty( $order_id ) ) {
    202                         $order = wc_get_order( $order_id );
    203                         if ( ! empty( $order ) ) {
     224                        $order       = wc_get_order( $order_id );
     225                        $request_key = (string) filter_input( INPUT_GET, 'key', FILTER_SANITIZE_SPECIAL_CHARS );
     226
     227                        // Only expose the order when WooCommerce would render the pay form, so the same checks in the same order.
     228                        if (
     229                                ! empty( $order )
     230                                && hash_equals( $order->get_order_key(), $request_key )
     231                                && current_user_can( 'pay_for_order', $order_id )
     232                                && $order->needs_payment()
     233                                && ! $this->guest_should_verify_email( $order )
     234                        ) {
    204235                                $order_key = $order->get_order_key();
     236                        } else {
     237                                $order_id = null;
    205238                        }
    206239                }
    … …  
    240273                        'order_key'               => $order_key,
    241274                        'submit_button_selectors' => array( 'input#place_order', 'button#place_order' ),
    242                         'addresses'               => $pay_for_order ? array(
     275                        'addresses'               => null !== $order_key ? array(
    243276                                'billing'  => $customer['billing'],
    244277                                'shipping' => $customer['shipping'],
  • klarna-payments-for-woocommerce/trunk/classes/class-kp-callbacks.php

    r3728366 r3740915  
    241241                                        $order->add_order_note( __( 'Failed to create a recurring token when returning from the hosted payment page.', 'klarna-payments-for-woocommerce' ) . $recurring_token->get_error_message() );
    242242
    243                                         KP_Logger::log( sprintf( '[AJAX]: Order ID: %s. Auth token: %s. %s', $order->get_id(), $auth_token, $recurring_token->get_error_message() ) );
     243                                        KP_Logger::log( sprintf( '[AJAX]: Order ID: %s. Failed to create a recurring token: %s', $order->get_id(), $recurring_token->get_error_message() ) );
    244244                                }
    245245
  • klarna-payments-for-woocommerce/trunk/classes/class-kp-logger.php

    r3716655 r3740915  
    99        exit; // Exit if accessed directly.
    1010}
     11
     12use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\KeyMasker;
    1113
    1214/**
    … …  
    2830        public static function log( $data ) {
    2931                $kp_settings = get_option( 'woocommerce_klarna_payments_settings', array() );
    30                 if ( 'no' !== ( $kp_settings['logging'] ?? 'no' ) ) {
    31                         $message = self::format_data( $data );
    32                         KP_WC()->logger()->info( wp_json_encode( $message ) );
     32                if ( 'no' === ( $kp_settings['logging'] ?? 'no' ) ) {
     33                        return;
    3334                }
     35
     36                $message = self::format_data( $data );
     37
     38                // A failure here costs the entry, it never lets an unmasked one through.
     39                try {
     40                        $message = KeyMasker::mask( self::format_data( $data ) );
     41                } catch ( \Throwable $e ) {
     42                        $message = array( 'error' => KeyMasker::FAILED );
     43                }
     44
     45                KP_WC()->logger()->info( wp_json_encode( $message ) );
    3446        }
    3547
    … …  
    4153         */
    4254        public static function format_data( $data ) {
    43                 if ( isset( $data['request']['body'] ) ) {
     55                if ( isset( $data['request']['body'] ) && is_string( $data['request']['body'] ) ) {
    4456                        $request_body            = json_decode( $data['request']['body'], true );
    4557                        $data['request']['body'] = $request_body;
  • klarna-payments-for-woocommerce/trunk/classes/requests/class-kp-requests.php

    r3664907 r3740915  
    99
    1010use Krokedil\Klarna\Utilities\ApiCredentialsUtility;
     11use Krokedil\Klarna\Logging\LogMasking;
     12use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\KeyMasker;
    1113use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Request;
    12 use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Logger;
    1314
    1415/**
    … …  
    8788                );
    8889
    89                 parent::__construct( $config, $settings, $arguments );
     90                parent::__construct( $config, $settings, $arguments, self::get_masked_fields() );
    9091                $this->set_credentials();
    9192                $this->iframe_options = new KP_IFrame( $settings );
    … …  
    285286
    286287        /**
    287          * Logs the response from the request.
    288          *
    289          * @param array|\WP_Error $response The response from the request.
    290          * @param array           $request_args The request args.
    291          * @param string          $request_url The request URL.
    292          *
    293          * @return void
    294          */
    295         protected function log_response( $response, $request_args, $request_url ) {
    296                 $code = wp_remote_retrieve_response_code( $response );
    297 
    298                 // Get the response body if its not a WP_Error.
    299                 $response_body = is_wp_error( $response ) ? array() : json_decode( wp_remote_retrieve_body( $response ), true );
    300 
    301                 // Parse the Request body into an array if its json format.
    302                 $request_body         = $request_args['body'] ?? '';
    303                 $decoded_body         = json_decode( $request_body );
    304                 $request_args['body'] = $decoded_body ?? $request_args['body'] ?? null;
    305                 $request_args         = $this->sanitize_request_args( $request_args );
    306 
    307                 $arguments = $this->arguments;
    308                 if ( isset( $arguments['username'] ) ) {
    309                         $arguments['username'] = '[REDACTED]';
    310                 }
    311                 if ( isset( $arguments['password'] ) ) {
    312                         $arguments['password'] = '[REDACTED]';
    313                 }
    314 
    315                 KP_WC()->logger()->info(
    316                         wp_json_encode(
    317                                 array(
    318                                         'type'        => $this->method,
    319                                         'title'       => $this->log_title,
    320                                         'arguments'   => $arguments,
    321                                         'request'     => $request_args,
    322                                         'request_url' => $request_url,
    323                                         'response'    => array(
    324                                         'body' => $response_body,
    325                                         'code' => $code,
    326                                         ),
    327                                         'timestamp'   => date( 'Y-m-d H:i:s' ),    // phpcs:ignore WordPress.DateTime.RestrictedFunctions -- Date is not used for display.
    328                                 'stack'           => Logger::get_stack( $this->config['extended_debugging'] ),
    329                                 'plugin_version'  => $this->config['plugin_version'],
    330                                 )
    331                         )
     288         * The fields masked out of the log, merged into what the package already masks.
     289         *
     290         * @return array
     291         */
     292        protected static function get_masked_fields() {
     293                return array(
     294                        'request'   => LogMasking::request_fields(),
     295                        'response'  => LogMasking::response_fields(),
     296                        'arguments' => array( 'api_password', 'auth_token', 'recurring_token' ),
     297                );
     298        }
     299
     300        /**
     301         * Mask the tokens Klarna addresses a resource by in the path. A session or order id is
     302         * left readable, since neither authorises anything and both are what a log is searched by.
     303         *
     304         * @param string $request_url The request URL.
     305         * @return string
     306         */
     307        protected function mask_request_url( $request_url ) {
     308                return preg_replace(
     309                        array( '#/authorizations/[^/?]+#', '#/tokens/[^/?]+#' ),
     310                        array( '/authorizations/' . KeyMasker::REDACTED, '/tokens/' . KeyMasker::REDACTED ),
     311                        $request_url
    332312                );
    333313        }
  • klarna-payments-for-woocommerce/trunk/classes/requests/post/class-kp-unavailable-features.php

    r3664907 r3740915  
    127127                return new WP_Error( $code, $error_message );
    128128        }
    129 
    130         /**
    131          * Logs the response from the request.
    132          *
    133          * @param array|\WP_Error $response The response from the request.
    134          * @param array           $request_args The request args.
    135          * @param string          $request_url The request URL.
    136          * @return void
    137          */
    138         protected function log_response( $response, $request_args, $request_url ) {
    139                 $this->arguments['api_password'] = '[REDACTED]';
    140                 parent::log_response( $response, $request_args, $request_url );
    141         }
    142129}
  • klarna-payments-for-woocommerce/trunk/dependencies/autoload.php

    r3728366 r3740915  
    2020require_once __DIR__ . '/composer/autoload_real.php';
    2121
    22 return ComposerAutoloaderInitc3911f91ac7a4070427b96bb312df1e3::getLoader();
     22return ComposerAutoloaderInit82df93fcb7d0af6c2c95e9a438100386::getLoader();
  • klarna-payments-for-woocommerce/trunk/dependencies/composer/autoload_classmap.php

    r3728366 r3740915  
    5858    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WooCommerce\\StoreApi\\StoreApiLineShipping' => $vendorDir . '/krokedil/woocommerce/src/StoreApi/StoreApiLineShipping.php',
    5959    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WooCommerce\\Subscription' => $vendorDir . '/krokedil/woocommerce/src/Subscription.php',
     60    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\FieldMasker' => $vendorDir . '/krokedil/wp-api/src/FieldMasker.php',
     61    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\KeyMasker' => $vendorDir . '/krokedil/wp-api/src/KeyMasker.php',
    6062    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Logger' => $vendorDir . '/krokedil/wp-api/src/Logger.php',
     63    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Masking' => $vendorDir . '/krokedil/wp-api/src/Masking.php',
    6164    'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Request' => $vendorDir . '/krokedil/wp-api/src/Request.php',
    6265);
  • klarna-payments-for-woocommerce/trunk/dependencies/composer/autoload_real.php

    r3728366 r3740915  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInitc3911f91ac7a4070427b96bb312df1e3
     5class ComposerAutoloaderInit82df93fcb7d0af6c2c95e9a438100386
    66{
    77    private static $loader;
    … …  
    2323        }
    2424
    25         spl_autoload_register(array('ComposerAutoloaderInitc3911f91ac7a4070427b96bb312df1e3', 'loadClassLoader'), true, true);
     25        spl_autoload_register(array('ComposerAutoloaderInit82df93fcb7d0af6c2c95e9a438100386', 'loadClassLoader'), true, true);
    2626        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
    27         spl_autoload_unregister(array('ComposerAutoloaderInitc3911f91ac7a4070427b96bb312df1e3', 'loadClassLoader'));
     27        spl_autoload_unregister(array('ComposerAutoloaderInit82df93fcb7d0af6c2c95e9a438100386', 'loadClassLoader'));
    2828
    2929        require __DIR__ . '/autoload_static.php';
    30         call_user_func(\Composer\Autoload\ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3::getInitializer($loader));
     30        call_user_func(\Composer\Autoload\ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386::getInitializer($loader));
    3131
    3232        $loader->register(true);
  • klarna-payments-for-woocommerce/trunk/dependencies/composer/autoload_static.php

    r3728366 r3740915  
    55namespace Composer\Autoload;
    66
    7 class ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3
     7class ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386
    88{
    99    public static $prefixLengthsPsr4 = array (
    … …  
    8888        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WooCommerce\\StoreApi\\StoreApiLineShipping' => __DIR__ . '/..' . '/krokedil/woocommerce/src/StoreApi/StoreApiLineShipping.php',
    8989        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WooCommerce\\Subscription' => __DIR__ . '/..' . '/krokedil/woocommerce/src/Subscription.php',
     90        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\FieldMasker' => __DIR__ . '/..' . '/krokedil/wp-api/src/FieldMasker.php',
     91        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\KeyMasker' => __DIR__ . '/..' . '/krokedil/wp-api/src/KeyMasker.php',
    9092        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Logger' => __DIR__ . '/..' . '/krokedil/wp-api/src/Logger.php',
     93        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Masking' => __DIR__ . '/..' . '/krokedil/wp-api/src/Masking.php',
    9194        'KrokedilKlarnaPaymentsDeps\\Krokedil\\WpApi\\Request' => __DIR__ . '/..' . '/krokedil/wp-api/src/Request.php',
    9295    );
    … …  
    9598    {
    9699        return \Closure::bind(function () use ($loader) {
    97             $loader->prefixLengthsPsr4 = ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3::$prefixLengthsPsr4;
    98             $loader->prefixDirsPsr4 = ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3::$prefixDirsPsr4;
    99             $loader->classMap = ComposerStaticInitc3911f91ac7a4070427b96bb312df1e3::$classMap;
     100            $loader->prefixLengthsPsr4 = ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386::$prefixLengthsPsr4;
     101            $loader->prefixDirsPsr4 = ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386::$prefixDirsPsr4;
     102            $loader->classMap = ComposerStaticInit82df93fcb7d0af6c2c95e9a438100386::$classMap;
    100103
    101104        }, null, ClassLoader::class);
  • klarna-payments-for-woocommerce/trunk/dependencies/composer/installed.json

    r3728366 r3740915  
    199199        {
    200200            "name": "krokedil\/wp-api",
    201             "version": "1.1.1",
    202             "version_normalized": "1.1.1.0",
     201            "version": "1.2.0",
     202            "version_normalized": "1.2.0.0",
    203203            "source": {
    204204                "type": "git",
    205205                "url": "https:\/\/github.com\/krokedil\/wp-api.git",
    206                 "reference": "a3ba33f7ac640456dad36a33d711d20091c71aea"
    207             },
    208             "dist": {
    209                 "type": "zip",
    210                 "url": "https:\/\/api.github.com\/repos\/krokedil\/wp-api\/zipball\/a3ba33f7ac640456dad36a33d711d20091c71aea",
    211                 "reference": "a3ba33f7ac640456dad36a33d711d20091c71aea",
    212                 "shasum": ""
    213             },
    214             "require-dev": {
     206                "reference": "3473dd2fb6b31ed5060ecd69877c98b5287ebe48"
     207            },
     208            "dist": {
     209                "type": "zip",
     210                "url": "https:\/\/api.github.com\/repos\/krokedil\/wp-api\/zipball\/3473dd2fb6b31ed5060ecd69877c98b5287ebe48",
     211                "reference": "3473dd2fb6b31ed5060ecd69877c98b5287ebe48",
     212                "shasum": ""
     213            },
     214            "require-dev": {
     215                "dealerdirect\/phpcodesniffer-composer-installer": "^1.0",
    215216                "php": "~7.1 || ~8.0",
    216217                "php-stubs\/woocommerce-stubs": "^7.2",
    217                 "php-stubs\/wordpress-stubs": "^6.1"
    218             },
    219             "time": "2024-04-22T09:03:39+00:00",
     218                "php-stubs\/wordpress-stubs": "^6.1",
     219                "phpstan\/phpstan": "^2.0",
     220                "phpunit\/phpunit": "^9.6",
     221                "squizlabs\/php_codesniffer": "^3.13",
     222                "szepeviktor\/phpstan-wordpress": "^2.0",
     223                "wp-coding-standards\/wpcs": "^3.1"
     224            },
     225            "time": "2026-10-07T08:46:15+00:00",
    220226            "type": "library",
    221227            "installation-source": "dist",
    … …  
    225231                }
    226232            },
     233            "scripts": {
     234                "test": [
     235                    "phpunit"
     236                ],
     237                "phpcs": [
     238                    "phpcs"
     239                ],
     240                "phpcbf": [
     241                    "phpcbf"
     242                ],
     243                "phpstan": [
     244                    "phpstan analyse --memory-limit=1G"
     245                ]
     246            },
    227247            "license": [
    228248                "GPL-V2"
    … …  
    236256            "description": "A composer library for Krokedil WordPress plugins that contains classes for working with wp_remote_request to setup request classes. Also uses the WooCommerce logger to log request reponses.",
    237257            "support": {
    238                 "source": "https:\/\/github.com\/krokedil\/wp-api\/tree\/1.1.1",
     258                "source": "https:\/\/github.com\/krokedil\/wp-api\/tree\/1.2.0",
    239259                "issues": "https:\/\/github.com\/krokedil\/wp-api\/issues"
    240260            },
  • klarna-payments-for-woocommerce/trunk/dependencies/composer/installed.php

    r3728366 r3740915  
    33namespace KrokedilKlarnaPaymentsDeps;
    44
    5 return array('root' => array('name' => '__root__', 'pretty_version' => '4.13.3', 'version' => '4.13.3.0', 'reference' => '0d6c8537e224d077da2c9b85736448423cc02baf', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \true), 'versions' => array('__root__' => array('pretty_version' => '4.13.3', 'version' => '4.13.3.0', 'reference' => '0d6c8537e224d077da2c9b85736448423cc02baf', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/settings-page' => array('pretty_version' => '1.3.7', 'version' => '1.3.7.0', 'reference' => 'ef40906dd574b8c61d5831d247661a909c0463e6', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/settings-page', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/support' => array('pretty_version' => '1.0.3', 'version' => '1.0.3.0', 'reference' => 'e4ee49d1c862e45fb284247c84d7c1f5583cb6ba', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/support', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/woocommerce' => array('pretty_version' => '1.9.2', 'version' => '1.9.2.0', 'reference' => '6c38cf569df5eeb3598f206d449eca86ae9ac402', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/woocommerce', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/wp-api' => array('pretty_version' => '1.1.1', 'version' => '1.1.1.0', 'reference' => 'a3ba33f7ac640456dad36a33d711d20091c71aea', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/wp-api', 'aliases' => array(), 'dev_requirement' => \false)));
     5return array('root' => array('name' => '__root__', 'pretty_version' => '4.13.4', 'version' => '4.13.4.0', 'reference' => '5eda0fcd3d6d0288827dc8b76adf3ae1e2d5133e', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \true), 'versions' => array('__root__' => array('pretty_version' => '4.13.4', 'version' => '4.13.4.0', 'reference' => '5eda0fcd3d6d0288827dc8b76adf3ae1e2d5133e', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/settings-page' => array('pretty_version' => '1.3.7', 'version' => '1.3.7.0', 'reference' => 'ef40906dd574b8c61d5831d247661a909c0463e6', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/settings-page', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/support' => array('pretty_version' => '1.0.3', 'version' => '1.0.3.0', 'reference' => 'e4ee49d1c862e45fb284247c84d7c1f5583cb6ba', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/support', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/woocommerce' => array('pretty_version' => '1.9.2', 'version' => '1.9.2.0', 'reference' => '6c38cf569df5eeb3598f206d449eca86ae9ac402', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/woocommerce', 'aliases' => array(), 'dev_requirement' => \false), 'krokedil/wp-api' => array('pretty_version' => '1.2.0', 'version' => '1.2.0.0', 'reference' => '3473dd2fb6b31ed5060ecd69877c98b5287ebe48', 'type' => 'library', 'install_path' => __DIR__ . '/../krokedil/wp-api', 'aliases' => array(), 'dev_requirement' => \false)));
  • klarna-payments-for-woocommerce/trunk/dependencies/krokedil/wp-api/CHANGELOG.md

    r3150014 r3740915  
    77
    88## [Unreleased]
     9
     10------------------
     11## [1.2.0] - 2026-10-07
     12### Added
     13* Added configurable masking for the request, the response and the request arguments, replacing the hardcoded Authorization header and `username`/`password` handling. Rules are declared as `$request_fields_to_mask`, `$response_fields_to_mask` and `$argument_fields_to_mask`, or passed as the `$masked_fields` constructor argument, where they merge into what the class already declares.
     14* Added an allow list to the masking configuration. The reserved `keep` key masks every key in a container that it does not name, so a field the provider adds later is masked by default.
     15* Added `Krokedil\WpApi\FieldMasker`, so a plugin that logs from outside a `Request` subclass can apply the same configured rules.
     16* Added `mask_request_url()` for APIs that address a resource by a token in the path. It returns the URL unchanged by default, and an override that throws costs the log line and not the API call.
     17* Added PHPUnit, PHPCS and PHPStan with `composer test`, `composer phpcs` and `composer phpstan`, and GitHub Actions workflows for pull requests.
     18* Added XML support for response bodies. `get_response_body()` parses the body by its content type: JSON is decoded, XML is parsed into an array, HTML is returned as a string. A body with no or an unknown content type is decoded as JSON when it is valid JSON, so the response masking rules still reach into it.
     19* Added log levels. `Logger::log()` writes with the WooCommerce logger at the `log_level` of the entry, `error` for a non 2xx response and for a body `status` of `error`, `warning` for a body `status` of `warning`, otherwise `info`.
     20* `WP_Error` responses from `wp_remote_request()` are now logged.
     21* Added the `krokedil_wp_api_mask_log_data` filter. Return `false` to turn the log masking off while debugging. It gets the plugin slug as its second argument. There is deliberately no setting for it.
     22* Added `Krokedil\WpApi\Masking`, with `mask_fields()`, `mask_keys()` and `is_enabled()`, so a plugin that masks data outside a `Request` subclass honours the filter too.
     23
     24### Changed
     25* `[MISSING]` now tells an empty value from a sent one on every masked field, rather than only on the Authorization header.
     26* Masking no longer decides by length whether a header holds a token. Any value a rule names is replaced, whatever its length.
     27* Data nested deeper than twelve levels is masked rather than walked.
     28* `request()` returns the body parsed by its content type rather than always through `json_decode()`. A 2xx HTML body, or a body with no or an unknown content type that is not JSON, is now returned as a string where it used to be `null`, and a body labelled JSON or XML that cannot be parsed returns a `WP_Error`.
     29* Request logs are written at the `info` level rather than the `notice` default of `WC_Logger::add()`, so a store with its log threshold at `notice` or higher no longer records successful requests.
     30
     31### Deprecated
     32* `sanitize_request_args()` is deprecated and delegates to the configured masking, keeping its behaviour.
     33
     34### Security
     35* The response body is now masked. It was logged in full, so every field the provider returned, customer details included, reached the log.
     36* Stack trace arguments are now masked. With extended debugging on, every value passed to every caller was json encoded into the log as it was. Frame arguments are positional, so a bare token with no key name still survives unless its shape gives it away.
     37* Added a key name pass over the finished log entry. `Krokedil\WpApi\KeyMasker` masks by key name wherever it appears, and by shape for an Authorization value, a JWT and a standalone base64 blob, catching what no rule describes. Consuming plugins widen the list with `KeyMasker::add_keys()`.
     38* Masking fails closed. A section that cannot be masked is logged as `[MASKING FAILED]` rather than in the clear.
    939
    1040------------------
  • klarna-payments-for-woocommerce/trunk/dependencies/krokedil/wp-api/src/Logger.php

    r3150014 r3740915  
    88namespace KrokedilKlarnaPaymentsDeps\Krokedil\WpApi;
    99
     10use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\KeyMasker;
     11use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Masking;
    1012/**
    1113 * Logger class for the api requests. Used the WooCommerce logger.
    … …  
    1416{
    1517    /**
     18     * The log levels the WooCommerce logger accepts.
     19     */
     20    const LOG_LEVELS = array('emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug');
     21    /**
    1622     * WC Logger instance.
    1723     *
    18      * @var \WC_Logger $log
     24     * @var \WC_Logger_Interface|null $log
    1925     */
    2026    public static $log;
    … …  
    2834    public static function log($slug, $log_data = array())
    2935    {
    30         // Log the data.
    3136        if (empty(self::$log)) {
    32             self::$log = new \WC_Logger();
     37            self::$log = wc_get_logger();
    3338        }
    34         self::$log->add($slug, wp_json_encode($log_data));
     39        // Read the level before masking, a failed pass replaces the whole entry.
     40        $log_level = $log_data['log_level'] ?? 'info';
     41        if (!\in_array($log_level, self::LOG_LEVELS, \true)) {
     42            $log_level = 'info';
     43        }
     44        // A failure here costs the entry, it never lets an unmasked one through.
     45        if (Masking::is_enabled($slug)) {
     46            try {
     47                $log_data = static::mask_log_data($log_data);
     48            } catch (\Throwable $e) {
     49                $log_data = array('error' => KeyMasker::FAILED);
     50            }
     51        }
     52        self::$log->log($log_level, wp_json_encode($log_data), array('source' => $slug));
     53    }
     54    /**
     55     * The last pass over the finished entry, catching anything no rule described.
     56     *
     57     * @param array $log_data The data to log.
     58     * @return mixed
     59     */
     60    protected static function mask_log_data($log_data)
     61    {
     62        return KeyMasker::mask($log_data);
    3563    }
    3664    /**
    … …  
    3866     *
    3967     * @param bool $extended_debugging Whether to include the arguments in the stack trace. This should never be turned on by default, but rather only used when extra information is needed.
     68     * @param bool $mask               Whether to mask the arguments in the stack trace.
    4069     * @return array
    4170     */
    42     public static function get_stack($extended_debugging = \false)
     71    public static function get_stack($extended_debugging = \false, $mask = \true)
    4372    {
    4473        $debug_data = \debug_backtrace();
    … …  
    4877        $count = \count($debug_data);
    4978        for ($i = 5; $i < $count; $i++) {
    50             self::process_debug_line($stack, $debug_data[$i], $extended_debugging);
     79            self::process_debug_line($stack, $debug_data[$i], $extended_debugging, $mask);
    5180        }
    5281        return $stack;
    … …  
    5887     * @param array $debug_line The debug info from the raw stack trace.
    5988     * @param bool  $extended_debugging Whether to include the arguments in the stack trace.
     89     * @param bool  $mask Whether to mask the arguments.
    6090     * @return void
    6191     */
    62     private static function process_debug_line(&$stack, $debug_line, $extended_debugging)
     92    private static function process_debug_line(&$stack, $debug_line, $extended_debugging, $mask = \true)
    6393    {
    6494        $class = $debug_line['class'] ?? '';
    … …  
    6898        self::handle_wp_hook($class, $function, $args, $debug_line);
    6999        // Construct a caller string.
    70         $caller = self::get_caller_string($class, $type, $function, $args, $extended_debugging);
     100        $caller = self::get_caller_string($class, $type, $function, $args, $extended_debugging, $mask);
    71101        $row = array('file' => $debug_line['file'] ?? '', 'line' => $debug_line['line'] ?? '', 'function' => $caller);
    72102        $stack[] = $row;
    73103    }
    74104    /**
    75      * Get the caller string from the stack trace line.
     105     * Get the caller string from the stack trace line. Argument values are masked first, but
     106     * frame arguments are positional, so there is no key name to match on: a bare token passed
     107     * as an argument survives unless its own shape gives it away. Treat these logs as sensitive.
    76108     *
    77109     * @param string $class The class name.
    … …  
    80112     * @param array  $args The arguments passed to the caller.
    81113     * @param bool   $extended_debugging Whether to include the arguments in the stack trace.
     114     * @param bool   $mask Whether to mask the arguments.
    82115     * @return string
    83116     */
    84     private static function get_caller_string($class, $type, $function, $args, $extended_debugging)
     117    private static function get_caller_string($class, $type, $function, $args, $extended_debugging, $mask = \true)
    85118    {
    86119        // Construct a caller string.
    … …  
    88121        $caller .= '(';
    89122        // Only add data if we are doing extended debugging.
    90         $caller .= $extended_debugging ? \implode(', ', \array_map(function ($value) {
     123        $caller .= $extended_debugging ? \implode(', ', \array_map(function ($value) use($mask) {
     124            try {
     125                $value = $mask ? KeyMasker::mask($value) : $value;
     126            } catch (\Throwable $e) {
     127                $value = KeyMasker::FAILED;
     128            }
    91129            // Json encode all values so that we can see what objects and arrays are passed. Dont escape anything, partial output on errors, and ignore slashes and line terminators.
    92130            return wp_json_encode($value, \JSON_UNESCAPED_UNICODE | \JSON_PARTIAL_OUTPUT_ON_ERROR | \JSON_UNESCAPED_LINE_TERMINATORS | \JSON_UNESCAPED_SLASHES);
  • klarna-payments-for-woocommerce/trunk/dependencies/krokedil/wp-api/src/Request.php

    r3150014 r3740915  
    99
    1010use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Logger;
     11use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\KeyMasker;
     12use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\Masking;
     13use KrokedilKlarnaPaymentsDeps\Krokedil\WpApi\FieldMasker;
    1114/**
    1215 * Base request class for the package.
    … …  
    1518{
    1619    /**
     20     * The reserved rule key that turns a rule into an allow list.
     21     */
     22    const MASK_KEEP = FieldMasker::KEEP;
     23    /**
    1724     * Config values.
    1825     *
    … …  
    5764    public $method = 'GET';
    5865    /**
     66     * Fields to mask in the request args before logging. Keys map to a list of field names or
     67     * to nested arrays, matched case insensitively wherever the name appears below that level.
     68     * The reserved 'keep' key turns a rule into an allow list, masking every key it does not name.
     69     *
     70     * Example:
     71     * array(
     72     *     'headers' => array( 'Authorization' ),
     73     *     'body'    => array(
     74     *         'billing_address' => array( 'email', 'phone' ),
     75     *         'customer'        => array( 'keep' => array( 'type' ) ),
     76     *         'attachment'      => 'mask',
     77     *     ),
     78     * )
     79     *
     80     * @var array
     81     */
     82    protected $request_fields_to_mask = array('headers' => array('Authorization'));
     83    /**
     84     * Fields to mask in the response body before logging. Uses the same structure as the
     85     * request fields, including the 'keep' allow list.
     86     *
     87     * Example:
     88     * array(
     89     *     'client_token',
     90     *     'billing_address' => array( 'email', 'phone' ),
     91     * )
     92     *
     93     * @var array
     94     */
     95    protected $response_fields_to_mask = array();
     96    /**
     97     * Fields to mask in the request arguments before logging, same structure as above.
     98     *
     99     * @var array
     100     */
     101    protected $argument_fields_to_mask = array('username', 'password');
     102    /**
    59103     * Constructor.
    60104     *
    61      * @param array $config Configuration array.
    62      */
    63     public function __construct($config = array(), $settings = array(), $arguments = array())
     105     * @param array $config       Configuration array.
     106     * @param array $settings     Plugin settings.
     107     * @param array $arguments    Request arguments.
     108     * @param array $masked_fields Optional extra fields to mask, with keys 'request', 'response' and/or 'arguments'.
     109     *                             Merged into the configured fields, so an allow list can be passed here as well.
     110     */
     111    public function __construct($config = array(), $settings = array(), $arguments = array(), $masked_fields = array())
    64112    {
    65113        $this->config = wp_parse_args($config, $this->defaults);
    66114        $this->settings = $settings;
    67115        $this->arguments = $arguments;
     116        if (!empty($masked_fields['request'])) {
     117            $this->request_fields_to_mask = FieldMasker::merge_config($this->request_fields_to_mask, $masked_fields['request']);
     118        }
     119        if (!empty($masked_fields['response'])) {
     120            $this->response_fields_to_mask = FieldMasker::merge_config($this->response_fields_to_mask, $masked_fields['response']);
     121        }
     122        if (!empty($masked_fields['arguments'])) {
     123            $this->argument_fields_to_mask = FieldMasker::merge_config($this->argument_fields_to_mask, $masked_fields['arguments']);
     124        }
    68125    }
    69126    /**
    … …  
    125182    {
    126183        if (is_wp_error($response)) {
     184            $this->log_response($response, $request_args, $request_url);
    127185            return $response;
    128186        }
    … …  
    131189            $return = $this->get_error_message($response);
    132190        } else {
    133             $return = \json_decode(wp_remote_retrieve_body($response), \true);
     191            $return = $this->get_response_body($response);
    134192        }
    135193        $this->log_response($response, $request_args, $request_url);
    … …  
    150208        }
    151209        // Get the response body if its not a WP_Error.
    152         $response_body = !is_wp_error($response) ? \json_decode(wp_remote_retrieve_body($response), \true) : array();
     210        $response_body = !is_wp_error($response) ? $this->get_response_body($response) : array();
    153211        $code = wp_remote_retrieve_response_code($response);
    154         // Parse the Request body into an array if its json format.
    155         $request_body = $request_args['body'] ?? '';
    156         $decoded_body = \json_decode($request_body);
    157         $request_args['body'] = $decoded_body ?? $request_args['body'] ?? null;
    158         $request_args = $this->sanitize_request_args($request_args);
    159         $arguments = $this->arguments;
    160         if (isset($arguments['username'])) {
    161             $arguments['username'] = '[REDACTED]';
    162         }
    163         if (isset($arguments['password'])) {
    164             $arguments['password'] = '[REDACTED]';
     212        // Set log level. Read from the body before it is masked. A body that failed to parse is a WP_Error.
     213        $body_status = \is_array($response_body) ? $response_body['status'] ?? null : null;
     214        if ($code < 200 || $code > 299 || is_wp_error($response_body)) {
     215            $log_level = 'error';
     216        } elseif ('error' === $body_status) {
     217            // Some APIs (e.g. Fraktjakt) return a successful HTTP status while reporting an
     218            // application-level error in the response body. Treat those as errors too.
     219            $log_level = 'error';
     220        } elseif ('warning' === $body_status) {
     221            $log_level = 'warning';
     222        } else {
     223            $log_level = 'info';
     224        }
     225        $mask = Masking::is_enabled($this->config['slug']);
     226        if ($mask) {
     227            $response_body = $this->mask_response($response_body);
     228            $arguments = $this->mask_arguments($this->arguments);
     229            $request_url = $this->get_masked_request_url($request_url);
     230            $request_args = $this->mask_request_args($request_args);
     231        } else {
     232            $arguments = $this->arguments;
     233            if (isset($request_args['body']) && \is_string($request_args['body'])) {
     234                $request_args['body'] = $this->decode_body($request_args['body']);
     235            }
    165236        }
    166237        // Log the response.
    … …  
    172243            'request_url' => $request_url,
    173244            'response' => array('body' => $response_body, 'code' => $code),
     245            'log_level' => $log_level,
    174246            'timestamp' => \date('Y-m-d H:i:s'),
    175247            // phpcs:ignore WordPress.DateTime.RestrictedFunctions -- Date is not used for display.
    176             'stack' => Logger::get_stack($this->config['extended_debugging']),
     248            'stack' => Logger::get_stack($this->config['extended_debugging'], $mask),
    177249            'plugin_version' => $this->config['plugin_version'],
    178250        ));
    179251    }
    180252    /**
     253     * Returns the body of the response based on the content type.
     254     *
     255     * @param array|\WP_Error $response The response from wp_remote_get() or wp_remote_post().
     256     * @return array|object|string|\WP_Error Parsed response body or WP_Error if an error occurs.
     257     */
     258    public function get_response_body($response)
     259    {
     260        if (is_wp_error($response)) {
     261            return $response;
     262        }
     263        $content_type = wp_remote_retrieve_header($response, 'content-type');
     264        $content_type = \is_array($content_type) ? \implode(', ', $content_type) : (string) $content_type;
     265        $body = wp_remote_retrieve_body($response);
     266        if (\strpos($content_type, 'application/json') !== \false) {
     267            $parsed_body = \json_decode($body, \true);
     268            if (\json_last_error() === \JSON_ERROR_NONE) {
     269                return $parsed_body;
     270            }
     271            return new \WP_Error('json_decode_error', 'Failed to decode JSON');
     272        } elseif (\strpos($content_type, 'text/html') !== \false) {
     273            return $body;
     274        } elseif (\strpos($content_type, 'application/xml') !== \false || \strpos($content_type, 'text/xml') !== \false) {
     275            // Collect parse errors rather than emitting a PHP warning per malformed node.
     276            $use_errors = \libxml_use_internal_errors(\true);
     277            $parsed_body = \simplexml_load_string($body);
     278            \libxml_clear_errors();
     279            \libxml_use_internal_errors($use_errors);
     280            if (\false !== $parsed_body) {
     281                return \json_decode(wp_json_encode($parsed_body), \true);
     282            }
     283            return new \WP_Error('xml_parse_error', 'Failed to parse XML');
     284        } else {
     285            // No or an unknown content type. Decode json anyway, so a body the API did not label
     286            // is still returned as an array and the response masking rules can reach into it.
     287            $parsed_body = \json_decode($body, \true);
     288            return \is_array($parsed_body) ? $parsed_body : $body;
     289        }
     290    }
     291    /**
     292     * Mask sensitive fields in the request args before logging. Reading the configuration
     293     * happens inside the guard, so a broken config costs the section and not the request.
     294     *
     295     * @param array $request_args The request args.
     296     * @return array|string The masked request args, or the failure marker.
     297     */
     298    protected function mask_request_args($request_args)
     299    {
     300        try {
     301            // Decode the body that was really sent, so the rules can reach into it and the log
     302            // holds a readable structure rather than an escaped string. A body no rule can reach
     303            // into is masked whole by a configured body rule, see FieldMasker.
     304            if (isset($request_args['body']) && \is_string($request_args['body'])) {
     305                $request_args['body'] = $this->decode_body($request_args['body']);
     306            }
     307            $fields = $this->request_fields_to_mask;
     308            return empty($fields) ? $request_args : $this->sanitize_field($request_args, $fields);
     309        } catch (\Throwable $e) {
     310            return KeyMasker::FAILED;
     311        }
     312    }
     313    /**
     314     * Decode a json or form encoded body string into an array, or return it as it was.
     315     *
     316     * @param string $body The body string.
     317     * @return array|string
     318     */
     319    private function decode_body($body)
     320    {
     321        $decoded = \json_decode($body, \true);
     322        if (\is_array($decoded)) {
     323            return $decoded;
     324        }
     325        // What http_build_query() produces: key=value pairs joined by '&', nothing else.
     326        if (\preg_match('/^[^=&\\s]+=[^&\\s]*(?:&[^=&\\s]+=[^&\\s]*)*$/', $body)) {
     327            \parse_str($body, $decoded);
     328            return $decoded;
     329        }
     330        return $body;
     331    }
     332    /**
     333     * Mask sensitive fields in the response body before logging.
     334     *
     335     * @param array|\WP_Error $response The decoded response body.
     336     * @return array|string|\WP_Error The masked response body, or the failure marker.
     337     */
     338    protected function mask_response($response)
     339    {
     340        if (is_wp_error($response) || empty($response)) {
     341            return $response;
     342        }
     343        try {
     344            $fields = $this->response_fields_to_mask;
     345            return empty($fields) ? $response : $this->sanitize_field($response, $fields);
     346        } catch (\Throwable $e) {
     347            return KeyMasker::FAILED;
     348        }
     349    }
     350    /**
     351     * Mask sensitive fields in the request arguments before logging.
     352     *
     353     * @param array $arguments The request arguments.
     354     * @return array|string The masked arguments, or the failure marker.
     355     */
     356    protected function mask_arguments($arguments)
     357    {
     358        try {
     359            $fields = $this->argument_fields_to_mask;
     360            return empty($fields) ? $arguments : $this->sanitize_field($arguments, $fields);
     361        } catch (\Throwable $e) {
     362            return KeyMasker::FAILED;
     363        }
     364    }
     365    /**
     366     * Mask the request URL before logging, unchanged by default. Override it when the API
     367     * addresses a resource by a token in the path, such as /authorizations/{token}/order.
     368     *
     369     * @param string $request_url The request URL.
     370     * @return string
     371     */
     372    protected function mask_request_url($request_url)
     373    {
     374        return $request_url;
     375    }
     376    /**
     377     * Call mask_request_url() without letting an override that throws kill the API call.
     378     * The fallback is a redaction, never the raw URL.
     379     *
     380     * @param string $request_url The request URL.
     381     * @return string
     382     */
     383    private function get_masked_request_url($request_url)
     384    {
     385        try {
     386            $masked = $this->mask_request_url($request_url);
     387            return \is_string($masked) ? $masked : KeyMasker::FAILED;
     388        } catch (\Throwable $e) {
     389            return KeyMasker::FAILED;
     390        }
     391    }
     392    /**
     393     * Recursively mask a set of fields within a data array. A rule keeps matching below the
     394     * level it was declared on, so a container nested in a list is treated like one at the root.
     395     *
     396     * @param array $data               The data array to sanitize.
     397     * @param array $fields_to_sanitize The fields to sanitize.
     398     * @return array
     399     */
     400    protected function sanitize_field($data, $fields_to_sanitize)
     401    {
     402        return FieldMasker::mask($data, $fields_to_sanitize);
     403    }
     404    /**
    181405     * Remove sensitive data from the log.
    182406     *
     407     * @deprecated Use mask_request_args() instead. Kept for backward compatibility.
    183408     * @param array $request_args The request data to sanitize.
    184      * @return array The request data sanitized.
     409     * @return array|string The request data sanitized, or the failure marker.
    185410     */
    186411    protected function sanitize_request_args($request_args)
    187412    {
    188         // Do not log the authorization token.
    189         foreach ($request_args['headers'] as $header => $value) {
    190             if ('authorization' === \strtolower($header)) {
    191                 // If it is longer than 15 char., it most likely has a token. This is an assumption that is safe even if it is wrong.
    192                 $request_args['headers'][$header] = \strlen($value) > 15 ? '[REDACTED]' : '[MISSING]';
    193                 break;
    194             }
    195         }
    196         return $request_args;
     413        return $this->mask_request_args($request_args);
    197414    }
    198415    /**
  • klarna-payments-for-woocommerce/trunk/klarna-payments-for-woocommerce.php

    r3728366 r3740915  
    66 * Author: klarna
    77 * Author URI: https://www.klarna.com/
    8  * Version: 4.13.3
     8 * Version: 4.13.4
    99 * Text Domain: klarna-payments-for-woocommerce
    1010 * Domain Path: /languages
    1111 *
    1212 * WC requires at least: 5.6.0
    13  * WC tested up to: 11.1.2
     13 * WC tested up to: 11.2.1
    1414 * Requires Plugins: woocommerce
    1515 *
    … …  
    3838use Krokedil\Klarna\ExpressCheckout;
    3939use Krokedil\Klarna\Utilities\ApiCredentialsUtility;
     40use Krokedil\Klarna\Logging\LogMasking;
     41use Krokedil\Klarna\Logging\LogWriter;
    4042
    4143if ( ! defined( 'ABSPATH' ) ) {
    … …  
    5355 * Required minimums and constants
    5456 */
    55 define( 'WC_KLARNA_PAYMENTS_VERSION', '4.13.3' );
     57define( 'WC_KLARNA_PAYMENTS_VERSION', '4.13.4' );
    5658define( 'WC_KLARNA_PAYMENTS_MIN_PHP_VER', '7.4.0' );
    5759define( 'WC_KLARNA_PAYMENTS_MIN_WC_VER', '5.6.0' );
    … …  
    331333                        $this->order_management        = new OrderManagement();
    332334                        $this->logger                  = new Logger( 'klarna_payments', 'no' !== ( $settings['logging'] ?? 'no' ) );
     335
     336                        // Masking has to be in place before anything can log.
     337                        LogMasking::register();
     338                        LogWriter::register();
    333339                        Compatibility::register();
    334340
  • klarna-payments-for-woocommerce/trunk/languages/klarna-payments-for-woocommerce.pot

    r3728366 r3740915  
    66"Report-Msgid-Bugs-To: "
    77"https://wordpress.org/support/plugin/klarna-payments-for-woocommerce\n"
    8 "POT-Creation-Date: 2026-10-05 08:10:20+00:00\n"
     8"POT-Creation-Date: 2026-10-12 09:04:01+00:00\n"
    99"MIME-Version: 1.0\n"
    1010"Content-Type: text/plain; charset=utf-8\n"
    … …  
    513513msgstr ""
    514514
    515 #: classes/class-kp-assets.php:250
     515#: classes/class-kp-assets.php:283
    516516#. translators: [customer-facing].
    517517msgid "Please read and accept the terms and conditions to proceed with your order."
    518518msgstr ""
    519519
    520 #: classes/class-kp-assets.php:301
     520#: classes/class-kp-assets.php:334
    521521#. translators: [merchant-facing].
    522522msgid "Select all"
    … …  
    728728#: dependencies/krokedil/settings-page/src/Navigation.php:54
    729729#: dependencies/krokedil/settings-page/src/Support.php:36
    730 #: klarna-payments-for-woocommerce.php:449
     730#: klarna-payments-for-woocommerce.php:455
    731731#. translators: [merchant-facing].
    732732msgid "Support"
    … …  
    879879msgstr ""
    880880
    881 #: includes/kp-functions.php:738 klarna-payments-for-woocommerce.php:665
     881#: includes/kp-functions.php:738 klarna-payments-for-woocommerce.php:671
    882882#. translators: [customer-facing].
    883883msgid "Pay with Klarna"
    884884msgstr ""
    885885
    886 #: klarna-payments-for-woocommerce.php:445
     886#: klarna-payments-for-woocommerce.php:451
    887887#. translators: [merchant-facing].
    888888msgid "Settings"
    889889msgstr ""
    890890
    891 #: klarna-payments-for-woocommerce.php:447
     891#: klarna-payments-for-woocommerce.php:453
    892892#. translators: [merchant-facing].
    893893msgid "Docs"
    894894msgstr ""
    895895
    896 #: klarna-payments-for-woocommerce.php:491
     896#: klarna-payments-for-woocommerce.php:497
    897897#. translators: [merchant-facing].
    898898msgid "The WooCommerce plugin must be active for Klarna Payments to work."
    899899msgstr ""
    900900
    901 #: klarna-payments-for-woocommerce.php:510
     901#: klarna-payments-for-woocommerce.php:516
    902902#. translators: [merchant-facing].
    903903msgid "Dismiss"
    904904msgstr ""
    905905
    906 #: klarna-payments-for-woocommerce.php:517
     906#: klarna-payments-for-woocommerce.php:523
    907907#. translators: [merchant-facing]. URL to docs.
    908908msgid ""
    … …  
    912912msgstr ""
    913913
    914 #: klarna-payments-for-woocommerce.php:611
    915 #: klarna-payments-for-woocommerce.php:620
     914#: klarna-payments-for-woocommerce.php:617
     915#: klarna-payments-for-woocommerce.php:626
    916916msgid ""
    917917"Your installation of Klarna Payments is not complete. If you installed this "
    … …  
    920920msgstr ""
    921921
    922 #: klarna-payments-for-woocommerce.php:655
     922#: klarna-payments-for-woocommerce.php:661
    923923#. translators: [customer-facing].
    924924msgid "Pay with Klarna (free)"
    … …  
    942942
    943943#: src/ExpressCheckout/Api/Notifications/PaymentStateExpired.php:43
    944 #: src/ExpressCheckout/KECOneStepIntegration.php:101
     944#: src/ExpressCheckout/KECOneStepIntegration.php:102
    945945#. translators: [merchant-facing].
    946946msgid "Order cancelled due to expired payment request."
    … …  
    14781478msgstr ""
    14791479
    1480 #: src/OrderManagement/Request/Request.php:164
     1480#: src/OrderManagement/Request/Request.php:165
    14811481#. translators: [merchant-facing].
    14821482msgid "Invalid JSON response from the server."
  • klarna-payments-for-woocommerce/trunk/readme.txt

    r3728366 r3740915  
    77Requires PHP: 8.0
    88WC requires at least: 5.6.0
    9 WC tested up to: 11.1.2
    10 Stable tag: 4.13.3
     9WC tested up to: 11.2.1
     10Stable tag: 4.13.4
    1111License: GPLv3 or later
    1212License URI: http://www.gnu.org/licenses/gpl-3.0.html
    … …  
    7070
    7171== Changelog ==
     72= 2026-10-12    - version 4.13.4 =
     73* Enhancement   - Masked customer personal, and other sensitive data and credentials in the plugin log.
     74* Fix           - Order details are now only added to the pay for order page when WooCommerce would show the payment form: the request carries the order's key, the customer may pay for the order, the order still needs payment, and a guest has confirmed the billing email.
     75
    7276= 2026-10-05    - version 4.13.3 =
    7377* Enhancement   - Improved handling of Klarna authorization callbacks and background processing.
  • klarna-payments-for-woocommerce/trunk/src/ExpressCheckout/KECOneStepIntegration.php

    r3716655 r3740915  
    9494
    9595                if ( ! $payment_request_id || ! $interoperability_token ) {
    96                         KP_WC()->logger()->error( '[KEC One Step] Missing required fields in payload for expired payment: ' . wp_json_encode( $payload ) );
     96                        $missing = $payment_request_id ? 'interoperability_token' : 'payment_request_id';
     97                        KP_WC()->logger()->error( "[KEC One Step] Missing {$missing} in the payload for expired payment on order {$order->get_id()}." );
    9798                        return;
    9899                }
  • klarna-payments-for-woocommerce/trunk/src/OrderManagement/Request/Request.php

    r3664907 r3740915  
    33
    44use Krokedil\Klarna\OrderManagement;
     5use Krokedil\Klarna\Logging\LogMasking;
    56use Krokedil\Klarna\Utilities\ApiCredentialsUtility;
    67
    … …  
    355356         */
    356357        protected function log_response( $response, $request_args, $request_url, $code ) {
    357                 foreach ( $request_args['headers'] as $header => $value ) {
    358                         if ( 'authorization' === strtolower( $header ) ) {
    359                                 // If it is longer than 15 char., it most likely has a token. This is an assumption that is safe even if it is wrong.
    360                                 $request_args['headers'][ $header ] = strlen( $value ) > 15 ? '[REDACTED]' : '[MISSING]';
    361                                 break;
    362                         }
    363                 }
    364                 $log = \KP_Logger::format_log( $this->klarna_order_id, $this->method, $this->log_title, $request_args, $response, $code, $request_url );
     358                // Decode the body so the masking can reach into it, and drop the rest of the HTTP
     359                // response, which holds nothing a log needs and hides the payload inside a string.
     360                $body = is_wp_error( $response ) ? array() : json_decode( wp_remote_retrieve_body( $response ), true );
     361
     362                $request_args = LogMasking::mask_request( $request_args );
     363                $body         = LogMasking::mask_response( $body );
     364
     365                $log = \KP_Logger::format_log( $this->klarna_order_id, $this->method, $this->log_title, $request_args, $body, $code, $request_url );
    365366                \KP_Logger::log( $log );
    366367        }
  • klarna-payments-for-woocommerce/trunk/vendor/composer/autoload_classmap.php

    r3664907 r3740915  
    4444    'Krokedil\\Klarna\\ExpressCheckout\\WebhookSetup' => $baseDir . '/src/ExpressCheckout/WebhookSetup.php',
    4545    'Krokedil\\Klarna\\Features' => $baseDir . '/src/Features.php',
     46    'Krokedil\\Klarna\\Logging\\LogMasking' => $baseDir . '/src/Logging/LogMasking.php',
     47    'Krokedil\\Klarna\\Logging\\LogWriter' => $baseDir . '/src/Logging/LogWriter.php',
    4648    'Krokedil\\Klarna\\OnsiteMessaging\\Blocks\\CartBlockIntegration' => $baseDir . '/src/OnsiteMessaging/Blocks/CartBlockIntegration.php',
    4749    'Krokedil\\Klarna\\OnsiteMessaging\\KlarnaOnsiteMessaging' => $baseDir . '/src/OnsiteMessaging/KlarnaOnsiteMessaging.php',
  • klarna-payments-for-woocommerce/trunk/vendor/composer/autoload_static.php

    r3696456 r3740915  
    6767        'Krokedil\\Klarna\\ExpressCheckout\\WebhookSetup' => __DIR__ . '/../..' . '/src/ExpressCheckout/WebhookSetup.php',
    6868        'Krokedil\\Klarna\\Features' => __DIR__ . '/../..' . '/src/Features.php',
     69        'Krokedil\\Klarna\\Logging\\LogMasking' => __DIR__ . '/../..' . '/src/Logging/LogMasking.php',
     70        'Krokedil\\Klarna\\Logging\\LogWriter' => __DIR__ . '/../..' . '/src/Logging/LogWriter.php',
    6971        'Krokedil\\Klarna\\OnsiteMessaging\\Blocks\\CartBlockIntegration' => __DIR__ . '/../..' . '/src/OnsiteMessaging/Blocks/CartBlockIntegration.php',
    7072        'Krokedil\\Klarna\\OnsiteMessaging\\KlarnaOnsiteMessaging' => __DIR__ . '/../..' . '/src/OnsiteMessaging/KlarnaOnsiteMessaging.php',
  • klarna-payments-for-woocommerce/trunk/vendor/composer/installed.php

    r3728366 r3740915  
    22    'root' => array(
    33        'name' => '__root__',
    4         'pretty_version' => '4.13.3',
    5         'version' => '4.13.3.0',
    6         'reference' => '0d6c8537e224d077da2c9b85736448423cc02baf',
     4        'pretty_version' => '4.13.4',
     5        'version' => '4.13.4.0',
     6        'reference' => '5eda0fcd3d6d0288827dc8b76adf3ae1e2d5133e',
    77        'type' => 'library',
    88        'install_path' => __DIR__ . '/../../',
    … …  
    1212    'versions' => array(
    1313        '__root__' => array(
    14             'pretty_version' => '4.13.3',
    15             'version' => '4.13.3.0',
    16             'reference' => '0d6c8537e224d077da2c9b85736448423cc02baf',
     14            'pretty_version' => '4.13.4',
     15            'version' => '4.13.4.0',
     16            'reference' => '5eda0fcd3d6d0288827dc8b76adf3ae1e2d5133e',
    1717            'type' => 'library',
    1818            'install_path' => __DIR__ . '/../../',
Note: See TracChangeset for help on using the changeset viewer.