'', 'plan' => '', 'trigger_display' => 'button', 'button_placement' => 'after-cart', 'show_size_finder' => true, 'button_align' => 'left', 'button_size' => 'md', 'size_finder_label' => '', 'embed_locale' => 'en', 'second_embed_locale' => '', 'asset_version' => '', ); } function entersize_normalize_trigger_display( $value ) { $value = is_string( $value ) ? strtolower( trim( $value ) ) : 'button'; return in_array( $value, array( 'text', 'button' ), true ) ? $value : 'button'; } function entersize_normalize_button_placement( $value ) { $value = is_string( $value ) ? strtolower( trim( $value ) ) : 'after-cart'; $allowed = array( 'before-size', 'after-size', 'before-cart', 'after-cart' ); return in_array( $value, $allowed, true ) ? $value : 'after-cart'; } function entersize_normalize_button_align( $value ) { $value = is_string( $value ) ? strtolower( trim( $value ) ) : 'left'; $allowed = array( 'left', 'center', 'right' ); return in_array( $value, $allowed, true ) ? $value : 'left'; } function entersize_normalize_button_size( $value ) { $value = is_string( $value ) ? strtolower( trim( $value ) ) : 'md'; return in_array( $value, array( 'sm', 'md', 'lg' ), true ) ? $value : 'md'; } function entersize_apply_button_style_status( &$options, $status ) { $changed = false; if ( isset( $status['button_size'] ) ) { $next_size = entersize_normalize_button_size( $status['button_size'] ); if ( ! isset( $options['button_size'] ) || $options['button_size'] !== $next_size ) { $options['button_size'] = $next_size; $changed = true; } } $next_finder_label = isset( $status['size_finder_label'] ) ? sanitize_text_field( (string) $status['size_finder_label'] ) : ''; if ( ! isset( $options['size_finder_label'] ) || $options['size_finder_label'] !== $next_finder_label ) { $options['size_finder_label'] = $next_finder_label; $changed = true; } return $changed; } function entersize_normalize_show_flag( $value ) { if ( false === $value || 0 === $value || '0' === $value || 'false' === $value ) { return false; } return true; } function entersize_normalize_show_size_finder( $value ) { return entersize_normalize_show_flag( $value ); } function entersize_placement_hook( $placement, $product = null ) { $placement = entersize_normalize_button_placement( $placement ); $is_variable = $product && is_a( $product, 'WC_Product' ) && $product->is_type( 'variable' ); switch ( $placement ) { case 'before-size': return $is_variable ? 'woocommerce_before_variations_form' : 'woocommerce_before_add_to_cart_form'; case 'after-size': return $is_variable ? 'woocommerce_after_variations_form' : 'woocommerce_before_add_to_cart_button'; case 'before-cart': return 'woocommerce_before_add_to_cart_button'; case 'after-cart': default: return 'woocommerce_after_add_to_cart_button'; } } function entersize_supported_locales() { return array( 'en', 'pl', 'de', 'es' ); } function entersize_match_embed_locale( $value ) { $value = is_string( $value ) ? strtolower( trim( $value ) ) : ''; if ( '' === $value ) { return ''; } $value = preg_split( '/[-_]/', $value )[0]; return in_array( $value, entersize_supported_locales(), true ) ? $value : ''; } function entersize_normalize_embed_locale( $value ) { $matched = entersize_match_embed_locale( $value ); return '' !== $matched ? $matched : 'en'; } function entersize_normalize_embed_second_locale( $value, $primary_locale = 'en' ) { if ( ! is_string( $value ) || '' === trim( $value ) ) { return ''; } if ( 'auto' === strtolower( trim( $value ) ) ) { return 'auto'; } $normalized = entersize_normalize_embed_locale( $value ); $primary = entersize_normalize_embed_locale( $primary_locale ); return $normalized === $primary ? '' : $normalized; } function entersize_read_html_locale() { $lang = ''; if ( function_exists( 'get_language_attributes' ) ) { $attrs = get_language_attributes( 'html' ); if ( is_string( $attrs ) && preg_match( '/lang=["\']([^"\']+)/', $attrs, $matches ) ) { $lang = $matches[1]; } } if ( '' === $lang && function_exists( 'determine_locale' ) ) { $lang = determine_locale(); } if ( '' === $lang ) { $lang = get_bloginfo( 'language' ); } if ( ! is_string( $lang ) || '' === trim( $lang ) ) { return ''; } return entersize_match_embed_locale( $lang ); } function entersize_resolve_embed_locale( $options ) { $primary = entersize_normalize_embed_locale( $options['embed_locale'] ?? 'en' ); $second_raw = is_string( $options['second_embed_locale'] ?? null ) ? strtolower( trim( $options['second_embed_locale'] ) ) : ''; if ( 'auto' === $second_raw ) { $html_locale = entersize_read_html_locale(); return '' !== $html_locale ? $html_locale : $primary; } $second = entersize_normalize_embed_second_locale( $second_raw, $primary ); if ( '' !== $second ) { $html_locale = entersize_read_html_locale(); if ( '' !== $html_locale && $html_locale === $second ) { return $second; } } return $primary; } function entersize_default_size_finder_label( $locale = 'en' ) { $labels = array( 'en' => 'Find your size', 'pl' => 'Znajdź swój rozmiar', 'de' => 'Finde deine Größe', 'es' => 'Encuentra tu talla', ); $locale = entersize_normalize_embed_locale( $locale ); return $labels[ $locale ] ?? $labels['en']; } function entersize_resolve_size_finder_label( $options ) { $custom = isset( $options['size_finder_label'] ) ? trim( (string) $options['size_finder_label'] ) : ''; if ( '' !== $custom ) { return $custom; } $locale = entersize_resolve_embed_locale( $options ); return entersize_default_size_finder_label( $locale ); } function entersize_backend_url() { $url = apply_filters( 'entersize_backend_url', ENTERSIZE_DEFAULT_BACKEND_URL ); if ( ! is_string( $url ) || '' === trim( $url ) ) { return ''; } return untrailingslashit( esc_url_raw( trim( $url ) ) ); } function entersize_get_options() { $options = get_option( ENTERSIZE_OPTION, array() ); return wp_parse_args( is_array( $options ) ? $options : array(), entersize_default_options() ); } function entersize_privacy_policy_content() { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } $content = '
' . esc_html__( 'This store uses Entersize to show size charts and size recommendations on product pages.', 'entersize' ) . '
' . '' . esc_html__( 'When a product page loads, the product name, SKU, brands, categories and tags are sent to Entersize to find the right size chart.', 'entersize' ) . '
' . '' . esc_html__( 'When you use the Size Finder, the details you enter (height, weight, age, gender, and optionally body shape and bra size), your fit preference, and your IP address are sent to Entersize to calculate a recommendation and to prevent abuse. Entersize does not receive your name, email or order details.', 'entersize' ) . '
' . '' . sprintf( /* translators: %s: Entersize privacy policy URL. */ wp_kses( __( 'See the Entersize privacy policy for how this data is handled.', 'entersize' ), array( 'a' => array( 'href' => array() ) ) ), esc_url( 'https://entersize.com/privacy' ) ) . '
'; wp_add_privacy_policy_content( 'Entersize', wp_kses_post( $content ) ); } add_action( 'admin_init', 'entersize_privacy_policy_content' ); function entersize_admin_menu() { add_menu_page( __( 'Entersize', 'entersize' ), __( 'Entersize', 'entersize' ), entersize_manage_capability(), ENTERSIZE_ADMIN_PAGE, 'entersize_render_settings_page', entersize_menu_icon(), 81 ); } /** * A base64 SVG data URI lets WordPress recolor the icon to match the admin * color scheme, like a dashicon. */ function entersize_menu_icon() { $path = plugin_dir_path( __FILE__ ) . 'assets/images/entersize-icon.svg'; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local plugin asset. $svg = is_readable( $path ) ? file_get_contents( $path ) : false; if ( ! $svg ) { return 'dashicons-editor-table'; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for admin menu SVG icons. return 'data:image/svg+xml;base64,' . base64_encode( $svg ); } add_action( 'admin_menu', 'entersize_admin_menu' ); function entersize_manage_capability() { return class_exists( 'WooCommerce' ) ? 'manage_woocommerce' : 'manage_options'; } function entersize_admin_page_url() { return admin_url( 'admin.php?page=' . ENTERSIZE_ADMIN_PAGE ); } function entersize_is_settings_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin page check. return isset( $_GET['page'] ) && ENTERSIZE_ADMIN_PAGE === sanitize_key( wp_unslash( $_GET['page'] ) ); } function entersize_connection_transient_key( $state ) { return 'entersize_conn_' . $state; } /** * The state travels through the browser; the verifier never leaves this * server except in the server-to-server exchange. Entersize only learns its * sha256 (the challenge), so a leaked return URL can't be swapped for the token. */ function entersize_create_connection_state() { $state = wp_generate_password( 32, false, false ); set_transient( entersize_connection_transient_key( $state ), array( 'user_id' => get_current_user_id(), 'verifier' => wp_generate_password( 64, false, false ), ), ENTERSIZE_CONNECTION_TTL ); return $state; } function entersize_get_connection_state( $state ) { if ( ! is_string( $state ) || ! preg_match( '/^[A-Za-z0-9]{32}$/', $state ) ) { return null; } $stored = get_transient( entersize_connection_transient_key( $state ) ); if ( ! is_array( $stored ) || empty( $stored['verifier'] ) || ! isset( $stored['user_id'] ) ) { return null; } return $stored; } /** * Returns the verifier when the state belongs to the current user, and * deletes it so it can only be used once. Empty string otherwise. */ function entersize_consume_connection_state( $state ) { $stored = entersize_get_connection_state( $state ); if ( ! $stored || get_current_user_id() !== (int) $stored['user_id'] ) { return ''; } delete_transient( entersize_connection_transient_key( $state ) ); return (string) $stored['verifier']; } function entersize_plugin_return_url() { return entersize_admin_page_url(); } function entersize_app_connect_url( $state ) { $backend_url = entersize_backend_url(); if ( empty( $backend_url ) || empty( $state ) ) { return ''; } return add_query_arg( array( 'platform' => 'woo', 'storeUrl' => home_url( '/' ), 'pluginReturnUrl' => entersize_plugin_return_url(), 'pluginConnectionState' => $state, ), trailingslashit( $backend_url ) . 'dashboard/integrations' ); } /** * Plain link to the app's integrations section. Disconnecting happens there; * the plugin only links out to it (no local state change on click). */ function entersize_app_integrations_url() { $backend_url = entersize_backend_url(); if ( empty( $backend_url ) ) { return ''; } return trailingslashit( $backend_url ) . 'dashboard/integrations'; } function entersize_app_dashboard_url() { $backend_url = entersize_backend_url(); if ( empty( $backend_url ) ) { return ''; } return trailingslashit( $backend_url ) . 'dashboard'; } function entersize_woo_icon_url() { return plugins_url( 'assets/images/woo.png', __FILE__ ); } function entersize_brand_icon_url() { return plugins_url( 'assets/images/entersize.png', __FILE__ ); } /** * Local button target. The button never links straight to the app; it links * back to this settings page with a nonce so a fresh connection state can be * minted at click time (see entersize_maybe_start_connection). This * keeps the state from going stale while the settings page sits open. */ function entersize_connect_action_url() { if ( empty( entersize_backend_url() ) ) { return ''; } return add_query_arg( array( 'entersize_connect' => '1', '_wpnonce' => wp_create_nonce( 'entersize_connect' ), ), entersize_admin_page_url() ); } function entersize_embed_status( $options ) { $backend_url = entersize_backend_url(); if ( empty( $backend_url ) || empty( $options['embed_token'] ) ) { return array( 'checked' => false, 'connected' => false, ); } $response = wp_remote_get( trailingslashit( $backend_url ) . 'api/embed/woo/status', array( 'timeout' => 5, 'headers' => array( 'Accept' => 'application/json', 'Authorization' => 'Bearer ' . $options['embed_token'], ), ) ); if ( is_wp_error( $response ) ) { return array( 'checked' => false, 'connected' => true, ); } $status = wp_remote_retrieve_response_code( $response ); if ( 200 === $status ) { $parsed = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $parsed ) || empty( $parsed['connected'] ) ) { return array( 'checked' => false, 'connected' => true, ); } return array( 'checked' => true, 'connected' => true, 'plan' => isset( $parsed['plan'] ) ? sanitize_text_field( $parsed['plan'] ) : '', 'status' => isset( $parsed['status'] ) ? sanitize_text_field( $parsed['status'] ) : '', 'trigger_display' => isset( $parsed['triggerDisplay'] ) ? entersize_normalize_trigger_display( $parsed['triggerDisplay'] ) : 'button', 'button_placement' => isset( $parsed['triggerPlacement'] ) ? entersize_normalize_button_placement( $parsed['triggerPlacement'] ) : 'after-cart', 'show_size_finder' => array_key_exists( 'showSizeFinder', $parsed ) ? entersize_normalize_show_size_finder( $parsed['showSizeFinder'] ) : true, 'button_align' => isset( $parsed['buttonAlign'] ) ? entersize_normalize_button_align( $parsed['buttonAlign'] ) : 'left', 'button_size' => isset( $parsed['buttonSize'] ) ? entersize_normalize_button_size( $parsed['buttonSize'] ) : 'md', 'size_finder_label' => array_key_exists( 'sizeFinderLabel', $parsed ) && is_string( $parsed['sizeFinderLabel'] ) ? sanitize_text_field( $parsed['sizeFinderLabel'] ) : '', 'embed_locale' => isset( $parsed['locale'] ) ? entersize_normalize_embed_locale( $parsed['locale'] ) : 'en', 'second_embed_locale' => isset( $parsed['secondLocale'] ) ? entersize_normalize_embed_second_locale( (string) $parsed['secondLocale'], isset( $parsed['locale'] ) ? (string) $parsed['locale'] : 'en' ) : '', 'asset_version' => isset( $parsed['assetVersion'] ) && is_string( $parsed['assetVersion'] ) ? substr( preg_replace( '/[^A-Za-z0-9._-]/', '', $parsed['assetVersion'] ), 0, 64 ) : '', ); } if ( 401 === $status ) { return array( 'checked' => true, 'connected' => false, ); } return array( 'checked' => false, 'connected' => true, ); } function entersize_sync_embed_settings( $options, $force = false ) { if ( empty( $options['embed_token'] ) ) { return $options; } $cache_key = 'entersize_embed_settings_' . md5( $options['embed_token'] ); if ( ! $force && get_transient( $cache_key ) ) { return entersize_get_options(); } entersize_apply_embed_status( entersize_embed_status( $options ) ); set_transient( $cache_key, 1, 60 ); return entersize_get_options(); } /** * Caches the app's appearance settings locally. The app is the source of * truth; an unchecked status (backend unreachable, bad response) carries no * settings, so applying it would wipe the cached labels and locales. */ function entersize_apply_embed_status( $status ) { if ( empty( $status['checked'] ) || empty( $status['connected'] ) ) { return; } $options = entersize_get_options(); $next = $options; if ( isset( $status['trigger_display'] ) ) { $next['trigger_display'] = entersize_normalize_trigger_display( $status['trigger_display'] ); } if ( isset( $status['embed_locale'] ) ) { $next['embed_locale'] = entersize_normalize_embed_locale( $status['embed_locale'] ); } $next['second_embed_locale'] = isset( $status['second_embed_locale'] ) ? entersize_normalize_embed_second_locale( (string) $status['second_embed_locale'], $next['embed_locale'] ?? 'en' ) : ''; if ( isset( $status['button_placement'] ) ) { $next['button_placement'] = entersize_normalize_button_placement( $status['button_placement'] ); } if ( array_key_exists( 'show_size_finder', $status ) ) { $next['show_size_finder'] = entersize_normalize_show_size_finder( $status['show_size_finder'] ); } if ( isset( $status['button_align'] ) ) { $next['button_align'] = entersize_normalize_button_align( $status['button_align'] ); } entersize_apply_button_style_status( $next, $status ); if ( ! empty( $status['asset_version'] ) ) { $next['asset_version'] = $status['asset_version']; } if ( ! empty( $status['plan'] ) ) { $next['plan'] = $status['plan']; } if ( $next !== $options ) { entersize_save_options( $next ); } } // Options hold the embed token, so keep them out of the autoloaded set. function entersize_save_options( $options ) { update_option( ENTERSIZE_OPTION, $options, false ); } function entersize_clear_embed_token( $options ) { $options['embed_token'] = ''; $options['plan'] = ''; entersize_save_options( $options ); return entersize_get_options(); } // Swaps the one-time connection state for the store's embed token via a // server-to-server call to Entersize. The token never travels through the // browser URL. function entersize_exchange_connection_state( $state, $verifier ) { $backend_url = entersize_backend_url(); if ( empty( $backend_url ) || empty( $verifier ) ) { return ''; } $response = wp_remote_post( trailingslashit( $backend_url ) . 'api/embed/woo/plugin/exchange', array( 'timeout' => 10, 'headers' => array( 'Accept' => 'application/json', 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'state' => $state, 'verifier' => $verifier, 'storeUrl' => home_url( '/' ), ) ), ) ); if ( is_wp_error( $response ) ) { return ''; } if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { return ''; } $parsed = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $parsed ) || empty( $parsed['embedToken'] ) ) { return ''; } return sanitize_text_field( $parsed['embedToken'] ); } /** * Step 1 of the connect handshake. The button only redirects here; we mint a * fresh, short-lived connection state at click time and bounce the admin to the * Entersize app, which drives WooCommerce auth and sends the token back. No * manual click in the app, no stale state. */ function entersize_maybe_start_connection() { if ( ! is_admin() || ! entersize_is_settings_page() ) { return; } if ( ! isset( $_GET['entersize_connect'] ) ) { return; } if ( ! current_user_can( entersize_manage_capability() ) ) { return; } $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( ! $nonce || ! wp_verify_nonce( $nonce, 'entersize_connect' ) ) { wp_safe_redirect( add_query_arg( 'entersize_connect_error', 'invalid_connection', entersize_admin_page_url() ) ); exit; } $app_url = entersize_app_connect_url( entersize_create_connection_state() ); if ( empty( $app_url ) ) { wp_safe_redirect( entersize_admin_page_url() ); exit; } add_filter( 'allowed_redirect_hosts', 'entersize_allowed_redirect_hosts' ); wp_safe_redirect( esc_url_raw( $app_url ) ); exit; } function entersize_allowed_redirect_hosts( $hosts ) { $host = wp_parse_url( entersize_backend_url(), PHP_URL_HOST ); if ( is_string( $host ) && '' !== $host ) { $hosts[] = $host; } return $hosts; } add_action( 'admin_init', 'entersize_maybe_start_connection' ); function entersize_maybe_apply_connection() { if ( ! is_admin() || ! entersize_is_settings_page() ) { return; } if ( ! current_user_can( entersize_manage_capability() ) ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- The one-time connection state is bound to the current user and acts as the CSRF token. $state = ''; if ( isset( $_GET['entersize_connection'] ) ) { $state = sanitize_text_field( wp_unslash( $_GET['entersize_connection'] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( '' === $state ) { return; } $verifier = entersize_consume_connection_state( $state ); if ( '' === $verifier ) { // The one-time state may already have been consumed by an earlier // successful return (e.g. a duplicate browser redirect). If the embed // token is already saved, treat this as success instead of an error. if ( entersize_is_configured() ) { wp_safe_redirect( add_query_arg( 'entersize_connected', '1', entersize_admin_page_url() ) ); exit; } wp_safe_redirect( add_query_arg( 'entersize_connect_error', 'invalid_connection', entersize_admin_page_url() ) ); exit; } $embed_token = entersize_exchange_connection_state( $state, $verifier ); if ( empty( $embed_token ) ) { wp_safe_redirect( add_query_arg( 'entersize_connect_error', 'exchange_failed', entersize_admin_page_url() ) ); exit; } $options = entersize_get_options(); $options['embed_token'] = $embed_token; entersize_save_options( $options ); wp_safe_redirect( add_query_arg( 'entersize_connected', '1', entersize_admin_page_url() ) ); exit; } add_action( 'admin_init', 'entersize_maybe_apply_connection' ); function entersize_maybe_migrate_options() { $stored_version = get_option( ENTERSIZE_DB_VERSION_OPTION, '' ); if ( is_string( $stored_version ) && version_compare( $stored_version, ENTERSIZE_VERSION, '>=' ) ) { return; } if ( function_exists( 'wp_set_option_autoload' ) ) { wp_set_option_autoload( ENTERSIZE_OPTION, false ); } update_option( ENTERSIZE_DB_VERSION_OPTION, ENTERSIZE_VERSION ); } add_action( 'admin_init', 'entersize_maybe_migrate_options' ); function entersize_admin_assets() { if ( ! entersize_is_settings_page() ) { return; } wp_enqueue_style( 'entersize-admin', plugins_url( 'assets/entersize.css', __FILE__ ), array(), ENTERSIZE_VERSION ); } add_action( 'admin_enqueue_scripts', 'entersize_admin_assets' ); function entersize_svg_allowed_html() { return array( 'svg' => array( 'xmlns' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, 'aria-hidden' => true, 'focusable' => true, ), 'path' => array( 'd' => true, 'stroke' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ), ); } function entersize_arrows_icon() { return ''; } function entersize_check_icon() { return ''; } function entersize_external_link_icon() { return ''; } function entersize_render_settings_page() { if ( ! current_user_can( entersize_manage_capability() ) ) { wp_die( esc_html__( 'You do not have permission to manage Entersize settings.', 'entersize' ) ); } $options = entersize_get_options(); $status = entersize_embed_status( $options ); $token_cleared = false; if ( $status['checked'] && ! $status['connected'] && ! empty( $options['embed_token'] ) ) { $options = entersize_clear_embed_token( $options ); $token_cleared = true; } entersize_apply_embed_status( $status ); $connect_url = entersize_connect_action_url(); $dashboard_url = entersize_app_dashboard_url(); $integrations_url = entersize_app_integrations_url(); $configured = entersize_is_configured(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Display-only notice flags. $connect_error = isset( $_GET['entersize_connect_error'] ) ? sanitize_key( wp_unslash( $_GET['entersize_connect_error'] ) ) : ''; $just_connected = isset( $_GET['entersize_connected'] ) && '1' === sanitize_key( wp_unslash( $_GET['entersize_connected'] ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended ?>