%s', esc_url( self::url() ), esc_html__( 'Settings', 'thridify' ) ) ); return $links; } /** * One contextual, dismissible prompt on the Plugins screen until connected. * * @return void */ public function plugins_screen_notice() { $screen = get_current_screen(); if ( ! $screen || 'plugins' !== $screen->id || ! current_user_can( 'manage_woocommerce' ) || 'disconnected' !== Connection::status() ) { return; } printf( '

%s %s

', esc_html__( 'Thridify is active. Connect your Thridify account to show 3D and AR on your products.', 'thridify' ), esc_url( self::url() ), esc_html__( 'Connect now', 'thridify' ) ); } /** * Settings page URL. * * @param string $tab Tab. * @return string */ public static function url( $tab = '' ) { $args = array( 'page' => self::SLUG ); if ( '' !== $tab ) { $args['tab'] = $tab; } return add_query_arg( $args, admin_url( 'admin.php' ) ); } /** * Page styles. * * @param string $hook Page hook. * @return void */ public function assets( $hook ) { if ( 'woocommerce_page_' . self::SLUG !== $hook ) { return; } wp_enqueue_style( 'thridify-admin', THRIDIFY_URL . 'assets/css/thridify-admin.css', array(), THRIDIFY_VERSION ); wp_enqueue_style( 'wp-color-picker' ); wp_enqueue_script( 'thridify-admin-settings', THRIDIFY_URL . 'assets/js/thridify-admin-settings.js', array( 'wp-color-picker' ), THRIDIFY_VERSION, true ); } /** * The current tab. * * @return string */ private function tab() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab navigation. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'connection'; return in_array( $tab, array( 'connection', 'display', 'catalog', 'switcher', 'insights' ), true ) ? $tab : 'connection'; } /** * Render the page. * * @return void */ public function render() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $tab = $this->tab(); $tabs = array( 'connection' => __( 'Connection', 'thridify' ), 'display' => __( 'Display', 'thridify' ), 'catalog' => __( 'Catalog', 'thridify' ), 'switcher' => __( 'Experience switcher', 'thridify' ), 'insights' => __( 'Insights', 'thridify' ), ); echo '
'; echo '

' . esc_html__( 'Thridify 3D & AR', 'thridify' ) . '

'; $this->notice(); echo ''; switch ( $tab ) { case 'display': $this->render_display(); break; case 'catalog': $this->render_catalog(); break; case 'switcher': $this->render_switcher(); break; case 'insights': $this->render_insights(); break; default: $this->render_connection(); } echo '
'; } /** * Result notice after a redirect. * * @return void */ private function notice() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only notice code. $code = isset( $_GET['thridify_notice'] ) ? sanitize_key( wp_unslash( $_GET['thridify_notice'] ) ) : ''; $messages = array( 'connected' => array( 'success', __( 'Connected. Your catalog is syncing to Thridify now.', 'thridify' ) ), 'linked_partial' => array( 'warning', __( 'Connected, but linking this site did not finish. See the Connection tab for details.', 'thridify' ) ), 'denied' => array( 'warning', __( 'The connection was cancelled in Thridify.', 'thridify' ) ), 'state' => array( 'error', __( 'The connection request expired or did not match. Please try connecting again.', 'thridify' ) ), 'failed' => array( 'error', __( 'Thridify did not complete the connection. Please try again.', 'thridify' ) ), 'disconnected' => array( 'success', __( 'Disconnected. The viewer no longer shows on this site.', 'thridify' ) ), 'saved' => array( 'success', __( 'Settings saved.', 'thridify' ) ), 'synced' => array( 'success', __( 'Catalog sync started. It runs in the background.', 'thridify' ) ), 'relinked' => array( 'success', __( 'Site linked.', 'thridify' ) ), 'remote_error' => array( 'error', __( 'Thridify could not save the change. Check the connection and try again.', 'thridify' ) ), ); if ( isset( $messages[ $code ] ) ) { printf( '

%2$s

', esc_attr( $messages[ $code ][0] ), esc_html( $messages[ $code ][1] ) ); } } /** * Connection tab. * * @return void */ private function render_connection() { $status = Connection::status(); $health = Connection::health(); $record = Connection::record(); echo '
'; if ( 'disconnected' === $status ) { echo '

' . esc_html__( 'Connect your Thridify account', 'thridify' ) . '

'; echo '

