. * @param bool $plugin_info Whether we're in the plugin information modal. * @param string $plugin_file The plugin filename. * * @return stdClass The converted subscription. */ protected function convert_subscription_to_plugin( $subscription, $yoast_free_data = null, $plugin_info = false, $plugin_file = '' ) { $changelog = ''; if ( isset( $subscription->product->changelog ) ) { // We need to replace h2's and h3's with h4's because the styling expects that. $changelog = str_replace( 'product->changelog ) ); $changelog = str_replace( ' ( $plugin_info ) ? YOAST_SEO_WP_REQUIRED : null, ]; return (object) [ 'new_version' => ( $subscription->product->version ?? '' ), 'name' => $subscription->product->name, 'slug' => $subscription->product->slug, 'plugin' => $plugin_file, 'url' => $subscription->product->store_url, 'last_update' => $subscription->product->last_updated, 'homepage' => $subscription->product->store_url, 'download_link' => $subscription->product->download, 'package' => $subscription->product->download, 'sections' => [ 'changelog' => $changelog, 'support' => $this->get_support_section(), ], 'icons' => [ '2x' => $this->get_icon( $subscription->product->slug ), ], 'update_supported' => true, 'banners' => $this->get_banners( $subscription->product->slug ), // If we have extracted Yoast Free's data before, use that. If not, resort to the defaults. 'tested' => YOAST_SEO_WP_TESTED, 'requires' => ( $yoast_free_data->requires ?? $defaults['requires'] ), 'requires_php' => YOAST_SEO_PHP_REQUIRED, ]; } /** * Returns the plugin's icon URL. * * @param string $slug The plugin slug. * * @return string The icon URL for this plugin. */ protected function get_icon( $slug ) { switch ( $slug ) { case self::LOCAL_SLUG: return 'https://yoa.st/local-seo-icon'; case self::NEWS_SLUG: return 'https://yoa.st/news-seo-icon'; case self::PREMIUM_SLUG: return 'https://yoa.st/yoast-seo-icon'; case self::VIDEO_SLUG: return 'https://yoa.st/video-seo-icon'; case self::WOOCOMMERCE_SLUG: return 'https://yoa.st/woo-seo-icon'; } } /** * Return an array of plugin banner URLs. * * @param string $slug The plugin slug. * * @return string[] */ protected function get_banners( $slug ) { switch ( $slug ) { case self::LOCAL_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-local', 'low' => 'https://yoa.st/yoast-seo-banner-low-local', ]; case self::NEWS_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-news', 'low' => 'https://yoa.st/yoast-seo-banner-low-news', ]; case self::PREMIUM_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-premium', 'low' => 'https://yoa.st/yoast-seo-banner-low-premium', ]; case self::VIDEO_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-video', 'low' => 'https://yoa.st/yoast-seo-banner-low-video', ]; case self::WOOCOMMERCE_SLUG: return [ 'high' => 'https://yoa.st/yoast-seo-banner-woo', 'low' => 'https://yoa.st/yoast-seo-banner-low-woo', ]; } } /** * Checks if the given plugin_file belongs to a Yoast addon. * * @param string $plugin_file Path to the plugin. * * @return bool True when plugin file is for a Yoast addon. */ protected function is_yoast_addon( $plugin_file ) { return $this->get_slug_by_plugin_file( $plugin_file ) !== ''; } /** * Retrieves the addon slug by given plugin file path. * * @param string $plugin_file The file path to the plugin. * * @return string The slug when found or empty string when not. */ protected function get_slug_by_plugin_file( $plugin_file ) { $addons = self::$addons; // Yoast SEO Free isn't an addon, but we needed it in Premium to fetch translations. if ( YoastSEO()->helpers->product->is_premium() ) { $addons['wp-seo.php'] = self::FREE_SLUG; } foreach ( $addons as $addon => $addon_slug ) { if ( strpos( $plugin_file, $addon ) !== false ) { return $addon_slug; } } return ''; } /** * Retrieves the installed Yoast addons. * * @return array The installed plugins. */ protected function get_installed_addons() { return array_filter( $this->get_plugins(), [ $this, 'is_yoast_addon' ], ARRAY_FILTER_USE_KEY ); } /** * Retrieves a list of active addons. * * @return array The active addons. */ protected function get_active_addons() { return array_filter( $this->get_installed_addons(), [ $this, 'is_plugin_active' ], ARRAY_FILTER_USE_KEY ); } /** * Retrieves the current sites from the API. * * @codeCoverageIgnore * * @return bool|stdClass Object when request is successful. False if not. */ protected function request_current_sites() { $api_request = new WPSEO_MyYoast_Api_Request( 'sites/current' ); if ( $api_request->fire() ) { return $api_request->get_response(); } return $this->get_site_information_default(); } /** * Retrieves the transient value with the site information. * * @codeCoverageIgnore * * @return stdClass|false The transient value. */ protected function get_site_information_transient() { global $pagenow; // Force re-check on license & dashboard pages. $current_page = null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing and thus no need to sanitize. $current_page = wp_unslash( $_GET['page'] ); } // Check whether the licenses are valid or whether we need to show notifications. $quick = ( $current_page === Plans_Page_Integration::PAGE || $current_page === General_Page_Integration::PAGE ); // Also do a fresh request on Plugins & Core Update pages. $quick = $quick || $pagenow === 'plugins.php'; $quick = $quick || $pagenow === 'update-core.php'; if ( $quick ) { return get_transient( self::SITE_INFORMATION_TRANSIENT_QUICK ); } return get_transient( self::SITE_INFORMATION_TRANSIENT ); } /** * Sets the site information transient. * * @codeCoverageIgnore * * @param stdClass $site_information The site information to save. * * @return void */ protected function set_site_information_transient( $site_information ) { set_transient( self::SITE_INFORMATION_TRANSIENT, $site_information, DAY_IN_SECONDS ); set_transient( self::SITE_INFORMATION_TRANSIENT_QUICK, $site_information, 60 ); } /** * Retrieves all installed WordPress plugins. * * @codeCoverageIgnore * * @return array The plugins. */ protected function get_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return get_plugins(); } /** * Checks if the given plugin file belongs to an active plugin. * * @codeCoverageIgnore * * @param string $plugin_file The file path to the plugin. * * @return bool True when plugin is active. */ protected function is_plugin_active( $plugin_file ) { return is_plugin_active( $plugin_file ); } /** * Returns an object with no subscriptions. * * @codeCoverageIgnore * * @return stdClass Site information. */ protected function get_site_information_default() { return (object) [ 'url' => WPSEO_Utils::get_home_url(), 'subscriptions' => [], ]; } /** * Maps the plugin API response. * * @param object $site_information Site information as received from the API. * * @return stdClass Mapped site information. */ protected function map_site_information( $site_information ) { return (object) [ 'url' => $site_information->url, 'subscriptions' => array_map( [ $this, 'map_subscription' ], $site_information->subscriptions ), ]; } /** * Maps a plugin subscription. * * @param object $subscription Subscription information as received from the API. * * @return stdClass Mapped subscription. */ protected function map_subscription( $subscription ) { // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Not our properties. return (object) [ 'renewal_url' => $subscription->renewalUrl, 'expiry_date' => $subscription->expiryDate, 'product' => (object) [ 'version' => $subscription->product->version, 'name' => $subscription->product->name, 'slug' => $subscription->product->slug, 'last_updated' => $subscription->product->lastUpdated, 'store_url' => $subscription->product->storeUrl, // Ternary operator is necessary because download can be undefined. 'download' => ( $subscription->product->download ?? null ), 'changelog' => $subscription->product->changelog, ], ]; // phpcs:enable } /** * Retrieves the site information. * * @return stdClass The site information. */ private function get_site_information() { if ( ! $this->has_installed_addons() ) { return $this->get_site_information_default(); } return $this->get_myyoast_site_information(); } /** * Retrieves the contents for the support section. * * @return string The support section content. */ protected function get_support_section() { return '

' . __( 'Need support?', 'wordpress-seo' ) . '

' . '

' /* translators: 1: expands to that refers to the help page, 2: closing tag. */ . sprintf( __( 'You can probably find an answer to your question in our %1$shelp center%2$s.', 'wordpress-seo' ), '', '' ) . ' ' /* translators: %s expands to a mailto support link. */ . sprintf( __( 'If you still need support and have an active subscription for this product, please email %s.', 'wordpress-seo' ), 'support@yoast.com' ) . '

'; } } ngs['title'] ) ) { return; } if ( empty( $post->post_content ) ) { return; } $title = $this->find_local_pickup_text_in_checkout_block( $post->post_content ); // Set the title to be an empty string if it isn't a string. This will make it fall back to the default value of "Pickup". if ( ! is_string( $title ) ) { $title = ''; } $pickup_location_settings['title'] = $title; update_option( 'woocommerce_pickup_location_settings', $pickup_location_settings ); } /** * Find the shipping methods block, then get the value of the localPickupText attribute from it. * * @param string $post_content The post content to search through. * @return null|string The local pickup text if found, otherwise null. */ private function find_local_pickup_text_in_checkout_block( $post_content ) { $scanner = Block_Scanner::create( $post_content ); while ( $scanner->next_delimiter() ) { if ( $scanner->opens_block( 'woocommerce/checkout-shipping-method-block' ) ) { $attributes = $scanner->allocate_and_return_parsed_attributes(); if ( isset( $attributes['localPickupText'] ) ) { return $attributes['localPickupText']; } } } return null; } /** * Extra data passed through from server to client for block. * * @param array $attributes Any attributes that currently are available from the block. * Note, this will be empty in the editor context when the block is * not in the post content on editor load. */ protected function enqueue_data( array $attributes = [] ) { parent::enqueue_data( $attributes ); $country_data = CartCheckoutUtils::get_country_data(); $address_formats = WC()->countries->get_address_formats(); // Move the address format into the 'countryData' setting. // We need to skip 'default' because that's not a valid country. foreach ( $address_formats as $country_code => $format ) { if ( 'default' === $country_code ) { continue; } $country_data[ $country_code ]['format'] = $format; } $providers_payload = []; if ( class_exists( AddressProviderController::class ) && 'no' !== get_option( 'woocommerce_address_autocomplete_enabled', 'no' ) ) { $controller = wc_get_container()->get( AddressProviderController::class ); $providers = $controller->get_providers(); $providers_payload = array_map( static function ( $provider ) { return array( 'id' => (string) $provider->id, 'name' => sanitize_text_field( (string) $provider->name ), 'branding_html' => wp_kses_post( (string) $provider->branding_html ), ); }, (array) $providers ); } $this->asset_data_registry->add( 'addressAutocompleteProviders', $providers_payload ); $this->asset_data_registry->add( 'countryData', $country_data ); $this->asset_data_registry->add( 'defaultAddressFormat', $address_formats['default'] ); // Prime caches to reduce future queries. wp_prime_option_caches( array( 'woocommerce_enable_guest_checkout', 'woocommerce_enable_signup_and_login_from_checkout', 'woocommerce_enable_checkout_login_reminder', 'woocommerce_tax_display_cart', // This one is autoloaded, but we add it here for clarity. 'woocommerce_tax_total_display', 'woocommerce_ship_to_destination', 'woocommerce_registration_generate_password', 'pickup_location_pickup_locations', ) ); $this->asset_data_registry->add( 'checkoutAllowsGuest', false === filter_var( wc()->checkout()->is_registration_required(), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'checkoutAllowsSignup', filter_var( wc()->checkout()->is_registration_enabled(), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'checkoutShowLoginReminder', filter_var( get_option( 'woocommerce_enable_checkout_login_reminder' ), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'displayCartPricesIncludingTax', TaxDisplayMode::INCLUSIVE === get_option( 'woocommerce_tax_display_cart' ) ); $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ) ); $this->asset_data_registry->add( 'forcedBillingAddress', 'billing_only' === get_option( 'woocommerce_ship_to_destination' ) ); $this->asset_data_registry->add( 'generatePassword', filter_var( get_option( 'woocommerce_registration_generate_password' ), FILTER_VALIDATE_BOOLEAN ) ); $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled() ); $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled() ); $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled() ); $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ) ); $this->asset_data_registry->register_page_id( isset( $attributes['cartPageId'] ) ? $attributes['cartPageId'] : 0 ); $this->asset_data_registry->add( 'isBlockTheme', wp_is_block_theme() ); $this->asset_data_registry->add( 'isCheckoutBlock', true ); $pickup_location_settings = LocalPickupUtils::get_local_pickup_settings(); $local_pickup_method_ids = LocalPickupUtils::get_local_pickup_method_ids(); $this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] ); $this->asset_data_registry->add( 'localPickupText', $pickup_location_settings['title'] ); $this->asset_data_registry->add( 'localPickupCost', $pickup_location_settings['cost'] ); $this->asset_data_registry->add( 'collectableMethodIds', $local_pickup_method_ids ); $this->asset_data_registry->add( 'shippingMethodsExist', CartCheckoutUtils::shipping_methods_exist() ); $is_block_editor = $this->is_block_editor(); if ( $is_block_editor ) { $this->asset_data_registry->add( 'localPickupLocations', array_filter( array_map( function ( $location ) { if ( ! wc_string_to_bool( $location['enabled'] ) ) { return null; } $location['formatted_address'] = wc()->countries->get_formatted_address( $location['address'], ', ' ); return $location; }, LocalPickupUtils::get_local_pickup_method_locations() ) ), ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalShippingMethods' ) ) { $shipping_methods = WC()->shipping()->get_shipping_methods(); $formatted_shipping_methods = array_reduce( $shipping_methods, function ( array $acc, $method ) use ( $local_pickup_method_ids ) { if ( in_array( $method->id, $local_pickup_method_ids, true ) ) { return $acc; } if ( $method->supports( 'settings' ) ) { $acc[] = [ 'id' => $method->id, 'title' => $method->method_title, 'description' => $method->method_description, ]; } return $acc; }, [] ); $this->asset_data_registry->add( 'globalShippingMethods', $formatted_shipping_methods ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'activeShippingZones' ) && class_exists( '\WC_Shipping_Zones' ) ) { $this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() ); } if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) { // These are used to show options in the sidebar. We want to get the full list of enabled payment methods, // not just the ones that are available for the current cart (which may not exist yet). $payment_methods = PaymentUtils::get_enabled_payment_gateways(); $formatted_payment_methods = array_reduce( $payment_methods, function ( array $acc, $method ) { $acc[] = [ 'id' => $method->id, 'title' => $method->get_method_title() !== '' ? $method->get_method_title() : $method->get_title(), 'description' => $method->get_method_description() !== '' ? $method->get_method_description() : $method->get_description(), ]; return $acc; }, [] ); $this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods ); } // Check `current_user_can` so we can show notices about incompatible extensions in the front-end to admins too. if ( ( $is_block_editor || current_user_can( 'manage_woocommerce' ) ) && ! $this->asset_data_registry->exists( 'incompatibleExtensions' ) ) { if ( ! class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) || ! function_exists( 'get_plugins' ) ) { return; } $declared_extensions = \Automattic\WooCommerce\Utilities\FeaturesUtil::get_compatible_plugins_for_feature( 'cart_checkout_blocks' ); $all_plugins = \get_plugins(); // Note that `get_compatible_plugins_for_feature` calls `get_plugins` internally, so this is already in cache. $incompatible_extensions = array_reduce( $declared_extensions['incompatible'], function ( array $acc, $item ) use ( $all_plugins ) { $plugin = $all_plugins[ $item ] ?? null; $plugin_id = $plugin['TextDomain'] ?? dirname( $item ); $plugin_name = $plugin['Name'] ?? $plugin_id; $acc[] = [ 'id' => $plugin_id, 'title' => $plugin_name, ]; return $acc; }, [] ); $this->asset_data_registry->add( 'incompatibleExtensions', $incompatible_extensions ); } if ( ! is_admin() && ! WC()->is_rest_api_request() ) { $this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' ); $this->asset_data_registry->hydrate_data_from_api_request( 'checkoutData', '/wc/store/v1/checkout' ); $this->hydrate_customer_payment_methods(); } /** * Fires after checkout block data is registered. * * @since 2.6.0 */ do_action( 'woocommerce_blocks_checkout_enqueue_data' ); } /** * Get saved customer payment methods for use in checkout. */ protected function hydrate_customer_payment_methods() { $payment_methods = PaymentUtils::get_saved_payment_methods(); if ( ! $payment_methods || $this->asset_data_registry->exists( 'customerPaymentMethods' ) ) { return; } $this->asset_data_registry->add( 'customerPaymentMethods', is_array( $payment_methods ) ? $payment_methods['enabled'] : null ); } /** * Register script and style assets for the block type before it is registered. * * This registers the scripts; it does not enqueue them. */ protected function register_block_type_assets() { parent::register_block_type_assets(); $chunks = $this->get_chunks_paths( $this->chunks_folder ); $vendor_chunks = $this->get_chunks_paths( 'vendors--checkout-blocks' ); $shared_chunks = [ 'cart-blocks/cart-express-payment--checkout-blocks/express-payment-frontend' ]; $this->register_chunk_translations( array_merge( $chunks, $vendor_chunks, $shared_chunks ) ); } /** * Get list of Checkout block & its inner-block types. * * @return array; */ public static function get_checkout_block_types() { return [ 'Checkout', 'CheckoutActionsBlock', 'CheckoutAdditionalInformationBlock', 'CheckoutBillingAddressBlock', 'CheckoutContactInformationBlock', 'CheckoutExpressPaymentBlock', 'CheckoutFieldsBlock', 'CheckoutOrderNoteBlock', 'CheckoutOrderSummaryBlock', 'CheckoutOrderSummaryCartItemsBlock', 'CheckoutOrderSummaryCouponFormBlock', 'CheckoutOrderSummaryDiscountBlock', 'CheckoutOrderSummaryFeeBlock', 'CheckoutOrderSummaryShippingBlock', 'CheckoutOrderSummarySubtotalBlock', 'CheckoutOrderSummaryTaxesBlock', 'CheckoutOrderSummaryTotalsBlock', 'CheckoutPaymentBlock', 'CheckoutShippingAddressBlock', 'CheckoutShippingMethodsBlock', 'CheckoutShippingMethodBlock', 'CheckoutPickupOptionsBlock', 'CheckoutTermsBlock', 'CheckoutTotalsBlock', ]; } }