feed_base ) && '' !== trim( $wp_rewrite->feed_base, '/' ) ) ? trim( $wp_rewrite->feed_base, '/' ) : 'feed'; foreach ( array( $feed_base, 'embed' ) as $endpoint ) { $patterns[] = '*/' . $endpoint . '/'; $patterns[] = $home . $endpoint; $patterns[] = $home . '*/' . $endpoint; } $patterns[] = '?feed='; $patterns[] = '&feed='; $patterns[] = 'embed=true'; // Sitemaps, robots, favicon, well-known URIs and Cloudflare helper paths. $patterns[] = $home . 'wp-sitemap*'; $patterns[] = '?sitemap='; $patterns[] = '&sitemap='; $patterns[] = 'sitemap-stylesheet='; $patterns[] = $home . 'robots.txt'; $patterns[] = $home . 'favicon.ico'; $patterns[] = $home . '.well-known/'; $patterns[] = '/cdn-cgi/'; // Query arguments: nonces, actions, previews, Customizer, comment moderation, cron. $patterns[] = 'nonce='; $patterns[] = '?action='; $patterns[] = '&action='; $patterns[] = '?preview='; $patterns[] = '&preview='; $patterns[] = 'preview_id='; $patterns[] = 'customize_changeset_uuid='; $patterns[] = 'customize_theme='; $patterns[] = 'customize_messenger_channel='; $patterns[] = 'customize_autosaved='; $patterns[] = 'replytocom='; $patterns[] = 'unapproved='; $patterns[] = 'moderation-hash='; $patterns[] = 'doing_wp_cron'; self::$cache['system_patterns'] = self::unique( $patterns ); return self::$cache['system_patterns']; } /** * Link selectors that must always load normally. * * @param array|null $settings Settings (integration modes decide the multilingual/BuddyPress selectors). * Null reads the effective plugin settings. * @return string[] */ public static function system_selectors( ?array $settings = null ): array { if ( null === $settings ) { $settings = self::current_settings(); } $selectors = array( // Opt-out markers (also checked by the engine; listed so integrators see the full set). '.no-ajaxy', '.no-ajax', '[data-no-ajax]', '[data-ajax-loading="false"]', // Admin bar. '#wpadminbar a', // Comment reply links (handled by comment-reply.js or ?replytocom= reloads). '.comment-reply-link', // Elementor action links, lightbox and Pro page-transition opt-out. 'a[href^="#elementor-action"]', 'a[href^="%23elementor-action"]', '[data-elementor-open-lightbox="yes"]', '[data-e-disable-page-transition]', // Common lightbox/modal scripts that open the link target themselves. 'a.thickbox', '[data-fancybox]', 'a.fancybox', '[data-lightbox]', 'a[rel^="lightbox"]', 'a[data-rel^="lightbox"]', 'a[rel^="prettyPhoto"]', 'a[data-rel^="prettyPhoto"]', 'a.glightbox', 'a.foobox', // Interactivity API router regions and directive-driven links. '[data-wp-router-region] a', 'a[data-wp-on--click]', 'a[data-wp-on-async--click]', ); if ( 'off' !== self::integration_mode( $settings, 'multilingual' ) ) { $selectors = array_merge( $selectors, array( '.wpml-ls a', '.wpml-ls-item a', '.lang-item a', '.pll-parent-menu-item a', '.trp-language-switcher a', '.menu-item-object-language_switcher a', 'a[hreflang]', 'a[lang]', ) ); } if ( 'exclude' === self::integration_mode( $settings, 'buddypress' ) ) { $selectors[] = '#buddypress a'; } return self::unique( $selectors ); } /** * Whether WooCommerce is loaded. * * @return bool */ public static function woocommerce_active(): bool { return class_exists( 'WooCommerce', false ) && function_exists( 'WC' ); } /** * Cart, checkout and account URLs plus endpoint slugs read from WooCommerce at runtime. * * URLs are '' when the page is not configured. Never returns the home URL as a page URL. * * @return array{cart:string,checkout:string,account:string,endpoints:string[]} */ public static function woocommerce_urls(): array { if ( isset( self::$cache['wc_urls'] ) ) { return self::$cache['wc_urls']; } $urls = array( 'cart' => '', 'checkout' => '', 'account' => '', 'endpoints' => array(), ); if ( self::woocommerce_active() ) { try { if ( function_exists( 'wc_get_page_id' ) ) { if ( wc_get_page_id( 'cart' ) > 0 && function_exists( 'wc_get_cart_url' ) ) { $urls['cart'] = self::page_url_or_empty( wc_get_cart_url() ); } if ( wc_get_page_id( 'checkout' ) > 0 && function_exists( 'wc_get_checkout_url' ) ) { $urls['checkout'] = self::page_url_or_empty( wc_get_checkout_url() ); } if ( wc_get_page_id( 'myaccount' ) > 0 && function_exists( 'wc_get_page_permalink' ) ) { $urls['account'] = self::page_url_or_empty( wc_get_page_permalink( 'myaccount', '' ) ); } } $wc = WC(); if ( is_object( $wc ) && isset( $wc->query ) && is_object( $wc->query ) && method_exists( $wc->query, 'get_query_vars' ) ) { $vars = $wc->query->get_query_vars(); if ( is_array( $vars ) ) { $endpoints = array(); foreach ( $vars as $slug ) { if ( is_scalar( $slug ) && '' !== trim( (string) $slug ) ) { $endpoints[] = trim( (string) $slug ); } } $urls['endpoints'] = self::unique( $endpoints ); } } } catch ( Throwable $e ) { // WooCommerce not fully initialised: keep what was collected. unset( $e ); } } self::$cache['wc_urls'] = $urls; return $urls; } /** * WooCommerce pages and GET actions that must load normally (woo-forms-i18n research §1 / (a)). * * Empty when WooCommerce is not active. * * @return string[] */ public static function woocommerce_patterns(): array { if ( ! self::woocommerce_active() ) { return array(); } if ( isset( self::$cache['wc_patterns'] ) ) { return self::$cache['wc_patterns']; } $home = self::home_path(); $patterns = array(); $urls = self::woocommerce_urls(); // Cart, checkout and My Account pages including every endpoint below them. foreach ( array( 'cart', 'checkout', 'account' ) as $page ) { $patterns = array_merge( $patterns, self::url_to_patterns( $urls[ $page ], true ) ); } // Legacy API and AJAX endpoints. $patterns[] = $home . 'wc-api/'; $patterns[] = $home . 'index.php/wc-api/'; $patterns[] = 'wc-api='; $patterns[] = 'wc-ajax='; // GET arguments that trigger handlers. $args = array( 'add-to-cart=', 'added-to-cart=', 'remove_item=', 'undo_item=', 'removed_item=', 'remove_coupon=', 'apply_coupon=', 'order_again=', 'cancel_order=', 'pay_for_order=', 'download_file=', 'copy-address=', 'wc-resend-set-password', 'reset-token=', 'show-reset-form=', 'add_to_wishlist=', 'remove_from_wishlist=', '?key=', '&key=', ); self::$cache['wc_patterns'] = self::unique( array_merge( $patterns, $args ) ); return self::$cache['wc_patterns']; } /** * WooCommerce action links that must not be intercepted. * * Empty when WooCommerce is not active. * * @return string[] */ public static function woocommerce_selectors(): array { if ( ! self::woocommerce_active() ) { return array(); } return array( 'a.ajax_add_to_cart', 'a.add_to_cart_button[href*="add-to-cart="]', '.remove_from_cart_button', '.woocommerce-cart-form a.remove', '.restore-item', '.woocommerce-remove-coupon', '.woocommerce-MyAccount-navigation-link--customer-logout a', ); } /** * Login, registration and account URLs of WordPress core and known membership plugins. * * @return string[] */ public static function membership_patterns(): array { if ( isset( self::$cache['membership_patterns'] ) ) { return self::$cache['membership_patterns']; } $home = self::home_path(); $patterns = array(); // Core login, registration and lost password URLs (custom ones come from login/register URL filters). foreach ( self::public_auth_urls() as $url ) { $patterns = array_merge( $patterns, self::url_to_patterns( $url, true, false ) ); } // Conventional front-end auth slugs (Theme My Login and most custom login pages). foreach ( array( 'login', 'logout', 'register', 'lostpassword', 'resetpass', 'password-reset' ) as $slug ) { $patterns[] = $home . $slug . '/'; } // Paid Memberships Pro, Ultimate Member and MemberPress pages configured by page ID. foreach ( self::membership_page_ids() as $page_id ) { $link = get_permalink( $page_id ); if ( is_string( $link ) ) { $patterns = array_merge( $patterns, self::url_to_patterns( $link, true ) ); } } if ( defined( 'PMPRO_VERSION' ) ) { $patterns[] = 'pmpro_level='; } if ( defined( 'UM_VERSION' ) || function_exists( 'UM' ) ) { $patterns[] = 'um_action='; $patterns[] = '?act='; $patterns[] = '&act='; $patterns[] = '?hash='; $patterns[] = '&hash='; } // MemberPress membership (registration) and group (pricing) pages. $patterns = array_merge( $patterns, self::post_type_patterns( array( 'memberpressproduct', 'memberpressgroup' ) ) ); // BuddyPress register and activate directory pages. try { if ( function_exists( 'bp_get_signup_page' ) ) { $patterns = array_merge( $patterns, self::url_to_patterns( (string) bp_get_signup_page(), true ) ); } if ( function_exists( 'bp_get_activation_page' ) ) { $patterns = array_merge( $patterns, self::url_to_patterns( (string) bp_get_activation_page(), true ) ); } } catch ( Throwable $e ) { unset( $e ); } self::$cache['membership_patterns'] = self::unique( $patterns ); return self::$cache['membership_patterns']; } /** * Core login, registration and lost-password URLs that may be published in page source. * * Login-hiding plugins replace wp-login.php with a secret slug; such custom URLs are only published to * logged-in users so the slug never appears in pages served to visitors (or stored in page caches). * * @return string[] */ public static function public_auth_urls(): array { $urls = array(); foreach ( array( wp_login_url(), wp_registration_url(), wp_lostpassword_url() ) as $url ) { if ( ! is_string( $url ) || '' === trim( $url ) ) { continue; } if ( false !== stripos( $url, 'wp-login.php' ) || is_user_logged_in() ) { $urls[] = $url; } } return $urls; } /** * URL of the core login screen that is safe to publish: wp_login_url() when it is the core screen or the * visitor is logged in, otherwise the unfiltered core path (never a login-hiding plugin's secret slug). * * @return string */ public static function public_login_url(): string { $url = wp_login_url(); if ( is_string( $url ) && '' !== $url && ( false !== stripos( $url, 'wp-login.php' ) || is_user_logged_in() ) ) { return $url; } // Built from the raw option: site_url() is filtered by login-hiding plugins. return set_url_scheme( trailingslashit( (string) get_option( 'siteurl' ) ) . 'wp-login.php', 'login' ); } /** * Login/logout link selectors (core block and generic logout links). * * @return string[] */ public static function membership_selectors(): array { return array( '.wp-block-loginout a', 'a[href*="action=logout"]', '.bp-login-widget a', ); } /** * URL patterns for post types: archive link paths, rewrite slug prefixes and query variables. * * 'post' and 'page' are skipped (their permalinks have no static prefix); they are excluded * server side through is_bypass_request() instead. * * @param array $post_types Post type names. * @return string[] */ public static function post_type_patterns( array $post_types ): array { global $wp_rewrite; $patterns = array(); $permalinks = ( $wp_rewrite instanceof WP_Rewrite ) && $wp_rewrite->using_permalinks(); $home = self::home_path(); foreach ( self::valid_post_types( $post_types ) as $post_type ) { if ( in_array( $post_type, array( 'post', 'page' ), true ) ) { continue; } $object = get_post_type_object( $post_type ); if ( ! $object ) { continue; } // Archive page (has_archive) – the archive and everything below it. $archive = get_post_type_archive_link( $post_type ); if ( is_string( $archive ) && '' !== $archive ) { $patterns = array_merge( $patterns, self::url_to_patterns( $archive, true ) ); } // Static rewrite slug prefix, e.g. /products/ or /blog/products/ (with_front). if ( $permalinks && is_array( $object->rewrite ) && ! empty( $object->rewrite['slug'] ) ) { $slug = trim( (string) $object->rewrite['slug'], '/' ); $tag_pos = strpos( $slug, '%' ); $static = trim( false === $tag_pos ? $slug : substr( $slug, 0, $tag_pos ), '/' ); if ( '' !== $static ) { $base = ! empty( $object->rewrite['with_front'] ) ? (string) $wp_rewrite->front : (string) $wp_rewrite->root; $base = trim( $base, '/' ); $path = $home . ( '' !== $base ? $base . '/' : '' ) . $static . '/'; if ( ! self::is_home_path( $path ) ) { $patterns[] = $path; } } } // Query variable URLs (?product=slug) work with every permalink setting. if ( is_string( $object->query_var ) && '' !== $object->query_var ) { $patterns[] = '?' . $object->query_var . '='; $patterns[] = '&' . $object->query_var . '='; } } return self::unique( $patterns ); } /** * Builds the `exclusions` block of the frontend config. * * Besides the three pattern lists it carries the "Protect WooCommerce pages" and "Protect login and * membership pages" switches: the engine reads them to decide whether destinations with WooCommerce * cart/checkout/account or membership body classes must load normally. * * @param array $settings Effective settings. * @return array{urls:string[],selectors:string[],prefetch:string[],woocommerce:bool,membership:bool} */ public static function for_config( array $settings ): array { $protect_woo = self::flag( $settings, 'exclude_woocommerce', true ); $protect_membership = self::flag( $settings, 'exclude_membership', true ); $urls = self::system_patterns(); if ( $protect_woo ) { $urls = array_merge( $urls, self::woocommerce_patterns() ); } if ( $protect_membership ) { $urls = array_merge( $urls, self::membership_patterns() ); } $post_types = isset( $settings['exclude_post_types'] ) && is_array( $settings['exclude_post_types'] ) ? $settings['exclude_post_types'] : array(); $urls = array_merge( $urls, self::post_type_patterns( $post_types ), self::lines( $settings['exclude_urls'] ?? '' ) ); $selectors = self::system_selectors( $settings ); if ( $protect_woo ) { $selectors = array_merge( $selectors, self::woocommerce_selectors() ); } if ( $protect_membership ) { $selectors = array_merge( $selectors, self::membership_selectors() ); } $selectors = array_merge( $selectors, self::lines( $settings['exclude_selectors'] ?? '' ) ); /** * Filters the link selectors that are never loaded through AJAX navigation. * * @since 2.0.0 * * @param string[] $selectors CSS selectors matched with link.closest(). * @param array $settings Effective settings. */ $filtered_selectors = apply_filters( 'ajax_loading_exclude_selectors', self::unique( $selectors ), $settings ); if ( is_array( $filtered_selectors ) ) { $selectors = $filtered_selectors; } $prefetch = self::lines( $settings['prefetch_exclude'] ?? '' ); if ( '' !== (string) get_option( 'permalink_structure' ) ) { // Like core speculative loading: never prefetch URLs with a query string on pretty-permalink sites. $prefetch[] = '*?*'; } $exclusions = array( 'urls' => self::unique( $urls ), 'selectors' => self::unique( $selectors ), 'prefetch' => self::unique( $prefetch ), 'woocommerce' => $protect_woo, 'membership' => $protect_membership, ); /** * Filters all exclusion lists passed to the frontend engine. * * The two booleans tell the engine whether destinations whose body carries WooCommerce * cart/checkout/account/order classes (`woocommerce`) or membership checkout/billing/cancel * classes (`membership`) must load normally. * * @since 2.0.0 * * @param array $exclusions { * @type string[] $urls URL patterns that load normally. * @type string[] $selectors Link selectors that load normally. * @type string[] $prefetch URL patterns that are never prefetched. * @type bool $woocommerce Protect WooCommerce pages (exclude_woocommerce). * @type bool $membership Protect login and membership pages (exclude_membership). * } * @param array $settings Effective settings. */ $filtered = apply_filters( 'ajax_loading_exclusions', $exclusions, $settings ); if ( is_array( $filtered ) ) { foreach ( array( 'urls', 'selectors', 'prefetch' ) as $key ) { if ( isset( $filtered[ $key ] ) && is_array( $filtered[ $key ] ) ) { $exclusions[ $key ] = self::unique( $filtered[ $key ] ); } } foreach ( array( 'woocommerce', 'membership' ) as $key ) { if ( isset( $filtered[ $key ] ) && is_bool( $filtered[ $key ] ) ) { $exclusions[ $key ] = $filtered[ $key ]; } } } return $exclusions; } /** * Whether the current front-end request must load normally (the engine prints a "bypass" marker * and loads no assets). Requires the main query (call after the 'wp' action). * * Any unexpected error results in a bypass (normal navigation is always safe). * * @param array $settings Effective settings. * @return bool */ public static function is_bypass_request( array $settings ): bool { try { if ( is_preview() ) { return true; } if ( is_singular() && post_password_required() ) { return true; } $post_types = isset( $settings['exclude_post_types'] ) && is_array( $settings['exclude_post_types'] ) ? self::valid_post_types( $settings['exclude_post_types'] ) : array(); if ( ! empty( $post_types ) && ( is_singular( $post_types ) || is_post_type_archive( $post_types ) ) ) { return true; } if ( self::flag( $settings, 'exclude_woocommerce', true ) && self::woocommerce_active() && self::is_woocommerce_request() ) { return true; } if ( self::flag( $settings, 'exclude_membership', true ) && self::is_membership_request() ) { return true; } if ( 'exclude' === self::integration_mode( $settings, 'buddypress' ) && function_exists( 'is_buddypress' ) && is_buddypress() ) { return true; } } catch ( Throwable $e ) { return true; } return false; } /** * Human-readable, translated list of the built-in safety rules (read-only list on the Exclusions tab). * * @param array|null $settings Settings; null reads the effective plugin settings. * @return string[] */ public static function readonly_safety_rules( ?array $settings = null ): array { if ( null === $settings ) { $settings = self::current_settings(); } $rules = array( __( 'WordPress admin, login, registration and system files (wp-admin, wp-login.php, wp-signup.php, wp-activate.php, wp-comments-post.php, xmlrpc.php, wp-cron.php).', 'ajax-loading' ), __( 'REST API, feeds, sitemaps, post embeds, robots.txt, favicon.ico and /.well-known/ URLs.', 'ajax-loading' ), __( 'Files in wp-content, wp-includes and the uploads folder, and links to non-HTML files (PDF, images, media, archives, documents).', 'ajax-loading' ), __( 'Previews, Customizer links and password-protected posts.', 'ajax-loading' ), __( 'Links with a nonce or an action parameter (_wpnonce, ?action=), comment reply and moderation links, cron links.', 'ajax-loading' ), __( 'Links to other sites, links outside the WordPress home URL, same-page #anchors, links with target, download or rel="external".', 'ajax-loading' ), __( 'Links inside .no-ajax, .no-ajaxy, [data-no-ajax] or [data-ajax-loading="false"], and links in the admin bar.', 'ajax-loading' ), __( 'Lightbox links, Elementor popup and lightbox actions, and Interactivity API router links.', 'ajax-loading' ), ); if ( 'off' !== self::integration_mode( $settings, 'multilingual' ) ) { $rules[] = __( 'Language switcher links (WPML, Polylang, TranslatePress, any link with hreflang or lang). A page in another language always loads normally.', 'ajax-loading' ); } if ( self::flag( $settings, 'exclude_woocommerce', true ) && self::woocommerce_active() ) { $urls = self::woocommerce_urls(); $paths = array(); foreach ( array( 'cart', 'checkout', 'account' ) as $page ) { if ( '' !== $urls[ $page ] ) { $paths[] = self::display_path( $urls[ $page ] ); } } $rules[] = empty( $paths ) ? __( 'WooCommerce cart, checkout and My Account pages including their endpoints.', 'ajax-loading' ) : sprintf( /* translators: %s: comma separated list of page paths, e.g. "/cart/, /checkout/". */ __( 'WooCommerce cart, checkout and My Account pages including their endpoints (%s).', 'ajax-loading' ), implode( ', ', $paths ) ); $rules[] = __( 'WooCommerce actions: add to cart, remove from cart, coupons, order again, cancel order, downloads, logout, password reset and the wc-api / wc-ajax endpoints.', 'ajax-loading' ); } if ( self::flag( $settings, 'exclude_membership', true ) ) { $rules[] = __( 'Login, logout, registration and password reset pages (including /login/, /logout/, /register/ and custom URLs set by plugins).', 'ajax-loading' ); $detected = array(); if ( defined( 'PMPRO_VERSION' ) ) { $detected[] = 'Paid Memberships Pro'; } if ( defined( 'UM_VERSION' ) || function_exists( 'UM' ) ) { $detected[] = 'Ultimate Member'; } if ( class_exists( 'MeprOptions', false ) || defined( 'MEPR_VERSION' ) ) { $detected[] = 'MemberPress'; } if ( function_exists( 'buddypress' ) ) { $detected[] = 'BuddyPress'; } if ( ! empty( $detected ) ) { $rules[] = sprintf( /* translators: %s: comma separated list of plugin names. */ __( 'Account, checkout, login and registration pages of %s.', 'ajax-loading' ), implode( ', ', $detected ) ); } } if ( 'exclude' === self::integration_mode( $settings, 'buddypress' ) && function_exists( 'buddypress' ) ) { $rules[] = __( 'BuddyPress pages and links inside BuddyPress content.', 'ajax-loading' ); } $post_types = isset( $settings['exclude_post_types'] ) && is_array( $settings['exclude_post_types'] ) ? self::valid_post_types( $settings['exclude_post_types'] ) : array(); if ( ! empty( $post_types ) ) { $labels = array(); foreach ( $post_types as $post_type ) { $object = get_post_type_object( $post_type ); $labels[] = ( $object && isset( $object->labels->name ) ) ? (string) $object->labels->name : $post_type; } $rules[] = sprintf( /* translators: %s: comma separated list of post type names. */ __( 'Excluded content types: %s.', 'ajax-loading' ), implode( ', ', $labels ) ); } $rules[] = __( 'Pages containing interactive blocks or forms that cannot be re-initialised (per the integration settings) and any page that fails to load are opened with a normal page load.', 'ajax-loading' ); return $rules; } /** * Page IDs of membership plugin core pages (PMPro, Ultimate Member, MemberPress). * * @return int[] */ public static function membership_page_ids(): array { if ( isset( self::$cache['membership_ids'] ) ) { return self::$cache['membership_ids']; } $ids = array(); // Paid Memberships Pro. if ( defined( 'PMPRO_VERSION' ) ) { global $pmpro_pages; if ( is_array( $pmpro_pages ) && ! empty( $pmpro_pages ) ) { foreach ( $pmpro_pages as $page_id ) { $ids[] = is_scalar( $page_id ) ? absint( $page_id ) : 0; } } else { foreach ( array( 'account', 'billing', 'cancel', 'checkout', 'confirmation', 'invoice', 'levels', 'login', 'member_profile_edit' ) as $page ) { $ids[] = absint( get_option( 'pmpro_' . $page . '_page_id' ) ); } } } // Ultimate Member core pages. if ( defined( 'UM_VERSION' ) || function_exists( 'UM' ) ) { $um_options = get_option( 'um_options', array() ); foreach ( array( 'user', 'login', 'register', 'members', 'logout', 'account', 'password-reset' ) as $slug ) { $page_id = 0; try { if ( function_exists( 'um_get_predefined_page_id' ) ) { $page_id = absint( um_get_predefined_page_id( $slug ) ); } } catch ( Throwable $e ) { $page_id = 0; } if ( ! $page_id && is_array( $um_options ) && isset( $um_options[ 'core_' . $slug ] ) && is_scalar( $um_options[ 'core_' . $slug ] ) ) { $page_id = absint( $um_options[ 'core_' . $slug ] ); } $ids[] = $page_id; } } // MemberPress account, login and thank-you pages. if ( class_exists( 'MeprOptions', false ) ) { try { $mepr = method_exists( 'MeprOptions', 'fetch' ) ? MeprOptions::fetch() : null; foreach ( array( 'account_page_id', 'login_page_id', 'thankyou_page_id' ) as $prop ) { if ( is_object( $mepr ) && isset( $mepr->$prop ) && is_scalar( $mepr->$prop ) ) { $ids[] = absint( $mepr->$prop ); } } } catch ( Throwable $e ) { unset( $e ); } } $ids = array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) ); self::$cache['membership_ids'] = $ids; return $ids; } /** * Whether the current request is a WooCommerce cart/checkout/account/endpoint page or contains the * block or shortcode cart/checkout. * * @return bool */ private static function is_woocommerce_request(): bool { if ( function_exists( 'is_cart' ) && is_cart() ) { return true; } if ( function_exists( 'is_checkout' ) && is_checkout() ) { return true; } if ( function_exists( 'is_account_page' ) && is_account_page() ) { return true; } if ( function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url() ) { return true; } if ( is_singular() ) { $post = get_queried_object(); if ( $post instanceof WP_Post ) { foreach ( array( 'woocommerce/cart', 'woocommerce/checkout' ) as $block ) { if ( has_block( $block, $post ) ) { return true; } } foreach ( array( 'woocommerce_cart', 'woocommerce_checkout', 'woocommerce_my_account' ) as $shortcode ) { if ( has_shortcode( (string) $post->post_content, $shortcode ) ) { return true; } } } } return false; } /** * Whether the current request is a membership/auth page. * * @return bool */ private static function is_membership_request(): bool { $ids = self::membership_page_ids(); if ( ! empty( $ids ) && is_page( $ids ) ) { return true; } $mepr_types = array_values( array_filter( array( 'memberpressproduct', 'memberpressgroup' ), 'post_type_exists' ) ); if ( ! empty( $mepr_types ) && is_singular( $mepr_types ) ) { return true; } if ( function_exists( 'bp_is_register_page' ) && bp_is_register_page() ) { return true; } if ( function_exists( 'bp_is_activation_page' ) && bp_is_activation_page() ) { return true; } return false; } /** * Reads an integration mode from settings, validated against the integration registry. * * @param array $settings Settings. * @param string $key Integration key. * @return string 'auto' | 'exclude' | 'off' */ private static function integration_mode( array $settings, string $key ): string { if ( class_exists( 'WPAJXL_Compat' ) ) { return WPAJXL_Compat::mode( $settings, $key ); } $fallback = array( 'buddypress' => 'exclude', 'multilingual' => 'auto', ); $modes = isset( $settings['integrations'] ) && is_array( $settings['integrations'] ) ? $settings['integrations'] : array(); $value = isset( $modes[ $key ] ) && is_string( $modes[ $key ] ) ? $modes[ $key ] : ''; return in_array( $value, array( 'auto', 'exclude', 'off' ), true ) ? $value : ( $fallback[ $key ] ?? 'auto' ); } /** * Effective plugin settings, or an empty array when the settings class is unavailable. * * @return array */ private static function current_settings(): array { if ( class_exists( 'WPAJXL_Settings' ) ) { try { $settings = WPAJXL_Settings::instance()->effective(); return is_array( $settings ) ? $settings : array(); } catch ( Throwable $e ) { return array(); } } return array(); } /** * Reads a boolean flag from settings, tolerating legacy string values. * * @param array $settings Settings. * @param string $key Key. * @param bool $fallback Value when missing. * @return bool */ private static function flag( array $settings, string $key, bool $fallback ): bool { if ( ! array_key_exists( $key, $settings ) ) { return $fallback; } $value = $settings[ $key ]; if ( is_bool( $value ) ) { return $value; } if ( is_string( $value ) ) { return in_array( strtolower( trim( $value ) ), array( '1', 'on', 'true', 'yes' ), true ); } return (bool) $value; } /** * Splits a `lines` setting (string or array) into trimmed, non-empty lines. * * @param mixed $value Setting value. * @return string[] */ private static function lines( $value ): array { if ( is_string( $value ) ) { $value = preg_split( '/\r\n|\r|\n/', $value ); } if ( ! is_array( $value ) ) { return array(); } $lines = array(); foreach ( $value as $line ) { if ( ! is_scalar( $line ) ) { continue; } $line = trim( (string) preg_replace( '/[\x00-\x1F\x7F]/', '', (string) $line ) ); if ( '' === $line ) { continue; } $lines[] = substr( $line, 0, self::MAX_LINE_LENGTH ); if ( count( $lines ) >= self::MAX_LINES ) { break; } } return $lines; } /** * Unique, trimmed, non-empty strings (list). * * @param array $values Values. * @return string[] */ private static function unique( array $values ): array { $out = array(); foreach ( $values as $value ) { if ( ! is_scalar( $value ) ) { continue; } $value = trim( (string) preg_replace( '/[\x00-\x1F\x7F]/', '', (string) $value ) ); if ( '' !== $value && ! isset( $out[ $value ] ) ) { $out[ $value ] = true; } } return array_map( 'strval', array_keys( $out ) ); } /** * Registered post type names from a list. * * @param array $post_types Names. * @return string[] */ private static function valid_post_types( array $post_types ): array { $valid = array(); foreach ( $post_types as $post_type ) { if ( is_string( $post_type ) && '' !== $post_type && post_type_exists( $post_type ) ) { $valid[] = $post_type; } } return array_values( array_unique( $valid ) ); } /** * Normalises a URL path to '/x/y/' form. * * @param string $path Path. * @return string */ private static function normalize_dir_path( string $path ): string { $path = '/' . trim( $path, '/' ); return '/' === $path ? '/' : $path . '/'; } /** * Whether a path is the site root or the home path (a pattern for it would exclude the whole site). * * @param string $path Path. * @return bool */ private static function is_home_path( string $path ): bool { $normalized = strtolower( self::normalize_dir_path( $path ) ); return '/' === $normalized || strtolower( self::home_path() ) === $normalized; } /** * Whether a host is the host of the home URL. * * @param string $host Host. * @return bool */ private static function is_home_host( string $host ): bool { $home_host = wp_parse_url( home_url( '/' ), PHP_URL_HOST ); return is_string( $home_host ) && strtolower( $home_host ) === strtolower( $host ); } /** * Returns the URL unless it is empty or points at the home page (WooCommerce falls back to the home URL * for missing pages). * * @param mixed $url URL. * @return string */ private static function page_url_or_empty( $url ): string { if ( ! is_string( $url ) || '' === trim( $url ) ) { return ''; } $url = trim( $url ); $parts = wp_parse_url( $url ); if ( ! is_array( $parts ) ) { return ''; } $path = isset( $parts['path'] ) ? (string) $parts['path'] : '/'; if ( empty( $parts['query'] ) && self::is_home_path( $path ) ) { return ''; } return $url; } /** * Path (and query) of a URL for display. * * @param string $url URL. * @return string */ private static function display_path( string $url ): string { $parts = wp_parse_url( $url ); if ( ! is_array( $parts ) ) { return $url; } $path = isset( $parts['path'] ) ? (string) $parts['path'] : '/'; return isset( $parts['query'] ) ? $path . '?' . $parts['query'] : $path; } }