' . esc_html__( 'Thridify hosts your 3D, AR and configurator experiences. Connecting lets this store show them on matching products and keeps your catalog in sync. You will sign in to Thridify (or create an account), confirm, and come straight back here.', 'thridify' ) . '

'; $this->action_button( OAuth::ACTION_START, __( 'Connect with Thridify', 'thridify' ), 'primary' ); printf( '

%s thridify.com

', esc_html__( 'New to Thridify? Learn more at', 'thridify' ) ); } else { if ( 'reconnect' === $status ) { echo '

' . esc_html__( 'Reconnect required: Thridify no longer accepts this site\'s connection (it was revoked or replaced). Published experiences keep showing, but settings and catalog changes cannot reach Thridify until you connect again.', 'thridify' ) . '

'; } echo '

' . esc_html__( 'Connected', 'thridify' ) . '

'; echo ''; $this->row( __( 'Thridify account', 'thridify' ), trim( ( $health['accountName'] ?? '' ) . ' ' . ( ! empty( $health['accountEmail'] ) ? '(' . $health['accountEmail'] . ')' : '' ) ) ); $this->row( __( 'Team', 'thridify' ), (string) ( $health['teamName'] ?? '' ) ); $this->row( __( 'Linked site', 'thridify' ), (string) ( $health['linkedSite'] ?? '' ) ); $this->row( __( 'Environment', 'thridify' ), Env::SANDBOX === Env::current() ? __( 'Sandbox (Thridify development)', 'thridify' ) : __( 'Production', 'thridify' ) ); echo ''; if ( ! empty( $record['link_error'] ) ) { printf( '

%s %s

', esc_html__( 'Linking this site did not finish:', 'thridify' ), esc_html( (string) $record['link_error'] ) ); $this->action_button( 'thridify_relink', __( 'Try again', 'thridify' ) ); } echo '

'; if ( 'reconnect' === $status ) { $this->action_button( OAuth::ACTION_START, __( 'Reconnect', 'thridify' ), 'primary', false ); } printf( '%s ', esc_url( Env::host( 'shopper' ) ), esc_html__( 'Open Thridify', 'thridify' ) ); $this->action_button( OAuth::ACTION_UNLINK, __( 'Disconnect', 'thridify' ), 'secondary', false, __( 'Disconnect this site from Thridify? The 3D viewer will stop showing.', 'thridify' ) ); echo '

'; } echo '
'; if ( Env::sandbox_allowed() ) { echo '

' . esc_html__( 'Environment', 'thridify' ) . '

'; echo '

' . esc_html__( 'Enterprise accounts can test experiences from Thridify\'s sandbox before publishing them to production. Each environment has its own connection.', 'thridify' ) . '

'; echo '
'; wp_nonce_field( 'thridify_environment' ); echo ''; $this->select( 'environment', Env::current(), array( Env::PRODUCTION => __( 'Production', 'thridify' ), Env::SANDBOX => __( 'Sandbox', 'thridify' ) ) ); submit_button( __( 'Switch environment', 'thridify' ), 'secondary', 'submit', false ); echo '
'; } } /** * Display tab. * * @return void */ private function render_display() { $s = Settings::all(); echo '
'; wp_nonce_field( 'thridify_save_display' ); echo ''; echo '

' . esc_html__( 'The viewer appears only on products that have a published Thridify experience. Products without one look exactly as before.', 'thridify' ) . '

'; echo ''; $this->field_row( __( 'Show on this site', 'thridify' ), $this->checkbox_html( 'enabled', $s['enabled'], __( 'Show Thridify experiences on the storefront', 'thridify' ) ) ); $this->field_row( __( 'Product pages', 'thridify' ), $this->select_html( 'product_mode', $s['product_mode'], array( 'icon' => __( '3D icon on the product gallery', 'thridify' ), 'button' => __( '"View in 3D" button', 'thridify' ), 'inline' => __( 'Images / 3D tabs under the gallery', 'thridify' ), 'off' => __( 'Off (only where I place the View in 3D block)', 'thridify' ), ) ) ); $this->field_row( __( 'Icon position', 'thridify' ), $this->select_html( 'trigger_position', $s['trigger_position'], array( 'top-right' => __( 'Top right', 'thridify' ), 'top-left' => __( 'Top left', 'thridify' ), 'bottom-right' => __( 'Bottom right', 'thridify' ), 'bottom-left' => __( 'Bottom left', 'thridify' ), ) ) ); $this->field_row( __( 'Button placement', 'thridify' ), $this->select_html( 'button_placement', $s['button_placement'], array( 'after-cta' => __( 'After Add to cart', 'thridify' ), 'after-description' => __( 'After the short description', 'thridify' ), 'gallery' => __( 'On the gallery', 'thridify' ), ) ) . '

