Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 257
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Global_Styles
0.00% covered (danger)
0.00%
0 / 256
0.00% covered (danger)
0.00%
0 / 14
1806
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 1
6
 register_customizer_option
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_tracks_events_fonts_section_control
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 update_plugin_settings
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
 rest_api_init
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 can_use_global_styles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 block_editor_settings
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 enqueue_block_editor_assets
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
2
 wp_enqueue_scripts
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 get_inline_css
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
132
 maybe_filter_font_list
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 get_font_values
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 filter_and_validate_font_options
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Global Styles file.
4 *
5 * @package automattic/jetpack-mu-wpcom
6 */
7
8namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\Global_Styles;
9
10use Automattic\Jetpack\Jetpack_Mu_Wpcom;
11
12/**
13 * Class Global_Styles
14 */
15class Global_Styles {
16
17    /**
18     * Class instance.
19     *
20     * @var \Automattic\Jetpack\Jetpack_Mu_Wpcom\Global_Styles\Global_Styles
21     */
22    private static $instance = null;
23
24    /**
25     * Holds the internal data description to be exposed through REST API.
26     *
27     * @var \Automattic\Jetpack\Jetpack_Mu_Wpcom\Global_Styles\Data_Set
28     */
29    private $rest_api_data;
30
31    /**
32     * Namespace to use for the REST Endpoint.
33     * This can be overrided at initialization.
34     *
35     * @var string
36     */
37    private $rest_namespace = 'jetpack-global-styles/v1';
38
39    /**
40     * Route to use for the REST Route.
41     * This can be overrided at initialization.
42     *
43     * @var string
44     */
45    private $rest_route = 'options';
46
47    /**
48     * Path the client will use to work with the options.
49     * This can be overrided at initialization.
50     *
51     * @var string
52     */
53    private $rest_path_client = 'jetpack-global-styles/v1/options';
54
55    /**
56     * Undocumented variable
57     *
58     * @var string
59     */
60    private $theme_support = 'jetpack-global-styles';
61
62    /**
63     * Undocumented variable
64     *
65     * @var string
66     */
67    private $option_name = 'jetpack_global_styles';
68
69    /**
70     * Undocumented variable
71     *
72     * @var string
73     */
74    private $redux_store_name = 'jetpack/global-styles';
75
76    /**
77     * Undocumented variable
78     *
79     * @var string
80     */
81    private $plugin_name = 'jetpack-global-styles';
82
83    /**
84     * The provider's root URL for retrieving font CSS.
85     *
86     * @var string
87     */
88    private $root_url = 'https://fonts.googleapis.com/css';
89
90    const VERSION = '2003121439';
91
92    const SYSTEM_FONT     = '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
93    const AVAILABLE_FONTS = array(
94        array(
95            'label' => 'Theme Default',
96            'value' => 'unset',
97        ),
98        array(
99            'label' => 'System Font',
100            'value' => self::SYSTEM_FONT,
101        ),
102        'Arvo',
103        'Bodoni Moda',
104        'Cabin',
105        'Chivo',
106        'Courier Prime',
107        'DM Sans',
108        'Domine',
109        'EB Garamond',
110        'Fira Sans',
111        'Inter',
112        'Josefin Sans',
113        'Libre Baskerville',
114        'Libre Franklin',
115        'Lora',
116        'Merriweather',
117        'Montserrat',
118        'Nunito',
119        'Open Sans',
120        'Overpass',
121        'Playfair Display',
122        'Poppins',
123        'Raleway',
124        'Roboto',
125        'Roboto Slab',
126        'Rubik',
127        'Source Sans Pro',
128        'Source Serif Pro',
129        'Space Mono',
130        'Work Sans',
131    );
132
133    /**
134     * Creates instance.
135     *
136     * @return \Automattic\Jetpack\Jetpack_Mu_Wpcom\Global_Styles\Global_Styles
137     */
138    public static function init() {
139        if ( self::$instance === null ) {
140            self::$instance = new self();
141        }
142
143        return self::$instance;
144    }
145
146    /**
147     * Global Styles setup.
148     */
149    public function __construct() {
150        $this->update_plugin_settings();
151
152        // DATA TO EXPOSE THROUGH THE REST API.
153        require_once __DIR__ . '/includes/class-data-set.php';
154        $this->rest_api_data = new Data_Set(
155            array(
156                'blogname'              => array(
157                    'type'    => 'option',
158                    'name'    => 'blogname',
159                    'default' => 'Your site name',
160                ),
161                'font_base'             => array(
162                    'type'      => 'option',
163                    'name'      => array( 'jetpack_global_styles', 'font_base' ),
164                    'default'   => 'unset',
165                    'updatable' => true,
166                ),
167                'font_headings'         => array(
168                    'type'      => 'option',
169                    'name'      => array( 'jetpack_global_styles', 'font_headings' ),
170                    'default'   => 'unset',
171                    'updatable' => true,
172                ),
173                'font_base_default'     => array(
174                    'type'    => 'theme',
175                    'name'    => array( 'jetpack-global-styles', 'font_base' ),
176                    'default' => self::SYSTEM_FONT,
177                ),
178                'font_headings_default' => array(
179                    'type'    => 'theme',
180                    'name'    => array( 'jetpack-global-styles', 'font_headings' ),
181                    'default' => self::SYSTEM_FONT,
182                ),
183                'font_options'          => array(
184                    'type'    => 'literal',
185                    'default' => self::AVAILABLE_FONTS,
186                ),
187                'font_pairings'         => array(
188                    'type'    => 'literal',
189                    'default' => array(
190                        array(
191                            'label'    => 'Playfair Display / Fira Sans',
192                            'headings' => 'Playfair Display',
193                            'base'     => 'Fira Sans',
194                        ),
195                        array(
196                            'label'    => 'Cabin / Raleway',
197                            'headings' => 'Cabin',
198                            'base'     => 'Raleway',
199                        ),
200                        array(
201                            'label'    => 'Chivo / Open Sans',
202                            'headings' => 'Chivo',
203                            'base'     => 'Open Sans',
204                        ),
205                        array(
206                            'label'    => 'Arvo / Montserrat',
207                            'headings' => 'Arvo',
208                            'base'     => 'Montserrat',
209                        ),
210                        array(
211                            'label'    => 'Space Mono / Roboto',
212                            'headings' => 'Space Mono',
213                            'base'     => 'Roboto',
214                        ),
215                        array(
216                            'label'    => 'Bodoni Moda / Overpass',
217                            'headings' => 'Bodoni Moda',
218                            'base'     => 'Overpass',
219                        ),
220                        array(
221                            'label'    => 'Inter / Source Serif Pro',
222                            'headings' => 'Inter',
223                            'base'     => 'Source Serif Pro',
224                        ),
225                    ),
226                ),
227            )
228        );
229
230        // Setup REST API for the editor. Some environments (WordPress.com)
231        // may not load the theme functions for REST API calls,
232        // so we need to initialize it independently of theme support.
233        add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
234
235        add_filter( 'jetpack_global_styles_data_set_get_data', array( $this, 'maybe_filter_font_list' ) );
236        add_filter( 'jetpack_global_styles_data_set_save_data', array( $this, 'filter_and_validate_font_options' ) );
237
238        // Setup editor.
239        if ( $this->can_use_global_styles() ) {
240            add_action(
241                'enqueue_block_editor_assets',
242                array( $this, 'enqueue_block_editor_assets' )
243            );
244            add_filter(
245                'block_editor_settings',
246                array( $this, 'block_editor_settings' ),
247                PHP_INT_MAX // So it runs last and overrides any style provided by the theme.
248            );
249            add_action( 'customize_register', array( $this, 'register_customizer_option' ) );
250        }
251
252        // Setup front-end.
253        add_action(
254            'wp_enqueue_scripts',
255            array( $this, 'wp_enqueue_scripts' ),
256            PHP_INT_MAX // So it runs last and overrides any style provided by the theme.
257        );
258    }
259
260    /**
261     * Register customizer modifications
262     * Add the 'Font' section to customizer.
263     *
264     * @param \WP_Customize_Manager $wp_customize an instance of WP_Customize_Manager.
265     */
266    public function register_customizer_option( $wp_customize ) {
267        require_once __DIR__ . '/class-global-styles-fonts-message-control.php';
268
269        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_tracks_events_fonts_section_control' ) );
270
271        $wp_customize->add_section(
272            'global_styles_fonts_section',
273            array(
274                'title' => __( 'Fonts', 'jetpack-mu-wpcom' ),
275            )
276        );
277
278        $wp_customize->add_control(
279            new Global_Styles_Fonts_Message_Control(
280                $wp_customize,
281                'global_styles_fonts_message_control',
282                array(
283                    'section'  => 'global_styles_fonts_section',
284                    'settings' => array(),
285                )
286            )
287        );
288    }
289
290    /**
291     * Enqueue script customizer_fonts which executes tracks events when clicking the block editor and support links from the 'Fonts' section.
292     */
293    public function enqueue_tracks_events_fonts_section_control() {
294        $asset_file = include Jetpack_Mu_Wpcom::BASE_DIR . 'build/jetpack-global-styles-customizer-fonts/jetpack-global-styles-customizer-fonts.asset.php';
295        wp_enqueue_script(
296            'jetpack-global-styles-customizer-fonts',
297            plugins_url( 'build/jetpack-global-styles-customizer-fonts/jetpack-global-styles-customizer-fonts.js', Jetpack_Mu_Wpcom::BASE_FILE ),
298            $asset_file['dependencies'] ?? array(),
299            $asset_file['version'] ?? filemtime( Jetpack_Mu_Wpcom::BASE_DIR . 'build/jetpack-global-styles-customizer-fonts/jetpack-global-styles-customizer-fonts.js' ),
300            true
301        );
302
303        $current_user       = wp_get_current_user();
304        $tracks_events_data = array();
305        if ( $current_user->exists() ) {
306            $tracks_events_data['user_id']    = (int) $current_user->ID;
307            $tracks_events_data['user_login'] = $current_user->user_login;
308        }
309
310        wp_localize_script(
311            'jetpack-global-styles-customizer-fonts',
312            'tracks_events_fonts_section_control_variables',
313            $tracks_events_data
314        );
315    }
316
317    /**
318     * Let 3rd parties configure plugin settings.
319     */
320    private function update_plugin_settings() {
321        $settings = apply_filters(
322            'jetpack_global_styles_settings',
323            array(
324                // Server-side settings.
325                'rest_namespace'   => $this->rest_namespace,
326                'rest_route'       => $this->rest_route,
327                'theme_support'    => $this->theme_support,
328                'option_name'      => $this->option_name,
329                // Client-side settings.
330                'rest_path_client' => $this->rest_path_client,
331                'redux_store_name' => $this->redux_store_name,
332                'plugin_name'      => $this->plugin_name,
333            )
334        );
335
336        $this->rest_namespace   = $settings['rest_namespace'];
337        $this->rest_route       = $settings['rest_route'];
338        $this->theme_support    = $settings['theme_support'];
339        $this->option_name      = $settings['option_name'];
340        $this->rest_path_client = $settings['rest_path_client'];
341        $this->redux_store_name = $settings['redux_store_name'];
342        $this->plugin_name      = $settings['plugin_name'];
343    }
344
345    /**
346     * Initialize REST API endpoint.
347     */
348    public function rest_api_init() {
349        require_once __DIR__ . '/includes/class-json-endpoint.php';
350        $rest_api = new JSON_Endpoint(
351            $this->rest_namespace,
352            $this->rest_route,
353            $this->rest_api_data,
354            array( $this, 'can_use_global_styles' )
355        );
356        $rest_api->setup();
357    }
358
359    /**
360     * Whether we should load Global Styles
361     * per this user and site.
362     *
363     * @return boolean
364     */
365    public function can_use_global_styles() {
366        return is_user_logged_in() &&
367            current_user_can( 'customize' ) &&
368            current_theme_supports( $this->theme_support ) &&
369            apply_filters( 'jetpack_global_styles_permission_check_additional', true );
370    }
371
372    /**
373     * We want the front-end styles enqueued in the editor
374     * and wrapped by the .editor-styles-wrapper class,
375     * so they don't bleed into other parts of the editor.
376     *
377     * We also want the global styles to override the theme's stylesheet.
378     * We do so by hooking into the block_editor_settings
379     * and append this style the last.
380     *
381     * @param array $settings The editor settings.
382     *
383     * @return array $settings array with the inline styles added.
384     */
385    public function block_editor_settings( $settings ) {
386        if ( empty( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
387            $settings['styles'] = array();
388        }
389
390        // Append them last, so it overrides any existing inline styles.
391        $settings['styles'][] = array(
392            'css' => $this->get_inline_css(),
393        );
394
395        return $settings;
396    }
397
398    /**
399     * Enqueues the assets for the editor.
400     *
401     * @return void
402     */
403    public function enqueue_block_editor_assets() {
404        $asset_file = include Jetpack_Mu_Wpcom::BASE_DIR . 'build/jetpack-global-styles/jetpack-global-styles.asset.php';
405        wp_enqueue_script(
406            'jetpack-global-styles-editor-script',
407            plugins_url( 'build/jetpack-global-styles/jetpack-global-styles.js', Jetpack_Mu_Wpcom::BASE_FILE ),
408            $asset_file['dependencies'] ?? array(),
409            $asset_file['version'] ?? filemtime( Jetpack_Mu_Wpcom::BASE_DIR . 'build/jetpack-global-styles/jetpack-global-styles.js' ),
410            true
411        );
412        wp_set_script_translations( 'jetpack-global-styles-editor-script', 'jetpack-mu-wpcom' );
413        wp_localize_script(
414            'jetpack-global-styles-editor-script',
415            'JETPACK_GLOBAL_STYLES_EDITOR_CONSTANTS',
416            array(
417                'PLUGIN_NAME' => $this->plugin_name,
418                'REST_PATH'   => $this->rest_path_client,
419                'STORE_NAME'  => $this->redux_store_name,
420            )
421        );
422        wp_enqueue_style(
423            'jetpack-global-styles-editor-style',
424            plugins_url( 'build/jetpack-global-styles/jetpack-global-styles.css', Jetpack_Mu_Wpcom::BASE_FILE ),
425            array(),
426            $asset_file['version'] ?? filemtime( Jetpack_Mu_Wpcom::BASE_DIR . 'build/jetpack-global-styles/jetpack-global-styles.css' )
427        );
428    }
429
430    /**
431     * Enqueues the assets for front-end.
432     *
433     * We want the global styles to override the theme's stylesheet,
434     * that's why they are inlined.
435     *
436     * @return void
437     */
438    public function wp_enqueue_scripts() {
439        wp_register_style(
440            'jetpack-global-styles-frontend-style',
441            false,
442            array(),
443            true
444        );
445        wp_add_inline_style( 'jetpack-global-styles-frontend-style', $this->get_inline_css( true ) );
446        wp_enqueue_style( 'jetpack-global-styles-frontend-style' );
447    }
448
449    /**
450     * Prepare the inline CSS.
451     *
452     * @param boolean $only_selected_fonts Whether it should load all the fonts or only the selected. False by default.
453     * @return string
454     */
455    private function get_inline_css( $only_selected_fonts = false ) {
456        /**
457         * Filters the Google Fonts API URL.
458         *
459         * @param string $url The Google Fonts API URL.
460         */
461        $root_url = esc_url( apply_filters( 'jetpack_global_styles_google_fonts_api_url', $this->root_url ) );
462
463        $result = '';
464
465        $data = $this->rest_api_data->get_data();
466
467        /*
468         * Add the fonts we need:
469         *
470         * - all of them for the backend
471         * - only the selected ones for the frontend
472         */
473        $font_list = array();
474        // We want $font_list to only contain valid Google Font values,
475        // so we filter out things like 'unset' on the system font.
476        $font_values = array_diff( $this->get_font_values( $data['font_options'] ), array( 'unset', self::SYSTEM_FONT ) );
477        if ( true === $only_selected_fonts ) {
478            foreach ( array( 'font_base', 'font_base_default', 'font_headings', 'font_headings_default' ) as $key ) {
479                if ( in_array( $data[ $key ], $font_values, true ) ) {
480                    $font_list[] = $data[ $key ];
481                }
482            }
483        } else {
484            $font_list = $font_values;
485        }
486
487        if ( count( $font_list ) > 0 ) {
488            $font_list_str = '';
489            foreach ( $font_list as $font ) {
490                // Some fonts lack italic variants,
491                // the API will return only the regular and bold CSS for those.
492                $font_list_str = $font_list_str . $font . ':thin,extralight,light,regular,medium,semibold,bold,italic,bolditalic,extrabold,black|';
493            }
494            $result = $result . "@import url('" . $root_url . '?family=' . $font_list_str . "');";
495        }
496
497        /*
498         * Add the CSS custom properties.
499         *
500         * Note that we transform var_name into var-name,
501         * so the output is:
502         *
503         * :root{
504         *     --var-name-1: value;
505         *     --var-name-2: value;
506         * }
507         *
508         */
509        $result .= ':root {';
510        $value   = '';
511        $keys    = array( 'font_headings', 'font_base', 'font_headings_default', 'font_base_default' );
512        foreach ( $keys as $key ) {
513            $value   = $data[ $key ];
514            $result .= ' --' . str_replace( '_', '-', $key ) . ': ' . $value . ';';
515        }
516        $result .= '}';
517
518        /*
519         * If the theme opts-in, also add a default stylesheet
520         * that uses the CSS custom properties.
521         *
522         * This is a fallback mechanism in case there are themes
523         * we don't want to / can't migrate to use CSS vars.
524         */
525        $theme_defaults = get_theme_support( $this->theme_support );
526        if ( is_array( $theme_defaults ) ) {
527            $theme_defaults = $theme_defaults[0];
528        }
529
530        if (
531            is_array( $theme_defaults ) &&
532            array_key_exists( 'enqueue_experimental_styles', $theme_defaults ) &&
533            true === $theme_defaults['enqueue_experimental_styles']
534        ) {
535            $result .= file_get_contents( plugin_dir_path( __FILE__ ) . 'static/style.css', true );
536        }
537
538        return $result;
539    }
540
541    /**
542     * Callback for get data filter.
543     *
544     * @param array $result Data to be sent through the REST API.
545     *
546     * @return array Filtered result.
547     */
548    public function maybe_filter_font_list( $result ) {
549        $theme_defaults = get_theme_support( $this->theme_support );
550        if ( is_array( $theme_defaults ) ) {
551            $theme_defaults = $theme_defaults[0];
552        }
553
554        if (
555            array_key_exists( 'font_options', $result ) &&
556            (
557                ! is_array( $theme_defaults ) ||
558                ! array_key_exists( 'enable_theme_default', $theme_defaults ) ||
559                true !== $theme_defaults['enable_theme_default']
560            )
561        ) {
562            $result['font_options'] = array_slice( $result['font_options'], 1 );
563        }
564
565        return $result;
566    }
567
568    /**
569     * Return the list of available font values.
570     *
571     * @param array $font_list Array of fonts to process.
572     * @return array Font values.
573     */
574    private function get_font_values( $font_list ) {
575        $font_values = array();
576        foreach ( $font_list as $font ) {
577            if ( is_array( $font ) ) {
578                $font_values[] = $font['value'];
579            } else {
580                $font_values[] = $font;
581            }
582        }
583        return $font_values;
584    }
585
586    /**
587     * Callback for save data filter.
588     *
589     * @param array $incoming_data The data to validate.
590     * @return array Filtered result.
591     */
592    public function filter_and_validate_font_options( $incoming_data ) {
593        $result = array();
594
595        $font_values = $this->get_font_values( self::AVAILABLE_FONTS );
596        foreach ( array( 'font_base', 'font_headings' ) as $key ) {
597            if (
598                array_key_exists( $key, $incoming_data ) &&
599                in_array( $incoming_data[ $key ], $font_values, true )
600            ) {
601                $result[ $key ] = $incoming_data[ $key ];
602            }
603        }
604
605        return $result;
606    }
607}
608
609add_action( 'init', array( __NAMESPACE__ . '\Global_Styles', 'init' ) );