wp_customize = $wp_customize; $this->reader_theme_loader = $reader_theme_loader; } /** * Initialize the template Customizer feature class. * * @static * @since 0.4 * @access public * * @param WP_Customize_Manager $wp_customize Customizer instance. * @param ReaderThemeLoader|null $reader_theme_loader Reader theme loader. * @return AMP_Template_Customizer Instance. */ public static function init( WP_Customize_Manager $wp_customize, ReaderThemeLoader $reader_theme_loader = null ) { if ( null === $reader_theme_loader ) { $reader_theme_loader = Services::get( 'reader_theme_loader' ); } $self = new self( $wp_customize, $reader_theme_loader ); $is_reader_mode = ( AMP_Theme_Support::READER_MODE_SLUG === AMP_Options_Manager::get_option( Option::THEME_SUPPORT ) ); $has_reader_theme = ( ReaderThemes::DEFAULT_READER_THEME !== AMP_Options_Manager::get_option( Option::READER_THEME ) ); // @todo Verify that the theme actually exists. if ( $is_reader_mode ) { add_action( 'customize_controls_init', [ $self, 'set_reader_preview_url' ] ); if ( $has_reader_theme ) { add_action( 'customize_save_after', [ $self, 'store_modified_theme_mod_setting_timestamps' ] ); } if ( $reader_theme_loader->is_theme_overridden() ) { add_action( 'customize_controls_enqueue_scripts', [ $self, 'add_customizer_scripts' ] ); add_action( 'customize_controls_print_footer_scripts', [ $self, 'render_setting_import_section_template' ] ); } elseif ( ! $has_reader_theme ) { /** * Fires when the AMP Template Customizer initializes. * * In practice the `customize_register` hook should be used instead. * * @param AMP_Template_Customizer $self Instance. * * @since 0.4 */ do_action( 'amp_customizer_init', $self ); $self->register_legacy_settings(); $self->register_legacy_ui(); add_action( 'customize_controls_print_footer_scripts', [ $self, 'print_legacy_controls_templates' ] ); add_action( 'customize_preview_init', [ $self, 'init_legacy_preview' ] ); add_action( 'customize_controls_enqueue_scripts', [ $self, 'add_legacy_customizer_scripts' ] ); } } $self->set_refresh_setting_transport(); $self->remove_cover_template_section(); $self->remove_homepage_settings_section(); if ( get_template() === 'twentytwentyone' ) { add_action( 'customize_controls_print_footer_scripts', [ $self, 'add_dark_mode_toggler_button_notice' ] ); } return $self; } /** * Set the preview URL when using a Reader theme if the AMP preview permalink was requested and no URL was provided. */ public function set_reader_preview_url() { if ( isset( $_GET[ QueryVar::AMP_PREVIEW ] ) && ! isset( $_GET['url'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $url = amp_admin_get_preview_permalink(); if ( $url ) { $this->wp_customize->set_preview_url( $url ); } } } /** * Force changes to header video to cause refresh since there are various JS dependencies that prevent selective refresh from working properly. * * In the AMP Customizer preview, selective refresh partial for `custom_header` will render or elements. * Nevertheless, custom-header.js in core is not expecting AMP components. Therefore the `wp-custom-header-video-loaded` event never * fires. This prevents themes from toggling the `has-header-video` class on the body. * * Additionally, the Twenty Seventeen core theme (the only which supports header videos) has two separate scripts * `twentyseventeen-global` and `twentyseventeen-skip-link-focus-fix` which are depended on for displaying the * video, for example toggling the 'has-header-video' class when the video is added or removed. * * This applies whenever AMP is being served in the Customizer preview, that is, in Standard mode or Reader mode with a Reader theme. */ protected function set_refresh_setting_transport() { if ( ! amp_is_canonical() && ! $this->reader_theme_loader->is_theme_overridden() ) { return; } $setting_ids = [ 'header_video', 'external_header_video', ]; foreach ( $setting_ids as $setting_id ) { $setting = $this->wp_customize->get_setting( $setting_id ); if ( $setting ) { $setting->transport = 'refresh'; } } } /** * Remove the Cover Template section if needed. * * Prevent showing the "Cover Template" section if the active (non-Reader) theme does not have the same template * as Twenty Twenty, as otherwise the user would be shown a section that would never reflect any preview change. */ protected function remove_cover_template_section() { if ( ! $this->reader_theme_loader->is_theme_overridden() ) { return; } $active_theme = $this->reader_theme_loader->get_active_theme(); $reader_theme = $this->reader_theme_loader->get_reader_theme(); if ( ! $active_theme instanceof WP_Theme || ! $reader_theme instanceof WP_Theme ) { return; } // This only applies to Twenty Twenty. if ( $reader_theme->get_template() !== 'twentytwenty' ) { return; } // Prevent deactivating the cover template if the active theme and reader theme both have a cover template. $cover_template_name = 'templates/template-cover.php'; if ( array_key_exists( $cover_template_name, $active_theme->get_page_templates() ) && array_key_exists( $cover_template_name, $reader_theme->get_page_templates() ) ) { return; } $this->wp_customize->remove_section( 'cover_template_options' ); } /** * Remove the Homepage Settings section in the AMP Customizer for a Reader theme if needed. * * The Homepage Settings section exclusively contains controls for options which apply to both AMP and non-AMP. * If this is the case and there are no other controls added to it, then remove the section. Otherwise, the controls * will all get the same notice added to them. */ protected function remove_homepage_settings_section() { if ( ! $this->reader_theme_loader->is_theme_overridden() ) { return; } $section_id = 'static_front_page'; $control_ids = []; foreach ( $this->wp_customize->controls() as $control ) { /** @var WP_Customize_Control $control */ if ( $section_id === $control->section ) { $control_ids[] = $control->id; } } $static_front_page_control_ids = [ 'show_on_front', 'page_on_front', 'page_for_posts', ]; if ( count( array_diff( $control_ids, $static_front_page_control_ids ) ) === 0 ) { $this->wp_customize->remove_section( $section_id ); } } /** * Add notice that the dark mode toggler button is not currently available on AMP pages. */ public function add_dark_mode_toggler_button_notice() { $message = __( 'While dark mode works on AMP pages, the toggle button is not currently available. It appears here only for preview purposes.', 'amp' ); ?> wp_customize->get_messenger_channel() ) { add_action( 'amp_post_template_head', [ $this->wp_customize, 'customize_preview_loading_style' ] ); add_action( 'amp_post_template_css', [ $this, 'add_legacy_customize_preview_styles' ] ); add_action( 'amp_post_template_head', [ $this->wp_customize, 'remove_frameless_preview_messenger_channel' ] ); add_action( 'amp_post_template_footer', [ $this, 'add_legacy_preview_scripts' ] ); } } /** * Sets up the AMP Customizer preview. */ public function register_legacy_ui() { $this->wp_customize->add_panel( self::PANEL_ID, [ 'type' => 'amp', 'title' => __( 'AMP', 'amp' ), 'description' => $this->get_amp_panel_description(), ] ); /** * Fires after the AMP panel has been registered for plugins to add additional controls. * * In practice the `customize_register` hook should be used instead. * * @since 0.4 * @param WP_Customize_Manager $manager Manager. */ do_action( 'amp_customizer_register_ui', $this->wp_customize ); } /** * Get AMP panel description. * * This is also added to the root panel description in the AMP Customizer when a Reader theme is being customized. * * @return string Description, with markup. */ protected function get_amp_panel_description() { return wp_kses_post( sprintf( /* translators: 1: URL to AMP project, 2: URL to admin settings screen */ __( 'While AMP works well on both desktop and mobile pages, your site is currently configured in Reader mode to serve AMP pages to mobile visitors. These settings customize the experience for these users.', 'amp' ), 'https://amp.dev', admin_url( 'admin.php?page=amp-options' ) ) ); } /** * Registers settings for customizing Legacy Reader AMP templates. * * @since 0.4 */ public function register_legacy_settings() { /** * Fires when plugins should register settings for AMP. * * In practice the `customize_register` hook should be used instead. * * @since 0.4 * @param WP_Customize_Manager $manager Manager. */ do_action( 'amp_customizer_register_settings', $this->wp_customize ); } /** * Load up AMP scripts needed for Customizer integrations when a Reader theme has been selected. * * @since 2.0 */ public function add_customizer_scripts() { $handle = 'amp-customize-controls'; $asset_file = AMP__DIR__ . '/assets/js/amp-customize-controls.asset.php'; $asset = require $asset_file; $dependencies = $asset['dependencies']; $version = $asset['version']; /** This action is documented in includes/class-amp-theme-support.php */ do_action( 'amp_register_polyfills' ); wp_enqueue_script( $handle, amp_get_asset_url( 'js/amp-customize-controls.js' ), array_merge( $dependencies, [ 'jquery', 'customize-controls' ] ), $version, true ); if ( function_exists( 'wp_set_script_translations' ) ) { wp_set_script_translations( $handle, 'amp' ); } elseif ( function_exists( 'wp_get_jed_locale_data' ) || function_exists( 'gutenberg_get_jed_locale_data' ) ) { $locale_data = function_exists( 'wp_get_jed_locale_data' ) ? wp_get_jed_locale_data( 'amp' ) : gutenberg_get_jed_locale_data( 'amp' ); wp_add_inline_script( $handle, sprintf( 'wp.i18n.setLocaleData( %s, "amp" );', wp_json_encode( $locale_data ) ), 'after' ); } $option_settings = []; foreach ( $this->wp_customize->settings() as $setting ) { /** @var WP_Customize_Setting $setting */ if ( 'option' === $setting->type ) { $option_settings[] = $setting->id; } } wp_add_inline_script( $handle, sprintf( 'ampCustomizeControls.boot( %s );', wp_json_encode( [ 'queryVar' => amp_get_slug(), 'optionSettings' => $option_settings, 'activeThemeSettingImports' => $this->get_active_theme_import_settings(), 'mimeTypeIcons' => [ 'image' => wp_mime_type_icon( 'image' ), 'document' => wp_mime_type_icon( 'document' ), ], 'l10n' => [ /* translators: placeholder is URL to non-AMP Customizer. */ 'ampVersionNotice' => wp_kses_post( sprintf( __( 'You are customizing the AMP version of your site. Customize non-AMP version.', 'amp' ), esc_url( admin_url( 'customize.php' ) ) ) ), 'rootPanelDescription' => $this->get_amp_panel_description(), ], ] ) ) ); wp_enqueue_style( 'amp-customizer', amp_get_asset_url( 'css/amp-customizer.css' ), [], AMP__VERSION ); wp_styles()->add_data( 'amp-customizer', 'rtl', 'replace' ); } /** * Store the timestamps for modified theme settings. * * This is used to determine which settings from the Active theme should be presented for importing into the Reader * theme. If a setting has been modified more recently in the Reader theme, then it doesn't make much sense to offer * for the user to re-import a customization they already made. */ public function store_modified_theme_mod_setting_timestamps() { $modified_setting_ids = []; foreach ( array_keys( $this->wp_customize->unsanitized_post_values() ) as $setting_id ) { $setting = $this->wp_customize->get_setting( $setting_id ); if ( ! ( $setting instanceof WP_Customize_Setting ) || $setting instanceof WP_Customize_Filter_Setting ) { continue; } if ( $setting instanceof WP_Customize_Custom_CSS_Setting ) { $modified_setting_ids[] = $setting->id_data()['base']; // Remove theme slug from ID. } elseif ( 'theme_mod' === $setting->type ) { $modified_setting_ids[] = $setting->id; } } if ( empty( $modified_setting_ids ) ) { return; } $theme_mod_timestamps = get_theme_mod( self::THEME_MOD_TIMESTAMPS_KEY, [] ); foreach ( $modified_setting_ids as $modified_setting_id ) { $theme_mod_timestamps[ $modified_setting_id ] = time(); } set_theme_mod( self::THEME_MOD_TIMESTAMPS_KEY, $theme_mod_timestamps ); } /** * Get settings to import from the active theme. * * @return array Map of setting IDs to setting values. */ protected function get_active_theme_import_settings() { $active_theme = $this->reader_theme_loader->get_active_theme(); if ( ! $active_theme instanceof WP_Theme ) { return []; } $active_theme_mods = get_option( 'theme_mods_' . $active_theme->get_stylesheet(), [] ); $import_settings = []; $active_setting_timestamps = isset( $active_theme_mods[ self::THEME_MOD_TIMESTAMPS_KEY ] ) ? $active_theme_mods[ self::THEME_MOD_TIMESTAMPS_KEY ] : []; $reader_setting_timestamps = get_theme_mod( self::THEME_MOD_TIMESTAMPS_KEY, [] ); // Remove theme mods which will not be imported directly. unset( $active_theme_mods['sidebars_widgets'], $active_theme_mods['custom_css_post_id'], $active_theme_mods['background_preset'], // Since a meta setting. When importing a background setting, will be set to 'custom'. $active_theme_mods[ self::THEME_MOD_TIMESTAMPS_KEY ] ); // Avoid offering to import background image settings if no background image is set. if ( empty( $active_theme_mods['background_image'] ) ) { foreach ( [ 'background_position_x', 'background_position_y', 'background_size', 'background_repeat', 'background_attachment' ] as $setting_id ) { unset( $active_theme_mods[ $setting_id ] ); } } // Map nav menus for importing. if ( isset( $active_theme_mods['nav_menu_locations'] ) ) { $nav_menu_locations = wp_map_nav_menu_locations( get_theme_mod( 'nav_menu_locations', [] ), $active_theme_mods['nav_menu_locations'] ); foreach ( $nav_menu_locations as $nav_menu_location => $menu_id ) { $setting = $this->wp_customize->get_setting( "nav_menu_locations[$nav_menu_location]" ); if ( $setting instanceof WP_Customize_Setting && // Skip presenting settings which have been more recently updated in the Reader theme. ( ! isset( $active_setting_timestamps[ $setting->id ], $reader_setting_timestamps[ $setting->id ] ) || $active_setting_timestamps[ $setting->id ] > $reader_setting_timestamps[ $setting->id ] ) ) { /** This filter is documented in wp-includes/class-wp-customize-manager.php */ $value = apply_filters( "customize_sanitize_js_{$setting->id}", $menu_id, $setting ); $import_settings[ $setting->id ] = $value; } } unset( $active_theme_mods['nav_menu_locations'] ); } foreach ( $this->wp_customize->settings() as $setting ) { /** @var WP_Customize_Setting $setting */ if ( 'theme_mod' !== $setting->type || // Skip presenting settings which have been more recently updated in the Reader theme. ( isset( $active_setting_timestamps[ $setting->id ], $reader_setting_timestamps[ $setting->id ] ) && $reader_setting_timestamps[ $setting->id ] > $active_setting_timestamps[ $setting->id ] ) ) { continue; } $id_data = $setting->id_data(); if ( ! array_key_exists( $id_data['base'], $active_theme_mods ) ) { continue; } $value = $active_theme_mods[ $id_data['base'] ]; $subkeys = $id_data['keys']; while ( ! empty( $subkeys ) ) { $subkey = array_shift( $subkeys ); if ( ! is_array( $value ) || ! array_key_exists( $subkey, $value ) ) { // Move on to the next setting. continue 2; } $value = $value[ $subkey ]; } /** This filter is documented in wp-includes/class-wp-customize-manager.php */ $value = apply_filters( "customize_sanitize_js_{$setting->id}", $value, $setting ); $import_settings[ $setting->id ] = $value; } // Import Custom CSS if it has not been more recently updated in the Reader theme. if ( ! isset( $active_setting_timestamps['custom_css'], $reader_setting_timestamps['custom_css'] ) || $active_setting_timestamps['custom_css'] > $reader_setting_timestamps['custom_css'] ) { $custom_css_setting = $this->wp_customize->get_setting( sprintf( 'custom_css[%s]', get_stylesheet() ) ); $custom_css_post = wp_get_custom_css_post( $active_theme->get_stylesheet() ); if ( $custom_css_setting instanceof WP_Customize_Custom_CSS_Setting && $custom_css_post instanceof WP_Post ) { $value = $custom_css_post->post_content; /** This filter is documented in wp-includes/class-wp-customize-setting.php */ $value = apply_filters( 'customize_value_custom_css', $value, $custom_css_setting ); /** This filter is documented in wp-includes/class-wp-customize-manager.php */ $value = apply_filters( "customize_sanitize_js_{$custom_css_setting->id}", $value, $custom_css_setting ); $import_settings[ $custom_css_setting->id ] = $value; } } return $import_settings; } /** * Render template for the setting import "section". * * This section only has a menu item and it is not intended to expand. */ public function render_setting_import_section_template() { ?> amp_get_slug(), 'panelId' => self::PANEL_ID, 'ampUrl' => amp_admin_get_preview_permalink(), 'l10n' => [ 'unavailableMessage' => __( 'AMP is not available for the page currently being previewed.', 'amp' ), 'unavailableLinkText' => __( 'Navigate to an AMP compatible page', 'amp' ), ], ] ) ) ); wp_enqueue_style( 'amp-customizer', amp_get_asset_url( 'css/amp-customizer-legacy.css' ), [], AMP__VERSION ); wp_styles()->add_data( 'amp-customizer', 'rtl', 'replace' ); /** * Fires when plugins should register settings for AMP. * * In practice the `customize_controls_enqueue_scripts` hook should be used instead. * * @since 0.4 * @param WP_Customize_Manager $manager Manager. */ do_action( 'amp_customizer_enqueue_scripts', $this->wp_customize ); } /** * Enqueues scripts used in both the AMP and non-AMP Customizer preview (only applies to Legacy Reader mode). * * @since 0.6 */ public function enqueue_legacy_preview_scripts() { // Bail if user can't customize anyway. if ( ! current_user_can( 'customize' ) ) { return; } /** This action is documented in includes/class-amp-theme-support.php */ do_action( 'amp_register_polyfills' ); $asset_file = AMP__DIR__ . '/assets/js/amp-customize-preview-legacy.asset.php'; $asset = require $asset_file; $dependencies = $asset['dependencies']; $version = $asset['version']; wp_enqueue_script( 'amp-customize-preview', amp_get_asset_url( 'js/amp-customize-preview-legacy.js' ), array_merge( $dependencies, [ 'jquery', 'customize-preview' ] ), $version, true ); wp_add_inline_script( 'amp-customize-preview', sprintf( 'ampCustomizePreview.boot( %s );', wp_json_encode( [ 'available' => amp_is_available(), 'enabled' => amp_is_request(), ] ) ) ); } /** * Add AMP Customizer preview styles for Legacy Reader mode. */ public function add_legacy_customize_preview_styles() { ?> /* Text meant only for screen readers; this is needed for wp.a11y.speak() */ .screen-reader-text { border: 0; clip: rect(1px, 1px, 1px, 1px); -webkit-clip-path: inset(50%); clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; word-wrap: normal !important; } body.wp-customizer-unloading { opacity: 0.25 !important; /* Because AMP sets body to opacity:1 once layout complete. */ } wp_customize ); $this->wp_customize->customize_preview_settings(); $this->wp_customize->selective_refresh->export_preview_data(); wp_print_footer_scripts(); } /** * Print templates needed for AMP in Customizer (for Legacy Reader mode). * * @since 0.6 */ public function print_legacy_controls_templates() { ?>