' . esc_html__( 'Or add the "View in 3D button" block (or the [thridify_3d_button] shortcode) exactly where you want it.', 'thridify' ) . '

' ); $this->field_row( __( 'Button text', 'thridify' ), sprintf( '', esc_attr( $s['button_text'] ), esc_attr__( 'View in 3D', 'thridify' ) ) ); $this->field_row( __( 'Tabs start on', 'thridify' ), $this->select_html( 'inline_start', $s['inline_start'], array( '3d' => __( '3D', 'thridify' ), 'images' => __( 'Images', 'thridify' ), ) ) ); $this->field_row( __( 'Shop pages', 'thridify' ), $this->checkbox_html( 'cards', $s['cards'], __( 'Show a 3D icon on product cards in the shop, categories and search', 'thridify' ) ) . sprintf( ' ', esc_html__( 'up to', 'thridify' ), (int) $s['cards_max'] ) ); $this->field_row( __( 'Add to cart from 3D', 'thridify' ), $this->checkbox_html( 'cart_from_viewer', $s['cart_from_viewer'], __( 'Let shoppers add their configuration to the cart from the viewer (WooCommerce price applies)', 'thridify' ) ) ); $this->field_row( __( 'Rendering', 'thridify' ), $this->select_html( 'render_mode', $s['render_mode'], array( 'native' => __( 'Native (recommended)', 'thridify' ), 'iframe' => __( 'Isolated frame (for themes with conflicting scripts or styles)', 'thridify' ), ) ) ); $this->field_row( __( 'Brand colour', 'thridify' ), sprintf( '', esc_attr( $s['brand_color'] ) ) . '

' . esc_html__( 'Buttons and highlights in the viewer. Leave empty for Thridify\'s default look.', 'thridify' ) . '

' ); $this->field_row( __( 'Button shape', 'thridify' ), $this->select_html( 'button_shape', $s['button_shape'], array( 'theme' => __( 'Thridify default', 'thridify' ), 'square' => __( 'Square', 'thridify' ), 'rounded' => __( 'Rounded', 'thridify' ), 'pill' => __( 'Pill', 'thridify' ), ) ) ); $this->field_row( __( 'Font', 'thridify' ), $this->checkbox_html( 'inherit_font', $s['inherit_font'], __( 'Use the theme\'s font in the viewer', 'thridify' ) ) ); echo ''; submit_button(); echo '
'; if ( '' !== $s['brand_color'] && 'connected' === Connection::status() ) { echo '

' . esc_html__( 'Brand kit', 'thridify' ) . '

'; echo '

' . esc_html__( 'Also use this colour on your Thridify experiences everywhere they appear (shared links, QR codes, other channels).', 'thridify' ) . '

'; $this->action_button( 'thridify_push_brand', __( 'Send brand colour to Thridify', 'thridify' ) ); echo '
'; } } /** * Catalog tab. * * @return void */ private function render_catalog() { $s = Settings::all(); $status = CatalogSync::status(); echo '
'; wp_nonce_field( 'thridify_save_catalog' ); echo ''; echo '

' . esc_html__( 'Match products by', 'thridify' ) . '

'; echo '

' . esc_html__( 'How Thridify recognises your WooCommerce products. Use the same value on the product in Thridify (for example the SKU).', 'thridify' ) . '

'; $this->select( 'match_field', $s['match_field'], array( 'sku' => __( 'SKU', 'thridify' ), 'slug' => __( 'Product slug', 'thridify' ), 'title' => __( 'Product name', 'thridify' ), 'productId' => __( 'WooCommerce product ID', 'thridify' ), ) ); submit_button( __( 'Save and re-sync', 'thridify' ), 'primary', 'submit', false ); echo '
'; echo '

' . esc_html__( 'Catalog sync', 'thridify' ) . '

'; echo '

' . esc_html__( 'Product IDs, SKUs, slugs and names of published products are sent to Thridify after connecting, a couple of minutes after products change, and daily. Prices, stock and customer data are never sent.', 'thridify' ) . '

'; echo ''; $states = array( 'queued' => __( 'Queued', 'thridify' ), 'running' => __( 'Running', 'thridify' ), 'ok' => __( 'Up to date', 'thridify' ), 'error' => __( 'Failed', 'thridify' ), ); $state = (string) ( $status['state'] ?? '' ); $this->row( __( 'Status', 'thridify' ), isset( $states[ $state ] ) ? $states[ $state ] : __( 'Not synced yet', 'thridify' ) ); if ( ! empty( $status['finishedAt'] ) ) { $this->row( __( 'Last finished', 'thridify' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $status['finishedAt'] ) ); } if ( 'ok' === $state ) { $this->row( __( 'Products sent', 'thridify' ), number_format_i18n( (int) ( $status['products'] ?? 0 ) ) ); $this->row( __( 'Matched to an experience', 'thridify' ), number_format_i18n( max( 0, (int) ( $status['products'] ?? 0 ) - (int) ( $status['unresolved'] ?? 0 ) ) ) ); } if ( ! empty( $status['error'] ) ) { $this->row( __( 'Error', 'thridify' ), (string) $status['error'] ); } if ( ! empty( $status['unresolvedSample'] ) && is_array( $status['unresolvedSample'] ) ) { $this->row( __( 'Examples without an experience', 'thridify' ), implode( ', ', array_map( 'strval', array_slice( $status['unresolvedSample'], 0, 10 ) ) ) ); } echo ''; if ( 'connected' === Connection::status() ) { $this->action_button( 'thridify_sync_now', __( 'Sync now', 'thridify' ) ); } printf( ' %s', esc_url( admin_url( 'edit.php?post_type=product' ) ), esc_html__( 'See 3D status per product', 'thridify' ) ); echo '
'; } /** * Experience switcher tab — a remote control for the Thridify setting. * * @return void */ private function render_switcher() { echo '
'; echo '

' . esc_html__( 'Experience switcher', 'thridify' ) . '

'; echo '

' . esc_html__( 'The switcher lets shoppers jump between your experiences inside the viewer. This setting is stored in your Thridify account and applies everywhere your experiences appear.', 'thridify' ) . '

'; if ( 'connected' !== Connection::status() ) { echo '

' . esc_html__( 'Connect your Thridify account to manage the switcher.', 'thridify' ) . '

'; return; } $result = Api::request( 'GET', '/switcher' ); if ( ! Api::ok( $result ) ) { echo '

' . esc_html( Api::error_message( $result ) ) . '

'; return; } $switcher = is_array( $result['body']['switcher'] ?? null ) ? $result['body']['switcher'] : array(); $index = is_array( $result['body']['experiences'] ?? null ) ? $result['body']['experiences'] : array(); $experiences = isset( $index['experiences'] ) && is_array( $index['experiences'] ) ? $index['experiences'] : array(); $enabled = ! isset( $switcher['enabled'] ) || false !== $switcher['enabled']; $pinned = isset( $switcher['presentOn'] ) && is_array( $switcher['presentOn'] ) ? array_map( 'strval', $switcher['presentOn'] ) : array(); echo '
'; wp_nonce_field( 'thridify_save_switcher' ); echo ''; printf( '

', checked( $enabled, true, false ), esc_html__( 'Show the experience switcher', 'thridify' ) ); if ( $experiences ) { echo '

' . esc_html__( 'Show only these experiences (none ticked = all published experiences):', 'thridify' ) . '

'; } else { echo '

' . esc_html__( 'No published experiences yet.', 'thridify' ) . '

'; } submit_button( __( 'Save to Thridify', 'thridify' ) ); echo '
'; } /** * Insights tab. * * @return void */ private function render_insights() { $enabled = Attribution::enabled(); echo '
'; echo '

' . esc_html__( '3D-assisted sales', 'thridify' ) . '

'; echo '

' . esc_html__( 'Measure how many orders follow a shopper using 3D or AR. When on, the storefront sets a first-party cookie (_thridify_3d, 30 days) after a shopper engages a 3D experience — only with statistics consent if you use a WP Consent API compatible cookie banner. Paid orders and refunds are labelled and reported to your Thridify analytics with the order ID, total, currency, item count and label. No customer data is sent.', 'thridify' ) . '

'; echo '
'; wp_nonce_field( 'thridify_save_insights' ); echo ''; printf( '

', checked( $enabled, true, false ), esc_html__( 'Measure 3D-assisted sales', 'thridify' ) ); submit_button( __( 'Save', 'thridify' ), 'secondary' ); echo '
'; if ( ! $enabled ) { return; } $summary = Attribution::summary( 30 ); echo '

' . esc_html__( 'Last 30 days', 'thridify' ) . '

'; echo ''; $labels = array( '3d' => __( 'After a 3D/AR engagement', 'thridify' ), 'none' => __( 'Without 3D', 'thridify' ), 'unknown' => __( 'Unknown (no consent)', 'thridify' ), ); foreach ( $labels as $key => $label ) { printf( '', esc_html( $label ), esc_html( number_format_i18n( $summary[ $key ]['orders'] ) ), wp_kses_post( wc_price( $summary[ $key ]['revenue'] ) ) ); } echo '
' . esc_html__( 'Orders', 'thridify' ) . '' . esc_html__( 'Revenue (net of refunds)', 'thridify' ) . '
%s%s%s
'; $total = $summary['3d']['orders'] + $summary['none']['orders']; if ( $total > 0 ) { printf( '

%s

', /* translators: %s: percentage of orders. */ esc_html( sprintf( __( '%s of orders with a known outcome followed a 3D/AR engagement.', 'thridify' ), number_format_i18n( 100 * $summary['3d']['orders'] / $total, 1 ) . '%' ) ) ); } echo '

' . esc_html__( 'Full funnel analytics (views, engagement, AR sessions) are in your Thridify dashboard.', 'thridify' ) . '

'; } /* ─── Handlers ─────────────────────────────────────────────────────── */ /** * Save the Display tab. * * @return void */ public function save_display() { $this->guard( 'thridify_save_display' ); $input = $this->posted_settings(); $keys = array( 'enabled', 'product_mode', 'trigger_position', 'button_placement', 'button_text', 'inline_start', 'cards', 'cards_max', 'cart_from_viewer', 'render_mode', 'brand_color', 'button_shape', 'inherit_font' ); $next = array(); foreach ( $keys as $key ) { $next[ $key ] = isset( $input[ $key ] ) ? $input[ $key ] : ''; } Settings::update( $next ); $this->back( 'display', 'saved' ); } /** * Save the match field, push it to Thridify and re-sync. * * @return void */ public function save_catalog() { $this->guard( 'thridify_save_catalog' ); $input = $this->posted_settings(); $saved = Settings::update( array( 'match_field' => isset( $input['match_field'] ) ? $input['match_field'] : '' ) ); if ( 'connected' === Connection::status() ) { $result = Api::request( 'PUT', '/matchfield', array( 'matchField' => $saved['match_field'] ) ); if ( ! Api::ok( $result ) ) { $this->back( 'catalog', 'remote_error' ); } CatalogSync::queue( 'settings' ); } $this->back( 'catalog', 'saved' ); } /** * Manual sync. * * @return void */ public function sync_now() { $this->guard( 'thridify_sync_now' ); if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( CatalogSync::ACTION_SYNC, array(), CatalogSync::GROUP ); } CatalogSync::queue( 'manual' ); $this->back( 'catalog', 'synced' ); } /** * Retry the site link steps. * * @return void */ public function relink() { $this->guard( 'thridify_relink' ); $this->back( 'connection', true === OAuth::link_site() ? 'relinked' : 'linked_partial' ); } /** * Save the switcher to Thridify. * * @return void */ public function save_switcher() { $this->guard( 'thridify_save_switcher' ); $present = array(); if ( isset( $_POST['present_on'] ) && is_array( $_POST['present_on'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in guard(). $present = array_values( array_filter( array_map( 'sanitize_text_field', wp_unslash( $_POST['present_on'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in guard(). } $result = Api::request( 'PUT', '/switcher', array( 'switcher' => array( 'enabled' => ! empty( $_POST['switcher_enabled'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in guard(). 'presentOn' => array_slice( $present, 0, 200 ), ), ) ); $this->back( 'switcher', Api::ok( $result ) ? 'saved' : 'remote_error' ); } /** * Push the brand colour to Thridify's theme (merchant-initiated only). * * @return void */ public function push_brand() { $this->guard( 'thridify_push_brand' ); $color = (string) Settings::get( 'brand_color' ); $result = '' === $color ? null : Api::request( 'PUT', '/theme', array( 'theme' => array( 'primary' => $color ) ) ); $this->back( 'display', null !== $result && Api::ok( $result ) ? 'saved' : 'remote_error' ); } /** * Save the attribution toggle. * * @return void */ public function save_insights() { $this->guard( 'thridify_save_insights' ); Settings::update( array( 'attribution' => ! empty( $_POST['attribution'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in guard(). $this->back( 'insights', 'saved' ); } /** * Switch environment. * * @return void */ public function save_environment() { $this->guard( 'thridify_environment' ); if ( Env::sandbox_allowed() ) { $input = $this->posted_settings(); Settings::update( array( 'environment' => isset( $input['environment'] ) ? $input['environment'] : Env::PRODUCTION ) ); CatalogSync::queue( 'environment' ); } $this->back( 'connection', 'saved' ); } /* ─── Helpers ──────────────────────────────────────────────────────── */ /** * Capability + nonce check for a handler. * * @param string $action Nonce action. * @return void */ private function guard( $action ) { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to manage Thridify.', 'thridify' ), 403 ); } check_admin_referer( $action ); } /** * Posted `thridify[...]` values (sanitized later by Settings::sanitize()). * * @return array */ private function posted_settings() { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified in guard(); every key is sanitized by Settings::sanitize(). $raw = isset( $_POST['thridify'] ) && is_array( $_POST['thridify'] ) ? wp_unslash( $_POST['thridify'] ) : array(); return array_map( static function ( $value ) { return is_scalar( $value ) ? sanitize_text_field( (string) $value ) : ''; }, $raw ); } /** * Redirect back to a tab. * * @param string $tab Tab. * @param string $notice Notice code. * @return void */ private function back( $tab, $notice ) { wp_safe_redirect( add_query_arg( 'thridify_notice', $notice, self::url( $tab ) ) ); exit; } /** * A button that posts a nonce-protected admin-post action. * * @param string $action Action (also the nonce action). * @param string $label Label. * @param string $type primary|secondary. * @param bool $block Wrap in a paragraph. * @param string|null $confirm Confirmation question. * @return void */ private function action_button( $action, $label, $type = 'secondary', $block = true, $confirm = null ) { if ( $block ) { echo '

'; } printf( '

', esc_url( admin_url( 'admin-post.php' ) ), null !== $confirm ? ' data-thridify-confirm="' . esc_attr( $confirm ) . '"' : '' ); wp_nonce_field( $action ); printf( '
', esc_attr( $action ), esc_attr( $type ), esc_html( $label ) ); echo $block ? '

' : ' '; } /** * A read-only table row. * * @param string $label Label. * @param string $value Value. * @return void */ private function row( $label, $value ) { printf( '%s%s', esc_html( $label ), esc_html( '' !== $value ? $value : '—' ) ); } /** * A settings row whose control HTML was built with escaped parts. * * @param string $label Label. * @param string $control Control HTML. * @return void */ private function field_row( $label, $control ) { printf( '%s%s', esc_html( $label ), wp_kses( $control, self::allowed_form_html() ) ); } /** * Checkbox control. * * @param string $key Setting key. * @param bool $checked Checked. * @param string $label Label. * @return string */ private function checkbox_html( $key, $checked, $label ) { return sprintf( '', esc_attr( $key ), checked( (bool) $checked, true, false ), esc_html( $label ) ); } /** * Select control. * * @param string $key Setting key. * @param string $current Current value. * @param array $options Options. * @return string */ private function select_html( $key, $current, array $options ) { $html = sprintf( ''; } /** * Echo a select control. * * @param string $key Setting key. * @param string $current Current value. * @param array $options Options. * @return void */ private function select( $key, $current, array $options ) { echo wp_kses( $this->select_html( $key, $current, $options ), self::allowed_form_html() ); } /** * Tags allowed in settings controls. * * @return array> */ private static function allowed_form_html() { return array( 'label' => array(), 'p' => array( 'class' => true ), 'select' => array( 'name' => true ), 'option' => array( 'value' => true, 'selected' => true, ), 'input' => array( 'type' => true, 'name' => true, 'value' => true, 'checked' => true, 'class' => true, 'min' => true, 'max' => true, 'maxlength' => true, 'placeholder' => true, 'data-default-color' => true, ), ); } }