gnu-taler-payment-for-woocommerce

WooCommerce plugin to enable payments with GNU Taler
Log | Files | Refs | LICENSE

class-wc-gateway-gnutaler.php (40379B)


      1 <?php
      2 /**
      3  * Plugin to add support for the GNU Taler payment system to WooCommerce.
      4  *
      5  * @package GNUTalerPayment
      6  */
      7 
      8 /**
      9  * Plugin Name: GNU Taler Payment for WooCommerce
     10  * Plugin URI: https://git.taler.net/gnu-taler-payment-for-woocommerce
     11  * Description: This plugin enables payments via the GNU Taler payment system
     12  * Version: 1.1.0
     13  * Author: Dominique Hofmann, Jan StrĂ¼bin, Christian Grothoff
     14  * Author URI: https://taler.net/
     15  * License:           GNU General Public License v3.0
     16  * License URI:       http://www.gnu.org/licenses/gpl-3.0.html
     17  * Requires Plugins: woocommerce
     18  * WC requires at least: 9.6
     19  * WC tested up to: 10.1
     20  * Text Domain: gnutaler
     21  **/
     22 
     23 /*
     24 	This program is free software: you can redistribute it and/or modify
     25 	it under the terms of the GNU General Public License as published by
     26 	the Free Software Foundation, either version 3 of the License, or
     27 	(at your option) any later version.
     28 
     29 	This program is distributed in the hope that it will be useful,
     30 	but WITHOUT ANY WARRANTY; without even the implied warranty of
     31 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     32 	GNU General Public License for more details.
     33 
     34 	You should have received a copy of the GNU General Public License
     35 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
     36 */
     37 
     38 /**
     39  * Which version of the Taler merchant protocol is implemented
     40  * by this implementation?  Used to determine compatibility.
     41  */
     42 define( 'GNU_TALER_MERCHANT_PROTOCOL_CURRENT', 21 );
     43 
     44 /**
     45  * How many merchant protocol versions are we backwards compatible with?
     46  */
     47 define( 'GNU_TALER_MERCHANT_PROTOCOL_AGE', 5 );
     48 
     49 
     50 
     51 /**
     52  * GNU Taler Payment Gateway class.
     53  *
     54  * Handles the payments from the Woocommerce Webshop and sends the transactions to the GNU Taler Backend and the GNU Taler Wallet.
     55  */
     56 class WC_Gateway_Gnutaler extends WC_Payment_Gateway {
     57 
     58 	/**
     59 	 * Cached handle to logging class.
     60 	 *
     61 	 * @var Plugin loggger.
     62 	 */
     63 	private static $log = false;
     64 
     65 	/**
     66 	 * True if logging is enabled in our configuration.
     67 	 *
     68 	 * @var Is logging enabled?
     69 	 */
     70 	private static $log_enabled = false;
     71 
     72 	/**
     73 	 * Base URL of the Taler merchant backend.
     74 	 *
     75 	 * @var string
     76 	 */
     77 	private $gnu_taler_backend_url;
     78 
     79 	/**
     80 	 * Unique id for the gateway.
     81 	 *
     82 	 * @var string
     83 	 */
     84 	public $id = 'gnutaler';
     85 
     86 	/**
     87 	 * FIXME: what does this do?
     88 	 *
     89 	 * @var boolean
     90 	 */
     91 	public $enable_for_virtual = true;
     92 
     93 	/**
     94 	 * Instructions for the user.
     95 	 *
     96 	 * @var string
     97 	 */
     98 	protected $instructions;
     99 
    100 	/**
    101 	 * Whether the gateway is visible for non-admin users.
    102 	 *
    103 	 * @var boolean
    104 	 */
    105 	protected $hide_for_non_admin_users;
    106 
    107 	/**
    108 	 * True if debugging is enabled.
    109 	 *
    110 	 * @var boolean
    111 	 */
    112 	public $debug;
    113 
    114 	/**
    115 	 * Class constructor
    116 	 */
    117 	public function __construct() {
    118 		$this->icon = plugins_url( '/../assets/images/taler.png', __FILE__ );
    119 
    120 		// We cannot use custom fields to show the QR code / do the wallet integration as WC doesn't give us the order_id at that time. Bummer.
    121 		$this->has_fields = false;
    122 
    123 		// Setup logging.
    124 		$this->debug       = 'yes' === $this->get_option( 'debug', 'no' );
    125 		self::$log_enabled = $this->debug;
    126 
    127 		// This gateway can support refunds, saved payment methods.
    128 		$this->supports = array(
    129 			'products',
    130 			'refunds',
    131 		);
    132 
    133 		$this->method_title       = _x( 'GNU Taler', 'GNU Taler payment method', 'woocommerce-gateway-gnutaler' );
    134 		$this->method_description = __( 'This plugin enables payments via the GNU Taler payment system', 'woocommerce-gateway-gnutaler' );
    135 		$this->init_form_fields();
    136 		$this->init_settings();
    137 
    138 		$this->title                    = $this->get_option( 'title' );
    139 		$this->description              = $this->get_option( 'description' );
    140 		$this->instructions             = $this->get_option( 'instructions' );
    141 		$this->hide_for_non_admin_users = $this->get_option( 'hide_for_non_admin_users' );
    142 
    143 		$this->enabled               = $this->get_option( 'enabled' );
    144 		$this->gnu_taler_backend_url = $this->get_option( 'gnu_taler_backend_url' );
    145 		// Remove trailing '/', we add one always ourselves...
    146 		if ( substr( $this->gnu_taler_backend_url, -1 ) === '/' ) {
    147 			$this->gnu_taler_backend_url = substr( $this->gnu_taler_backend_url, 0, -1 );
    148 		}
    149 
    150 		// Make transaction ID a link. We use the public version
    151 		// here, as a user clicking on the link could not supply
    152 		// the authorization header.
    153 		// See also: https://woocommerce.wordpress.com/2014/08/05/wc-2-2-payment-gateways-adding-refund-support-and-transaction-ids/.
    154 		$this->view_transaction_url = $this->gnu_taler_backend_url . '/orders/%s';
    155 
    156 		// Register handler for the fulfillment URL.
    157 		add_action(
    158 			'woocommerce_api_' . strtolower( get_class( $this ) ),
    159 			array( &$this, 'fulfillment_url_handler' )
    160 		);
    161 
    162 		// This action hook saves the settings.
    163 		add_action(
    164 			'woocommerce_update_options_payment_gateways_' . $this->id,
    165 			array( $this, 'process_admin_options' )
    166 		);
    167 
    168 		// Modify WC canonical refund e-mail notifications to add link to order status page.
    169 		// (according to https://www.businessbloomer.com/woocommerce-add-extra-content-order-email/).
    170 		add_action(
    171 			'woocommerce_email_before_order_table',
    172 			array( $this, 'add_content_refund_email' ),
    173 			20,
    174 			4
    175 		);
    176 	}
    177 
    178 	/**
    179 	 * Check if this payment method is actually available.
    180 	 *
    181 	 * @return boolean
    182 	 */
    183 	public function is_available() {
    184 		if ( ! WC()->session || ! WC()->cart ) {
    185 			$this->warning( 'WC session or cart not available' );
    186 			return false;
    187 		}
    188 		$res = ( 'yes' === $this->enabled );
    189 	        // Deactivate payment method if currency does not match!
    190 		if ( $res && ( ! $this->verify_backend_url( $this->gnu_taler_backend_url, get_woocommerce_currency() ) ) ) {
    191 			$this->warning( 'Backend URL verification failed or currency mismatch between backend and shop!' );
    192 			$res = false;
    193 		}
    194   		$this->debug(
    195 			$res
    196 			? __( 'Returning payment method is available', 'woocommerce-gateway-gnutaler' )
    197 			: __( 'Returning payment method is unavailable', 'woocommerce-gateway-gnutaler' )
    198 		);
    199 		$context = '';
    200 		if ( is_admin() ) {
    201 			$context .= 'admin ';
    202 		}
    203 		if ( is_checkout() ) {
    204 			$context .= 'checkout ';
    205 		}
    206 		if ( WC()->cart && WC()->cart->is_empty() ) {
    207 			$context .= 'empty-cart ';
    208 		}
    209 		if ( WC()->cart ) {
    210 			$context .= 'total-' . WC()->cart->get_total( 'edit' ) . ' ';
    211 		}
    212 		$this->debug( $context . ' returning ' . $res );
    213 
    214 		return $res;
    215 	}
    216 
    217 
    218 	/**
    219 	 * Initialise Gateway Settings Form Fields.
    220 	 */
    221 	public function init_form_fields() {
    222 		$this->form_fields = array(
    223 			'enabled'                   => array(
    224 				'title'       => __( 'Enable/Disable', 'woocommerce-gateway-gnutaler' ),
    225 				'label'       => __( 'Enable GNU Taler Gateway', 'woocommerce-gateway-gnutaler' ),
    226 				'type'        => 'checkbox',
    227 				'description' => '',
    228 				'default'     => 'no',
    229 			),
    230 			'hide_for_non_admin_users'  => array(
    231 				'type'    => 'checkbox',
    232 				'label'   => __( 'Hide at checkout for non-admin users', 'woocommerce-gateway-gnutaler' ),
    233 				'default' => 'no',
    234 			),
    235 			'title'                     => array(
    236 				'title'       => __( 'Title', 'woocommerce-gateway-gnutaler' ),
    237 				'type'        => 'text',
    238 				'description' => __( 'This is what the customer will see when choosing payment methods.', 'woocommerce-gateway-gnutaler' ),
    239 				'default'     => 'GNU Taler',
    240 				'desc_tip'    => true,
    241 			),
    242 			'description'               => array(
    243 				'title'       => __( 'Description', 'woocommerce-gateway-gnutaler' ),
    244 				'type'        => 'textarea',
    245 				'description' => __( 'Payment method description which the customer sees during checkout.', 'woocommerce-gateway-gnutaler' ),
    246 				'default'     => __( 'Pay digitally with GNU Taler', 'woocommerce-gateway-gnutaler' ),
    247 				'desc_tip'    => true,
    248 			),
    249 			'instructions'              => array(
    250 				'title'       => __( 'Instructions', 'woocommerce-gateway-gnutaler' ),
    251 				'type'        => 'textarea',
    252 				'description' => __( 'Instructions that will be added to the thank you page.', 'woocommerce-gateway-gnutaler' ),
    253 				'default'     => __( 'Thank you for paying with GNU Taler', 'woocommerce-gateway-gnutaler' ),
    254 				'desc_tip'    => true,
    255 			),
    256 			'gnu_taler_backend_url'     => array(
    257 				'title'       => __( 'Taler backend URL', 'woocommerce-gateway-gnutaler' ),
    258 				'type'        => 'text',
    259 				'description' => __( 'Set the URL of the Taler backend. (Example: https://backend.demo.taler.net/)', 'woocommerce-gateway-gnutaler' ),
    260 				'default'     => 'https://backend.demo.taler.net/instances/sandbox/',
    261 			),
    262 			'GNU_Taler_Backend_API_Key' => array(
    263 				'title'       => __( 'Taler Backend API Key', 'woocommerce-gateway-gnutaler' ),
    264 				'type'        => 'text',
    265 				'description' => __( 'Enter your API key to authenticate with the Taler backend. Will be sent as a "Bearer" token using the HTTP "Authorization" header. Must be prefixed with "secret-token:" (RFC 8959). This is not your password but a token to be obtained via the /token endpoint from the REST service.', 'woocommerce-gateway-gnutaler' ),
    266 				'default'     => '',
    267 			),
    268 			'Order_text'                => array(
    269 				'title'       => __( 'Summary Text of the Order', 'woocommerce-gateway-gnutaler' ),
    270 				'type'        => 'text',
    271 				'description' => __( 'Set the text the customer will see when confirming payment. #%%s will be substituted with the order number. (Example: MyShop #%%s)', 'woocommerce-gateway-gnutaler' ),
    272 				'default'     => 'WooTalerShop #%s',
    273 			),
    274 			'GNU_Taler_refund_delay'    => array(
    275 				'title'       => __( 'How long should refunds be possible', 'woocommerce-gateway-gnutaler' ),
    276 				'type'        => 'number',
    277 				'description' => __( 'Set the number of days a customer has to request a refund', 'woocommerce-gateway-gnutaler' ),
    278 				'default'     => '14',
    279 			),
    280 			'debug'                     => array(
    281 				'title'       => __( 'Debug Log', 'woocommerce' ),
    282 				'label'       => __( 'Enable logging', 'woocommerce' ),
    283 				'description' => sprintf(
    284 					/* translators: placeholder will be replaced with the path to the log file */
    285 					__( 'Log GNU Taler events inside %s.', 'woocommerce-gateway-gnutaler' ),
    286 					'<code>' . WC_Log_Handler_File::get_log_file_path( 'gnutaler' ) . '</code>'
    287 				),
    288 				'type'        => 'checkbox',
    289 				'default'     => 'no',
    290 			),
    291 		);
    292 	}
    293 
    294 	/**
    295 	 * Called when WC sends out the e-mail notification for refunds.
    296 	 * Adds a Taler-specific notice for where to click to obtain
    297 	 * the refund.
    298 	 *
    299 	 * @param WC_Order $wc_order      The order.
    300 	 * @param bool     $sent_to_admin Not well documented by WooCommerce.
    301 	 * @param string   $plain_text    The plain text of the email.
    302 	 * @param string   $email         Target email address.
    303 	 */
    304 	public function add_content_refund_email( $wc_order, $sent_to_admin, $plain_text, $email ) {
    305 		if ( 'customer_refunded_order' === $email->id ) {
    306 			$backend_url = $this->gnu_taler_backend_url;
    307 			$wc_order_id = $wc_order->get_order_key() . '-' . $wc_order->get_order_number();
    308 			$refund_url  = $wc_order->get_meta( 'GNU_TALER_REFUND_URL' );
    309 			printf(
    310 				/* translators: placeholder will be replaced with the refund URL */
    311 				esc_html( __( 'Refund granted. Visit <a href="%1$s">%1$s</a> to obtain the refund.', 'woocommerce-gateway-gnutaler' ) ),
    312 				esc_url( $refund_url )
    313 			);
    314 		}
    315 	}
    316 
    317 	/**
    318 	 * Processes and saves options.
    319 	 * If there is an error thrown, will continue to save and validate fields, but
    320 	 * will leave the erroring field out.
    321 	 *
    322 	 * @return bool was anything saved?
    323 	 */
    324 	public function process_admin_options() {
    325 		$saved = parent::process_admin_options();
    326 
    327 		// Maybe clear logs.
    328 		if ( 'yes' !== $this->get_option( 'debug', 'no' ) ) {
    329 			if ( empty( self::$log ) ) {
    330 				self::$log = wc_get_logger();
    331 			}
    332 			self::$log->clear( 'gnutaler' );
    333 		}
    334 
    335 		return $saved;
    336 	}
    337 
    338 	/**
    339 	 * Required function to add the fields we want to show in the
    340 	 * payment method selection dialog. We show none.
    341 	 */
    342 	public function payment_fields() {
    343 		$this->debug( __( 'payment_fields() called for gnutaler', 'woocommerce-gateway-gnutaler' ) );
    344 		// We do not have any.
    345 	}
    346 
    347 	/**
    348 	 * Callback is called, when the user goes to the fulfillment URL.
    349 	 *
    350 	 * We check that the payment actually was made, and update WC accordingly.
    351 	 * If the order ID is unknown and/or the payment did not succeed, we
    352 	 * redirect to the home page and/or the user's order page (for logged in users).
    353 	 */
    354 	public function fulfillment_url_handler(): void {
    355 		global $woocommerce;
    356 
    357 		// We intentionally do NOT verify the nonce here, as this page
    358 		// should work even if the deep link is shared with other users
    359 		// or even non-users.
    360         // phpcs:disable WordPress.Security.NonceVerification
    361 		if ( ! isset( $_GET['order_id'] ) ) {
    362 			$this->debug( __( "Lacking 'order_id', forwarding user to neutral page", 'woocommerce-gateway-gnutaler' ) );
    363 			if ( is_user_logged_in() ) {
    364 				wp_safe_redirect( get_home_url() . wc_get_page_permalink( 'myaccount' ) );
    365 			} else {
    366 				wp_safe_redirect( get_home_url() . wc_get_page_permalink( 'shop' ) );
    367 			}
    368 			exit;
    369 		}
    370 
    371 		// Gets the order id from the fulfillment url.
    372 		$taler_order_id = sanitize_text_field( wp_unslash( $_GET['order_id'] ) );
    373         // phpcs:enable
    374 		$order_id_array = explode( '-', $taler_order_id );
    375 		$order_id_name  = $order_id_array[0];
    376 		$order_id       = $order_id_array[1];
    377 		$wc_order       = wc_get_order( $order_id );
    378 		$backend_url    = $this->gnu_taler_backend_url;
    379 
    380 		$payment_confirmation = $this->call_api(
    381 			'GET',
    382 			$backend_url . '/private/orders/' . $taler_order_id,
    383 			false
    384 		);
    385 		$payment_body         = $payment_confirmation['message'];
    386 		$payment_http_status  = $payment_confirmation['http_code'];
    387 
    388 		switch ( $payment_http_status ) {
    389 			case 200:
    390 				// Here we check what kind of http code came back from the backend.
    391 				$merchant_order_status_response = json_decode(
    392 					$payment_body,
    393 					$assoc                      = true
    394 				);
    395 				if ( ! $merchant_order_status_response ) {
    396 					wc_add_notice(
    397 						__( 'Payment error:', 'woocommerce-gateway-gnutaler' ) .
    398 						__( 'backend did not respond', 'woocommerce-gateway-gnutaler' )
    399 					);
    400 					$this->notice( __( 'Payment failed: no reply from Taler backend', 'woocommerce-gateway-gnutaler' ) );
    401 					wp_safe_redirect( $this->get_return_url( $order_id ) );
    402 					exit;
    403 				}
    404 				if ( 'paid' === $merchant_order_status_response['order_status'] ) {
    405 					$this->notice( __( 'Payment succeeded and the user was forwarded to the order confirmed page', 'woocommerce-gateway-gnutaler' ) );
    406 					// Completes the order, storing a transaction ID.
    407 					$wc_order->payment_complete( $taler_order_id );
    408 					// Empties the shopping cart.
    409 					WC()->cart->empty_cart();
    410 				} else {
    411 					wc_add_notice(
    412 						__( 'Payment error:', 'woocommerce-gateway-gnutaler' ) .
    413 						__( 'backend did not confirm payment', 'woocommerce-gateway-gnutaler' )
    414 					);
    415 					$this->notice( __( 'Backend did not confirm payment', 'woocommerce-gateway-gnutaler' ) );
    416 				}
    417 				wp_safe_redirect( $this->get_return_url( $wc_order ) );
    418 				exit;
    419 			default:
    420 				$this->error(
    421 					__( 'An error occurred during the second request to the GNU Taler backend: ', 'woocommerce-gateway-gnutaler' )
    422 					. $payment_http_status . ' - ' . $payment_body
    423 				);
    424 				wc_add_notice( __( 'Payment error:', 'woocommerce-gateway-gnutaler' ) . $payment_http_status . ' - ' . $payment_body );
    425 				wp_safe_redirect( $this->get_return_url( $order_id ) );
    426 				break;
    427 		}
    428 		$cart_url = $woocommerce->cart->wc_get_cart_url();
    429 		if ( is_set( $cart_url ) ) {
    430 			wp_safe_redirect( get_home_url() . $cart_url );
    431 		} else {
    432 			wp_safe_redirect( wc_get_page_permalink( 'shop' ) );
    433 		}
    434 		exit;
    435 	}
    436 
    437 	/**
    438 	 * Sends a request to a url via HTTP.
    439 	 *
    440 	 * Sends a request to a GNU Taler Backend over HTTP and returns the result.
    441 	 * The request can be sent as POST or GET. PATCH is not supported.
    442 	 *
    443 	 * @param string $method POST or GET supported only. Thanks WordPress.
    444 	 * @param string $url    URL for the request to make to the GNU Taler Backend.
    445 	 * @param string $body   The content of the request (for POST).
    446 	 *
    447 	 * @return array The return array will either have the successful return value or a detailed error message.
    448 	 */
    449 	private function call_api( $method, $url, $body ): array {
    450 		$apikey = $this->get_option( 'GNU_Taler_Backend_API_Key' );
    451 		$args   = array(
    452 			'timeout'             => 30, // In seconds.
    453 			'redirection'         => 2, // How often.
    454 			'httpversion'         => '1.1', // Taler will support.
    455 			'user-agent'          => '', // Minimize information leakage.
    456 			'blocking'            => true, // We do nothing without it.
    457 			'headers'             => array(
    458 				'Authorization' => 'Bearer ' . $apikey,
    459 			),
    460 			'decompress'          => true,
    461 			'limit_response_size' => 1024 * 1024, // More than enough.
    462 		);
    463 		if ( $body ) {
    464 			$args['body']                    = wp_json_encode( $body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, 16 );
    465 			$args['headers']['Content-type'] = 'application/json';
    466 			$args['compress']                = true;
    467 		}
    468 		$this->debug( 'Issuing HTTP ' . $method . ' request to ' . $url . ' with options ' . wp_json_encode( $args ) . ' and body ' . $body );
    469 
    470 		switch ( $method ) {
    471 			case 'POST':
    472 				$response = wp_remote_post( $url, $args );
    473 				break;
    474 			case 'GET':
    475 				$response = wp_remote_get( $url, $args );
    476 				break;
    477 			default:
    478 				$this->debug( 'HTTP method ' . $method . ' not supported' );
    479 				return null;
    480 		}
    481 		if ( is_wp_error( $response ) ) {
    482 			$error_code = $response->get_error_code();
    483 			$error_data = $response->get_error_data( $error_code );
    484 			$this->warning(
    485 				sprintf(
    486 					/* translators: first placeholder is the error code, second the error data */
    487 					__( 'HTTP failure %1$s with data %2$s', 'woocommerce-gateway-gnutaler' ),
    488 					$error_code,
    489 					$error_data
    490 				)
    491 			);
    492 
    493 			return array(
    494 				'http_code' => 0,
    495 				'message'   => $error_code,
    496 			);
    497 		}
    498 		$http_code = wp_remote_retrieve_response_code( $response );
    499 		$body      = wp_remote_retrieve_body( $response );
    500 		$this->debug(
    501 			sprintf(
    502 				/* translators: first placeholder is the HTTP status code, second the body of the HTTP reply */
    503 				__( 'HTTP status %1$s with response body %2$s', 'woocommerce-gateway-gnutaler' ),
    504 				$http_code,
    505 				$body
    506 			)
    507 		);
    508 		return array(
    509 			'http_code' => $http_code,
    510 			'message'   => $body,
    511 		);
    512 	}
    513 
    514 	/**
    515 	 * Checks if the configuration is valid. Used by WC to redirect
    516 	 * the admin to the setup dialog if they enable the backend and
    517 	 * it is not properly configured.
    518 	 */
    519 	public function needs_setup() {
    520 		$backend_url   = $this->gnu_taler_backend_url;
    521 		$verify_result = $this->verify_backend_url(
    522 			$backend_url,
    523 			null
    524 		);
    525 
    526 		if ($verify_result) {
    527 			$this->debug( __( 'Backend is ready', 'woocommerce-gateway-gnutaler' ) );
    528 		} else {
    529 			$this->warning( __( 'GNU Taler merchant backend is not setup correctly', 'woocommerce-gateway-gnutaler' ) );
    530 		}
    531 
    532 		return ! $verify_result;
    533 	}
    534 
    535 	/**
    536 	 * Verifying if the url to the backend given in the plugin options is valid or not.
    537 	 *
    538 	 * @param string $url       URL to the backend.
    539 	 * @param string $ecurrency expected currency of the order, null to match any.
    540 	 *
    541 	 * @return bool - Returns if valid or not.
    542 	 */
    543 	private function verify_backend_url( $url, $ecurrency ): bool {
    544 		$this->debug( 'Verifying backend URL ' . $url );
    545 
    546 		$config             = $this->call_api( 'GET', $url . '/config', false );
    547 		$config_http_status = $config['http_code'];
    548 		$config_body        = $config['message'];
    549 		switch ( $config_http_status ) {
    550 			case 200:
    551 				$info = json_decode( $config_body, $assoc = true );
    552 				if ( ! $info ) {
    553 					$this->error(
    554 						sprintf(
    555 							/* translators: placeholder will be replaced with the URL of the backend */
    556 							__( '/config response of backend at %s did not decode as JSON', 'woocommerce-gateway-gnutaler' ),
    557 							$url
    558 						)
    559 					);
    560 					return false;
    561 				}
    562 				$version = $info['version'];
    563 				if ( ! $version ) {
    564 					$this->error(
    565 						sprintf(
    566 							/* translators: placeholder will be replaced with the URL of the backend */
    567 							__( "No 'version' field in /config reply from Taler backend at %s", 'woocommerce-gateway-gnutaler' ),
    568 							$url
    569 						)
    570 					);
    571 					return false;
    572 				}
    573 				$ver      = explode( ':', $version, 3 );
    574 				$current  = $ver[0];
    575 				$revision = $ver[1];
    576 				$age      = $ver[2];
    577 				if ( ( ! is_numeric( $current ) )
    578 				|| ( ! is_numeric( $revision ) )
    579 				|| ( ! is_numeric( $age )
    580 				|| ( $age > $current ) )
    581 				) {
    582 					$this->error(
    583 						sprintf(
    584 							/* translators: placeholder will be replaced with the (malformed) version number */
    585 							__( "/config response at backend malformed: '%s' is not a valid version", 'woocommerce-gateway-gnutaler' ),
    586 							$version
    587 						)
    588 					);
    589 					return false;
    590 				}
    591 				if ( GNU_TALER_MERCHANT_PROTOCOL_CURRENT < $current - $age ) {
    592 					// Our implementation is too old!
    593 					$this->error(
    594 						sprintf(
    595 							/* translators: placeholder will be replaced with the version number */
    596 							__( 'Backend protocol version %s is too new: please update the GNU Taler plugin', 'woocommerce-gateway-gnutaler' ),
    597 							$version
    598 						)
    599 					);
    600 					return false;
    601 				}
    602 				if ( GNU_TALER_MERCHANT_PROTOCOL_CURRENT - GNU_TALER_MERCHANT_PROTOCOL_AGE > $current ) {
    603 					// Merchant implementation is too old!
    604 					$this->error(
    605 						sprintf(
    606 							/* translators: placeholder will be replaced with the version number */
    607 							__( 'Backend protocol version %s unsupported: please update the backend', 'woocommerce-gateway-gnutaler' ),
    608 							$version
    609 						)
    610 					);
    611 					return false;
    612 				}
    613 
    614 				$currency_found = is_null( $ecurrency );
    615 				if (! $currency_found) {
    616 					$exchanges = $info['exchanges'];
    617 					foreach ($exchanges as $exchange) {
    618 						$currency = $info['currency'];
    619 						if ( ! is_null( $currency ) &&
    620 						     ( 0 === strcasecmp( $currency, $ecurrency ) ) ) {
    621 							$currency_found = true;
    622 							break;
    623 						}
    624 					}
    625 				}
    626 				if ( ! $currency_found ) {
    627 					$this->error(
    628 						sprintf(
    629 							/* translators: first placeholder is the Taler backend currency, second the expected currency from WooCommerce */
    630 							__( 'Backend currency %1$s does not match order currency %2$s', 'woocommerce-gateway-gnutaler' ),
    631 							$currency,
    632 							$ecurrency
    633 						)
    634 					);
    635 					return false;
    636 				}
    637 				$this->debug(
    638 					sprintf(
    639 						/* translators: placeholder will be replaced with the URL of the backend */
    640 						__( '/config check for Taler backend at %s succeeded', 'woocommerce-gateway-gnutaler' ),
    641 						$url
    642 					)
    643 				);
    644 				return true;
    645 			default:
    646 				$this->error(
    647 					sprintf(
    648 						/* translators: placeholder will be replaced with the HTTP status code returned by the backend */
    649 						__( 'Backend failed /config request with unexpected HTTP status %s', 'woocommerce-gateway-gnutaler' ),
    650 						$config_http_status
    651 					)
    652 				);
    653 				return false;
    654 		}
    655 	}
    656 
    657 	/**
    658 	 * Processes the payment after the checkout
    659 	 *
    660 	 * If the payment process finished successfully the user is being redirected to its GNU Taler Wallet.
    661 	 * If an error occurs it returns void and throws an error.
    662 	 *
    663 	 * @param string $order_id ID of the order to get the Order from the WooCommerce Webshop.
    664 	 *
    665 	 * @return array|void - Array with result => success and redirection url otherwise it returns void.
    666 	 */
    667 	public function process_payment( $order_id ) {
    668 		// We need the order ID to get any order detailes.
    669 		$wc_order = wc_get_order( $order_id );
    670 
    671 		// Gets the url of the backend from the WooCommerce Settings.
    672 		$backend_url = $this->gnu_taler_backend_url;
    673 
    674 		// Log entry that the customer started the payment process.
    675 		$this->info( __( 'User started the payment process with GNU Taler.', 'woocommerce-gateway-gnutaler' ) );
    676 
    677 		if ( ! $this->verify_backend_url( $backend_url, $wc_order->get_currency() ) ) {
    678 			wc_add_notice( __( 'Something went wrong please contact the system administrator of the webshop and send the following error: GNU Taler backend URL invalid', 'woocommerce-gateway-gnutaler' ), 'error' );
    679 			$this->error( __( 'Checkout process failed: Invalid backend url.', 'woocommerce-gateway-gnutaler' ) );
    680 			return;
    681 		}
    682 		$order_json = $this->convert_to_checkout_json( $order_id );
    683 
    684 		$this->info( __( 'Sending POST /private/orders request send to Taler backend', 'woocommerce-gateway-gnutaler' ) );
    685 		$order_confirmation = $this->call_api(
    686 			'POST',
    687 			$backend_url . '/private/orders',
    688 			$order_json
    689 		);
    690 		$order_body         = $order_confirmation['message'];
    691 		$order_http_status  = $order_confirmation['http_code'];
    692 		switch ( $order_http_status ) {
    693 			case 200:
    694 				$post_order_response = json_decode( $order_body, $assoc = true );
    695 				if ( ! $post_order_response ) {
    696 					$this->error(
    697 						__( 'POST /private/orders request to Taler backend returned 200 OK, but not a JSON body: ', 'woocommerce-gateway-gnutaler' )
    698 						. $order_body
    699 					);
    700 					wc_add_notice( __( 'Malformed response from Taler backend. Please contact the system administrator.' ) );
    701 					$wc_order->set_status( 'cancelled' );
    702 					return;
    703 				}
    704 				$taler_order_id    = $post_order_response ['order_id'];
    705 				$taler_order_token = $post_order_response ['token'];
    706 				if ( ! $taler_order_id ) {
    707 					$this->error(
    708 						__( 'Response to POST /private/orders request to Taler backend lacks order_id field: ', 'woocommerce-gateway-gnutaler' )
    709 						. $order_body
    710 					);
    711 					wc_add_notice( __( 'Malformed response from Taler backend. Please contact the system administrator.', 'woocommerce-gateway-gnutaler' ) );
    712 					$wc_order->set_status( 'cancelled' );
    713 					return;
    714 				}
    715 				$this->info( __( 'POST /private/orders successful. Redirecting user to Taler Backend.', 'woocommerce-gateway-gnutaler' ) );
    716 				return array(
    717 					'result'   => 'success',
    718 					'redirect' => $backend_url . '/orders/' . $taler_order_id . '?token=' . $taler_order_token,
    719 				);
    720 			case 404:
    721 				$post_order_error = json_decode( $order_body, $assoc = true );
    722 				if ( ! $post_order_error ) {
    723 					$this->error(
    724 						__( 'POST /private/orders request to Taler backend returned 404, but not a JSON body: ', 'woocommerce-gateway-gnutaler' )
    725 						. $order_body
    726 					);
    727 					wc_add_notice( __( 'Malformed response from Taler backend. Please contact the system administrator.', 'woocommerce-gateway-gnutaler' ) );
    728 					$wc_order->set_status( 'cancelled' );
    729 					return;
    730 				}
    731 				$this->error(
    732 					__( 'POST /private/orders request to Taler backend failed: ', 'woocommerce-gateway-gnutaler' )
    733 								. $post_order_error['code'] . '('
    734 					. $order_http_status . '): ' . $order_body
    735 				);
    736 				wc_add_notice( __( 'Taler backend not configured correctly. Please contact the system administrator.', 'woocommerce-gateway-gnutaler' ) );
    737 				$wc_order->set_status( 'cancelled' );
    738 				return;
    739 			case 410:
    740 				// We don't use inventory management via GNU Taler's backend, so this error should never apply.
    741 				// Handle with 'default' case below.
    742 			default:
    743 				$this->error(
    744 					__( 'POST /private/orders request to Taler backend failed: ', 'woocommerce-gateway-gnutaler' )
    745 						. $post_order_error['code'] . '('
    746 					. $order_http_status . '): '
    747 					. $order_body
    748 				);
    749 				wc_add_notice( __( 'Unexpected problem with the Taler backend. Please contact the system administrator.', 'woocommerce-gateway-gnutaler' ) );
    750 				$wc_order->set_status( 'cancelled' );
    751 				return;
    752 		}
    753 	}
    754 
    755 	/**
    756 	 * Converts the order into a JSON format that can be send to the GNU Taler Backend.
    757 	 *
    758 	 * @param string $order_id ID of the order to get the Order from the WooCommerce Webshop.
    759 	 *
    760 	 * @return array - return the JSON Format.
    761 	 */
    762 	public function convert_to_checkout_json( $order_id ): array {
    763 		$wc_order                = wc_get_order( $order_id );
    764 		$wc_order_total_amount   = $wc_order->get_total();
    765 		$wc_order_currency       = $wc_order->get_currency();
    766 		$wc_cart                 = WC()->cart->get_cart();
    767 		$wc_order_id             = $wc_order->get_order_key() . '-' . $wc_order->get_order_number();
    768 		$wc_order_products_array = $this->mutate_products_to_json_format( $wc_cart, $wc_order_currency );
    769 		$refund_delay            = $this->get_option( 'GNU_Taler_refund_delay' );
    770 		$order_json              = array(
    771 			'order' => array(
    772 				'amount'            => $wc_order_currency . ':' . $wc_order_total_amount,
    773 				'summary'           => sprintf(
    774 					$this->get_option( 'Order_text' ),
    775 					$wc_order->get_order_number()
    776 				),
    777 				// NOTE: This interacts with the 'add_action' call
    778 				// to invoke the 'fulfillment_url_handler' when the
    779 				// user goes to this URL!
    780 				'fulfillment_url'   => get_home_url()
    781 						. '/?wc-api='
    782 					. strtolower( get_class( $this ) )
    783 					. '&order_id='
    784 					. $wc_order_id,
    785 				'order_id'          => $wc_order_id,
    786 				'products'          => $wc_order_products_array,
    787 				'delivery_location' => $this->mutate_shipping_information_to_json_format( $wc_order ),
    788 			),
    789 		);
    790 		if ( isset( $refund_delay ) ) {
    791 			$order_json['refund_delay'] = array(
    792 				'd_us' => 1000 * 1000 * 60 * 60 * 24 * intval( $refund_delay ),
    793 			);
    794 		}
    795 		return $order_json;
    796 	}
    797 
    798 	/**
    799 	 * Mutates the products in the cart into a format which can be included in a JSON file.
    800 	 *
    801 	 * @param WC_Cart $wc_cart          The content of the WooCommerce Cart.
    802 	 * @param string  $wc_order_currency The currency the WooCommerce Webshop uses.
    803 	 *
    804 	 * @return array - Returns an array of products.
    805 	 */
    806 	private function mutate_products_to_json_format( $wc_cart, $wc_order_currency ): array {
    807 		$wc_order_products_array = array();
    808 		foreach ( $wc_cart as $product ) {
    809 			$wc_order_products_array[] = array(
    810 				'description' => $product['data']->get_title(),
    811 				'quantity'    => $product['quantity'],
    812 				'price'       => $wc_order_currency . ':' . $product['data']->get_price(),
    813 				'product_id'  => strval( $product['data']->get_id() ),
    814 			);
    815 		}
    816 		return $wc_order_products_array;
    817 	}
    818 
    819 	/**
    820 	 * Processes the refund transaction if requested by the system administrator of the webshop
    821 	 *
    822 	 * If the refund request is finished successfully it returns an refund url, which can be send to the customer to finish the refund transaction.
    823 	 * If an error it will throw a WP_Error message and inform the system administrator.
    824 	 *
    825 	 * @param WC_Order $wc_order The WooCommerce order object we are processing.
    826 	 *
    827 	 * @return array
    828 	 */
    829 	private function mutate_shipping_information_to_json_format( $wc_order ): array {
    830 		$whitechar_encounter        = false;
    831 		$shipping_address_street    = '';
    832 		$shipping_address_street_nr = '';
    833 
    834 		$store_address = $wc_order->get_shipping_address_1();
    835 		if ( is_null( $store_address ) || empty( $store_address ) ) {
    836 			$store_address = $wc_order->get_billing_address_1();
    837 		}
    838 		$store_address_inverted = strrev( $store_address );
    839 		$store_address_array    = str_split( $store_address_inverted );
    840 		$country                = $wc_order->get_shipping_country();
    841 		if ( is_null( $country ) || empty( $country ) ) {
    842 			$country = $wc_order->get_billing_country();
    843 		}
    844 		$state = $wc_order->get_shipping_state();
    845 		if ( is_null( $state ) || empty( $state ) ) {
    846 			$state = $wc_order->get_billing_state();
    847 		}
    848 		$city = $wc_order->get_shipping_city();
    849 		if ( is_null( $city ) || empty( $city ) ) {
    850 			$city = $wc_order->get_billing_city();
    851 		}
    852 		$postcode = $wc_order->get_shipping_postcode();
    853 		if ( is_null( $postcode ) || empty( $postcode ) ) {
    854 			$postcode = $wc_order->get_billing_postcode();
    855 		}
    856 
    857 		$this->info(
    858 			sprintf(
    859 				'Shipping address is %s - %s - %s - %s - %s',
    860 				$store_address,
    861 				$wc_order->get_shipping_country(),
    862 				$wc_order->get_shipping_state(),
    863 				$wc_order->get_shipping_city(),
    864 				$wc_order->get_shipping_postcode()
    865 			)
    866 		);
    867 
    868 		$this->info(
    869 			sprintf(
    870 				'Billing address is %s - %s - %s - %s - %s',
    871 				$store_address,
    872 				$wc_order->get_billing_country(),
    873 				$wc_order->get_billing_state(),
    874 				$wc_order->get_billing_city(),
    875 				$wc_order->get_billing_postcode()
    876 			)
    877 		);
    878 		$this->info(
    879 			sprintf(
    880 				'Using address is %s - %s - %s - %s - %s',
    881 				$store_address,
    882 				$country,
    883 				$state,
    884 				$city,
    885 				$postcode
    886 			)
    887 		);
    888 
    889 		// Split the address into street and street number.
    890 		foreach ( $store_address_array as $char ) {
    891 			if ( ! $whitechar_encounter ) {
    892 				$shipping_address_street .= $char;
    893 			} elseif ( ctype_space( $char ) ) {
    894 				$whitechar_encounter = true;
    895 			} else {
    896 				$shipping_address_street .= $char;
    897 			}
    898 		}
    899 		$ret = array(
    900 			'country'             => $country,
    901 			'country_subdivision' => $state,
    902 			'town'                => $city,
    903 			'post_code'           => $postcode,
    904 			'street'              => $shipping_address_street,
    905 			'building_number'     => $shipping_address_street_nr,
    906 		);
    907 		if ( null !== $wc_order->get_shipping_address_2() ) {
    908 			$address_lines        = array(
    909 				$wc_order->get_shipping_address_1(),
    910 				$wc_order->get_shipping_address_2(),
    911 			);
    912 			$ret['address_lines'] = $address_lines;
    913 		}
    914 		return $ret;
    915 	}
    916 
    917 	/**
    918 	 * Processes the refund transaction if requested by the system administrator of the webshop
    919 	 *
    920 	 * If the refund request is finished successfully it returns an refund url, which can be send to the customer to finish the refund transaction.
    921 	 * If an error it will throw a WP_Error message and inform the system administrator.
    922 	 *
    923 	 * @param string $order_id Order id for logging.
    924 	 * @param string $amount Amount that is requested to be refunded.
    925 	 * @param string $reason Reason for the refund request.
    926 	 *
    927 	 * @return bool|WP_Error - Returns true or throws an WP_Error message in case of error.
    928 	 */
    929 	public function process_refund( $order_id, $amount = null, $reason = '' ) {
    930 		$wc_order = wc_get_order( $order_id );
    931 
    932 		$this->info(
    933 			sprintf(
    934 				/* translators: first placeholder is the numeric amount, second the currency, and third the reason for the refund */
    935 				__( 'Refund process started with the refunded amount %1$s %2$s and the reason %3$s.' ),
    936 				$amount,
    937 				$wc_order->get_currency(),
    938 				$reason
    939 			)
    940 		);
    941 
    942 		// Gets the url of the backend from the WooCommerce Settings.
    943 		$backend_url = $this->gnu_taler_backend_url;
    944 
    945 		// Get the current status of the order.
    946 		$wc_order_status = $wc_order->get_status();
    947 
    948 		// Checks if current status is already set as paid.
    949 		if ( ! ( 'processing' === $wc_order_status
    950 		|| 'on hold' === $wc_order_status
    951 		|| 'completed' === $wc_order_status )
    952 		) {
    953 			$this->error( __( 'The status of the order does not allow a refund', 'woocommerce-gateway-gnutaler' ) );
    954 			return new WP_Error( 'error', __( 'The status of the order does not allow for a refund.', 'woocommerce-gateway-gnutaler' ) );
    955 		}
    956 
    957 		$refund_request = array(
    958 			'refund' => $wc_order->get_currency() . ':' . $amount,
    959 			'reason' => $reason,
    960 		);
    961 		$wc_order_id    = $wc_order->get_order_key() . '-' . $wc_order->get_order_number();
    962 		$refund_result  = $this->call_api(
    963 			'POST',
    964 			$backend_url . '/private/orders/' . $wc_order_id . '/refund',
    965 			$refund_request
    966 		);
    967 
    968 		$refund_http_status = $refund_result['http_code'];
    969 		$refund_body        = $refund_result['message'];
    970 		switch ( $refund_http_status ) {
    971 			case 200:
    972 				$refund_response = json_decode( $refund_body, $assoc = true );
    973 				if ( ! $refund_response ) {
    974 					$this->error( __( 'Malformed 200 response from Taler backend: not even in JSON', 'woocommerce-gateway-gnutaler' ) );
    975 					return new WP_Error( 'error', __( 'Malformed response from Taler backend', 'woocommerce-gateway-gnutaler' ) );
    976 				}
    977 				$refund_uri = $refund_response['taler_refund_uri'];
    978 				$h_contract = $refund_response['h_contract'];
    979 				if ( ( ! $refund_uri ) || ( ! $h_contract ) ) {
    980 					$this->error( __( 'Malformed 200 response from Taler backend: lacks taler_refund_uri', 'woocommerce-gateway-gnutaler' ) );
    981 					return new WP_Error( 'error', __( 'Malformed response from Taler backend', 'woocommerce-gateway-gnutaler' ) );
    982 				}
    983 				$refund_url = $backend_url
    984 				. '/orders/'
    985 				. $wc_order_id
    986 				. '?h_contract='
    987 				. $h_contract;
    988 				$wc_order->add_meta_data( 'GNU_TALER_REFUND_URL', $refund_url );
    989 				$wc_order->update_status( 'refunded' );
    990 				$this->debug(
    991 					sprintf(
    992 						/* translators: argument is the Taler refund URI */
    993 
    994 						__( 'Received refund URI %s from Taler backend', 'woocommerce-gateway-gnutaler' ),
    995 						$refund_uri
    996 					)
    997 				);
    998 				$this->notice(
    999 					sprintf(
   1000 						/* translators: argument is the Taler refund URL */
   1001 						__( 'The user must visit %s to obtain the refund', 'woocommerce-gateway-gnutaler' ),
   1002 						$refund_url
   1003 					)
   1004 				);
   1005 				return true;
   1006 			case 403:
   1007 				return new WP_Error(
   1008 					'error',
   1009 					__( 'Refunds are disabled for this order. Check the refund_delay option for the Taler payment plugin.', 'woocommerce-gateway-gnutaler' )
   1010 				);
   1011 			case 404:
   1012 				$refund_error = json_decode( $refund_body, $assoc = true );
   1013 				if ( ! $refund_error ) {
   1014 					return new WP_Error(
   1015 						'error',
   1016 						sprintf(
   1017 							/* translators: argument is the HTTP status returned by the backend */
   1018 							__( 'Unexpected failure %s without Taler error code from Taler backend', 'woocommerce-gateway-gnutaler' ),
   1019 							$refund_http_status
   1020 						)
   1021 					);
   1022 				}
   1023 				$ec = $refund_error['code'];
   1024 				switch ( $ec ) {
   1025 					case 2000: // TALER_EC_INSTANCE_UNKNOWN!
   1026 						return new WP_Error(
   1027 							'error',
   1028 							__( 'Instance unknown reported by Taler backend', 'woocommerce-gateway-gnutaler' )
   1029 						);
   1030 					case 2601: // TALER_EC_REFUND_ORDER_ID_UNKNOWN!
   1031 						return new WP_Error(
   1032 							'error',
   1033 							__( 'Order unknown reported by Taler backend', 'woocommerce-gateway-gnutaler' )
   1034 						);
   1035 					default:
   1036 						return new WP_Error(
   1037 							'error',
   1038 							sprintf(
   1039 								/* translators: placeholder will be replaced with the numeric GNU Taler error code */
   1040 								__( 'Unexpected error %s reported by Taler backend', 'woocommerce-gateway-gnutaler' ),
   1041 								$ec
   1042 							)
   1043 						);
   1044 				}
   1045 				// This line is unreachable.
   1046 				// Thus, no break is needed!
   1047 			case 409:
   1048 				return new WP_Error(
   1049 					'error',
   1050 					__( 'Requested refund amount exceeds original payment. This is not allowed!', 'woocommerce-gateway-gnutaler' )
   1051 				);
   1052 			case 410:
   1053 				return new WP_Error(
   1054 					'error',
   1055 					__( 'Wire transfer already happened. It is too late for a refund with Taler!', 'woocommerce-gateway-gnutaler' )
   1056 				);
   1057 			default:
   1058 				$refund_error = json_decode( $refund_body, $assoc = true );
   1059 				if ( ! $refund_error ) {
   1060 					$ec = $refund_error['code'];
   1061 				} else {
   1062 					$ec = 0;
   1063 				}
   1064 				return new WP_Error(
   1065 					'error',
   1066 					sprintf(
   1067 						/* translators: first placeholder is the HTTP status code, second the numeric GNU Taler error code */
   1068 						__( 'Unexpected failure %1$s/%2$s from Taler backend', 'woocommerce-gateway-gnutaler' ),
   1069 						$refund_http_status,
   1070 						$ec
   1071 					)
   1072 				);
   1073 		}
   1074 	}
   1075 
   1076 	/**
   1077 	 * Log $msg for debugging
   1078 	 *
   1079 	 * @param string $msg message to log.
   1080 	 */
   1081 	private function debug( $msg ): void {
   1082 		$this->log( 'debug', $msg );
   1083 	}
   1084 
   1085 	/**
   1086 	 * Log $msg as a informational
   1087 	 *
   1088 	 * @param string $msg message to log.
   1089 	 */
   1090 	private function info( $msg ): void {
   1091 		$this->log( 'info', $msg );
   1092 	}
   1093 
   1094 	/**
   1095 	 * Log $msg as a notice
   1096 	 *
   1097 	 * @param string $msg message to log.
   1098 	 */
   1099 	private function notice( $msg ): void {
   1100 		$this->log( 'notice', $msg );
   1101 	}
   1102 
   1103 	/**
   1104 	 * Log $msg as a warning.
   1105 	 *
   1106 	 * @param string $msg message to log.
   1107 	 */
   1108 	private function warning( $msg ): void {
   1109 		$this->log( 'warning', $msg );
   1110 	}
   1111 
   1112 	/**
   1113 	 * Log $msg as an error
   1114 	 *
   1115 	 * @param string $msg message to log.
   1116 	 */
   1117 	private function error( $msg ): void {
   1118 		$this->log( 'error', $msg );
   1119 	}
   1120 
   1121 	/**
   1122 	 * Log $msg at log $level.
   1123 	 *
   1124 	 * @param string $level log level to use when logging.
   1125 	 * @param string $msg message to log.
   1126 	 */
   1127 	private function log( $level, $msg ) {
   1128 		if ( ! self::$log_enabled ) {
   1129 			return;
   1130 		}
   1131 		if ( function_exists( 'wp_get_current_user()' ) ) {
   1132 			$user_id = wp_get_current_user();
   1133 			if ( ! isset( $user_id ) ) {
   1134 				$user_id = __( '<user ID not set>', 'woocommerce-gateway-gnutaler' );
   1135 			}
   1136 		} else {
   1137 			$user_id = 'Guest';
   1138 		}
   1139 		// We intentionally do NOT verify the nonce here, as logging
   1140 		// should always work.
   1141         // phpcs:disable WordPress.Security.NonceVerification
   1142 		if ( isset( $_GET['order_id'] ) ) {
   1143 			$order_id = sanitize_text_field( wp_unslash( $_GET['order_id'] ) );
   1144 		} else {
   1145 			$order_id = 'NONE';
   1146 		}
   1147         // phpcs:enable
   1148 		if ( empty( self::$log ) ) {
   1149 			self::$log = wc_get_logger();
   1150 		}
   1151 		self::$log->log( $level, $user_id . '-' . $order_id . ': ' . $msg, array( 'source' => 'gnutaler' ) );
   1152 	}
   1153 }