Файловый менеджер - Редактировать - /home/tuudkjt/globeasy/wp-includes/ID3/Domain.tar
Назад
Services/CheckoutFieldsSchema/checkout-document-schema.json 0000777 00000021362 15251721451 0020134 0 ustar 00 { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Cart and Checkout Document Object Schema", "description": "Document object schema for cart, checkout, and customer information, to be used for conditional visibility, requirement, and validation of fields.", "type": "object", "properties": { "cart": { "type": "object", "description": "Information about the shopping cart", "properties": { "coupons": { "type": "array", "description": "List of coupon codes applied to the cart", "items": { "type": "string" } }, "shipping_rates": { "type": "array", "description": "List of currently selected shipping rates", "items": { "type": "string", "description": "Shipping rate identifier using the full shipping rate ID so method_id:instance_id, for example: flat_rate:1" } }, "items": { "type": "array", "description": "List of product IDs in the cart, IDs will be duplicated depending on the quantity of the product in the cart, so if you have 2 of product ID 1, the array will have 2 entries of product ID 1", "items": { "type": "integer" } }, "items_type": { "type": "array", "description": "Types of items in the cart, for example: simple, variation, subscription, etc.", "items": { "type": "string" } }, "items_count": { "type": "integer", "description": "Total number of items in the cart", "minimum": 0 }, "items_weight": { "type": "number", "description": "Total weight of items in the cart", "minimum": 0 }, "needs_shipping": { "type": "boolean", "description": "Whether the items in the cart require shipping" }, "prefers_collection": { "type": "boolean", "description": "Whether the customer prefers using Local Pickup" }, "totals": { "type": "object", "description": "Cart totals information", "properties": { "total_price": { "type": "integer", "description": "Total price of the cart in smallest currency unit (e.g., cents), after applying all discounts, shipping, and taxes" }, "total_tax": { "type": "integer", "description": "Total tax amount in smallest currency unit (e.g., cents), after applying all discounts, shipping, and taxes" } }, "additionalProperties": false }, "extensions": { "type": "object", "description": "Additional cart extension data, this is similar to what's passed in Store API's extensions parameter" } }, "additionalProperties": false }, "checkout": { "type": "object", "description": "Checkout preferences and settings", "properties": { "create_account": { "type": "boolean", "description": "Whether the customer checked the create account checkbox, this will be false if the customer is logged in, cannot create an account, or forced to create an account." }, "customer_note": { "type": "string", "description": "Customer's note or special instructions for the order, this will be empty if the customer didn't add a note." }, "additional_fields": { "type": "object", "description": "Additional checkout fields, limited to the order location.", "additionalProperties": { "type": "string" }, "patternProperties": { "^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$": { "type": "string", "description": "Custom fields with namespace identifiers" } } }, "payment_method": { "type": "string", "description": "Selected payment method identifier, this will be the payment method ID regardless if the customer selected a saved payment method or new payment method" } }, "additionalProperties": false }, "customer": { "type": "object", "description": "Customer information", "properties": { "id": { "type": "integer", "description": "Customer ID, this will be 0 if the customer is not logged in" }, "billing_address": { "$ref": "#/definitions/address", "description": "Customer's billing address" }, "shipping_address": { "$ref": "#/definitions/address", "description": "Customer's shipping address" }, "additional_fields": { "type": "object", "description": "Additional checkout fields, limited to the contact location.", "additionalProperties": { "type": "string" } }, "address": { "$ref": "#/definitions/address", "description": "This is a dynamic field that will be the billing or shipping address depending on the context of the field being evaluated." } }, "additionalProperties": false } }, "additionalProperties": false, "definitions": { "address": { "type": "object", "description": "Customer address information", "properties": { "first_name": { "type": "string", "description": "First name of the recipient" }, "last_name": { "type": "string", "description": "Last name of the recipient" }, "company": { "type": "string", "description": "Company name" }, "address_1": { "type": "string", "description": "Primary address line" }, "address_2": { "type": "string", "description": "Secondary address line" }, "city": { "type": "string", "description": "City name" }, "state": { "type": "string", "description": "State or province, this will be the state code if it's a predefined list, for example: CA, TX, NY, etc, or the field value if it's a freeform state, for example: London." }, "postcode": { "type": "string", "description": "Postal or ZIP code" }, "country": { "type": "string", "description": "Country code (e.g., US, UK)" }, "email": { "type": "string", "description": "Email address" }, "phone": { "type": "string", "description": "Phone number" } }, "additionalProperties": { "type": "string", "description": "Custom fields with namespace identifiers" }, "patternProperties": { "^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$": { "type": "string", "description": "Custom fields with namespace identifiers" } } } } } Services/CheckoutFieldsSchema/json-schema-draft-07.json 0000777 00000011563 15251721451 0017010 0 ustar 00 { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://json-schema.org/draft-07/schema#", "title": "Core schema meta-schema", "definitions": { "schemaArray": { "type": "array", "minItems": 1, "items": { "$ref": "#" } }, "nonNegativeInteger": { "type": "integer", "minimum": 0 }, "nonNegativeIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/nonNegativeInteger" }, { "default": 0 } ] }, "simpleTypes": { "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] }, "stringArray": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "default": [] } }, "type": ["object", "boolean"], "properties": { "$id": { "type": "string", "format": "uri-reference" }, "$schema": { "type": "string", "format": "uri" }, "$ref": { "type": "string", "format": "uri-reference" }, "$comment": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, "default": true, "readOnly": { "type": "boolean", "default": false }, "writeOnly": { "type": "boolean", "default": false }, "examples": { "type": "array", "items": true }, "multipleOf": { "type": "number", "exclusiveMinimum": 0 }, "maximum": { "type": "number" }, "exclusiveMaximum": { "type": "number" }, "minimum": { "type": "number" }, "exclusiveMinimum": { "type": "number" }, "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, "additionalItems": { "$ref": "#" }, "items": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/schemaArray" } ], "default": true }, "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": false }, "contains": { "$ref": "#" }, "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, "additionalProperties": { "$ref": "#" }, "definitions": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "properties": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "patternProperties": { "type": "object", "additionalProperties": { "$ref": "#" }, "propertyNames": { "format": "regex" }, "default": {} }, "dependencies": { "type": "object", "additionalProperties": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/stringArray" } ] } }, "propertyNames": { "$ref": "#" }, "const": true, "enum": { "type": "array", "items": true, "minItems": 1, "uniqueItems": true }, "type": { "anyOf": [ { "$ref": "#/definitions/simpleTypes" }, { "type": "array", "items": { "$ref": "#/definitions/simpleTypes" }, "minItems": 1, "uniqueItems": true } ] }, "format": { "type": "string" }, "contentMediaType": { "type": "string" }, "contentEncoding": { "type": "string" }, "if": { "$ref": "#" }, "then": { "$ref": "#" }, "else": { "$ref": "#" }, "allOf": { "$ref": "#/definitions/schemaArray" }, "anyOf": { "$ref": "#/definitions/schemaArray" }, "oneOf": { "$ref": "#/definitions/schemaArray" }, "not": { "$ref": "#" } }, "default": true } Services/CheckoutFieldsSchema/DocumentObject.php 0000777 00000014612 15251721451 0016000 0 ustar 00 <?php declare( strict_types = 1); namespace Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema; use WC_Cart; use WC_Customer; use Automattic\WooCommerce\StoreApi\StoreApi; use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema; use Automattic\WooCommerce\StoreApi\Schemas\V1\BillingAddressSchema; use Automattic\WooCommerce\StoreApi\Schemas\V1\ShippingAddressSchema; use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils; use Automattic\WooCommerce\StoreApi\Utilities\CartController; use Automattic\WooCommerce\Utilities\NumberUtil; /** * DocumentObject class. * * This will combine and format given cart/customer/checkout data into a standard object format that can be queried through * JSON. This is used for conditional fields and validation during checkout. */ class DocumentObject { /** * Docuemnt object context which may adjust the schema response. * * @var null|string */ protected $context = null; /** * Valid contexts. * * @var array */ protected $valid_contexts = [ 'shipping_address', 'billing_address', 'contact', 'order', ]; /** * The cart object. * * @var WC_Cart|null */ protected $cart = null; /** * The customer object. * * @var WC_Customer|null */ protected $customer = null; /** * Cart controller class instance. * * @var CartController */ protected $cart_controller; /** * Schema controller class instance. * * @var SchemaController */ protected $schema_controller; /** * The request data. * * @var array */ protected $request_data = []; /** * The constructor. * * @param array $request_data Data that overrides the default values. */ public function __construct( array $request_data = [] ) { $this->cart_controller = new CartController(); $this->schema_controller = StoreApi::container()->get( SchemaController::class ); $this->request_data = $request_data; } /** * Set document object context. * * @param null|string $context Context to set. */ public function set_context( $context = null ) { if ( ! in_array( $context, $this->valid_contexts, true ) ) { return; } $this->context = $context; } /** * Set the customer object. * * @param WC_Customer $customer The customer object. */ public function set_customer( WC_Customer $customer ) { $this->customer = $customer; } /** * Set the cart object. * * @param WC_Cart $cart The cart object. */ public function set_cart( WC_Cart $cart ) { $this->cart = $cart; } /** * Gets a subset of cart data. * * @return array The cart data. */ protected function get_cart_data() { $cart_data = StoreApi::container()->get( SchemaController::class )->get( CartSchema::IDENTIFIER )->get_item_response( $this->cart ); $selected_shipping_rates = array_filter( array_map( function ( $package ) { $selected_rate = array_search( true, array_column( $package['shipping_rates'], 'selected' ), true ); return false !== $selected_rate && isset( $package['shipping_rates'][ $selected_rate ] ) ? $package['shipping_rates'][ $selected_rate ] : null; }, $cart_data['shipping_rates'] ) ); $local_pickup_method_ids = LocalPickupUtils::get_local_pickup_method_ids(); return wp_parse_args( $this->request_data['cart'] ?? [], [ 'coupons' => array_values( wc_list_pluck( $cart_data['coupons'], 'code' ) ), 'shipping_rates' => array_values( wc_list_pluck( $selected_shipping_rates, 'rate_id' ) ), 'items' => array_merge( ...array_map( function ( $item ) { return array_fill( 0, (int) NumberUtil::ceil( $item['quantity'] ), $item['id'] ); }, $cart_data['items'] ) ), 'items_type' => array_unique( array_values( wc_list_pluck( $cart_data['items'], 'type' ) ) ), 'items_count' => $cart_data['items_count'], 'items_weight' => $cart_data['items_weight'], 'needs_shipping' => $cart_data['needs_shipping'], 'prefers_collection' => count( array_intersect( $local_pickup_method_ids, wc_list_pluck( $selected_shipping_rates, 'method_id' ) ) ) > 0, 'totals' => [ 'total_price' => (int) $cart_data['totals']->total_price, 'total_tax' => (int) $cart_data['totals']->total_tax, ], 'extensions' => (object) $cart_data['extensions'], ] ); } /** * Get checkout data. * * @return array Checkout data context. */ protected function get_checkout_data() { return $this->request_data['checkout'] ?? []; } /** * Get the customer data. * * @return array The customer data. */ protected function get_customer_data() { $customer_data = [ 'id' => $this->request_data['customer']['id'] ?? $this->customer->get_id(), 'shipping_address' => wp_parse_args( $this->request_data['customer']['shipping_address'] ?? (object) [], $this->schema_controller->get( ShippingAddressSchema::IDENTIFIER )->get_item_response( $this->customer ) ), 'billing_address' => wp_parse_args( $this->request_data['customer']['billing_address'] ?? (object) [], $this->schema_controller->get( BillingAddressSchema::IDENTIFIER )->get_item_response( $this->customer ) ), 'additional_fields' => $this->request_data['customer']['additional_fields'] ?? (object) [], ]; if ( 'shipping_address' === $this->context ) { $customer_data['address'] = $customer_data['shipping_address']; } if ( 'billing_address' === $this->context ) { $customer_data['address'] = $customer_data['billing_address']; } return $customer_data; } /** * Get the data for the document object. * * This isn't a 1:1 match with Store API because some data is simplified to make it easier to parse as JSON. * * @return array The data for the document object. */ public function get_data() { // Get cart and customer objects before returning data if they are null. if ( is_null( $this->cart ) ) { $this->cart = $this->cart_controller->get_cart_for_response(); } if ( is_null( $this->customer ) ) { $this->customer = ! empty( WC()->customer ) ? WC()->customer : new WC_Customer(); } return [ 'cart' => $this->get_cart_data(), 'customer' => $this->get_customer_data(), 'checkout' => $this->get_checkout_data(), ]; } /** * Get the current context. * * @return null|string The context. */ public function get_context() { return $this->context; } } Services/CheckoutFieldsSchema/Validation.php 0000777 00000010753 15251721451 0015167 0 ustar 00 <?php declare( strict_types = 1); namespace Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\DocumentObject; use Opis\JsonSchema\{ Helper, Validator }; use WP_Error; /** * Service class validating checkout field schema. */ class Validation { /** * Meta schema. * * @var string */ private static $meta_schema_json = ''; /** * Get the field schema with context. * * @param string $field_id The field ID. * @param array $field_schema The field schema. * @param string $context The context. * @return array */ public static function get_field_schema_with_context( $field_id, $field_schema, $context ) { $primary_key = 'checkout'; $secondary_key = 'additional_fields'; switch ( $context ) { case 'billing_address': case 'shipping_address': $primary_key = 'customer'; $secondary_key = $context; break; case 'contact': $primary_key = 'customer'; $secondary_key = 'additional_fields'; break; } return [ $primary_key => [ 'properties' => [ $secondary_key => [ 'properties' => [ $field_id => $field_schema, ], ], ], ], ]; } /** * Check if the schema is unwrapped (has cart, checkout, customer, as top level keys). * * @param array $schema The schema to check. * @return bool */ private static function schema_is_unwrapped( $schema ) { return isset( $schema['cart'] ) || isset( $schema['checkout'] ) || isset( $schema['customer'] ); } /** * Validate the field rules. * * @param DocumentObject $document_object The document object to validate. * @param array $rules The rules to validate against. * @return bool|WP_Error */ public static function validate_document_object( DocumentObject $document_object, $rules ) { if ( self::schema_is_unwrapped( $rules ) ) { $rules = [ '$schema' => 'http://json-schema.org/draft-07/schema#', 'type' => 'object', 'properties' => $rules, ]; } else { if ( ! isset( $rules['$schema'] ) ) { $rules['$schema'] = 'http://json-schema.org/draft-07/schema#'; } if ( ! isset( $rules['type'] ) ) { $rules['type'] = 'object'; } } try { $validator = new Validator(); $result = $validator->validate( Helper::toJSON( $document_object->get_data() ), Helper::toJSON( $rules ) ); if ( ! $result->hasError() ) { return true; } } catch ( \Exception $e ) { return new WP_Error( 'woocommerce_rest_checkout_validation_failed', __( 'Validation failed.', 'woocommerce' ) ); } // Return generic error message. return new WP_Error( 'woocommerce_rest_checkout_invalid_field', __( 'Invalid field.', 'woocommerce' ) ); } /** * Check if the fields have defined schema. * * @param array $fields The fields. * @return bool */ public static function has_field_schema( $fields ) { $return = false; foreach ( $fields as $field ) { if ( ( ! empty( $field['validation'] ) && is_array( $field['validation'] ) ) || ( ! empty( $field['required'] ) && is_array( $field['required'] ) ) || ( ! empty( $field['hidden'] ) && is_array( $field['hidden'] ) ) ) { $return = true; break; } } return $return; } /** * Validate meta schema for field rules. * * @param mixed $rules The rules to validate. * @return bool|WP_Error True if the field options are valid, a WP_Error otherwise. */ public static function is_valid_schema( $rules ) { if ( ! is_array( $rules ) ) { return new WP_Error( 'woocommerce_rest_checkout_invalid_field_schema', 'Rules must be defined as an array.' ); } if ( empty( $rules ) ) { return true; } if ( self::schema_is_unwrapped( $rules ) ) { $rules = [ 'type' => 'object', 'properties' => $rules, ]; } if ( empty( self::$meta_schema_json ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents self::$meta_schema_json = file_get_contents( __DIR__ . '/json-schema-draft-07.json' ); } $validator = new Validator(); $result = $validator->validate( Helper::toJSON( [ '$schema' => 'http://json-schema.org/draft-07/schema#', 'type' => 'object', 'properties' => [ 'test' => $rules, ], 'required' => [ 'test' ], ] ), self::$meta_schema_json ); if ( $result->hasError() ) { return new WP_Error( 'woocommerce_rest_checkout_invalid_field_schema', esc_html( (string) $result->error() ) ); } return true; } } Services/CheckoutLink.php 0000777 00000013272 15251721451 0011442 0 ustar 00 <?php /** * Functionality that takes a static URL, constructs a cart, and redirects to the checkout with a cart session. */ declare(strict_types=1); namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils; use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils; use Automattic\WooCommerce\StoreApi\Utilities\CartController; defined( 'ABSPATH' ) || exit; /** * Checkout Link class. */ class CheckoutLink { /** * Initialize the checkout link service. */ public function init() { add_action( 'init', array( $this, 'add_checkout_link_endpoint' ) ); add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); add_action( 'template_redirect', array( $this, 'handle_checkout_link_endpoint' ) ); } /** * Add the checkout link endpoint. */ public function add_checkout_link_endpoint() { // get registered rewrite rules. $rules = get_option( 'rewrite_rules', array() ); $regex = '^checkout-link$'; add_rewrite_rule( $regex, 'index.php?checkout-link=true', 'top' ); // maybe flush rewrite rules if it was not previously in the option. if ( ! isset( $rules[ $regex ] ) ) { flush_rewrite_rules(); } } /** * Add the checkout link query var. * * @param array $vars The query vars. * @return array The query vars. */ public function add_query_vars( $vars ) { $vars[] = 'checkout-link'; return $vars; } /** * Handle the checkout link endpoint. * * @return void */ public function handle_checkout_link_endpoint() { if ( ! get_query_var( 'checkout-link' ) ) { return; } if ( ! $this->validate_checkout_link() ) { $redirect = add_query_arg( 'wc_error', rawurlencode( __( 'The provided checkout link was out of date or invalid. No products were added to the cart.', 'woocommerce' ) ), wc_get_cart_url() ); } else { wc()->cart->empty_cart(); $redirect = $this->get_checkout_link(); } wp_safe_redirect( $redirect ); exit; } /** * Validate the checkout link. * * @return bool True if the checkout link is valid, false otherwise. */ protected function validate_checkout_link() { $products = $this->get_products_from_checkout_link(); return ! empty( $products ); } /** * Get the products from the checkout link. * * @return array The products (keys) and their quantities (values). */ protected function get_products_from_checkout_link() { $raw_products = array_filter( explode( ',', wc_clean( wp_unslash( $_GET['products'] ?? '' ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $products = []; foreach ( $raw_products as $product_id_qty ) { if ( strpos( $product_id_qty, ':' ) !== false ) { list( $product_id, $qty ) = explode( ':', $product_id_qty ); } else { $product_id = $product_id_qty; $qty = 1; } $product_id = absint( $product_id ); $qty = absint( $qty ); if ( ! $product_id || ! $qty ) { continue; } $products[ $product_id ] = $qty; } return $products; } /** * Add error notices to the cart. * * @param \WP_Error $errors The errors. * @return void */ protected function add_error_notices( \WP_Error $errors ) { foreach ( $errors->get_error_messages() as $message ) { wc_add_notice( $message, 'error' ); } } /** * Process the query params and return the checkout link to redirect to complete with session token. * * @return string The checkout link. */ protected function get_checkout_link() { $controller = new CartController(); $products = $this->get_products_from_checkout_link(); $errors = new \WP_Error(); foreach ( $products as $product_id => $qty ) { try { $controller->add_to_cart( [ 'id' => $product_id, 'quantity' => $qty, ] ); } catch ( \Exception $e ) { $errors->add( 'error', $e->getMessage() ); } } // Nothing was added to the cart. We need to redirect to the cart page with an error notice. Since guests may not // have a session, add the notice in the query string. if ( wc()->cart->is_empty() ) { $errors->add( 'error', __( 'The provided checkout link was out of date or invalid. No products were added to the cart.', 'woocommerce' ) ); if ( ! wc()->session->has_session() ) { return add_query_arg( 'wc_error', rawurlencode( $errors->get_error_message() ), wc_get_cart_url() ); } else { $this->add_error_notices( $errors ); } return wc_get_cart_url(); } // Apply coupon if provided. $coupon = wc_format_coupon_code( wp_unslash( $_GET['coupon'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( wc_coupons_enabled() && ! empty( $coupon ) ) { try { $controller->apply_coupon( $coupon ); } catch ( \Exception $e ) { $errors->add( 'error', $e->getMessage() ); } } // Add error notices to the cart. This requires a session otherwise the notices will not be displayed. $this->add_error_notices( $errors ); $redirect_url = wc_get_checkout_url(); // Preserve the query string--pass it to the checkout page. if ( ! empty( $_SERVER['QUERY_STRING'] ) ) { $redirect_url = remove_query_arg( [ 'products', 'coupon', 'checkout-link', ], add_query_arg( wp_unslash( $_SERVER['QUERY_STRING'] ), '', $redirect_url ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized ); } // If the user is logged in, the session is tied to the user ID. Do not use a cart token. if ( ! is_user_logged_in() ) { $session_token = CartTokenUtils::get_cart_token( (string) wc()->session->get_customer_id() ); $redirect_url = add_query_arg( 'session', $session_token, $redirect_url ); } return $redirect_url; } } Services/CheckoutFieldsFrontend.php 0000777 00000033406 15251721451 0013454 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\DocumentObject; use WC_Customer; use WC_Order; /** * Service class managing checkout fields and its related extensibility points on the frontend. */ class CheckoutFieldsFrontend { /** * Checkout field controller. * * @var CheckoutFields */ private $checkout_fields_controller; /** * Sets up core fields. * * @param CheckoutFields $checkout_fields_controller Instance of the checkout field controller. */ public function __construct( CheckoutFields $checkout_fields_controller ) { $this->checkout_fields_controller = $checkout_fields_controller; } /** * Initialize hooks. This is not run Store API requests. */ public function init() { // Show custom checkout fields on the order details page. add_action( 'woocommerce_order_details_after_customer_address', array( $this, 'render_order_address_fields' ), 10, 2 ); add_action( 'woocommerce_order_details_after_customer_details', array( $this, 'render_order_other_fields' ), 10 ); // Show custom checkout fields on the My Account page. add_action( 'woocommerce_my_account_after_my_address', array( $this, 'render_address_fields' ), 10, 1 ); // Edit account form under my account (for contact details). add_filter( 'woocommerce_edit_account_form_fields', array( $this, 'edit_account_form_fields' ), 10, 1 ); add_action( 'woocommerce_save_account_details', array( $this, 'save_account_form_fields' ), 10, 1 ); // Edit address form under my account. add_filter( 'woocommerce_address_to_edit', array( $this, 'edit_address_fields' ), 10, 2 ); add_action( 'woocommerce_customer_save_address', array( $this, 'save_address_fields' ), 10, 4 ); } /** * Render custom fields. * * @param array $fields List of additional fields with values. * @return string */ protected function render_additional_fields( $fields ) { return ! empty( $fields ) ? '<dl class="wc-block-components-additional-fields-list">' . implode( '', array_map( array( $this, 'render_additional_field' ), $fields ) ) . '</dl>' : ''; } /** * Render custom field. * * @param array $field An additional field and value. * @return string */ protected function render_additional_field( $field ) { return sprintf( '<dt>%1$s</dt><dd>%2$s</dd>', esc_html( $field['label'] ), esc_html( $field['value'] ) ); } /** * Renders address fields on the order details page. * * @param string $address_type Type of address (billing or shipping). * @param WC_Order $order Order object. */ public function render_order_address_fields( $address_type, $order ) { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $this->render_additional_fields( $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $address_type, 'view' ) ); } /** * Renders additional fields on the order details page. * * @param WC_Order $order Order object. */ public function render_order_other_fields( $order ) { $fields = array_merge( $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', 'other', 'view' ), $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'order', 'other', 'view' ), ); $context = array( 'caller' => 'CheckoutFieldsFrontend::render_order_other_fields', 'order' => $order, ); $fields = $this->checkout_fields_controller->filter_fields_for_order_confirmation( $fields, $context ); if ( ! $fields ) { return; } echo '<section class="wc-block-order-confirmation-additional-fields-wrapper">'; echo '<h2>' . esc_html__( 'Additional information', 'woocommerce' ) . '</h2>'; echo $this->render_additional_fields( $fields ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '</section>'; } /** * Renders address fields on the account page. * * @param string $address_type Type of address (billing or shipping). */ public function render_address_fields( $address_type ) { if ( ! in_array( $address_type, array( 'billing', 'shipping' ), true ) ) { return; } $customer = new WC_Customer( get_current_user_id() ); $document_object = new DocumentObject(); $document_object->set_customer( $customer ); $document_object->set_context( $address_type . '_address' ); $fields = $this->checkout_fields_controller->get_contextual_fields_for_location( 'address', $document_object ); if ( ! $fields || ! $customer ) { return; } foreach ( $fields as $key => $field ) { $value = $this->checkout_fields_controller->format_additional_field_value( $this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type ), $field ); if ( ! $value ) { continue; } printf( '<br><strong>%s</strong>: %s', wp_kses_post( $field['label'] ), wp_kses_post( $value ) ); } } /** * Adds additional contact fields to the My Account edit account form. */ public function edit_account_form_fields() { $customer = new WC_Customer( get_current_user_id() ); $document_object = new DocumentObject(); $document_object->set_customer( $customer ); $document_object->set_context( 'contact' ); $fields = $this->checkout_fields_controller->get_contextual_fields_for_location( 'contact', $document_object ); foreach ( $fields as $key => $field ) { $field_key = CheckoutFields::get_group_key( 'other' ) . $key; $form_field = $field; $form_field['id'] = $field_key; $form_field['value'] = $this->checkout_fields_controller->get_field_from_object( $key, $customer, 'contact' ); if ( 'select' === $field['type'] ) { $form_field['options'] = array_column( $field['options'], 'label', 'value' ); } if ( 'checkbox' === $field['type'] ) { $form_field['checked_value'] = '1'; $form_field['unchecked_value'] = '0'; } woocommerce_form_field( $field_key, $form_field, wc_get_post_data_by_key( $key, $form_field['value'] ) ); } } /** * Adds additional address fields to the My Account edit address form. * * @param array $address Address fields. * @param string $address_type Type of address (billing or shipping). * @return array Updated address fields. */ public function edit_address_fields( $address, $address_type ) { $customer = new WC_Customer( get_current_user_id() ); $document_object = new DocumentObject(); $document_object->set_customer( $customer ); $document_object->set_context( $address_type . '_address' ); $fields = $this->checkout_fields_controller->get_contextual_fields_for_location( 'address', $document_object ); foreach ( $fields as $key => $field ) { $field_key = CheckoutFields::get_group_key( $address_type ) . $key; $address[ $field_key ] = $field; $address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_object( $key, $customer, $address_type ); if ( 'select' === $field['type'] ) { $address[ $field_key ]['options'] = array_column( $field['options'], 'label', 'value' ); // If a placeholder is set, add a placeholder option if it doesn't exist already. if ( ! empty( $address[ $field_key ]['placeholder'] ) && ! array_key_exists( '', $address[ $field_key ]['options'] ) ) { $address[ $field_key ]['options'] = array( '' => $address[ $field_key ]['placeholder'] ) + $address[ $field_key ]['options']; } } if ( 'checkbox' === $field['type'] ) { $address[ $field_key ]['checked_value'] = '1'; $address[ $field_key ]['unchecked_value'] = '0'; } } return $address; } /** * Validates and saves additional address fields to the customer object on the My Account page. * * Customer is not provided by this hook so we handle save here. * * @param integer $user_id User ID. */ public function save_account_form_fields( $user_id ) { try { $customer = new WC_Customer( $user_id ); $result = $this->update_additional_fields_for_customer( $customer, 'contact', 'other' ); if ( is_wp_error( $result ) ) { foreach ( $result->get_error_messages() as $error_message ) { wc_add_notice( $error_message, 'error' ); } } $customer->save(); } catch ( \Exception $e ) { wc_add_notice( sprintf( /* translators: %s: Error message. */ __( 'An error occurred while saving account details: %s', 'woocommerce' ), esc_html( $e->getMessage() ) ), 'error' ); } } /** * For the My Account page, save address fields. This uses the Store API endpoint for saving addresses so * extensibility hooks are consistent across the codebase. * * The caller saves the customer object if there are no errors. Nonces are checked before this method executes. * * @param integer $user_id User ID. * @param string $address_type Type of address (billing or shipping). * @param array $address Address fields. * @param WC_Customer $customer Customer object. */ public function save_address_fields( $user_id, $address_type, $address = [], $customer = null ) { try { $customer = $customer ?? new WC_Customer( $user_id ); $result = $this->update_additional_fields_for_customer( $customer, 'address', $address_type ); if ( is_wp_error( $result ) ) { foreach ( $result->get_error_messages() as $error_message ) { wc_add_notice( $error_message, 'error' ); } } $customer->save(); } catch ( \Exception $e ) { wc_add_notice( sprintf( /* translators: %s: Error message. */ __( 'An error occurred while saving address details: %s', 'woocommerce' ), esc_html( $e->getMessage() ) ), 'error' ); } } /** * Get posted additional field values. * * @param string $location The location to get fields for. * @param string $group The group to get fields for. * @param boolean $sanitize Whether to sanitize the field values. * @return array The posted field values and sanitized field values. */ protected function get_posted_additional_field_values( $location, $group, $sanitize = true ) { $additional_fields = $this->checkout_fields_controller->get_fields_for_location( $location ); $field_values = []; // phpcs:disable WordPress.Security.NonceVerification.Missing foreach ( $additional_fields as $field_key => $field_data ) { $post_key = CheckoutFields::get_group_key( $group ) . $field_key; $field_values[ $field_key ] = wc_clean( wp_unslash( $_POST[ $post_key ] ?? '' ) ); if ( $sanitize ) { $field_values[ $field_key ] = $this->checkout_fields_controller->sanitize_field( $field_key, $field_values[ $field_key ] ); } } // phpcs:enable WordPress.Security.NonceVerification.Missing return $field_values; } /** * Validate and save additional fields for a given customer. * * @param WC_Customer $customer Customer object. * @param string $location Location to save fields for. * @param string $group Group to save fields for. * @return true|\WP_Error True if successful, \WP_Error if there are errors. */ protected function update_additional_fields_for_customer( $customer, $location, $group ) { // Get all values from the POST request before validating. $field_values = $this->get_posted_additional_field_values( $location, $group, false ); // These values are used to see if required fields have values. $sanitized_field_values = $this->get_posted_additional_field_values( $location, $group ); // These values are used to validate custom rules, generate the document object, and save fields to the account. $document_object = new DocumentObject( [ 'customer' => [ ( 'address' === $location ? $group . '_address' : 'additional_fields' ) => $sanitized_field_values, ], ] ); $document_object->set_customer( $customer ); $document_object->set_context( 'address' === $location ? $group . '_address' : $location ); $fields = $this->checkout_fields_controller->get_contextual_fields_for_location( $location, $document_object ); // Holds values to be persisted to the customer object. $persist_fields = []; $errors = new \WP_Error(); // Validate individual fields agains the document object. Errors are added to the $errors object, and each field is validated regardless of other field errors. foreach ( $fields as $field_key => $field ) { $field_value = $field_values[ $field_key ]; if ( empty( $field_value ) ) { if ( true === $field['required'] ) { $errors->add( 'required_field', /* translators: %s: is the field label */ sprintf( __( '%s is required', 'woocommerce' ), '<strong>' . $field['label'] . '</strong>' ) ); continue; } $persist_fields[ $field_key ] = ''; continue; } $sanitized_field_value = $sanitized_field_values[ $field_key ]; $valid_check = $this->checkout_fields_controller->validate_field( $field, $sanitized_field_value ); if ( is_wp_error( $valid_check ) && $valid_check->has_errors() ) { // Get one error message from the WP_Error object per field to avoid overlapping error messages. $errors->add( $valid_check->get_error_code(), $valid_check->get_error_message() ); continue; } $persist_fields[ $field_key ] = $sanitized_field_value; } // Validate all fields for this location (this runs custom validation callbacks). If an error is found, no values will be persisted to the customer object. $location_validation = $this->checkout_fields_controller->validate_fields_for_location( $sanitized_field_values, $location, $group ); if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) { $errors->merge_from( $location_validation ); return $errors; } foreach ( $persist_fields as $field_key => $field_value ) { $this->checkout_fields_controller->persist_field_for_customer( $field_key, $field_value, $customer, $group ); } return $errors->has_errors() ? $errors : true; } } Services/CheckoutFieldsAdmin.php 0000777 00000012454 15251721451 0012725 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; /** * Service class managing checkout fields and its related extensibility points in the admin area. */ class CheckoutFieldsAdmin { /** * Checkout field controller. * * @var CheckoutFields */ private $checkout_fields_controller; /** * Sets up core fields. * * @param CheckoutFields $checkout_fields_controller Instance of the checkout field controller. */ public function __construct( CheckoutFields $checkout_fields_controller ) { $this->checkout_fields_controller = $checkout_fields_controller; } /** * Initialize hooks. This is not run Store API requests. */ public function init() { add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_address_fields' ), 10, 3 ); add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_contact_fields' ), 10, 3 ); add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_address_fields' ), 10, 3 ); add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_order_fields' ), 10, 3 ); } /** * Converts the shape of a checkout field to match whats needed in the WooCommerce meta boxes. * * @param array $field The field to format. * @param string $key The field key. This will be used for the ID of the field when passed to the meta box. * @return array Formatted field. */ protected function format_field_for_meta_box( $field, $key ) { $formatted_field = array( 'id' => $key, 'label' => $field['label'], 'value' => $field['value'], 'type' => $field['type'], 'update_callback' => array( $this, 'update_callback' ), 'show' => true, 'wrapper_class' => 'form-field-wide', ); if ( 'select' === $field['type'] ) { $formatted_field['options'] = array_column( $field['options'], 'label', 'value' ); } if ( 'checkbox' === $field['type'] ) { $formatted_field['checked_value'] = '1'; $formatted_field['unchecked_value'] = '0'; } return $formatted_field; } /** * Updates a field value for an order. * * @param string $key The field key. * @param mixed $value The field value. * @param \WC_Order $order The order to update the field for. */ public function update_callback( $key, $value, $order ) { list( $group, $key ) = explode( '/', $key, 2 ); $group = CheckoutFields::get_group_name( $group ); $this->checkout_fields_controller->persist_field_for_order( $key, $value, $order, $group, false ); } /** * Injects address fields in WC admin orders screen. * * @param array $fields The fields to show. * @param \WC_Order|boolean $order The order to show the fields for. * @param string $context The context to show the fields for. * @return array */ public function admin_address_fields( $fields, $order = null, $context = 'edit' ) { if ( ! $order instanceof \WC_Order ) { return $fields; } $group_name = doing_action( 'woocommerce_admin_billing_fields' ) ? 'billing' : 'shipping'; $additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $group_name, $context ); foreach ( $additional_fields as $key => $field ) { $prefixed_key = CheckoutFields::get_group_key( $group_name ) . $key; $additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $prefixed_key ); } array_splice( $fields, array_search( 'state', array_keys( $fields ), true ) + 1, 0, $additional_fields ); return $fields; } /** * Injects contact fields in WC admin orders screen. * * @param array $fields The fields to show. * @param \WC_Order|boolean $order The order to show the fields for. * @param string $context The context to show the fields for. * @return array */ public function admin_contact_fields( $fields, $order = null, $context = 'edit' ) { if ( ! $order instanceof \WC_Order ) { return $fields; } $additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', 'other', $context ); foreach ( $additional_fields as $key => $field ) { $prefixed_key = CheckoutFields::get_group_key( 'other' ) . $key; $additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $prefixed_key ); } return array_merge( $fields, $additional_fields ); } /** * Injects additional fields in WC admin orders screen. * * @param array $fields The fields to show. * @param \WC_Order|boolean $order The order to show the fields for. * @param string $context The context to show the fields for. * @return array */ public function admin_order_fields( $fields, $order = null, $context = 'edit' ) { if ( ! $order instanceof \WC_Order ) { return $fields; } $additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'order', 'other', $context ); foreach ( $additional_fields as $key => $field ) { $prefixed_key = CheckoutFields::get_group_key( 'other' ) . $key; $additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $prefixed_key ); } return array_merge( $fields, $additional_fields ); } } Services/CheckoutFields.php 0000777 00000147511 15251721451 0011757 0 ustar 00 <?php declare( strict_types = 1); namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsSchema\{ DocumentObject, Validation }; use WC_Customer; use WC_Data; use WC_Order; use WP_Error; /** * Service class managing checkout fields and its related extensibility points. */ class CheckoutFields { /** * Additional checkout fields. * * @var array */ private $additional_fields = []; /** * Fields locations. * * @var array */ private $fields_locations; /** * Supported field types * * @var array */ private $supported_field_types = [ 'text', 'select', 'checkbox' ]; /** * Groups of fields to be saved. * * @var array */ private $groups = [ 'billing', 'shipping', 'other' ]; /** * Instance of the asset data registry. * * @var AssetDataRegistry */ private $asset_data_registry; /** * Billing fields meta key. * * @var string */ const BILLING_FIELDS_PREFIX = '_wc_billing/'; /** * Shipping fields meta key. * * @var string */ const SHIPPING_FIELDS_PREFIX = '_wc_shipping/'; /** * Additional fields meta key. * * @var string * @deprecated 8.9.0 Use OTHER_FIELDS_PREFIX instead. */ const ADDITIONAL_FIELDS_PREFIX = '_wc_additional/'; /** * Other fields meta key. * * @var string */ const OTHER_FIELDS_PREFIX = '_wc_other/'; /** * Sets up core fields. * * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry. */ public function __construct( AssetDataRegistry $asset_data_registry ) { $this->asset_data_registry = $asset_data_registry; $this->fields_locations = [ // omit email from shipping and billing fields. 'address' => array_merge( \array_diff_key( $this->get_core_fields_keys(), array( 'email' ) ) ), 'contact' => array( 'email' ), 'order' => [], ]; } /** * Initialize hooks. */ public function init() { add_filter( 'woocommerce_get_country_locale_default', array( $this, 'update_default_locale_with_fields' ) ); add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) ); add_action( 'woocommerce_blocks_cart_enqueue_data', array( $this, 'add_fields_data' ) ); add_filter( 'woocommerce_customer_allowed_session_meta_keys', array( $this, 'add_session_meta_keys' ) ); } /** * Add fields data to the asset data registry. */ public function add_fields_data() { $this->asset_data_registry->add( 'defaultFields', array_merge( $this->get_core_fields(), $this->get_additional_fields() ) ); $this->asset_data_registry->add( 'addressFieldsLocations', $this->fields_locations ); } /** * Add session meta keys. * * This is an allow-list of meta data keys which we want to store in session. * * @param array $keys Session meta keys. * @return array */ public function add_session_meta_keys( $keys ) { $meta_keys = array(); try { foreach ( $this->get_additional_fields() as $field_key => $field ) { if ( 'address' === $field['location'] ) { $meta_keys[] = self::BILLING_FIELDS_PREFIX . $field_key; $meta_keys[] = self::SHIPPING_FIELDS_PREFIX . $field_key; } else { $meta_keys[] = self::OTHER_FIELDS_PREFIX . $field_key; } } } catch ( \Throwable $e ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( sprintf( 'Error adding session meta keys for checkout fields. %s', esc_attr( $e->getMessage() ) ), E_USER_WARNING ); return $keys; } return array_merge( $keys, $meta_keys ); } /** * If a field does not declare a sanitization callback, this is the default sanitization callback. * * @param mixed $value Value to sanitize. * @param array $field Field data. * @return mixed */ public function default_sanitize_callback( $value, $field ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed return $value; } /** * If a field does not declare a validation callback, this is the default validation callback. * * @param mixed $value Value to sanitize. * @param array $field Field data. * @return WP_Error|void If there is a validation error, return an WP_Error object. */ public function default_validate_callback( $value, $field ) { if ( true === $field['required'] && empty( $value ) ) { return new WP_Error( 'woocommerce_required_checkout_field', sprintf( // translators: %s is field key. __( 'The field %s is required.', 'woocommerce' ), $field['id'] ) ); } } /** * Registers an additional field for Checkout. * * @param array $options The field options. * * @return WP_Error|void True if the field was registered, a WP_Error otherwise. */ public function register_checkout_field( $options ) { // Check the options and show warnings if they're not supplied. Return early if an error that would prevent registration is encountered. if ( false === $this->validate_options( $options ) ) { return; } // The above validate_options function ensures these options are valid. Type might not be supplied but then it defaults to text. $field_data = wp_parse_args( $options, [ 'id' => '', 'label' => '', /* translators: %s Field label. */ 'optionalLabel' => sprintf( __( '%s (optional)', 'woocommerce' ), $options['label'] ), 'location' => '', 'type' => 'text', 'hidden' => false, 'required' => false, 'attributes' => [], 'show_in_order_confirmation' => true, 'sanitize_callback' => array( $this, 'default_sanitize_callback' ), 'validate_callback' => array( $this, 'default_validate_callback' ), 'validation' => [], ], ); $field_data['attributes'] = $this->register_field_attributes( $field_data['id'], $field_data['attributes'] ); $field_data = $this->process_field_options( $field_data, $options ); // $field_data will be false if an error that will prevent the field being registered is encountered. if ( false === $field_data ) { return; } // Insert new field into the correct location array. $this->additional_fields[ $field_data['id'] ] = $field_data; $this->fields_locations[ $field_data['location'] ][] = $field_data['id']; } /** * Returns true if the field is required. Takes rules into consideration if a document object is provided. * * @param array|string $field The field array or field key. * @param DocumentObject|null $document_object The document object. * @return bool */ public function is_required_field( $field, $document_object = null ) { if ( is_string( $field ) ) { $field = $this->additional_fields[ $field ] ?? []; } if ( empty( $field ) ) { return false; } if ( $document_object ) { // Hidden fields cannot be required. if ( $this->is_hidden_field( $field, $document_object ) ) { return false; } if ( $this->contains_valid_rules( $field['required'] ) ) { return true === Validation::validate_document_object( $document_object, $field['required'] ); } } return true === $field['required']; } /** * Returns true if the field is hidden. Takes rules into consideration if a document object is provided. * * @param array|string $field The field array or field key. * @param DocumentObject|null $document_object The document object. * @return bool */ public function is_hidden_field( $field, $document_object = null ) { if ( is_string( $field ) ) { $field = $this->additional_fields[ $field ] ?? []; } if ( $document_object && $this->contains_valid_rules( $field['hidden'] ) ) { return true === Validation::validate_document_object( $document_object, $field['hidden'] ); } return false; // Fields cannot be registered as hidden. } /** * Returns true if the field is conditionally required or rendered. * * @param array|string $field The field array or field key. * @return bool */ public function is_conditional_field( $field ) { if ( is_string( $field ) ) { $field = $this->additional_fields[ $field ] ?? []; } return $this->contains_valid_rules( $field['required'] ) || $this->contains_valid_rules( $field['hidden'] ); } /** * Validates a field against the given document object and context. * * @param array $field The field. * @param DocumentObject|null $document_object The document object. * @return bool|\WP_Error True if the field is valid, a WP_Error otherwise. */ public function is_valid_field( $field, $document_object = null ) { if ( $document_object && $this->contains_valid_rules( $field['validation'] ) ) { $field_schema = Validation::get_field_schema_with_context( $field['id'], $field['validation'], $document_object->get_context() ); return Validation::validate_document_object( $document_object, $field_schema ); } return true; } /** * Returns true if the property is an array and not empty. * * @param mixed $property The property to check. * @return bool */ protected function contains_valid_rules( $property ) { return is_array( $property ) && ! empty( $property ); } /** * Returns the validate callback for a given field. * * @param array $field The field. * @param DocumentObject|null $document_object The document object. * @return callable The validate callback. */ public function get_validate_callback( $field, $document_object = null ) { if ( is_string( $field ) ) { $field = $this->additional_fields[ $field ] ?? []; } if ( $document_object && $this->contains_valid_rules( $field['validation'] ) ) { return function ( $field_value, $field ) use ( $document_object ) { $errors = new WP_Error(); // Only validate if we have a field. if ( ! $field ) { return true; } // Evaluate custom validation schema rules on the field. $validate_result = $this->is_valid_field( $field, $document_object ); if ( is_wp_error( $validate_result ) ) { /* translators: %s: is the field label */ $error_message = sprintf( __( 'Please provide a valid %s', 'woocommerce' ), $field['label'] ); $error_code = 'woocommerce_invalid_checkout_field'; $errors->add( $error_code, $error_message ); } return $errors->has_errors() ? $errors : true; }; } return $field['validate_callback'] ?? null; } /** * Deregister a checkout field. * * @param string $field_id The field ID. * * @internal */ public function deregister_checkout_field( $field_id ) { if ( empty( $this->additional_fields[ $field_id ] ) ) { return; } $location = $this->get_field_location( $field_id ); if ( ! $location ) { return; } // Remove the field from the fields_locations array. $this->fields_locations[ $location ] = array_diff( $this->fields_locations[ $location ], array( $field_id ) ); // Remove the field from the additional_fields array. unset( $this->additional_fields[ $field_id ] ); } /** * Validates the "base" options (id, label, location) and shows warnings if they're not supplied. * * @param array $options The options supplied during field registration. * @return bool false if an error was encountered, true otherwise. */ private function validate_options( &$options ) { if ( empty( $options['id'] ) ) { _doing_it_wrong( 'woocommerce_register_additional_checkout_field', 'A checkout field cannot be registered without an id.', '8.6.0' ); return false; } // Having fewer than 2 after exploding around a / means there is no namespace. if ( count( explode( '/', $options['id'] ) ) < 2 ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'A checkout field id must consist of namespace/name.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } if ( empty( $options['label'] ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field label is required.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } if ( empty( $options['location'] ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is required.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } if ( 'additional' === $options['location'] ) { wc_deprecated_argument( 'location', '8.9.0', 'The "additional" location is deprecated. Use "order" instead.' ); $options['location'] = 'order'; } if ( ! in_array( $options['location'], array_keys( $this->fields_locations ), true ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is invalid.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } // At this point, the essentials fields and its location should be set and valid. $location = $options['location']; $id = $options['id']; // Check to see if field is already in the array. if ( ! empty( $this->additional_fields[ $id ] ) || in_array( $id, $this->fields_locations[ $location ], true ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The field is already registered.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } if ( ! empty( $options['type'] ) ) { if ( ! in_array( $options['type'], $this->supported_field_types, true ) ) { $message = sprintf( 'Unable to register field with id: "%s". Registering a field with type "%s" is not supported. The supported types are: %s.', $id, $options['type'], implode( ', ', $this->supported_field_types ) ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } } if ( ! empty( $options['sanitize_callback'] ) && ! is_callable( $options['sanitize_callback'] ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The sanitize_callback must be a valid callback.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } if ( ! empty( $options['validate_callback'] ) && ! is_callable( $options['validate_callback'] ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The validate_callback must be a valid callback.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } if ( ! empty( $options['hidden'] ) && true === $options['hidden'] ) { // Hidden fields are not supported right now. They will be registered with hidden => false. $message = sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', $id ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); // Don't return here unlike the other fields because this is not an issue that will prevent registration. } $rule_fields = [ 'required', 'hidden', 'validation' ]; $allow_bool = [ 'required', 'hidden' ]; foreach ( $rule_fields as $rule_field ) { if ( ! empty( $options[ $rule_field ] ) ) { if ( in_array( $rule_field, $allow_bool, true ) && is_bool( $options[ $rule_field ] ) ) { continue; } $valid = Validation::is_valid_schema( $options[ $rule_field ] ); if ( is_wp_error( $valid ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], $rule_field . ': ' . $valid->get_error_message() ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } } } return true; } /** * Processes the options for a field type and returns the new field_options array. * * @param array $field_data The field data array to be updated. * @param array $options The options supplied during field registration. * @return array The updated $field_data array. */ private function process_field_options( $field_data, $options ) { if ( 'checkbox' === $field_data['type'] ) { $field_data = $this->process_checkbox_field( $field_data, $options ); } elseif ( 'select' === $field_data['type'] ) { $field_data = $this->process_select_field( $field_data, $options ); } return $field_data; } /** * Processes the options for a select field and returns the new field_options array. * * @param array $field_data The field data array to be updated. * @param array $options The options supplied during field registration. * * @return array|false The updated $field_data array or false if an error was encountered. */ private function process_select_field( $field_data, $options ) { $id = $options['id']; if ( empty( $options['options'] ) || ! is_array( $options['options'] ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options".' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } $cleaned_options = []; $added_values = []; // Check all entries in $options['options'] has a key and value member. foreach ( $options['options'] as $option ) { if ( ! isset( $option['value'] ) || ! isset( $option['label'] ) ) { $message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options" and each option must contain a "value" and "label" member.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return false; } $sanitized_value = sanitize_text_field( $option['value'] ); $sanitized_label = sanitize_text_field( $option['label'] ); if ( in_array( $sanitized_value, $added_values, true ) ) { $message = sprintf( 'Duplicate key found when registering field with id: "%s". The value in each option of "select" fields must be unique. Duplicate value "%s" found. The duplicate key will be removed.', $id, $sanitized_value ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); continue; } $added_values[] = $sanitized_value; $cleaned_options[] = [ 'value' => $sanitized_value, 'label' => $sanitized_label, ]; } $field_data['options'] = $cleaned_options; if ( isset( $field_data['placeholder'] ) ) { $field_data['placeholder'] = sanitize_text_field( $field_data['placeholder'] ); } return $field_data; } /** * Processes the options for a checkbox field and returns the new field_options array. * * @param array $field_data The field data array to be updated. * @param array $options The options supplied during field registration. * * @return array|false The updated $field_data array or false if an error was encountered. */ private function process_checkbox_field( $field_data, $options ) { $id = $options['id']; $field_data['required'] = $options['required'] ?? false; if ( false === $field_data['required'] && ! empty( $options['error_message'] ) ) { $message = sprintf( 'Passing an error message to a non-required checkbox "%s" will have no effect. The error message has been removed from the field.', $id ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '9.8.0' ); unset( $field_data['error_message'] ); } if ( isset( $options['error_message'] ) && ! is_string( $options['error_message'] ) ) { $message = sprintf( 'The error_message property for field with id: "%s" must be a string, you passed %s. A default message will be shown.', $id, gettype( $options['error_message'] ) ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '9.8.0' ); unset( $field_data['error_message'] ); } // Get the error message property and set it to errorMessage for use in JS. if ( isset( $field_data['error_message'] ) ) { $field_data['errorMessage'] = $field_data['error_message']; unset( $field_data['error_message'] ); } return $field_data; } /** * Processes the attributes supplied during field registration. * * @param array $id The field ID. * @param array $attributes The attributes supplied during field registration. * * @return array The processed attributes. */ private function register_field_attributes( $id, $attributes ) { // We check if attributes are valid. This is done to prevent too much nesting and also to allow field registration // even if the attributes property is invalid. We can just skip it and register the field without attributes. if ( empty( $attributes ) ) { return []; } if ( ! is_array( $attributes ) || 0 === count( $attributes ) ) { $message = sprintf( 'An invalid attributes value was supplied when registering field with id: "%s". %s', $id, 'Attributes must be a non-empty array.' ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); return []; } // These are formatted in camelCase because React components expect them that way. $allowed_attributes = [ 'maxLength', 'readOnly', 'pattern', 'autocomplete', 'autocapitalize', 'title', ]; $valid_attributes = array_filter( $attributes, function ( $_, $key ) use ( $allowed_attributes ) { return in_array( $key, $allowed_attributes, true ) || strpos( $key, 'aria-' ) === 0 || strpos( $key, 'data-' ) === 0; }, ARRAY_FILTER_USE_BOTH ); // Any invalid attributes should show a doing_it_wrong warning. It shouldn't stop field registration, though. if ( count( $attributes ) !== count( $valid_attributes ) ) { $invalid_attributes = array_keys( array_diff_key( $attributes, $valid_attributes ) ); $message = sprintf( 'Invalid attribute found when registering field with id: "%s". Attributes: %s are not allowed.', $id, implode( ', ', $invalid_attributes ) ); _doing_it_wrong( 'woocommerce_register_additional_checkout_field', esc_html( $message ), '8.6.0' ); } // Escape attributes to remove any malicious code and return them. return array_map( function ( $value ) { return esc_attr( $value ); }, $valid_attributes ); } /** * Returns the keys of all core fields. * * @return array An array of field keys. */ public function get_core_fields_keys() { return [ 'email', 'country', 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'phone', ]; } /** * Returns an array of all core fields. * * @return array An array of fields. */ public function get_core_fields() { return [ 'email' => [ 'label' => __( 'Email address', 'woocommerce' ), 'optionalLabel' => __( 'Email address (optional)', 'woocommerce' ), 'required' => true, 'hidden' => false, 'autocomplete' => 'email', 'autocapitalize' => 'none', 'type' => 'email', 'index' => 0, ], 'country' => [ 'label' => __( 'Country/Region', 'woocommerce' ), 'optionalLabel' => __( 'Country/Region (optional)', 'woocommerce' ), 'required' => true, 'hidden' => false, 'autocomplete' => 'country', 'index' => 1, ], 'first_name' => [ 'label' => __( 'First name', 'woocommerce' ), 'optionalLabel' => __( 'First name (optional)', 'woocommerce' ), 'required' => true, 'hidden' => false, 'autocomplete' => 'given-name', 'autocapitalize' => 'sentences', 'index' => 10, ], 'last_name' => [ 'label' => __( 'Last name', 'woocommerce' ), 'optionalLabel' => __( 'Last name (optional)', 'woocommerce' ), 'required' => true, 'hidden' => false, 'autocomplete' => 'family-name', 'autocapitalize' => 'sentences', 'index' => 20, ], 'company' => [ 'label' => __( 'Company', 'woocommerce' ), 'optionalLabel' => __( 'Company (optional)', 'woocommerce' ), 'required' => 'required' === CartCheckoutUtils::get_company_field_visibility(), 'hidden' => 'hidden' === CartCheckoutUtils::get_company_field_visibility(), 'autocomplete' => 'organization', 'autocapitalize' => 'sentences', 'index' => 30, ], 'address_1' => [ 'label' => __( 'Address', 'woocommerce' ), 'optionalLabel' => __( 'Address (optional)', 'woocommerce' ), 'required' => true, 'hidden' => false, 'autocomplete' => 'address-line1', 'autocapitalize' => 'sentences', 'index' => 40, ], 'address_2' => [ 'label' => __( 'Apartment, suite, etc.', 'woocommerce' ), 'optionalLabel' => __( 'Apartment, suite, etc. (optional)', 'woocommerce' ), 'required' => 'required' === CartCheckoutUtils::get_address_2_field_visibility(), 'hidden' => 'hidden' === CartCheckoutUtils::get_address_2_field_visibility(), 'autocomplete' => 'address-line2', 'autocapitalize' => 'sentences', 'index' => 50, ], 'city' => [ 'label' => __( 'City', 'woocommerce' ), 'optionalLabel' => __( 'City (optional)', 'woocommerce' ), 'required' => true, 'hidden' => false, 'autocomplete' => 'address-level2', 'autocapitalize' => 'sentences', 'index' => 70, ], 'state' => [ 'label' => __( 'State/County', 'woocommerce' ), 'optionalLabel' => __( 'State/County (optional)', 'woocommerce' ), 'required' => true, 'hidden' => false, 'autocomplete' => 'address-level1', 'autocapitalize' => 'sentences', 'index' => 80, ], 'postcode' => [ 'label' => __( 'Postal code', 'woocommerce' ), 'optionalLabel' => __( 'Postal code (optional)', 'woocommerce' ), 'required' => true, 'hidden' => false, 'autocomplete' => 'postal-code', 'autocapitalize' => 'characters', 'index' => 90, ], 'phone' => [ 'label' => __( 'Phone', 'woocommerce' ), 'optionalLabel' => __( 'Phone (optional)', 'woocommerce' ), 'required' => 'required' === CartCheckoutUtils::get_phone_field_visibility(), 'hidden' => 'hidden' === CartCheckoutUtils::get_phone_field_visibility(), 'type' => 'tel', 'autocomplete' => 'tel', 'autocapitalize' => 'characters', 'index' => 100, ], ]; } /** * Returns an array of all additional fields. * * @return array An array of fields. */ public function get_additional_fields() { return $this->additional_fields; } /** * Gets the location of a field. * * @param string $field_key The key of the field to get the location for. * @return string The location of the field. */ public function get_field_location( $field_key ) { if ( ! $this->is_field( $field_key ) ) { return ''; } foreach ( $this->fields_locations as $location => $fields ) { if ( in_array( $field_key, $fields, true ) ) { return $location; } } return ''; } /** * Sanitize an additional field against any custom sanitization rules. * * @since 8.7.0 * @param string $field_key The key of the field. * @param mixed $field_value The value of the field. * @return mixed */ public function sanitize_field( $field_key, $field_value ) { try { $field = $this->additional_fields[ $field_key ] ?? null; if ( $field ) { $field_value = call_user_func( $field['sanitize_callback'], $field_value, $field ); } /** * Allow custom sanitization of an additional field. * * @param mixed $field_value The value of the field being sanitized. * @param string $field_key Key of the field being sanitized. * * @since 8.6.0 * @deprecated 8.7.0 Use woocommerce_sanitize_additional_field instead. */ $field_value = apply_filters_deprecated( '__experimental_woocommerce_blocks_sanitize_additional_field', array( $field_value, $field_key ), '8.7.0', 'woocommerce_sanitize_additional_field', 'This action has been graduated, use woocommerce_sanitize_additional_field instead.' ); /** * Allow custom sanitization of an additional field. * * @param mixed $field_value The value of the field being sanitized. * @param string $field_key Key of the field being sanitized. * * @since 8.7.0 */ return apply_filters( 'woocommerce_sanitize_additional_field', $field_value, $field_key ); } catch ( \Throwable $e ) { // One of the filters errored so skip it. This allows the checkout process to continue. // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( sprintf( 'Field sanitization for %s encountered an error. %s', esc_html( $field_key ), esc_html( $e->getMessage() ) ), E_USER_WARNING ); } return $field_value; } /** * Validate an additional field. * * @since 8.6.0 * * @param array $field The field. * @param mixed $field_value The value of the field. * @return WP_Error */ public function validate_field( $field, $field_value ) { $errors = new WP_Error(); try { // Only validate if we have a field. if ( ! $field ) { return $errors; } if ( ! empty( $field['validate_callback'] ) && is_callable( $field['validate_callback'] ) ) { $validate_callback_result = call_user_func( $field['validate_callback'], $field_value, $field ); if ( is_wp_error( $validate_callback_result ) ) { $errors->merge_from( $validate_callback_result ); } elseif ( false === $validate_callback_result ) { /* translators: %s: is the field label */ $error_message = sprintf( __( 'Please provide a valid %s', 'woocommerce' ), $field['label'] ); $errors->add( 'woocommerce_invalid_checkout_field', $error_message ); } } wc_do_deprecated_action( '__experimental_woocommerce_blocks_validate_additional_field', array( $errors, $field['id'], $field_value ), '8.7.0', 'woocommerce_validate_additional_field', 'This action has been graduated, use woocommerce_validate_additional_field instead.' ); /** * Pass an error object to allow validation of an additional field. * * @param WP_Error $errors A WP_Error object that extensions may add errors to. * @param string $field_key Key of the field being sanitized. * @param mixed $field_value The value of the field being validated. * * @since 8.7.0 */ do_action( 'woocommerce_validate_additional_field', $errors, $field['id'], $field_value ); } catch ( \Throwable $e ) { // One of the filters errored so skip them and validate the field. This allows the checkout process to continue. // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( sprintf( 'Field validation for %s encountered an error. %s', esc_html( $field['id'] ), esc_html( $e->getMessage() ) ), E_USER_WARNING ); } return $errors; } /** * Update the default locale with additional fields without country limitations. * * @param array $locale The locale to update. * @return mixed */ public function update_default_locale_with_fields( $locale ) { foreach ( $this->get_fields_for_location( 'address' ) as $field_key => $field ) { if ( empty( $locale[ $field_key ] ) ) { // If the field has conditional rules, we need to set the required property to false so it can be evaluated. if ( $this->is_conditional_field( $field_key ) ) { $field['required'] = false; } $locale[ $field_key ] = $field; } } return $locale; } /** * Returns an array of fields keys for the address location. * * @return array An array of fields keys. */ public function get_address_fields_keys() { return $this->fields_locations['address']; } /** * Returns an array of fields keys for the contact location. * * @return array An array of fields keys. */ public function get_contact_fields_keys() { return $this->fields_locations['contact']; } /** * Returns an array of fields keys for the additional area location. * * @return array An array of fields keys. * @deprecated 8.9.0 Use get_order_fields_keys instead. */ public function get_additional_fields_keys() { wc_deprecated_function( __METHOD__, '8.9.0', 'get_order_fields_keys' ); return $this->get_order_fields_keys(); } /** * Returns an array of fields keys for the additional area group. * * @return array An array of fields keys. */ public function get_order_fields_keys() { return $this->fields_locations['order']; } /** * Returns an array of fields for a given location. * * @param string $location The location to get fields for (address|contact|order). * @return array An array of fields definitions. */ public function get_fields_for_location( $location ) { $location = $this->prepare_location_name( $location ); if ( in_array( $location, array_keys( $this->fields_locations ), true ) ) { $order_fields_keys = $this->fields_locations[ $location ]; return array_filter( $this->get_additional_fields(), function ( $key ) use ( $order_fields_keys ) { return in_array( $key, $order_fields_keys, true ); }, ARRAY_FILTER_USE_KEY ); } return []; } /** * Returns an array of fields for a given location and uses context to evaluate hidden and required fields. * * @param string $location The location to get fields for (address|contact|order). * @param DocumentObject|null $document_object The document object. * @return array An array of fields definitions. */ public function get_contextual_fields_for_location( $location, $document_object = null ) { $location_fields = $this->get_fields_for_location( $location ); $fields = []; foreach ( $location_fields as $key => $field ) { if ( $this->is_hidden_field( $key, $document_object ) ) { continue; } $field['required'] = $this->is_required_field( $field, $document_object ); $field['validate_callback'] = $this->get_validate_callback( $field, $document_object ); $fields[ $key ] = $field; } return $fields; } /** * Validates a set of fields for a given location against custom validation rules. * * @param array $fields Array of key value pairs of field values to validate. * @param string $location The location being validated (address|contact|order). * @param string $group The group to get the field value for (shipping|billing|other). * @return WP_Error */ public function validate_fields_for_location( $fields, $location, $group = 'other' ) { $errors = new WP_Error(); $location = $this->prepare_location_name( $location ); $group = $this->prepare_group_name( $group ); try { wc_do_deprecated_action( '__experimental_woocommerce_blocks_validate_location_' . $location . '_fields', array( $errors, $fields, $group ), '8.9.0', 'woocommerce_blocks_validate_location_' . $location . '_fields', 'This action has been graduated, use woocommerce_blocks_validate_location_' . $location . '_fields instead.' ); /** * Pass an error object to allow validation of an additional field. * * @param WP_Error $errors A WP_Error object that extensions may add errors to. * @param mixed $fields List of fields (key value pairs) in this location. * @param string $group The group of this location (shipping|billing|other). * * @since 8.7.0 */ do_action( 'woocommerce_blocks_validate_location_' . $location . '_fields', $errors, $fields, $group ); } catch ( \Throwable $e ) { // One of the filters errored so skip them. This allows the checkout process to continue. // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( sprintf( 'The action %s encountered an error. The field location %s may not have any custom validation applied to it. %s', esc_html( 'woocommerce_blocks_validate_' . $location . '_fields' ), esc_html( $location ), esc_html( $e->getMessage() ) ), E_USER_WARNING ); } return $errors; } /** * Validates a field to check it belongs to the given location and is valid according to its registration. * * This does not apply any custom validation rules on the value. * * @param string $key The field key. * @param mixed $value The field value. * @param string $location The location to validate the field for (address|contact|order). * * @return true|WP_Error True if the field is valid, a WP_Error otherwise. */ public function validate_field_for_location( $key, $value, $location ) { $location = $this->prepare_location_name( $location ); if ( ! $this->is_field( $key ) ) { return new WP_Error( 'woocommerce_invalid_checkout_field', \sprintf( // translators: % is field key. __( 'The field %s is invalid.', 'woocommerce' ), $key ) ); } if ( ! in_array( $key, $this->fields_locations[ $location ], true ) ) { return new WP_Error( 'woocommerce_invalid_checkout_field_location', \sprintf( // translators: %1$s is field key, %2$s location. __( 'The field %1$s is invalid for the location %2$s.', 'woocommerce' ), $key, $location ) ); } return true; } /** * Returns all fields key for a given group. * * @param string $group The group to get the key for (shipping|billing|other). * * @return string[] Field keys. */ public function get_fields_for_group( $group = 'other' ) { $group = $this->prepare_group_name( $group ); if ( 'shipping' === $group || 'billing' === $group ) { return $this->get_fields_for_location( 'address' ); } return \array_merge( $this->get_fields_for_location( 'contact' ), $this->get_fields_for_location( 'order' ) ); } /** * Returns true if the given key is a valid field. * * @param string $key The field key. * * @return bool True if the field is valid, false otherwise. */ public function is_field( $key ) { return array_key_exists( $key, $this->additional_fields ); } /** * Returns true if the given key is a valid customer field. * * Customer fields are fields saved to the customer data, like address and contact fields. * * @param string $key The field key. * * @return bool True if the field is valid, false otherwise. */ public function is_customer_field( $key ) { return in_array( $key, array_intersect( array_merge( $this->get_address_fields_keys(), $this->get_contact_fields_keys() ), array_keys( $this->additional_fields ) ), true ); } /** * Persists a field value for a given order. This would also optionally set the field value on the customer object if the order is linked to a registered customer. * * @param string $key The field key. * @param mixed $value The field value. * @param WC_Order $order The order to persist the field for. * @param string $group The group to persist the field for (shipping|billing|other). * @param bool $set_customer Whether to set the field value on the customer or not. * * @return void */ public function persist_field_for_order( string $key, $value, WC_Order $order, string $group = 'other', bool $set_customer = true ) { $group = $this->prepare_group_name( $group ); $this->set_array_meta( $key, $value, $order, $group ); if ( $set_customer && $order->get_customer_id() ) { $customer = new WC_Customer( $order->get_customer_id() ); $this->persist_field_for_customer( $key, $value, $customer, $group ); } } /** * Persists a field value for a given customer. * * @param string $key The field key. * @param mixed $value The field value. * @param WC_Customer $customer The customer to persist the field for. * @param string $group The group to persist the field for (shipping|billing|other). * * @return void */ public function persist_field_for_customer( string $key, $value, WC_Customer $customer, string $group = 'other' ) { $group = $this->prepare_group_name( $group ); $this->set_array_meta( $key, $value, $customer, $group ); } /** * Sets a field value in an array meta, supporting routing things to billing, shipping, or additional fields, based on a prefix for the key. * * @param string $key The field key. * @param mixed $value The field value. * @param WC_Customer|WC_Order $wc_object The object to set the field value for. * @param string $group The group to set the field value for (shipping|billing|other). * * @return void */ private function set_array_meta( string $key, $value, WC_Data $wc_object, string $group ) { $meta_key = self::get_group_key( $group ) . $key; /** * Allow reacting for saving an additional field value. * * @param string $key The key of the field being saved. * @param mixed $value The value of the field being saved. * @param string $group The group of this location (shipping|billing|other). * @param WC_Customer|WC_Order $wc_object The object to set the field value for. * * @since 8.9.0 */ do_action( 'woocommerce_set_additional_field_value', $key, $value, $group, $wc_object ); // Convert boolean values to strings because Data Stores will skip false values. if ( is_bool( $value ) ) { $value = $value ? '1' : '0'; } $wc_object->update_meta_data( $meta_key, $value ); } /** * Returns a field value for a given object. * * @param string $key The field key. * @param WC_Customer|WC_Order $wc_object The customer or order to get the field value for. * @param string $group The group to get the field value for (shipping|billing|other). * * @return mixed The field value. */ public function get_field_from_object( string $key, WC_Data $wc_object, string $group = 'other' ) { $group = $this->prepare_group_name( $group ); $meta_key = self::get_group_key( $group ) . $key; $value = $wc_object->get_meta( $meta_key, true ); if ( ! $value && '0' !== $value ) { /** * Allow providing a default value for additional fields if no value is already set. * * @param null $value The default value for the filter, always null. * @param string $group The group of this key (shipping|billing|other). * @param WC_Data $wc_object The object to get the field value for. * * @since 8.9.0 */ $value = apply_filters( "woocommerce_get_default_value_for_{$key}", null, $group, $wc_object ); } // We cast the value to a boolean if the field is a checkbox. if ( $this->is_field( $key ) && 'checkbox' === $this->additional_fields[ $key ]['type'] ) { return '1' === $value; } if ( null === $value ) { return ''; } return $value; } /** * Returns an array of all fields values for a given object in a group. * * @param WC_Data $wc_object The object or order to get the fields for. * @param string $group The group to get the fields for (shipping|billing|other). * @param bool $all Whether to return all fields or only the ones that are still registered. Default false. * @return array An array of fields. */ public function get_all_fields_from_object( WC_Data $wc_object, string $group = 'other', bool $all = false ) { $meta_data = []; $group = $this->prepare_group_name( $group ); $prefix = self::get_group_key( $group ); if ( $wc_object instanceof WC_Data ) { $meta = $wc_object->get_meta_data(); foreach ( $meta as $meta_data_object ) { if ( 0 === \strpos( $meta_data_object->key, $prefix ) ) { $key = \str_replace( $prefix, '', $meta_data_object->key ); if ( $all || $this->is_field( $key ) ) { $meta_data[ $key ] = $meta_data_object->value; } } } } $missing_fields = array_diff( array_keys( $this->get_fields_for_group( $group ) ), array_keys( $meta_data ) ); foreach ( $missing_fields as $missing_field ) { /** * Allow providing a default value for additional fields if no value is already set. * * @param null $value The default value for the filter, always null. * @param string $group The group of this key (shipping|billing|other). * @param WC_Data $wc_object The object to get the field value for. * * @since 8.9.0 */ $value = apply_filters( "woocommerce_get_default_value_for_{$missing_field}", null, $group, $wc_object ); if ( isset( $value ) ) { $meta_data[ $missing_field ] = $value; } } return $meta_data; } /** * Copies additional fields from an order to a customer. * * @param WC_Order $order The order to sync the fields for. * @param WC_Customer $customer The customer to sync the fields for. */ public function sync_customer_additional_fields_with_order( WC_Order $order, WC_Customer $customer ) { foreach ( $this->groups as $group ) { $order_additional_fields = $this->get_all_fields_from_object( $order, $group, true ); // Sync customer additional fields with order additional fields. foreach ( $order_additional_fields as $key => $value ) { if ( $this->is_customer_field( $key ) ) { $this->persist_field_for_customer( $key, $value, $customer, $group ); } } } } /** * Copies additional fields from a customer to an order. * * @param WC_Order $order The order to sync the fields for. * @param WC_Customer $customer The customer to sync the fields for. */ public function sync_order_additional_fields_with_customer( WC_Order $order, WC_Customer $customer ) { foreach ( $this->groups as $group ) { $customer_additional_fields = $this->get_all_fields_from_object( $customer, $group, true ); // Sync order additional fields with customer additional fields. foreach ( $customer_additional_fields as $key => $value ) { if ( $this->is_field( $key ) ) { $this->persist_field_for_order( $key, $value, $order, $group, false ); } } } } /** * From a set of fields, returns only the ones for a given location. * * @param array $fields The fields to filter. * @param string $location The location to validate the field for (address|contact|order). * @return array The filtered fields. */ public function filter_fields_for_location( array $fields, string $location ) { $location = $this->prepare_location_name( $location ); return array_filter( $fields, function ( $key ) use ( $location ) { return $this->get_field_location( $key ) === $location; }, ARRAY_FILTER_USE_KEY ); } /** * Filter fields for order confirmation. * * @param array $fields The fields to filter. * @param array $context Additional context for the filter. * @return array The filtered fields. */ public function filter_fields_for_order_confirmation( $fields, $context = array() ) { return array_filter( $fields, function ( $field ) use ( $fields, $context ) { /** * Filter fields for order confirmation (thank you page, email). * * Used in methods: * WC_Email::additional_checkout_fields * WC_Email::additional_address_fields * CheckoutFieldsFrontend::render_order_other_fields * AdditionalFields::render_content * * @param bool Whether the field should be shown. * @param array $field Field data. * @param array $fields All fields for better context when field should be shown or hidden based on other fields values. * @param array $context Additional context for the filter. Data depends in which method filter_fields_for_order_confirmation is called. * @param CheckoutFields $this The CheckoutFields instance. * @since 10.1.0 */ return apply_filters( 'woocommerce_filter_fields_for_order_confirmation', ! empty( $field['show_in_order_confirmation'] ), $field, $fields, $context, $this ); } ); } /** * Get additional fields for an order. * * @param WC_Order $order Order object. * @param string $location The location to get fields for (address|contact|order). * @param string $group The group to get the field value for (shipping|billing|other). * @param string $context The context to get the field value for (edit|view). * @return array An array of fields definitions as well as their values formatted for display. */ public function get_order_additional_fields_with_values( WC_Order $order, string $location, string $group = 'other', string $context = 'edit' ) { // Because the Additional Checkout Fields API only applies to orders created with Store API, we should not // return any values unless it was created using Store API. This is mainly to prevent "empty" checkbox values // from being shown on the order confirmation page for orders placed using the shortcode. It's rare that this // will happen but not impossible. if ( 'store-api' !== $order->get_created_via() ) { return []; } $location = $this->prepare_location_name( $location ); $group = $this->prepare_group_name( $group ); $fields = $this->get_fields_for_location( $location ); $fields_with_values = []; foreach ( $fields as $field_key => $field ) { $value = $this->get_field_from_object( $field_key, $order, $group ); if ( '' === $value || null === $value ) { continue; } if ( 'view' === $context ) { $value = $this->format_additional_field_value( $value, $field ); } $field['value'] = $value; $fields_with_values[ $field_key ] = $field; } return $fields_with_values; } /** * Formats a raw field value for display based on its type definition. * * @param string $value Value to format. * @param array $field Additional field definition. * @return string */ public function format_additional_field_value( $value, $field ) { if ( 'checkbox' === $field['type'] ) { $value = $value ? __( 'Yes', 'woocommerce' ) : __( 'No', 'woocommerce' ); } if ( 'select' === $field['type'] ) { $options = array_column( $field['options'], 'label', 'value' ); $value = isset( $options[ $value ] ) ? $options[ $value ] : $value; } return $value; } /** * Prepares a group name for use. * * @param string $group The group name to prepare. * @return string The prepared group name. */ private function prepare_group_name( $group ) { if ( ! in_array( $group, $this->groups, true ) ) { $group = 'other'; } return $group; } /** * Prepares a location name for use. * * @param string $location The location name to prepare. * @return string The prepared location name. */ private function prepare_location_name( $location ) { if ( 'additional' === $location ) { $location = 'order'; } return $location; } /** * Returns a group meta prefix based on its name. * * @param string $group_name The group name (billing|shipping|other). * @return string The group meta prefix. */ public static function get_group_key( $group_name ) { if ( 'additional' === $group_name ) { wc_deprecated_argument( 'group_name', '8.9.0', 'The "additional" group is deprecated. Use "other" instead.' ); $group_name = 'other'; } if ( 'billing' === $group_name ) { return self::BILLING_FIELDS_PREFIX; } if ( 'shipping' === $group_name ) { return self::SHIPPING_FIELDS_PREFIX; } return self::OTHER_FIELDS_PREFIX; } /** * Returns a group name based on passed group key. * * @param string $group_key The group name (_wc_billing|_wc_shipping|_wc_other). * @return string The group meta prefix. */ public static function get_group_name( $group_key ) { if ( '_wc_additional' === $group_key ) { wc_deprecated_argument( 'group_key', '8.9.0', 'The "_wc_additional" group key is deprecated. Use "_wc_other" instead.' ); $group_key = '_wc_other'; } if ( 0 === \strpos( self::BILLING_FIELDS_PREFIX, $group_key ) ) { return 'billing'; } if ( 0 === \strpos( self::SHIPPING_FIELDS_PREFIX, $group_key ) ) { return 'shipping'; } return 'other'; } } Services/CreateAccount.php 0000777 00000002202 15251721451 0011566 0 ustar 00 <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Domain\Package; /** * Service class implementing new create account emails used for order processing via the Block Based Checkout. * * @deprecated This class can't be removed due to https://github.com/woocommerce/woocommerce/issues/52311. */ class CreateAccount { /** * Reference to the Package instance * * @var Package */ private $package; /** * Constructor. * * @param Package $package An instance of (Woo Blocks) Package. */ public function __construct( Package $package ) { $this->package = $package; } /** * Init - register handlers for WooCommerce core email hooks. */ public function init() { // This method is intentionally left blank. } /** * Trigger new account email. * * @param int $customer_id The ID of the new customer account. * @param array $new_customer_data Assoc array of data for the new account. */ public function customer_new_account( $customer_id = 0, array $new_customer_data = array() ) { // This method is intentionally left blank. } } Services/Hydration.php 0000777 00000022546 15251721451 0011024 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\StoreApi\RoutesController; use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\StoreApi; /** * Service class that handles hydration of API data for blocks. */ class Hydration { /** * Instance of the asset data registry. * * @var AssetDataRegistry */ protected $asset_data_registry; /** * Cached notices to restore after hydrating the API. * * @var array */ protected $cached_store_notices = array(); /** * Constructor. * * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry. */ public function __construct( AssetDataRegistry $asset_data_registry ) { $this->asset_data_registry = $asset_data_registry; } /** * Hydrates the asset data registry with data from the API. Disables notices and nonces so requests contain valid * data that is not polluted by the current session. * * @param array $path API paths to hydrate e.g. '/wc/store/v1/cart'. * @return array Response data. */ public function get_rest_api_response_data( $path = '' ) { if ( ! str_starts_with( $path, '/wc/store' ) ) { return array(); } // Allow-list only store API routes. No other request can be hydrated for safety. $available_routes = StoreApi::container()->get( RoutesController::class )->get_all_routes( 'v1', true ); $route_match = $this->match_route_to_handler( $path, $available_routes ); /** * We disable nonce check to support endpoints such as checkout. The caveat here is that we need to be careful to only support GET requests. No other request type should be processed without nonce check. Additionally, no GET request can modify data as part of hydration request, for example adding items to cart. * * Long term, we should consider validating nonce here, instead of disabling it temporarily. */ $this->disable_nonce_check(); $this->cache_store_notices(); $preloaded_data = array(); if ( null !== $route_match ) { try { $response = $this->get_response_from_controller( $route_match['controller'], $path, $route_match['url_params'], $route_match['query_params'] ); if ( $response ) { $preloaded_data = array( 'body' => $response->get_data(), 'headers' => $response->get_headers(), ); } } catch ( \Exception $e ) { // This is executing in frontend of the site, a failure in hydration should not stop the site from working. wc_get_logger()->warning( 'Error in hydrating REST API request: ' . $e->getMessage(), array( 'source' => 'blocks-hydration', 'data' => array( 'path' => $path, 'controller' => $route_match['controller'] ?? null, ), 'backtrace' => true, ) ); } } else { // Preload the request and add it to the array. It will be $preloaded_requests['path'] and contain 'body' and 'headers'. $preloaded_requests = rest_preload_api_request( array(), $path ); $preloaded_data = $preloaded_requests[ $path ] ?? array(); } $this->restore_cached_store_notices(); $this->restore_nonce_check(); // Returns just the single preloaded request, or an empty array if it doesn't exist. return $preloaded_data; } /** * Helper method to generate GET response from a controller. Also fires the `rest_request_after_callbacks` for backward compatibility. * * @param string $controller_class Controller class FQN that will respond to the request. * @param string $path Request path regex. * @param array $url_params URL parameters extracted from route (e.g., ['id' => '123']). * @param array $query_params Query string parameters (e.g., ['key' => 'value']). * * @return false|mixed|null Response */ private function get_response_from_controller( $controller_class, $path, $url_params = array(), $query_params = array() ) { if ( null === $controller_class ) { return false; } $request = new \WP_REST_Request( 'GET', $path ); // Set URL parameters (from route segments like /products/123). if ( ! empty( $url_params ) ) { $request->set_url_params( $url_params ); } // Set query parameters (from query string like ?key=value). if ( ! empty( $query_params ) ) { $request->set_query_params( $query_params ); } $schema_controller = StoreApi::container()->get( SchemaController::class ); $controller = new $controller_class( $schema_controller, $schema_controller->get( $controller_class::SCHEMA_TYPE, $controller_class::SCHEMA_VERSION ) ); $controller_args = is_callable( array( $controller, 'get_args' ) ) ? $controller->get_args() : array(); if ( empty( $controller_args ) ) { return false; } // Get the handler that responds to read request. $handler = current( array_filter( $controller_args, function ( $method_handler ) { return is_array( $method_handler ) && isset( $method_handler['methods'] ) && \WP_REST_Server::READABLE === $method_handler['methods']; } ) ); if ( ! $handler ) { return false; } /** * Similar to WP core's `rest_dispatch_request` filter, this allows plugin to override hydrating the request. * Allows backward compatibility with the `rest_dispatch_request` filter by providing the same arguments. * * @since 8.9.0 * * @param mixed $hydration_result Result of the hydration. If not null, this will be used as the response. * @param WP_REST_Request $request Request used to generate the response. * @param string $path Request path matched for the request.. * @param array $handler Route handler used for the request. */ $hydration_result = apply_filters( 'woocommerce_hydration_dispatch_request', null, $request, $path, $handler ); if ( null !== $hydration_result ) { $response = $hydration_result; } else { $response = call_user_func_array( $handler['callback'], array( $request ) ); } /** * Similar to WP core's `rest_request_after_callbacks` filter, this allows to modify the response after it has been generated. * Allows backward compatibility with the `rest_request_after_callbacks` filter by providing the same arguments. * * @since 8.9.0 * * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. * Usually a WP_REST_Response or WP_Error. * @param array $handler Route handler used for the request. * @param WP_REST_Request $request Request used to generate the response. */ $response = apply_filters( 'woocommerce_hydration_request_after_callbacks', $response, $handler, $request ); return $response; } /** * Inspired from WP core's `match_request_to_handler`, this matches a given path from available route regexes. * Extracts URL parameters from regex named groups and query string parameters. * * @param string $path The path to match (may include query string). * @param array $available_routes Available routes in { $regex1 => $contoller_class1, ... } format. * * @return array|null Array with 'controller', 'url_params', and 'query_params' keys, or null if no match. */ private function match_route_to_handler( $path, $available_routes ) { // Parse query string if present. $query_params = array(); $parsed_url = wp_parse_url( $path ); $clean_path = $parsed_url['path'] ?? $path; if ( isset( $parsed_url['query'] ) ) { parse_str( $parsed_url['query'], $query_params ); } // Match route and extract URL parameters. foreach ( $available_routes as $route_path => $controller ) { if ( preg_match( '@^' . $route_path . '$@i', $clean_path, $matches ) ) { // Extract named groups (URL parameters like 'id'). $url_params = array_intersect_key( $matches, array_flip( array_filter( array_keys( $matches ), 'is_string' ) ) ); return array( 'controller' => $controller, 'url_params' => $url_params, 'query_params' => $query_params, ); } } return null; } /** * Disable the nonce check temporarily. */ protected function disable_nonce_check() { add_filter( 'woocommerce_store_api_disable_nonce_check', array( $this, 'disable_nonce_check_callback' ) ); } /** * Callback to disable the nonce check. While we could use `__return_true`, we use a custom named callback so that * we can remove it later without affecting other filters. */ public function disable_nonce_check_callback() { return true; } /** * Restore the nonce check. */ protected function restore_nonce_check() { remove_filter( 'woocommerce_store_api_disable_nonce_check', array( $this, 'disable_nonce_check_callback' ) ); } /** * Cache notices before hydrating the API if the customer has a session. */ protected function cache_store_notices() { if ( ! did_action( 'woocommerce_init' ) || null === WC()->session ) { return; } $this->cached_store_notices = wc_get_notices(); wc_clear_notices(); } /** * Restore notices into current session from cache. */ protected function restore_cached_store_notices() { if ( ! did_action( 'woocommerce_init' ) || null === WC()->session ) { return; } wc_set_notices( $this->cached_store_notices ); $this->cached_store_notices = array(); } } Services/Email/CustomerNewAccount.php 0000777 00000001102 15251721451 0013663 0 ustar 00 <?php declare( strict_types=1 ); namespace Automattic\WooCommerce\Blocks\Domain\Services\Email; use Automattic\WooCommerce\Blocks\Domain\Package; /** * Customer New Account. Previously used for blocks, but now replaced by the core email. * * @deprecated This class can't be removed due to https://github.com/woocommerce/woocommerce/issues/52311. */ class CustomerNewAccount extends \WC_Email { /** * Constructor. * * @param Package $package An instance of (Woo Blocks) Package. */ public function __construct( Package $package ) { parent::__construct(); } } Services/DraftOrders.php 0000777 00000017432 15251721451 0011300 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Domain\Package; use Exception; use WC_Order; /** * Service class for adding DraftOrder functionality to WooCommerce core. * * Sets up all logic related to the Checkout Draft Orders service * * @internal */ class DraftOrders { const DB_STATUS = 'wc-checkout-draft'; const STATUS = 'checkout-draft'; const DRAFT_CLEANUP_EVENT_HOOK = 'woocommerce_cleanup_draft_orders'; /** * Holds the Package instance * * @var Package */ private $package; /** * Constructor * * @param Package $package An instance of the package class. */ public function __construct( Package $package ) { $this->package = $package; } /** * Set all hooks related to adding Checkout Draft order functionality to Woo Core. */ public function init() { add_filter( 'wc_order_statuses', [ $this, 'register_draft_order_status' ] ); add_filter( 'woocommerce_register_shop_order_post_statuses', [ $this, 'register_draft_order_post_status' ] ); add_filter( 'woocommerce_analytics_excluded_order_statuses', [ $this, 'append_draft_order_post_status' ] ); add_filter( 'woocommerce_valid_order_statuses_for_payment', [ $this, 'append_draft_order_post_status' ], 999 ); add_filter( 'woocommerce_valid_order_statuses_for_payment_complete', [ $this, 'append_draft_order_post_status' ], 999 ); // Hook into the query to retrieve My Account orders so draft status is excluded. add_action( 'woocommerce_my_account_my_orders_query', [ $this, 'delete_draft_order_post_status_from_args' ] ); add_action( self::DRAFT_CLEANUP_EVENT_HOOK, [ $this, 'delete_expired_draft_orders' ] ); add_action( 'admin_init', [ $this, 'install' ] ); if ( defined( 'WC_PLUGIN_BASENAME' ) ) { add_action( 'deactivate_' . WC_PLUGIN_BASENAME, [ $this, 'unschedule_cronjobs' ] ); } } /** * Installation related logic for Draft order functionality. * * @internal */ public function install() { $this->maybe_create_cronjobs(); } /** * Unschedule recurring actions when plugin is deactivated. * * @since 10.0.0 * @internal */ public function unschedule_cronjobs() { WC()->queue()->cancel_all( self::DRAFT_CLEANUP_EVENT_HOOK ); } /** * Maybe create cron events. */ protected function maybe_create_cronjobs() { $has_scheduled_action = function_exists( 'as_has_scheduled_action' ) ? 'as_has_scheduled_action' : 'as_next_scheduled_action'; if ( false === call_user_func( $has_scheduled_action, self::DRAFT_CLEANUP_EVENT_HOOK ) ) { as_schedule_recurring_action( strtotime( 'midnight tonight' ), DAY_IN_SECONDS, self::DRAFT_CLEANUP_EVENT_HOOK ); } } /** * Register custom order status for orders created via the API during checkout. * * Draft order status is used before payment is attempted, during checkout, when a cart is converted to an order. * * @param array $statuses Array of statuses. * @internal * @return array */ public function register_draft_order_status( array $statuses ) { $statuses[ self::DB_STATUS ] = _x( 'Draft', 'Order status', 'woocommerce' ); return $statuses; } /** * Register custom order post status for orders created via the API during checkout. * * @param array $statuses Array of statuses. * @internal * @return array */ public function register_draft_order_post_status( array $statuses ) { $statuses[ self::DB_STATUS ] = $this->get_post_status_properties(); return $statuses; } /** * Returns the properties of this post status for registration. * * @return array */ private function get_post_status_properties() { return [ 'label' => _x( 'Draft', 'Order status', 'woocommerce' ), 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => false, 'show_in_admin_status_list' => true, /* translators: %s: number of orders */ 'label_count' => _n_noop( 'Drafts <span class="count">(%s)</span>', 'Drafts <span class="count">(%s)</span>', 'woocommerce' ), ]; } /** * Remove draft status from the 'status' argument of an $args array. * * @param array $args Array of arguments containing statuses in the status key. * @internal * @return array */ public function delete_draft_order_post_status_from_args( $args ) { if ( ! array_key_exists( 'status', $args ) ) { $statuses = []; foreach ( wc_get_order_statuses() as $key => $label ) { if ( self::DB_STATUS !== $key ) { $statuses[] = str_replace( 'wc-', '', $key ); } } $args['status'] = $statuses; } elseif ( self::DB_STATUS === $args['status'] ) { $args['status'] = ''; } elseif ( is_array( $args['status'] ) ) { $args['status'] = array_diff_key( $args['status'], array( self::STATUS => null ) ); } return $args; } /** * Append draft status to a list of statuses. * * @param array $statuses Array of statuses. * @internal * @return array */ public function append_draft_order_post_status( $statuses ) { $statuses[] = self::STATUS; return $statuses; } /** * Delete draft orders older than a day in batches of 20. * * Ran on a daily cron schedule. * * @internal */ public function delete_expired_draft_orders() { $count = 0; $batch_size = 20; $this->ensure_draft_status_registered(); $orders = wc_get_orders( [ 'date_modified' => '<=' . strtotime( '-1 DAY' ), 'limit' => $batch_size, 'status' => self::DB_STATUS, 'type' => 'shop_order', ] ); // do we bail because the query results are unexpected? try { $this->assert_order_results( $orders, $batch_size ); if ( $orders ) { foreach ( $orders as $order ) { $order->delete( true ); ++$count; } } if ( $batch_size === $count && function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( self::DRAFT_CLEANUP_EVENT_HOOK ); } } catch ( Exception $error ) { wc_caught_exception( $error, __METHOD__ ); } } /** * Since it's possible for third party code to clobber the `$wp_post_statuses` global, * we need to do a final check here to make sure the draft post status is * registered with the global so that it is not removed by WP_Query status * validation checks. */ private function ensure_draft_status_registered() { $is_registered = get_post_stati( [ 'name' => self::DB_STATUS ] ); if ( empty( $is_registered ) ) { register_post_status( self::DB_STATUS, $this->get_post_status_properties() ); } } /** * Asserts whether incoming order results are expected given the query * this service class executes. * * @param WC_Order[] $order_results The order results being asserted. * @param int $expected_batch_size The expected batch size for the results. * @throws Exception If any assertions fail, an exception is thrown. */ private function assert_order_results( $order_results, $expected_batch_size ) { // if not an array, then just return because it won't get handled // anyways. if ( ! is_array( $order_results ) ) { return; } $suffix = ' This is an indicator that something is filtering WooCommerce or WordPress queries and modifying the query parameters.'; // if count is greater than our expected batch size, then that's a problem. if ( count( $order_results ) > 20 ) { throw new Exception( 'There are an unexpected number of results returned from the query.' . $suffix ); } // if any of the returned orders are not draft (or not a WC_Order), then that's a problem. foreach ( $order_results as $order ) { if ( ! ( $order instanceof WC_Order ) ) { throw new Exception( 'The returned results contain a value that is not a WC_Order.' . $suffix ); } if ( ! $order->has_status( self::STATUS ) ) { throw new Exception( 'The results contain an order that is not a `wc-checkout-draft` status in the results.' . $suffix ); } } } } Services/Notices.php 0000777 00000007057 15251721451 0010467 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Domain\Package; use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils; /** * Service class for adding new-style Notices to WooCommerce core. * * @internal */ class Notices { /** * Holds the Package instance * * @var Package */ private $package; /** * Templates used for notices. * * @var array */ private $notice_templates = array( 'notices/error.php', 'notices/notice.php', 'notices/success.php', ); /** * Constructor * * @param Package $package An instance of the package class. */ public function __construct( Package $package ) { $this->package = $package; } /** * Initialize notice hooks. */ public function init() { add_action( 'after_setup_theme', function () { /** * Allow classic theme developers to opt-in to using block notices. * * @since 8.8.0 * @param bool $use_block_notices_in_classic_theme Whether to use block notices in classic theme. * @return bool */ if ( wp_is_block_theme() || apply_filters( 'woocommerce_use_block_notices_in_classic_theme', false ) ) { add_filter( 'wc_get_template', [ $this, 'get_notices_template' ], 10, 5 ); } } ); add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] ); add_action( 'wp_head', [ $this, 'enqueue_notice_styles' ] ); } /** * Allow SVG icon in notices. * * @param array $allowed_tags Allowed tags. * @return array */ public function add_kses_notice_allowed_tags( $allowed_tags ) { $svg_args = array( 'svg' => array( 'aria-hidden' => true, 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'focusable' => true, ), 'path' => array( 'd' => true, ), ); return array_merge( $allowed_tags, $svg_args ); } /** * Replaces core notice templates with those from blocks. * * The new notice templates match block components with matching icons and styling. The differences are: * 1. Core has notices for info, success, and error notices, blocks has notices for info, success, error, * warning, and a default notice type. * 2. The block notices use different CSS classes to the core notices. Core uses `woocommerce-message`, `is-info` * and `is-error` classes, blocks uses `wc-block-components-notice-banner is-error`, * `wc-block-components-notice-banner is-info`, and `wc-block-components-notice-banner is-success`. * 3. The markup of the notices is different, with the block notices using SVG icons and a slightly different * structure to accommodate this. * * @param string $template Located template path. * @param string $template_name Template name. * @param array $args Template arguments. * @param string $template_path Template path. * @param string $default_path Default path. * @return string */ public function get_notices_template( $template, $template_name, $args, $template_path, $default_path ) { if ( in_array( $template_name, $this->notice_templates, true ) ) { $directory = get_stylesheet_directory(); $file = $directory . '/woocommerce/' . $template_name; if ( file_exists( $file ) ) { return $file; } $template = $this->package->get_path( 'templates/block-' . $template_name ); wp_enqueue_style( 'wc-blocks-style' ); } return $template; } /** * Replaces all notices with the new block-based notices. * * @return void */ public function enqueue_notice_styles() { wp_enqueue_style( 'wc-blocks-style' ); } } Services/GoogleAnalytics.php 0000777 00000007141 15251721451 0012141 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; /** * Service class to integrate Blocks with the Google Analytics extension, */ class GoogleAnalytics { /** * Instance of the asset API. * * @var AssetApi */ protected $asset_api; /** * Constructor. * * @param AssetApi $asset_api Instance of the asset API. */ public function __construct( AssetApi $asset_api ) { $this->asset_api = $asset_api; } /** * Hook into WP. */ public function init() { // Require Google Analytics Integration to be activated. if ( ! class_exists( 'WC_Google_Analytics_Integration', false ) ) { return; } add_action( 'init', array( $this, 'register_assets' ) ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_filter( 'script_loader_tag', array( $this, 'async_script_loader_tags' ), 10, 3 ); } /** * Register scripts. */ public function register_assets() { $this->asset_api->register_script( 'wc-blocks-google-analytics', 'assets/client/blocks/wc-blocks-google-analytics.js', [ 'google-tag-manager' ] ); } /** * Enqueue the Google Tag Manager script if prerequisites are met. */ public function enqueue_scripts() { $settings = $this->get_google_analytics_settings(); $prefix = strstr( strtoupper( $settings['ga_id'] ), '-', true ); // Require tracking to be enabled with a valid GA ID. if ( ! in_array( $prefix, [ 'G', 'GT' ], true ) ) { return; } /** * Filter to disable Google Analytics tracking. * * @internal Matches filter name in GA extension. * @since 4.9.0 * * @param boolean $disable_tracking If true, tracking will be disabled. */ if ( apply_filters( 'woocommerce_ga_disable_tracking', ! wc_string_to_bool( $settings['ga_event_tracking_enabled'] ) ) ) { return; } if ( ! wp_script_is( 'google-tag-manager', 'registered' ) ) { // Using an array with strategies as the final argument to wp_register_script was introduced in WP 6.3. // WC requires at least 6.3 at the point of adding this, so it's safe to leave in without version checks. // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_script( 'google-tag-manager', 'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'], [], null, [ 'in_footer' => false, 'strategy' => 'async', ] ); wp_add_inline_script( 'google-tag-manager', " window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '" . esc_js( $settings['ga_id'] ) . "', { 'send_page_view': false });" ); } wp_enqueue_script( 'wc-blocks-google-analytics' ); } /** * Get settings from the GA integration extension. * * @return array */ private function get_google_analytics_settings() { return wp_parse_args( get_option( 'woocommerce_google_analytics_settings' ), [ 'ga_id' => '', 'ga_event_tracking_enabled' => 'no', ] ); } /** * Add async to script tags with defined handles. * * @param string $tag HTML for the script tag. * @param string $handle Handle of script. * @param string $src Src of script. * @return string */ public function async_script_loader_tags( $tag, $handle, $src ) { if ( ! in_array( $handle, array( 'google-tag-manager' ), true ) ) { return $tag; } // If script was output manually in wp_head, abort. if ( did_action( 'woocommerce_gtag_snippet' ) ) { return ''; } return str_replace( '<script src', '<script async src', $tag ); } } Services/functions.php 0000777 00000005617 15251721451 0011073 0 ustar 00 <?php use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) { /** * Register a checkout field. * * @param array $options Field arguments. See CheckoutFields::register_checkout_field() for details. * @throws \Exception If field registration fails. */ function woocommerce_register_additional_checkout_field( $options ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore // Check if `woocommerce_blocks_loaded` ran. If not then the CheckoutFields class will not be available yet. // In that case, re-hook `woocommerce_blocks_loaded` and try running this again. $woocommerce_blocks_loaded_ran = did_action( 'woocommerce_blocks_loaded' ); if ( ! $woocommerce_blocks_loaded_ran ) { add_action( 'woocommerce_blocks_loaded', function () use ( $options ) { woocommerce_register_additional_checkout_field( $options ); } ); return; } $checkout_fields = Package::container()->get( CheckoutFields::class ); $result = $checkout_fields->register_checkout_field( $options ); if ( is_wp_error( $result ) ) { throw new \Exception( esc_attr( $result->get_error_message() ) ); } } } if ( ! function_exists( '__experimental_woocommerce_blocks_register_checkout_field' ) ) { /** * Register a checkout field. * * @param array $options Field arguments. See CheckoutFields::register_checkout_field() for details. * @throws \Exception If field registration fails. * @deprecated 5.6.0 Use woocommerce_register_additional_checkout_field() instead. */ function __experimental_woocommerce_blocks_register_checkout_field( $options ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore wc_deprecated_function( __FUNCTION__, '8.9.0', 'woocommerce_register_additional_checkout_field' ); woocommerce_register_additional_checkout_field( $options ); } } if ( ! function_exists( '__internal_woocommerce_blocks_deregister_checkout_field' ) ) { /** * Deregister a checkout field. * * @param string $field_id Field ID. * @throws \Exception If field deregistration fails. * @internal */ function __internal_woocommerce_blocks_deregister_checkout_field( $field_id ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore $checkout_fields = Package::container()->get( CheckoutFields::class ); $result = $checkout_fields->deregister_checkout_field( $field_id ); if ( is_wp_error( $result ) ) { throw new \Exception( esc_attr( $result->get_error_message() ) ); } } } Services/FeatureGating.php 0000777 00000001304 15251721451 0011575 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain\Services; use Automattic\WooCommerce\Admin\DeprecatedClassFacade; /** * Service class that used to handle feature flags. That functionality * is removed now and it is only used to determine "environment". * * @internal * * @deprecated since 9.6.0, use wp_get_environment_type() instead. */ class FeatureGating extends DeprecatedClassFacade { /** * The version that this class was deprecated in. * * @var string */ protected static $deprecated_in_version = '9.6.0'; /** * Constructor * * @param string $environment Hardcoded environment value. Useful for tests. */ public function __construct( $environment = 'unset' ) { } } Bootstrap.php 0000777 00000041351 15251721451 0007250 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain; use Automattic\Jetpack\Constants; use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi; use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry; use Automattic\WooCommerce\Blocks\AssetsController; use Automattic\WooCommerce\Blocks\BlockPatterns; use Automattic\WooCommerce\Blocks\BlockTemplatesRegistry; use Automattic\WooCommerce\Blocks\BlockTemplatesController; use Automattic\WooCommerce\Blocks\BlockTypesController; use Automattic\WooCommerce\Blocks\DependencyDetection; use Automattic\WooCommerce\Blocks\Patterns\AIPatterns; use Automattic\WooCommerce\Blocks\Patterns\PatternRegistry; use Automattic\WooCommerce\Blocks\Patterns\PTKClient; use Automattic\WooCommerce\Blocks\Patterns\PTKPatternsStore; use Automattic\WooCommerce\Blocks\QueryFilters; use Automattic\WooCommerce\Blocks\Domain\Services\Notices; use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders; use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics; use Automattic\WooCommerce\Blocks\Domain\Services\Hydration; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsAdmin; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsFrontend; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutLink; use Automattic\WooCommerce\Blocks\InboxNotifications; use Automattic\WooCommerce\Blocks\Installer; use Automattic\WooCommerce\Blocks\Payments\Api as PaymentsApi; use Automattic\WooCommerce\Blocks\Payments\Integrations\BankTransfer; use Automattic\WooCommerce\Blocks\Payments\Integrations\CashOnDelivery; use Automattic\WooCommerce\Blocks\Payments\Integrations\Cheque; use Automattic\WooCommerce\Blocks\Payments\Integrations\PayPal; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; use Automattic\WooCommerce\Blocks\Registry\Container; use Automattic\WooCommerce\Blocks\Templates\ClassicTemplatesCompatibility; use Automattic\WooCommerce\StoreApi\RoutesController; use Automattic\WooCommerce\StoreApi\SchemaController; use Automattic\WooCommerce\StoreApi\StoreApi; use Automattic\WooCommerce\Blocks\Shipping\ShippingController; use Automattic\WooCommerce\Blocks\TemplateOptions; /** * Takes care of bootstrapping the plugin. * * @since 2.5.0 */ class Bootstrap { /** * Holds the Dependency Injection Container * * @var Container */ private $container; /** * Holds the Package instance * * @var Package */ private $package; /** * Constructor * * @param Container $container The Dependency Injection Container. */ public function __construct( Container $container ) { $this->container = $container; $this->package = $container->get( Package::class ); $this->init(); /** * Fires when the woocommerce blocks are loaded and ready to use. * * This hook is intended to be used as a safe event hook for when the plugin * has been loaded, and all dependency requirements have been met. * * To ensure blocks are initialized, you must use the `woocommerce_blocks_loaded` * hook instead of the `plugins_loaded` hook. This is because the functions * hooked into plugins_loaded on the same priority load in an inconsistent and unpredictable manner. * * @since 2.5.0 */ do_action( 'woocommerce_blocks_loaded' ); } /** * Init the package - load the blocks library and define constants. */ protected function init() { $this->register_dependencies(); $this->register_payment_methods(); add_action( 'admin_init', function () { // Delete this notification because the blocks are included in WC Core now. This will handle any sites // with lingering notices. InboxNotifications::delete_surface_cart_checkout_blocks_notification(); }, 10, 0 ); // We need to initialize BlockTemplatesController and BlockTemplatesRegistry at the end of `after_setup_theme` // so themes had the opportunity to declare support for template parts. add_action( 'after_setup_theme', function () { $is_store_api_request = wc()->is_store_api_request(); if ( ! $is_store_api_request && ( wp_is_block_theme() || current_theme_supports( 'block-template-parts' ) ) ) { $this->container->get( BlockTemplatesRegistry::class )->init(); $this->container->get( BlockTemplatesController::class )->init(); } }, 999 ); $is_rest = wc()->is_rest_api_request(); $is_store_api_request = wc()->is_store_api_request(); // Initialize Store API in non-admin context. if ( ! is_admin() ) { $this->container->get( StoreApi::class )->init(); } // Load and init assets. $this->container->get( PaymentsApi::class )->init(); $this->container->get( DraftOrders::class )->init(); $this->container->get( ShippingController::class )->init(); $this->container->get( CheckoutFields::class )->init(); $this->container->get( CheckoutLink::class )->init(); $this->container->get( AssetDataRegistry::class ); $this->container->get( AssetsController::class ); $this->container->get( DependencyDetection::class ); // Load assets in admin and on the frontend. if ( ! $is_rest ) { $this->add_build_notice(); $this->container->get( Installer::class )->init(); $this->container->get( GoogleAnalytics::class )->init(); $this->container->get( is_admin() ? CheckoutFieldsAdmin::class : CheckoutFieldsFrontend::class )->init(); } // Load assets unless this is a request specifically for the store API. if ( ! $is_store_api_request ) { // Template related functionality. These won't be loaded for store API requests, but may be loaded for // regular rest requests to maintain compatibility with the store editor. $this->container->get( BlockPatterns::class ); $this->container->get( BlockTypesController::class ); $this->container->get( ClassicTemplatesCompatibility::class ); $this->container->get( Notices::class )->init(); if ( is_admin() || $is_rest ) { $this->container->get( AIPatterns::class ); $this->container->get( PTKPatternsStore::class ); } if ( is_admin() ) { $this->container->get( TemplateOptions::class )->init(); } } $this->container->get( QueryFilters::class )->init(); } /** * See if files have been built or not. * * @return bool */ protected function is_built() { return file_exists( $this->package->get_path( 'assets/client/blocks/featured-product.js' ) ); } /** * Add a notice stating that the build has not been done yet. */ protected function add_build_notice() { if ( $this->is_built() ) { return; } add_action( 'admin_notices', function () { echo '<div class="error"><p>'; printf( /* translators: %1$s is the node install command, %2$s is the install command, %3$s is the build command, %4$s is the watch command. */ esc_html__( 'WooCommerce Blocks development mode requires files to be built. From the root directory, run %1$s to ensure your node version is aligned, run %2$s to install dependencies, %3$s to build the files or %4$s to build the files and watch for changes.', 'woocommerce' ), '<code>nvm use</code>', '<code>pnpm install</code>', '<code>pnpm --filter="@woocommerce/plugin-woocommerce" build</code>', '<code>pnpm --filter="@woocommerce/plugin-woocommerce" watch:build</code>' ); echo '</p></div>'; } ); } /** * Register core dependencies with the container. */ protected function register_dependencies() { $this->container->register( AssetApi::class, function ( Container $container ) { return new AssetApi( $container->get( Package::class ) ); } ); $this->container->register( AssetDataRegistry::class, function ( Container $container ) { return new AssetDataRegistry( $container->get( AssetApi::class ) ); } ); $this->container->register( AssetsController::class, function ( Container $container ) { return new AssetsController( $container->get( AssetApi::class ) ); } ); $this->container->register( DependencyDetection::class, function () { return new DependencyDetection(); } ); $this->container->register( PaymentMethodRegistry::class, function () { return new PaymentMethodRegistry(); } ); $this->container->register( Installer::class, function () { return new Installer(); } ); $this->container->register( BlockTypesController::class, function ( Container $container ) { $asset_api = $container->get( AssetApi::class ); $asset_data_registry = $container->get( AssetDataRegistry::class ); return new BlockTypesController( $asset_api, $asset_data_registry ); } ); $this->container->register( ClassicTemplatesCompatibility::class, function ( Container $container ) { $asset_data_registry = $container->get( AssetDataRegistry::class ); return new ClassicTemplatesCompatibility( $asset_data_registry ); } ); $this->container->register( DraftOrders::class, function ( Container $container ) { return new DraftOrders( $container->get( Package::class ) ); } ); $this->container->register( GoogleAnalytics::class, function ( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new GoogleAnalytics( $asset_api ); } ); $this->container->register( Notices::class, function ( Container $container ) { return new Notices( $container->get( Package::class ) ); } ); $this->container->register( Hydration::class, function ( Container $container ) { return new Hydration( $container->get( AssetDataRegistry::class ) ); } ); $this->container->register( CheckoutFields::class, function ( Container $container ) { return new CheckoutFields( $container->get( AssetDataRegistry::class ) ); } ); $this->container->register( CheckoutFieldsAdmin::class, function ( Container $container ) { $checkout_fields_controller = $container->get( CheckoutFields::class ); return new CheckoutFieldsAdmin( $checkout_fields_controller ); } ); $this->container->register( CheckoutFieldsFrontend::class, function ( Container $container ) { $checkout_fields_controller = $container->get( CheckoutFields::class ); return new CheckoutFieldsFrontend( $checkout_fields_controller ); } ); $this->container->register( PaymentsApi::class, function ( Container $container ) { $payment_method_registry = $container->get( PaymentMethodRegistry::class ); $asset_data_registry = $container->get( AssetDataRegistry::class ); return new PaymentsApi( $payment_method_registry, $asset_data_registry ); } ); $this->container->register( CheckoutLink::class, function () { return new CheckoutLink(); } ); $this->container->register( StoreApi::class, function () { return new StoreApi(); } ); $this->container->register( TemplateOptions::class, function () { return new TemplateOptions(); } ); // Maintains backwards compatibility with previous Store API namespace. $this->container->register( 'Automattic\WooCommerce\Blocks\StoreApi\Formatters', function ( Container $container ) { $this->deprecated_dependency( 'Automattic\WooCommerce\Blocks\StoreApi\Formatters', '6.4.0', 'Automattic\WooCommerce\StoreApi\Formatters', '6.5.0' ); return $container->get( StoreApi::class )->container()->get( \Automattic\WooCommerce\StoreApi\Formatters::class ); } ); $this->container->register( 'Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi', function ( Container $container ) { $this->deprecated_dependency( 'Automattic\WooCommerce\Blocks\Domain\Services\ExtendRestApi', '6.4.0', 'Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema', '6.5.0' ); return $container->get( StoreApi::class )->container()->get( \Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::class ); } ); $this->container->register( 'Automattic\WooCommerce\Blocks\StoreApi\SchemaController', function ( Container $container ) { $this->deprecated_dependency( 'Automattic\WooCommerce\Blocks\StoreApi\SchemaController', '6.4.0', 'Automattic\WooCommerce\StoreApi\SchemaController', '6.5.0' ); return $container->get( StoreApi::class )->container()->get( SchemaController::class ); } ); $this->container->register( 'Automattic\WooCommerce\Blocks\StoreApi\RoutesController', function ( Container $container ) { $this->deprecated_dependency( 'Automattic\WooCommerce\Blocks\StoreApi\RoutesController', '6.4.0', 'Automattic\WooCommerce\StoreApi\RoutesController', '6.5.0' ); return $container->get( StoreApi::class )->container()->get( RoutesController::class ); } ); $this->container->register( PTKClient::class, function () { return new PTKClient(); } ); $this->container->register( PTKPatternsStore::class, function () { return new PTKPatternsStore( $this->container->get( PTKClient::class ) ); } ); $this->container->register( BlockPatterns::class, function () { return new BlockPatterns( $this->package, new PatternRegistry(), $this->container->get( PTKPatternsStore::class ) ); } ); $this->container->register( AIPatterns::class, function () { return new AIPatterns(); } ); $this->container->register( ShippingController::class, function ( $container ) { $asset_api = $container->get( AssetApi::class ); $asset_data_registry = $container->get( AssetDataRegistry::class ); return new ShippingController( $asset_api, $asset_data_registry ); } ); $this->container->register( QueryFilters::class, function () { return new QueryFilters(); } ); $this->container->register( BlockTemplatesRegistry::class, function () { return new BlockTemplatesRegistry(); } ); $this->container->register( BlockTemplatesController::class, function () { return new BlockTemplatesController(); } ); } /** * Throws a deprecation notice for a dependency without breaking requests. * * @param string $function Class or function being deprecated. * @param string $version Version in which it was deprecated. * @param string $replacement Replacement class or function, if applicable. * @param string $trigger_error_version Optional version to start surfacing this as a PHP error rather than a log. Defaults to $version. */ protected function deprecated_dependency( $function, $version, $replacement = '', $trigger_error_version = '' ) { if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { return; } $trigger_error_version = $trigger_error_version ? $trigger_error_version : $version; $error_message = $replacement ? sprintf( '%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.', $function, $version, $replacement ) : sprintf( '%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.', $function, $version ); /** * Fires when a deprecated function is called. * * @since 7.3.0 */ do_action( 'deprecated_function_run', $function, $replacement, $version ); $log_error = false; // If headers have not been sent yet, log to avoid breaking the request. if ( ! headers_sent() ) { $log_error = true; } // If the $trigger_error_version was not yet reached, only log the error. if ( version_compare( Constants::get_constant( 'WC_VERSION' ), $trigger_error_version, '<' ) ) { $log_error = true; } /** * Filters whether to trigger an error for deprecated functions. (Same as WP core) * * @since 7.3.0 * * @param bool $trigger Whether to trigger the error for deprecated functions. Default true. */ if ( ! apply_filters( 'deprecated_function_trigger_error', true ) ) { $log_error = true; } if ( $log_error ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( $error_message ); } else { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( $error_message, E_USER_DEPRECATED ); } } /** * Register payment method integrations with the container. */ protected function register_payment_methods() { $this->container->register( Cheque::class, function ( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new Cheque( $asset_api ); } ); $this->container->register( PayPal::class, function ( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new PayPal( $asset_api ); } ); $this->container->register( BankTransfer::class, function ( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new BankTransfer( $asset_api ); } ); $this->container->register( CashOnDelivery::class, function ( Container $container ) { $asset_api = $container->get( AssetApi::class ); return new CashOnDelivery( $asset_api ); } ); } } Package.php 0000777 00000006123 15251721451 0006624 0 ustar 00 <?php namespace Automattic\WooCommerce\Blocks\Domain; use Automattic\WooCommerce\Blocks\Options; use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating; /** * Main package class. * * Returns information about the package and handles init. * * @since 2.5.0 */ class Package { /** * Holds the current version of the blocks plugin. * * @var string */ private $version; /** * Holds the main path to the blocks plugin directory. * * @var string */ private $path; /** * Holds locally the plugin_dir_url to avoid recomputing it. * * @var string */ private $plugin_dir_url; /** * Holds the feature gating class instance. * * @var FeatureGating */ private $feature_gating; /** * Constructor * * @param string $version Version of the plugin. * @param string $plugin_path Path to the main plugin file. * @param FeatureGating $deprecated Deprecated Feature gating class. */ public function __construct( $version, $plugin_path, $deprecated = null ) { if ( null !== $deprecated ) { wc_deprecated_argument( 'FeatureGating', '9.6', 'FeatureGating class is deprecated, please use wp_get_environment_type() instead.' ); $this->feature_gating = new FeatureGating(); } $this->version = $version; $this->path = $plugin_path; } /** * Returns the version of WooCommerce Blocks. * * Note: since Blocks was merged into WooCommerce Core, the version of * WC Blocks doesn't update anymore. Use * `Constants::get_constant( 'WC_VERSION' )` when possible to get the * WooCommerce Core version. * * @return string */ public function get_version() { return $this->version; } /** * Returns the version of WooCommerce Blocks stored in the database. * * @return string */ public function get_version_stored_on_db() { return get_option( Options::WC_BLOCK_VERSION, '' ); } /** * Sets the version of WooCommerce Blocks in the database. * This is useful during the first installation or after the upgrade process. */ public function set_version_stored_on_db() { update_option( Options::WC_BLOCK_VERSION, $this->get_version() ); } /** * Returns the path to the plugin directory. * * @param string $relative_path If provided, the relative path will be * appended to the plugin path. * * @return string */ public function get_path( $relative_path = '' ) { return trailingslashit( $this->path ) . $relative_path; } /** * Returns the url to the blocks plugin directory. * * @param string $relative_url If provided, the relative url will be * appended to the plugin url. * * @return string */ public function get_url( $relative_url = '' ) { if ( ! $this->plugin_dir_url ) { // Append index.php so WP does not return the parent directory. $this->plugin_dir_url = plugin_dir_url( $this->path . '/index.php' ); } return $this->plugin_dir_url . $relative_url; } /** * Returns an instance of the FeatureGating class. * * @return FeatureGating */ public function feature() { return $this->feature_gating; } }
| ver. 1.6 |
Github
|
.
| PHP 8.1.34 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка