Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 842
0.00% covered (danger)
0.00%
0 / 62
CRAP
0.00% covered (danger)
0.00%
0 / 1
add_color_rule
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
add_color_palette
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
colors_manager_gutenberg_load
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
load_corresponding_color_manager
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
Colors_Manager_Common
0.00% covered (danger)
0.00%
0 / 833
0.00% covered (danger)
0.00%
0 / 58
65792
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
56
 is_gutenberg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_classic_stats
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 modify_admin_menu_links
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 core_bg_enqueue_styles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 core_bg_admin_notice
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 pick_theme
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 has_annotations
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 theme_has_set_colors
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 will_never_support
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 admin_scripts_and_css
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 register_scripts_and_styles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 body_class
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 spinner_scripts
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_colors
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_default_colors
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_color_slots
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 color_grid
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
30
 print_current_color_grid
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 ajax_color_palettes
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ajax_generate_palette
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 ajax_color_recommendations
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 ajax_pattern_recommendations
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 format_colourlovers_urls
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 save_colourlovers_metadata
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
552
 is_same_color
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 is_default_palette
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 is_featured_palette
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 should_enable_colors
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 get_color_palettes
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
132
 gravatar_image_url
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 get_generated_palette
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 get_theme_color_palettes
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 get_patterns
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 normalize_color
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 get_color_recommendations
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
272
 get_pattern_recommendations
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
72
 color_palettes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 color_patterns
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 in_customizer
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
6
 sanitize_colors_on_save
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sanitize_colors
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 override_themecolors
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 theme_colors_js
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 print_theme_css
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 print_block_editor_css
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 get_theme_css
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 css_rule
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
272
 get_extra_css
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 add_color_rule
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 add_color_palette
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 load_annotations
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 handle_unset_colors
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 prime_color_labels
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 color_suggestions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 color_suggestions_from_palette
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
20
 exception_mailer
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 color_suggestions_from_math
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
380
Colors_Manager
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
Colors_Manager_Gutenberg
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
1<?php  // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2/**
3 * Plugin Name: Custom Colors
4 * Plugin URI: http://automattic.com/
5 * Description: Part of the WordPress.com Custom Design upgrade, this plugin allows you to easily add a customized color palette and background pattern to your blog.
6 * Version: 1.1
7 * Author: Automattic
8 * Author URI: http://automattic.com/
9 * License: GNU General Public License v2 or later
10 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
11 */
12
13// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed
14// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
15
16/**
17 * The common color manager class.
18 */
19class Colors_Manager_Common {
20
21    /**
22     * Colors.
23     *
24     * @var array
25     */
26    protected static $colors = array();
27
28    /**
29     * Default colors.
30     *
31     * @var array
32     */
33    protected static $default_colors = array();
34
35    /**
36     * Text colors.
37     *
38     * @var array
39     */
40    protected static $text_colors = array();
41
42    /**
43     * Extra colors.
44     *
45     * @var array
46     */
47    protected static $extra_colors = array();
48
49    /**
50     * Labels.
51     *
52     * @var array
53     */
54    protected static $labels = array();
55
56    /**
57     * Color pallettes..
58     *
59     * @var array
60     */
61    protected static $color_palettes = array();
62
63    /**
64     * If we're using Gutenberg or not.
65     *
66     * @var boolean
67     */
68    protected static $is_gutenberg = false;
69
70    /**
71     * Themes that will never support Custom Colors.
72     *
73     * Criteria for not supporting:
74     * 1. Two years or older and not in the top 50 is usage.
75     * 2. Not a good match because of odd use of colors or images.
76     * 3. Retired, ignored, and mobile.
77     *
78     * @var array
79     */
80    protected static $never_support = array(
81        'pub/almost-spring',
82        'pub/banana-smoothie',
83        'pub/blue-green',
84        'pub/classic',
85        'pub/connections',
86        'pub/dark-wood',
87        'pub/daydream',
88        'pub/duotone',
89        'pub/dusk',
90        'pub/duster',
91        'pub/emire',
92        'pub/fadtastic',
93        'pub/fauna',
94        'pub/fleur',
95        'pub/flower-power',
96        'pub/fresh-bananas',
97        'pub/fusion',
98        'pub/green-marinee',
99        'pub/grid-focus',
100        'pub/hemingway',
101        'pub/jentri',
102        'pub/journalist-13',
103        'pub/k2',
104        'pub/kubrick',
105        'pub/light',
106        'pub/minileven',
107        'pub/monotone',
108        'pub/neat',
109        'pub/neo-sapien-05',
110        'pub/notesil',
111        'pub/ocadia',
112        'pub/pool',
113        'pub/prologue',
114        'pub/quentin',
115        'pub/redoable-lite',
116        'pub/rounded',
117        'pub/rubric',
118        'pub/sandbox',
119        'pub/sandbox-10',
120        'pub/sandbox-16',
121        'pub/sandbox-161',
122        'pub/sandbox-162',
123        'pub/sapphire',
124        'pub/silver-black',
125        'pub/solipsus',
126        'pub/steira',
127        'pub/sunburn',
128        'pub/supposedly-clean',
129        'pub/sweet-blossoms',
130        'pub/tarski',
131        'pub/thirteen',
132        'pub/toni',
133        'pub/toolbox',
134        'pub/treba',
135        'pub/twenty-eight',
136        'pub/under-the-influence',
137        'pub/unsleepable',
138        'pub/vermilion-christmas',
139        'pub/whiteasmilk',
140        'pub/wp-mobile',
141        'pub/wptouch',
142        'pub/_s',
143    );
144
145    const COLOURLOVERS_HOST = 'http://colourlovers.com.s3.amazonaws.com/';
146
147    /**
148     * Initialize the object.
149     */
150    public static function init() {
151        if ( ! apply_filters( 'enable_custom_customizer', true ) ) {
152            return;
153        }
154
155        if ( ! self::is_gutenberg() ) {
156            // Classic Background stats
157            add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_classic_stats' ) );
158            // always load ajax actions
159            add_action( 'wp_ajax_color_palettes', array( __CLASS__, 'ajax_color_palettes' ) );
160            add_action( 'wp_ajax_generate_palette', array( __CLASS__, 'ajax_generate_palette' ) );
161            add_action( 'wp_ajax_color_recommendations', array( __CLASS__, 'ajax_color_recommendations' ) );
162            add_action( 'wp_ajax_pattern_recommendations', array( __CLASS__, 'ajax_pattern_recommendations' ) );
163
164            // Notice in the core bg admin screen
165            add_action( 'admin_print_styles-appearance_page_custom-background', array( __CLASS__, 'core_bg_enqueue_styles' ) );
166            add_action( 'admin_notices', array( __CLASS__, 'core_bg_admin_notice' ) );
167
168            // Replace the Backgrounds link with a link to this plugin's section
169            add_action( 'admin_menu', array( __CLASS__, 'modify_admin_menu_links' ) );
170
171            // Load the Colors API class for fetching palettes and patterns from WordPress.com.
172            require_once __DIR__ . '/colors-api.php';
173
174            $current_theme = get_option( 'stylesheet' );
175
176            // High priority so that no other code manages to modify our URL before we do.  The default URL
177            // saved for background_image isn't meant to ever be used as is.
178            add_filter( 'pre_update_option_theme_mods_' . $current_theme, array( __CLASS__, 'format_colourlovers_urls' ), 1, 2 );
179            add_action( 'update_option_theme_mods_' . $current_theme, array( __CLASS__, 'save_colourlovers_metadata' ), 10, 2 );
180
181            add_action( 'init', array( __CLASS__, 'register_scripts_and_styles' ), 20 );
182
183            // stuff for the customizer - only load if there are annotations.
184            if ( self::has_annotations() ) {
185                add_action( 'customize_register', array( __CLASS__, 'in_customizer' ), 10 );
186                add_action( 'customize_register', array( __CLASS__, 'theme_colors_js' ) );
187                add_action( 'customize_controls_init', array( __CLASS__, 'spinner_scripts' ) );
188            }
189
190            // CSS only to be printed if colors are set.
191            if ( self::theme_has_set_colors() ) {
192                self::override_themecolors();
193                add_filter( 'body_class', array( __CLASS__, 'body_class' ) );
194                add_action( 'wp_head', array( __CLASS__, 'print_theme_css' ), 20 );
195            }
196
197            add_filter( 'tonesque_image_url', array( __CLASS__, 'gravatar_image_url' ) );
198        }
199
200        if ( self::is_gutenberg() ) {
201            // If colors are set, print them in the Block Editor as well.
202            if ( self::theme_has_set_colors() ) {
203                self::override_themecolors();
204                add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'print_block_editor_css' ), 20 );
205            }
206        }
207    }
208
209    /**
210     * Checks if we're in Gutenberg (Editor) mode.
211     *
212     * @see https://stackoverflow.com/a/14919877
213     */
214    private static function is_gutenberg() {
215        return static::$is_gutenberg;
216    }
217
218    /**
219     * Adds classic Stats assets to loading queue.
220     *
221     * @param string $hook the current hook name.
222     */
223    public static function enqueue_classic_stats( $hook ) {
224        if ( 'appearance_page_custom-background' === $hook ) {
225            wp_enqueue_script(
226                'custom-bg-classic-stats',
227                plugins_url( 'js/classic-background-stats.js', __FILE__ ),
228                array( 'jquery' ),
229                '20140310',
230                true
231            );
232        }
233    }
234
235    /**
236     * The Background menu in wp-admin autofocuses the Background section in the
237     * Customizer, but on wpcom that section is removed, so we need to redirect
238     * that link to this section instead.
239     */
240    public static function modify_admin_menu_links() {
241        global $submenu;
242        if ( ! isset( $submenu ) || ! isset( $submenu['themes.php'] ) || ! isset( $submenu['themes.php'][20] ) ) {
243            return;
244        }
245        $colors_section               = admin_url( 'customize.php?autofocus%5Bsection%5D=colors_manager_tool' );
246        $submenu['themes.php'][20][2] = esc_url( $colors_section ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
247    }
248
249    /**
250     * Enqueues styles for Core notices.
251     */
252    public static function core_bg_enqueue_styles() {
253        wp_enqueue_style( 'colors-core-bg-notice', plugins_url( 'css/core-bg-notice.css', __FILE__ ) ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
254    }
255
256    /**
257     * Enqueues styles for Core admin notices.
258     */
259    public static function core_bg_admin_notice() {
260        // just Appearance -> Background
261        if ( 'appearance_page_custom-background' !== $GLOBALS['page_hook'] ) {
262            return;
263        }
264
265        require __DIR__ . '/core-bg-admin-notice.php';
266    }
267
268    /**
269     * A helper function to pick an unspecified theme based on the current context.
270     *
271     * @param  ?boolean|string $theme A theme that, if false, the function will specify.
272     * @return string         The theme.
273     */
274    protected static function pick_theme( $theme = false ) {
275        if ( false !== $theme ) {
276            return $theme;
277        }
278
279        $theme = get_option( 'stylesheet' );
280
281        // In an Ajax call from the Customizer, we might be previewing a separate theme.
282        // Detect that and use it if it's there.
283
284        if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
285            if ( ! isset( $_SERVER['HTTP_REFERER'] ) ) {
286                return $theme;
287            }
288            $parsed_url = wp_parse_url( sanitize_url( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) );
289            if ( $parsed_url && ! isset( $parsed_url['query'] ) ) {
290                return $theme;
291            }
292            wp_parse_str( $parsed_url['query'] ?? '', $query_parts );
293            if ( isset( $query_parts['theme'] ) ) {
294                return $query_parts['theme'];
295            }
296        }
297        return $theme;
298    }
299
300    /**
301     * Does the theme have annotations? Will load them as well.
302     *
303     * @param  ?boolean|string $theme A theme or false.
304     * @return boolean theme has annotations
305     */
306    public static function has_annotations( $theme = false ) {
307        // if we're not gonna support it, avoid the filesys hit
308        if ( self::will_never_support( $theme ) ) {
309            return false;
310        }
311        // if $colors is populated, we've run some add_color_rule calls.
312        // but skip if we directly asked for a theme to avoid false positives
313        if ( ! $theme && ! empty( self::$colors ) ) {
314            return true;
315        }
316        // if we called a direct string, we probably don't want to actually load them
317        if ( $theme ) {
318            $file = get_template_directory() . '/inc/wpcom-colors.php';
319            return is_readable( $file );
320        }
321        // try to load annotations, which returns status of finding them.
322        return self::load_annotations( $theme );
323    }
324
325    /**
326     * Do we have colors to work with?
327     *
328     * @return boolean active state
329     */
330    public static function theme_has_set_colors() {
331        $opts = get_theme_mod( 'colors_manager', array( 'colors' => false ) );
332
333        if ( ! isset( $opts['colors'] ) ) {
334            return false;
335        }
336
337        $opts = $opts['colors'];
338        // need the softer non-equal on the last in case keys are in different order.
339        return self::has_annotations() && (bool) $opts && $opts !== self::get_default_colors();
340    }
341
342    /**
343     * Will the theme never support Custom Colors?
344     *
345     * @param  boolean|string $theme Optional theme slug. Uses current theme by default.
346     * @return boolean
347     */
348    public static function will_never_support( $theme = false ) {
349        $theme = self::pick_theme( $theme );
350        return in_array( $theme, self::$never_support, true );
351    }
352
353    /**
354     * Admin Javascript and CSS
355     */
356    public static function admin_scripts_and_css() {
357        wp_enqueue_style( 'colors-tool' );
358        wp_enqueue_style( 'noticons' );
359        wp_enqueue_script( 'colors-tool' );
360
361        $settings = array(
362            'defaultColors'     => self::get_default_colors(),
363            'themeSupport'      => array( 'customBackground' => current_theme_supports( 'custom-background' ) ),
364            'defaultImage'      => get_theme_support( 'custom-background', 'default-image' ),
365            'topPatterns'       => self::get_patterns( array( 'limit' => 30 ) ),
366            'genPalette'        => __( 'Generating...', 'wpcomsh' ),
367            'backgroundTitle'   => __( 'Background', 'wpcomsh' ),
368            'colorsTitle'       => __( 'Colors', 'wpcomsh' ),
369            'mediaTitle'        => __( 'Select background image', 'wpcomsh' ),
370            'mediaSelectButton' => __( 'Select', 'wpcomsh' ),
371        );
372
373        wp_localize_script( 'colors-tool', 'ColorsTool', $settings );
374    }
375
376    /**
377     * Registers scripts and styles.
378     */
379    public static function register_scripts_and_styles() {
380        // register styles
381        wp_register_style( 'colors-tool', plugins_url( 'css/colors-control.css', __FILE__ ), array(), '20220727' );
382        wp_register_style( 'noticons', '//s0.wp.com/i/noticons/noticons.css', array(), '20120621', 'all' );
383
384        // register scripts
385        wp_register_script( 'Color.js', plugins_url( 'js/color.js', __FILE__ ), array(), '20121210', true );
386        wp_register_script( 'colors-instapreview', plugins_url( 'js/colors-theme-preview.js', __FILE__ ), array( 'customize-preview', 'jquery', 'Color.js' ), '20121210', true );
387        wp_register_script( 'colors-tool', plugins_url( 'js/colors-control.js', __FILE__ ), array( 'customize-controls', 'iris' ), '20250102', true );
388        wp_register_script( 'spin', plugins_url( 'js/spin.js', __FILE__ ), array(), '1.3', true );
389        wp_register_script( 'jquery.spin', plugins_url( 'js/jquery.spin.js', __FILE__ ), array( 'spin' ), '20210111', true );
390    }
391
392    /**
393     * Add a 'custom-colors' body class to blogs with Custom Colors active.
394     *
395     * @param array $classes the array of classes to add custom class to.
396     */
397    public static function body_class( $classes ) {
398        $classes[] = 'custom-colors';
399        return $classes;
400    }
401
402    /**
403     * Enqueue WP.com spinner scripts.
404     */
405    public static function spinner_scripts() {
406        wp_enqueue_script( 'spin' );
407        wp_enqueue_script( 'jquery.spin' );
408    }
409
410    /**
411     * Constructs the color array
412     */
413    public static function get_colors() {
414        $opts   = get_theme_mod( 'colors_manager', array( 'colors' => false ) );
415        $colors = ! empty( $opts['colors'] ) ? $opts['colors'] : self::$default_colors;
416        unset( $colors['undefined'] );
417        return $colors;
418    }
419
420    /**
421     * Returns default colors.
422     */
423    public static function get_default_colors() {
424        return self::$default_colors;
425    }
426
427    /**
428     * Returns color slots.
429     */
430    public static function get_color_slots() {
431        return array( 'bg', 'txt', 'link', 'fg1', 'fg2' );
432    }
433
434    /**
435     * The Color Grid
436     *
437     * This method outputs the core UI structure of the colors tool
438     * Includes color_palettes.
439     */
440    public static function color_grid() {
441        ?>
442        <script type="text/template" id="tmpl-background-change">
443            <div class="background-rectangle">
444                <div class="done"><span class="float-button"><?php esc_html_e( 'Done', 'wpcomsh' ); ?></span></div>
445            </div>
446            <a class="button background-options"><?php esc_html_e( 'Options', 'wpcomsh' ); ?></a>
447            <a class="button select-image"><?php esc_html_e( 'Select Image', 'wpcomsh' ); ?></a>
448            <div class="sep"></div>
449            <div class="view background-options"></div>
450        </script>
451
452        <script type="text/template" id="tmpl-background-options">
453            <p class="radios">
454                <?php esc_html_e( 'Position', 'wpcomsh' ); ?>
455                <input type="radio" id="position_x_right" name="position_x" value="right">
456                <label title="<?php esc_attr_e( 'Right', 'wpcomsh' ); ?>" for="position_x_right"><span class="dashicons dashicons-editor-alignright"></span></label>
457                <input type="radio" id="position_x_center" name="position_x" value="center">
458                <label title="<?php esc_attr_e( 'Center', 'wpcomsh' ); ?>" for="position_x_center"><span class="dashicons dashicons-editor-aligncenter"></span></label>
459                <input type="radio" id="position_x_left" name="position_x" value="left">
460                <label title="<?php esc_attr_e( 'Left', 'wpcomsh' ); ?>" for="position_x_left"><span class="dashicons dashicons-editor-alignleft"></span></label>
461            </p>
462
463            <p class="radios">
464                <?php esc_html_e( 'Repeat', 'wpcomsh' ); ?>
465                <input type="radio" id="repeat" name="repeat" value="repeat">
466                <label title="<?php esc_attr_e( 'Tile', 'wpcomsh' ); ?>" for="repeat"><span class="noticon noticon-gridview"></span></label>
467                <input type="radio" id="repeat-y" name="repeat" value="repeat-y">
468                <label title="<?php esc_attr_e( 'Vertically', 'wpcomsh' ); ?>" for="repeat-y"><span class="noticon noticon-tile-vertically"></label>
469                <input type="radio" id="repeat-x" name="repeat" value="repeat-x">
470                <label title="<?php esc_attr_e( 'Horizontally', 'wpcomsh' ); ?>" for="repeat-x"><span class="noticon noticon-tile-horizontally"></label>
471                <input type="radio" id="repeat-no-repeat" name="repeat" value="no-repeat">
472                <label title="<?php esc_attr_e( 'None', 'wpcomsh' ); ?>" for="repeat-no-repeat"><span class="noticon noticon-tile-none"></label>
473            </p>
474
475            <p class="radios">
476                <?php esc_html_e( 'Fixed Position', 'wpcomsh' ); ?>
477                <input id="attachment-fixed" type="checkbox" name="attachment" value="fixed">
478                <label for="attachment-fixed"><span class="dashicons dashicons-admin-post"></span></label>
479            </p>
480
481            <p class="radios">
482                <?php esc_html_e( 'Underlying color', 'wpcomsh' ); ?>
483                <input id="underlying-color" class="underlying-color" name="color">
484                <label for="underlying-color" class="underlying-color"><span class="dashicons"></span></label>
485            </p>
486
487            <div class="iris-container"></div>
488
489            <p class="bottom">
490                <a href="#" class="hide-image"><?php esc_html_e( 'Hide background image', 'wpcomsh' ); ?></a>
491            </p>
492
493        </script>
494
495        <div id="background-change">
496        </div>
497        <div id="color-picker" class="color-picker">
498            <ul class="color-grid main" id="color-grid">
499                <?php
500                foreach ( self::get_color_slots() as $cat ) {
501                    $class = isset( self::$colors[ $cat ] ) ? $cat : "{$cat} unavailable";
502                    if ( 'bg' === $cat ) {
503                        // background is always available for back compat with core
504                        $class = 'bg';
505                    }
506                    printf(
507                        '<li data-role="%s" class="%s clr" data-title="%s">',
508                        esc_attr( $cat ),
509                        esc_attr( $class ),
510                        esc_attr( self::$labels[ $cat ] )
511                    );
512                    if ( 'bg' === $cat ) {
513                        printf(
514                            '<span class="change-background float-button">%s</span>',
515                            esc_html__( 'Change', 'wpcomsh' )
516                        );
517                    }
518                    printf( '</li>' );
519                }
520                ?>
521            </ul>
522            <span class="action-button-wrap">
523                <a class="revert revert-default button" title="<?php esc_attr_e( 'Go back to your theme&rsquo;s default colors', 'wpcomsh' ); ?>"><?php esc_html_e( 'Default', 'wpcomsh' ); ?></a>
524            </span>
525            <span id="color-tooltip"></span>
526            <div id="the-bg-picker-prompt" style="display: none;">
527                <span class="customize-control-title"><?php esc_html_e( 'Customize Your Background', 'wpcomsh' ); ?></span>
528                <div>
529                    <a href="#" class="bg choose-color">O</a>
530                    <h4>Change <b>Color</b></h4>
531                </div>
532                <div>
533                    <a href="#" class="bg choose-pattern">O</a>
534                    <h4>Choose <b>Image</b></h4>
535                </div>
536            </div>
537            <div class="the-picker" id="the-picker">
538                <span class="color-label" id="color-reference"></span>
539                <p><?php esc_html_e( 'These are colors that work well with the other colors in your palette:', 'wpcomsh' ); ?></p>
540                <ul class="color-suggestions">
541                    <li></li>
542                    <li></li>
543                    <li></li>
544                    <li></li>
545                    <li></li>
546                    <li></li>
547                    <li></li>
548                    <li></li>
549                    <li></li>
550                    <li></li>
551                    <li></li>
552                    <li></li>
553                </ul>
554                <p class="iris-launch">
555                    <?php
556                    echo wp_kses(
557                        __( 'You can also <a href="#" id="pick-your-nose">pick your own color</a>.', 'wpcomsh' ),
558                        array(
559                            'a' => array(
560                                'href' => array(),
561                                'id'   => array(),
562                            ),
563                        )
564                    );
565                    ?>
566                </p>
567                <div id="iris-container" class="hidden">
568                    <input type="text" id="iris" />
569                </div>
570            </div>
571            <?php Colors_Manager::color_palettes(); ?>
572            <?php Colors_Manager::color_patterns(); ?>
573        </div>
574        <?php
575    }
576
577    /**
578     * Prints current color grid.
579     */
580    public static function print_current_color_grid() {
581        if ( ! self::theme_has_set_colors() ) {
582            return;
583        }
584        ?>
585        <ul class="color-grid main">
586            <?php
587            foreach ( self::get_colors() as $cat => $value ) {
588                $class = isset( self::$colors[ $cat ] ) ? $cat : "{$cat} unavailable";
589                printf(
590                    '<li class="%s" style="background-color: %s" title="%s">%s</li>',
591                    esc_attr( $class ),
592                    esc_attr( $value ),
593                    esc_attr( self::$labels[ $cat ] ),
594                    esc_html( $value )
595                );
596            }
597            ?>
598        </ul>
599        <?php
600    }
601
602    /**
603     * Outputs color pallettes for AJAX requests.
604     *
605     * @return never
606     */
607    public static function ajax_color_palettes() {
608        $palettes = self::get_color_palettes( $_REQUEST ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is a GET request that doesn't change anything.
609
610        $response = array( 'palettes' => $palettes );
611
612        header( 'Content-Type: text/javascript' );
613        // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
614        wp_send_json( $response, null, JSON_UNESCAPED_SLASHES );
615    }
616
617    /**
618     * Outputs generated color pallette for AJAX requests.
619     *
620     * @return never
621     */
622    public static function ajax_generate_palette() {
623        $response = self::get_generated_palette( $_REQUEST );  // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is a GET request that doesn't change anything.
624        header( 'Content-Type: text/javascript' );
625        // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
626        wp_send_json( $response, null, JSON_UNESCAPED_SLASHES );
627    }
628
629    /**
630     * Outputs color recommendations for AJAX requests.
631     *
632     * @return never
633     */
634    public static function ajax_color_recommendations() {
635        $colors = self::get_color_recommendations( $_REQUEST );  // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is a GET request that doesn't change anything.
636
637        $response = array( 'colors' => $colors );
638
639        header( 'Content-Type: text/javascript' );
640        // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
641        wp_send_json( $response, null, JSON_UNESCAPED_SLASHES );
642    }
643
644    /**
645     * Outputs pattern recommendations for AJAX requests.
646     *
647     * @return never
648     */
649    public static function ajax_pattern_recommendations() {
650        $patterns = self::get_pattern_recommendations( $_REQUEST );  // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is a GET request that doesn't change anything.
651
652        $response = array( 'patterns' => $patterns );
653
654        header( 'Content-Type: text/javascript' );
655        // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
656        wp_send_json( $response, null, JSON_UNESCAPED_SLASHES );
657    }
658
659    /**
660     * Ensure that COLOURLovers URLs are saved without any imgpress stuff.
661     *
662     * @param array $new_theme_mods new theme mods.
663     * @return array
664     */
665    public static function format_colourlovers_urls( $new_theme_mods ) {
666        if ( ! empty( $new_theme_mods['background_image'] ) && false !== strpos( $new_theme_mods['background_image'], '/imgpress?url=' . rawurlencode( self::COLOURLOVERS_HOST ) ) ) {
667            $parts                              = explode( '/imgpress?url=', $new_theme_mods['background_image'], 2 );
668            $new_theme_mods['background_image'] = urldecode( array_pop( $parts ) );
669        }
670
671        return $new_theme_mods;
672    }
673
674    /**
675     * When a user saves a COLOURLovers palette or pattern, save the CL metadata
676     * for attribution later on.
677     *
678     * Also used to track COLOURlovers asset usage.
679     * See https://mc.a8c.com/s/colourlovers-pattern/ and
680     * https://mc.a8c.com/s/colourlovers-palette/
681     *
682     * 1. Which color palettes are chosen, and overall number of times a pattern is switched to.
683     * 2. Which background patterns are chosen, and the overall number of times a pattern is switched to.
684     *
685     * @param array $oldvalue old metadata.
686     * @param array $newvalue new metadata value.
687     */
688    public static function save_colourlovers_metadata( $oldvalue, $newvalue ) {
689        $mods = $newvalue;
690
691        if ( isset( $oldvalue['background_image'] ) && isset( $newvalue['background_image'] ) && $oldvalue['background_image'] !== $newvalue['background_image'] ) {
692            $using_colourlovers_pattern = false;
693
694            if ( 0 === strpos( $mods['background_image'], self::COLOURLOVERS_HOST ) ) {
695                $matches = array();
696
697                if ( preg_match( '/\/([0-9]+)\.png$/i', $mods['background_image'], $matches ) ) {
698                    $using_colourlovers_pattern = true;
699
700                    $pattern_id = $matches[1];
701
702                    if ( empty( $mods['background_image_metadata'] ) || $pattern_id !== $mods['background_image_metadata']['pattern_id'] ) {
703                        $pattern = Colors_API::call( 'patterns', array(), (int) $pattern_id );
704                        if ( ! is_wp_error( $pattern ) && is_array( $pattern ) ) {
705                            set_theme_mod(
706                                'background_image_metadata',
707                                array(
708                                    'pattern_id' => $pattern_id,
709                                    'username'   => $pattern['username'],
710                                    'title'      => $pattern['title'],
711                                )
712                            );
713                        }
714                    }
715                }
716            }
717
718            if ( ! $using_colourlovers_pattern && ! empty( $mods['background_image_metadata'] ) ) {
719                remove_theme_mod( 'background_image_metadata' );
720            }
721        }
722
723        if ( isset( $newvalue['background_image'] ) && 0 === strpos( $newvalue['background_image'], self::COLOURLOVERS_HOST ) && $newvalue['background_image'] !== $newvalue['background_image_thumb'] ) {
724            /**
725             * Due to a bug with percent signs in background_image URLs, we need to make sure that
726             * our background image is also saved as the background_image_thumb value.  We need to
727             * do this any time theme_mods is updated, because there is other code aggressively
728             * trying to delete background_image_thumb completely.
729             */
730            set_theme_mod( 'background_image_thumb', $newvalue['background_image'] );
731        }
732
733        if ( isset( $oldvalue['colors_manager'] ) && isset( $newvalue['colors_manager'] ) && $newvalue['colors_manager']['colors'] !== $oldvalue['colors_manager']['colors'] ) {
734            if ( empty( $newvalue['colors_manager']['colors'] ) && $newvalue['color_palette_metadata'] ) {
735                remove_theme_mod( 'color_palette_metadata' );
736            } else {
737                require_once __DIR__ . '/class-palette.php';
738
739                $palette = Palette::get( array( 'colors' => $newvalue['colors_manager']['colors'] ) );
740
741                if ( $palette ) {
742                    if ( empty( $newvalue['color_palette_metadata'] ) || $palette->id !== $newvalue['color_palette_metadata']['palette_id'] ) {
743                        set_theme_mod(
744                            'color_palette_metadata',
745                            array(
746                                'palette_id' => $palette->id,
747                                'username'   => $palette->username,
748                                'title'      => $palette->title,
749                            )
750                        );
751                    }
752                } else {
753                    remove_theme_mod( 'color_palette_metadata' );
754                }
755            }
756        }
757    }
758
759    /**
760     * Are colors the same?
761     *
762     * @param string $a color A.
763     * @param string $b color B.
764     * @return boolean
765     */
766    public static function is_same_color( $a, $b ) {
767        $a = trim( strtolower( $a ), ' #' );
768        $b = trim( strtolower( $b ), ' #' );
769        return $a === $b;
770    }
771
772    /**
773     * Are we on the default pallette?
774     *
775     * @param array $colors tested colors.
776     * @return boolean
777     */
778    public static function is_default_palette( $colors ) {
779        // a saved palette may have more colors than the default palette. So,
780        // iterate over the default palette
781        foreach ( self::$default_colors as $id => $default_color ) {
782            if ( ! isset( $colors[ $id ] ) ) {
783                return false;
784            }
785            if ( ! self::is_same_color( $default_color, $colors[ $id ] ) ) {
786                return false;
787            }
788        }
789        return true;
790    }
791
792    /**
793     * Are we on the featured pallette?
794     *
795     * @param array $colors tested colors.
796     * @return boolean
797     */
798    public static function is_featured_palette( $colors ) {
799
800        $featured_palettes = self::$color_palettes;
801
802        foreach ( $colors as $c ) {
803            $c = strtolower( $c );
804        }
805
806        // look for our palette in featured palettes
807        foreach ( $featured_palettes as $p ) {
808            $p     = $p['palette'];
809            $found = true;
810            // for each color of the featured palette
811            foreach ( $p as $i => $c ) {
812                // we don't care about the background color; non-CD users are
813                // free to change it
814                if ( 0 === $i ) {
815                    continue;
816                }
817
818                $c = strtolower( $c );
819                // if that color isn't in our palette
820                if ( ! empty( $c ) && ! in_array( $c, $colors, true ) ) {
821                    // try another featured palette
822                    $found = false;
823                    break;
824                }
825            }
826            if ( $found ) {
827                return true;
828            }
829        }
830        return false;
831    }
832
833    /**
834     * Should we enable custom colors?
835     */
836    public static function should_enable_colors() {
837        $opts = get_theme_mod( 'colors_manager', array( 'colors' => false ) );
838        if ( ! $opts['colors'] ) {
839            return false;
840        }
841
842        $colors = $opts['colors'];
843
844        // If we managed to save the default palette, bail. It does not actually render
845        // the same thing as the theme's default style
846        if ( self::is_default_palette( $colors ) ) {
847            return false;
848        }
849
850        return apply_filters( 'custom_colors_enable', true );
851    }
852
853    /**
854     * Query and return palette data.
855     *
856     * @param array{color?:string,limit?:int,offset?:int} $args initial color settings.
857     * @return array An array of color palettes.
858     */
859    public static function get_color_palettes( $args = array() ) {
860        $defaults = array(
861            'color'  => false,
862            'limit'  => 6,
863            'offset' => 0,
864        );
865
866        $args = wp_parse_args( $args, $defaults );
867
868        if ( $args['color'] ) {
869            $args['color'] = self::normalize_color( $args['color'] );
870
871            $palettes = wp_cache_get( 'color-palettes-from-' . $args['color'], 'colors' );
872
873            if ( false === $palettes ) {
874                $palettes = Colors_API::call( 'palettes', array( 'color' => $args['color'] ) );
875                if ( ! is_wp_error( $palettes ) ) {
876                    wp_cache_set( 'color-palettes-from-' . $args['color'], $palettes, 'colors', MONTH_IN_SECONDS );
877                }
878            }
879        } else {
880            $palettes = wp_cache_get( 'color-palettes-top', 'colors' );
881
882            if ( false === $palettes ) {
883                $palettes = Colors_API::call( 'palettes' );
884                if ( ! is_wp_error( $palettes ) ) {
885                    wp_cache_set( 'color-palettes-top', $palettes, 'colors', MONTH_IN_SECONDS );
886                }
887            }
888        }
889
890        $palettes = array_slice( $palettes, $args['offset'], $args['limit'] );
891
892        if ( ! empty( $palettes ) ) {
893            foreach ( $palettes as $palette_index => $palette ) {
894                $colors = array();
895
896                foreach ( self::get_color_slots() as $color_index => $color_key ) {
897                    if ( count( $palette['colors'] ) === $color_index ) {
898                        break;
899                    }
900
901                    $colors[ $color_key ] = $palette['colors'][ $color_index ]['hex'];
902                }
903
904                $palettes[ $palette_index ]['colors'] = $colors;
905            }
906        }
907
908        // Shuffle palettes to make them less repetitive
909        shuffle( $palettes );
910
911        // Prepend theme-defined palettes to the first set of palettes
912        if ( 0 === (int) $args['offset'] ) {
913            $palettes = array_merge( self::get_theme_color_palettes(), $palettes );
914            $palettes = array_slice( $palettes, 0, (int) $args['limit'] );
915        }
916
917        return $palettes;
918    }
919
920    /**
921     * Return an image URL based on Gravatar URL.
922     *
923     * @param string $image_url URL to be transformed.
924     * @return string
925     */
926    public static function gravatar_image_url( $image_url ) {
927        $prefix_http     = preg_quote( 'http://www.gravatar.com/avatar/', '/' );
928        $prefix_https    = preg_quote( 'https://secure.gravatar.com/avatar/', '/' );
929        $gravatar_prefix = sprintf( '/^(%s|%s)/', $prefix_http, $prefix_https );
930        $is_gravatar_url = preg_match( $gravatar_prefix, $image_url );
931
932        if ( $is_gravatar_url ) {
933            $image_url = preg_replace( '#/([0-9a-f]+)/#', '/$1.jpg', $image_url );
934        }
935
936        return $image_url;
937    }
938
939    /**
940     * Returns a color palette matching a given image thanks to the Tonesque
941     * lib.
942     *
943     * @param array{image?:string} $args an image URL in the form of an array.
944     * @return array A single color palette
945     */
946    public static function get_generated_palette( $args = array() ) {
947        // Some themes, like Ryu, include an older version of Tonesque, which is loaded instead of the version in `/wp-content/lib/`.
948        // For now, only load the shared library if Tonesque isn't already present. See #5557.
949        if ( ! class_exists( 'Tonesque' ) ) {
950            require_lib( 'tonesque' );
951        }
952
953        // If the loaded version doesn't have the method needed to support palette generation, abort for now until the themes are updated. See #5557.
954        if ( ! method_exists( 'Tonesque', 'grab_points' ) ) {
955            return array();
956        }
957
958        $defaults = array(
959            'image' => false,
960        );
961
962        $args  = wp_parse_args( $args, $defaults );
963        $image = $args['image'] ?? '';
964
965        if ( ! $image ) {
966            return array();
967        }
968
969        $tonesque = new Tonesque( $image );
970        $points   = $tonesque->grab_points( 'hex' );
971
972        $roles = self::get_color_slots();
973        shuffle( $roles );
974
975        if ( ! is_array( $points ) ) {
976            return array();
977        }
978
979        $colors = array_combine( $roles, $points );
980
981        $palette = array(
982            'id'     => 'generated-palette',
983            'colors' => $colors,
984        );
985
986        return $palette;
987    }
988
989    /**
990     * Returns theme color pallettes.
991     */
992    public static function get_theme_color_palettes() {
993        if ( empty( self::$color_palettes ) ) {
994            return array();
995        }
996
997        $map                = self::get_color_slots();
998        $formatted_palettes = array();
999        foreach ( self::$color_palettes as $id => $palette ) {
1000            $formatted_palette = array(
1001                'id'     => $id,
1002                'colors' => array(),
1003            );
1004            foreach ( $map as $index => $key ) {
1005                if ( ! isset( $palette['palette'][ $index ] ) ) {
1006                    continue;
1007                }
1008                $formatted_palette['colors'][ $key ] = str_replace( '#', '', $palette['palette'][ $index ] );
1009            }
1010
1011            $formatted_palettes[] = $formatted_palette;
1012        }
1013
1014        return $formatted_palettes;
1015    }
1016
1017    /**
1018     * Query and return pattern data.
1019     *
1020     * @param array{color?:string,limit?:int,offset?:int} $args initial settings.
1021     * @return array An array of patterns.
1022     */
1023    public static function get_patterns( $args = array() ) {
1024        $defaults = array(
1025            'color'  => false,
1026            'limit'  => 4,
1027            'offset' => 0,
1028        );
1029
1030        $args = wp_parse_args( $args, $defaults );
1031
1032        if ( $args['color'] ) {
1033            $args['color'] = self::normalize_color( $args['color'] );
1034
1035            $patterns = wp_cache_get( 'patterns-from-' . $args['color'], 'colors' );
1036
1037            if ( false === $patterns ) {
1038                $patterns = Colors_API::call( 'patterns', array( 'color' => $args['color'] ) );
1039                if ( ! is_wp_error( $patterns ) ) {
1040                    wp_cache_set( 'patterns-from-' . $args['color'], $patterns, 'colors', MONTH_IN_SECONDS );
1041                }
1042            }
1043        } else {
1044            $patterns = wp_cache_get( 'patterns-top', 'colors' );
1045
1046            if ( false === $patterns ) {
1047                $patterns = Colors_API::call( 'patterns' );
1048                if ( ! is_wp_error( $patterns ) ) {
1049                    wp_cache_set( 'patterns-top', $patterns, 'colors', MONTH_IN_SECONDS );
1050                }
1051            }
1052        }
1053
1054        $patterns = array_slice( $patterns, $args['offset'], $args['limit'] );
1055
1056        if ( ! empty( $patterns ) ) {
1057            foreach ( $patterns as $pattern_index => $pattern ) {
1058                $colors = array();
1059
1060                foreach ( self::get_color_slots() as $color_index => $color_key ) {
1061                    if ( count( $pattern['colors'] ) === $color_index ) {
1062                        break;
1063                    }
1064
1065                    $colors[ $color_key ] = $pattern['colors'][ $color_index ]['hex'];
1066                }
1067
1068                $patterns[ $pattern_index ]['colors']            = $colors;
1069                $patterns[ $pattern_index ]['preview_image_url'] = apply_filters( 'jetpack_photon_url', $pattern['preview_image_url'], array(), 'network_path' );
1070            }
1071        }
1072
1073        return $patterns;
1074    }
1075
1076    /**
1077     * Converts rgb() or hex color codes to the AABBCC format:
1078     *
1079     * @param string $color An rgb or hex color code.
1080     * @return string
1081     */
1082    public static function normalize_color( $color ) {
1083        if ( false !== strpos( $color, 'rgb' ) ) {
1084            $color_data       = preg_replace( '/[^0-9\.,]/', '', $color );
1085            $color_components = explode( ',', $color_data );
1086
1087            $hex_color = '';
1088
1089            for ( $i = 0; $i < 3; $i++ ) {
1090                $hex_equivalent = dechex( intval( $color_components[ $i ] ) );
1091                if ( strlen( $hex_equivalent ) < 2 ) {
1092                    $hex_color .= '0';
1093                }
1094                $hex_color .= $hex_equivalent;
1095            }
1096
1097            return strtoupper( $hex_color );
1098        } else {
1099            $hex = strtoupper( substr( preg_replace( '/[^0-9A-Z]/i', '', $color ), 0, 6 ) );
1100
1101            if ( strlen( $hex ) === 3 ) {
1102                $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
1103            } else {
1104                for ( $i = strlen( $hex ); $i < 6; $i++ ) {
1105                    $hex = '0' . $hex;
1106                }
1107            }
1108
1109            return $hex;
1110        }
1111    }
1112
1113    /**
1114     * Finds colors that could be suitable complement to a given set of colors.
1115     *
1116     * @param array{color?:string,role?:string,colors?:array,limit?:int} $args initial settings.
1117     * @return array An array of color codes.
1118     */
1119    public static function get_color_recommendations( $args ) {
1120        $defaults = array(
1121            'color'  => false,
1122            'role'   => false,
1123            'colors' => false,
1124            'limit'  => 8,
1125        );
1126
1127        $args = wp_parse_args( $args, $defaults );
1128
1129        if ( $args['color'] ) {
1130            $args['color'] = self::normalize_color( $args['color'] );
1131        }
1132
1133        $colors = array();
1134
1135        foreach ( $args['colors'] as $role => $color ) {
1136            $color                   = self::normalize_color( $color );
1137            $args['colors'][ $role ] = $color;
1138
1139            $palettes = Colors_API::call(
1140                'palettes',
1141                array(
1142                    'color' => $color,
1143                    'limit' => 8,
1144                )
1145            );
1146
1147            if ( is_array( $palettes ) ) {
1148                foreach ( $palettes as $palette ) {
1149                    $multiplier = 0;
1150
1151                    foreach ( $palette['colors'] as $_color ) {
1152                        if ( ! $_color ) {
1153                            continue;
1154                        }
1155
1156                        // If this palette contains more than one of the guide colors,
1157                        // give it more weight.
1158                        if ( in_array( $_color, $args['colors'], true ) ) {
1159                            ++$multiplier;
1160                        }
1161                    }
1162
1163                    foreach ( $palette['colors'] as $palette_role => $_color ) {
1164                        if ( ! $_color ) {
1165                            continue;
1166                        }
1167
1168                        $colors[ $_color ] += ( 1 * $multiplier );
1169
1170                        if ( $palette_role === $args['role'] ) {
1171                            $colors[ $_color ] += ( 1 * $multiplier );
1172                        }
1173                    }
1174                }
1175            }
1176        }
1177
1178        foreach ( $args['colors'] as $color ) {
1179            unset( $colors[ $color ] );
1180        }
1181
1182        if ( $args['color'] ) {
1183            unset( $colors[ $args['color'] ] );
1184        }
1185
1186        arsort( $colors );
1187        $colors = array_keys( $colors );
1188
1189        if ( count( $colors ) < 8 ) {
1190            $more_suggestions = self::color_suggestions( $args['colors'], $args['role'] );
1191            $colors           = array_merge( $colors, $more_suggestions );
1192
1193            foreach ( $args['colors'] as $color ) {
1194                unset( $colors[ $color ] );
1195            }
1196
1197            if ( $args['color'] ) {
1198                unset( $colors[ $args['color'] ] );
1199            }
1200        }
1201
1202        $colors = array_slice( $colors, 0, $args['limit'] );
1203
1204        return $colors;
1205    }
1206
1207    /**
1208     * Finds patterns that could be suitable complement to a given set of colors.
1209     *
1210     * @param array{colors?:array,limit?:int} $args initial settings.
1211     * @return array An array of patterns.
1212     */
1213    public static function get_pattern_recommendations( $args ) {
1214        $defaults = array(
1215            'colors' => false,
1216            'limit'  => 4,
1217        );
1218
1219        $args = wp_parse_args( $args, $defaults );
1220
1221        $patterns_by_id = array();
1222        $pattern_ids    = array();
1223
1224        foreach ( $args['colors'] as $role => $color ) {
1225            $color                   = self::normalize_color( $color );
1226            $args['colors'][ $role ] = $color;
1227
1228            $color_patterns = Colors_API::call(
1229                'patterns',
1230                array(
1231                    'color' => $color,
1232                    'limit' => 5,
1233                )
1234            );
1235
1236            if ( is_array( $color_patterns ) ) {
1237                foreach ( $color_patterns as $pattern ) {
1238                    $patterns_by_id[ $pattern['id'] ] = $pattern;
1239
1240                    if ( ! isset( $pattern_ids[ $pattern['id'] ] ) ) {
1241                        $pattern_ids[ $pattern['id'] ] = 0;
1242                    }
1243                    $pattern_ids[ $pattern['id'] ] += 1;
1244
1245                    foreach ( $pattern['colors'] as $value ) {
1246                        if ( in_array( $value, $args['colors'], true ) ) {
1247                            $pattern_ids[ $pattern['id'] ] += 1;
1248                        }
1249                    }
1250                }
1251            }
1252        }
1253
1254        arsort( $pattern_ids );
1255        $pattern_ids = array_keys( $pattern_ids );
1256        $pattern_ids = array_slice( $pattern_ids, 0, $args['limit'] );
1257
1258        $patterns = array();
1259
1260        foreach ( $pattern_ids as $pattern_id ) {
1261            unset( $patterns_by_id[ $pattern_id ]['colors'] );
1262            $patterns[] = $patterns_by_id[ $pattern_id ];
1263        }
1264
1265        return $patterns;
1266    }
1267
1268    /**
1269     * Renders the color palettes
1270     */
1271    public static function color_palettes() {
1272        ?>
1273        <div id="colourlovers-palettes-container">
1274            <h3><?php esc_html_e( 'Choose a Palette', 'wpcomsh' ); ?></h3>
1275            <div id="colourlovers-palettes"></div>
1276            <div class="palette-buttons">
1277                <a class="button next" id="more-palettes"><?php esc_html_e( 'More', 'wpcomsh' ); ?></a>
1278                <a class="button previous" id="less-palettes" style="display: none;"><?php esc_html_e( 'Back', 'wpcomsh' ); ?></a>
1279                <a class="button generate" id="generate-palette"><?php esc_html_e( 'Match header image', 'wpcomsh' ); ?></a>
1280            </div>
1281        </div>
1282        <?php
1283    }
1284
1285    /**
1286     * Renders the pattern grid
1287     */
1288    public static function color_patterns() {
1289        ?>
1290        <div class="the-pattern-picker" id="the-pattern-picker" style="display: none;">
1291            <span class="customize-control-title">
1292                <?php esc_html_e( 'Pick a Background Pattern', 'wpcomsh' ); ?>
1293            </span>
1294            <ul id="colourlovers-patterns"></ul>
1295            <div class="pagination">
1296                <a id="more-patterns" class="button"><?php esc_html_e( 'More', 'wpcomsh' ); ?></a>
1297                <a id="less-patterns" class="button previous" style="display: none;"><?php esc_html_e( 'Back', 'wpcomsh' ); ?></a>
1298            </div>
1299            <p class="noresults" style="display: none;"><?php esc_html_e( "There aren't any patterns that match your chosen color scheme. It's just too unique!", 'wpcomsh' ); ?></p>
1300        </div>
1301        <?php
1302    }
1303
1304    /**
1305     * Make this work inside the Customizer.
1306     *
1307     * @param WP_Customize_Manager $wp_customize the customizer manager instance.
1308     */
1309    public static function in_customizer( $wp_customize ) {
1310        // Include controller class
1311        require_once __DIR__ . '/class-colors-controller.php';
1312
1313        $wp_customize->add_section(
1314            'colors_manager_tool',
1315            array(
1316                'title'    => __( 'Colors & Backgrounds', 'wpcomsh' ),
1317                'priority' => 35,
1318            )
1319        );
1320
1321        $setting_opts = array(
1322            'default'    => self::get_colors(),
1323            'capability' => 'edit_theme_options',
1324            'transport'  => 'postMessage',
1325            'type'       => 'theme_mod',
1326        );
1327
1328        if ( is_admin() ) {
1329            $setting_opts = array_merge(
1330                $setting_opts,
1331                array(
1332                    'sanitize_callback'    => array( __CLASS__, 'sanitize_colors_on_save' ),
1333                    'sanitize_js_callback' => array( __CLASS__, 'sanitize_colors' ),
1334                )
1335            );
1336        }
1337
1338        $wp_customize->add_setting( 'colors_manager[colors]', $setting_opts );
1339
1340        $wp_customize->add_control(
1341            new Colors_Manager_Control(
1342                $wp_customize,
1343                'colors-tool',
1344                array(
1345                    'label'    => __( 'Colors', 'wpcomsh' ),
1346                    'section'  => 'colors_manager_tool',
1347                    'settings' => 'colors_manager[colors]',
1348                )
1349            )
1350        );
1351    }
1352
1353    /**
1354     * Sanitizes colors on save.
1355     *
1356     * @param array $set_colors saved colors.
1357     * @return array
1358     */
1359    public static function sanitize_colors_on_save( $set_colors ) {
1360        return self::sanitize_colors( $set_colors );
1361    }
1362
1363    /**
1364     * Sanitizes colors.
1365     *
1366     * @param array $set_colors saved colors.
1367     * @return array
1368     */
1369    public static function sanitize_colors( $set_colors ) {
1370        if ( ! is_array( $set_colors ) && ! is_object( $set_colors ) ) {
1371            return array();
1372        }
1373        // let's make sure all of our keys/values are proper
1374        $colors_wanted = array();
1375        $cats          = self::get_color_slots();
1376        if ( ! class_exists( 'Jetpack_color' ) ) {
1377            require_lib( 'class.color' );
1378        }
1379        foreach ( $set_colors as $key => $color ) {
1380            if ( ! in_array( $key, $cats, true ) || ! $color ) {
1381                continue;
1382            }
1383            try {
1384                $color_object          = new Jetpack_Color( $color );
1385                $colors_wanted[ $key ] = '#' . $color_object->toHex();
1386            } catch ( Exception $e ) { // phpcs:ignore
1387                // Exception not handled to avoid it propagating further, apparently.
1388            }
1389        }
1390        return $colors_wanted;
1391    }
1392
1393    /**
1394     * Overriding theme colors.
1395     */
1396    public static function override_themecolors() {
1397        global $themecolors;
1398
1399        if ( ! self::should_enable_colors() ) {
1400            return;
1401        }
1402
1403        $opts = get_theme_mod( 'colors_manager', array( 'colors' => false ) );
1404        if ( ! isset( $opts ) ) {
1405            return;
1406        }
1407
1408        $colors = $opts['colors'];
1409
1410        if ( isset( $colors['fg1'] ) ) {
1411            $colors['border'] = $colors['fg1'];
1412        }
1413        if ( isset( $colors['link'] ) ) {
1414            $colors['url'] = $colors['link'];
1415        }
1416        if ( isset( $colors['txt'] ) ) {
1417            $colors['text'] = $colors['txt'];
1418        }
1419
1420        unset( $colors['fg1'] );
1421        unset( $colors['fg2'] );
1422        unset( $colors['txt'] );
1423
1424        foreach ( $colors as $role => $color ) {
1425            if ( $color ) {
1426                $themecolors[ $role ] = substr( $color, 1 );
1427            }
1428        }
1429    }
1430
1431    /**
1432     * Injects our postMessage listener scripts into the theme
1433     *
1434     * @param WP_Customize_Manager $wp_customize the customizer manager instance.
1435     */
1436    public static function theme_colors_js( $wp_customize ) {
1437        if ( $wp_customize->is_preview() && ! is_admin() ) {
1438            wp_enqueue_script( 'colors-instapreview' );
1439            $js_data = array(
1440                'colors'        => self::$colors,
1441                'defaultColors' => self::get_default_colors(),
1442                'extraCss'      => self::get_extra_css( true ),
1443                'extraColors'   => self::$extra_colors,
1444            );
1445            wp_localize_script( 'colors-instapreview', 'ColorsTool', $js_data );
1446        }
1447    }
1448
1449    /**
1450     * Prints theme CSS.
1451     */
1452    public static function print_theme_css() {
1453        if ( ! self::should_enable_colors() ) {
1454            return;
1455        }
1456        $css = self::get_theme_css();
1457        printf(
1458            '<style type="text/css" id="custom-colors-css">%s</style>%s',
1459            wp_strip_all_tags( $css ), // phpcs:ignore -- CSS can't be properly escaped with esc_html
1460            "\n"
1461        );
1462    }
1463
1464    /**
1465     * Enqueue theme CSS for the block editor.
1466     *
1467     * @since 9.0.0
1468     */
1469    public static function print_block_editor_css() {
1470        if ( ! self::should_enable_colors() ) {
1471            return;
1472        }
1473        $css = self::get_theme_css();
1474
1475        // Gutenberg 22.6.0+ renders the editor in an iframe for all themes.
1476        // #editor no longer exists inside the iframe, so extend any
1477        // "#editor .editor-styles-wrapper" selector to also match the
1478        // iframe context while keeping the original for older versions.
1479        $css = str_replace(
1480            '#editor .editor-styles-wrapper',
1481            '#editor .editor-styles-wrapper, :root :where(.editor-styles-wrapper)',
1482            $css
1483        );
1484
1485        wp_register_style( 'custom-colors-editor-css', false, array(), '20210311' ); // Register an empty stylesheet to append custom CSS to.
1486        wp_enqueue_style( 'custom-colors-editor-css' );
1487        wp_add_inline_style( 'custom-colors-editor-css', $css ); // Append inline style to our new stylesheet
1488    }
1489
1490    /**
1491     * Return theme CSS.
1492     */
1493    public static function get_theme_css() {
1494        $opts   = get_theme_mod(
1495            'colors_manager',
1496            array(
1497                'colors' => false,
1498            )
1499        );
1500        $colors = $opts['colors'];
1501
1502        // extra colors/CSS: always on
1503        $css = self::get_extra_css();
1504
1505        // user colors
1506        foreach ( self::$colors as $cat => $rules ) {
1507            if ( ! isset( $colors[ $cat ] ) ) {
1508                continue;
1509            }
1510
1511            $color = $colors[ $cat ];
1512            foreach ( $rules as $rule ) {
1513                $css .= self::css_rule( $rule, $color );
1514            }
1515        }
1516
1517        // Minify & cache for future use.
1518        $minifier = new tubalmartin\CssMin\Minifier();
1519        $css      = $minifier->run( $css );
1520
1521        return $css;
1522    }
1523
1524    /**
1525     * Get CSS rule.
1526     *
1527     * @todo possibly combine all of this into a keyed array to prevent selector duplication bloat
1528     * @param array  $rule the CSS rule.
1529     * @param string $color the color string.
1530     * @return string
1531     */
1532    public static function css_rule( $rule, $color ) {
1533        $css = '';
1534
1535        if ( ! isset( $rule[0] ) || ! isset( $rule[1] ) ) {
1536            return $css;
1537        }
1538
1539        if ( isset( $rule[2] ) ) {
1540            // we'll need it in either case
1541            if ( ! class_exists( 'Jetpack_color' ) ) {
1542                require_lib( 'class.color' );
1543            }
1544
1545            try {
1546                $working_color = new Jetpack_Color( $color );
1547            } catch ( RangeException $e ) {
1548                $message  = 'rule: ' . print_r( $rule, 1 ) . "\n"; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1549                $message .= 'call: $working_color = new Jetpack_Color( ' . $color . ' );' . "\n";
1550                self::exception_mailer( $message );
1551                return '';
1552            }
1553
1554            $number = (float) $rule[2];
1555            // ensure contrast or darken/lighten
1556            if ( is_string( $rule[2] ) ) {
1557                $first_char = substr( $rule[2], 0, 1 );
1558                // darken/lighten
1559                if ( '+' === $first_char || '-' === $first_char ) {
1560                    $modify = 10 * $number;
1561                    $color  = $working_color->incrementLightness( intval( $modify ) )->toString();
1562                } else {
1563                    // hex bg for contrast
1564                    if ( '#' === $first_char ) {
1565                        try {
1566                            $bg_color = new Jetpack_Color( $rule[2] );
1567                        } catch ( RangeException $e ) {
1568                            $message  = 'function: ' . __FUNCTION__ . "\n";
1569                            $message .= 'call: $bg_color = new Jetpack_Color( ' . $rule[2] . ' );' . "\n";
1570                            self::exception_mailer( $message );
1571                            return '';
1572                        }
1573                    } elseif ( isset( self::$colors[ $rule[2] ] ) ) { // set color bg for contrast
1574
1575                        $set_colors = self::get_colors();
1576                        try {
1577                            $bg_color = new Jetpack_Color( $set_colors[ $rule[2] ] ?? null );
1578                        } catch ( RangeException $e ) {
1579                            $message  = 'function: ' . __FUNCTION__ . "\n";
1580                            $message .= 'call: $bg_color = new Jetpack_Color( ' . $set_colors[ $rule[2] ] . ' );' . "\n";
1581                            self::exception_mailer( $message );
1582                            return '';
1583                        }
1584                    }
1585
1586                    // we have a bg color to contrast
1587                    if ( isset( $bg_color ) && is_a( $bg_color, 'Jetpack_Color' ) ) {
1588                        // default contrast of 5, can be overridden with 4th arg.
1589                        $contrast = $rule[3] ?? 5;
1590                        $color    = $working_color->getReadableContrastingColor( $bg_color, $contrast )->toString();
1591                    }
1592                }
1593            } elseif ( $rule[2] < 1 ) { // alpha
1594                unset( $rule[2] );
1595                // back compat for non-rgba browsers
1596                $css  .= self::css_rule( $rule, $color );
1597                $color = $working_color->toCSS( 'rgba', intval( $number ) );
1598            }
1599        }
1600
1601        $css .= "{$rule[0]} { {$rule[1]}{$color};}\n";
1602        return $css;
1603    }
1604
1605    /**
1606     * Get extra CSS.
1607     *
1608     * @param boolean $only_callback no processing, just callback.
1609     */
1610    public static function get_extra_css( $only_callback = false ) {
1611        $css      = '';
1612        $extra_cb = get_theme_support( 'custom_colors_extra_css' );
1613
1614        if ( is_array( $extra_cb ) && is_callable( $extra_cb[0] ) ) {
1615            // will work with return values or straight printing
1616            ob_start();
1617            $css  = call_user_func( $extra_cb[0] );
1618            $css .= ob_get_clean();
1619        }
1620
1621        if ( $only_callback ) {
1622            return $css;
1623        }
1624
1625        foreach ( self::$extra_colors as $extra ) {
1626            if ( ! isset( $extra['rules'] ) || ! is_array( $extra['rules'] ) ) {
1627                continue;
1628            }
1629            $color = $extra['color'];
1630            foreach ( $extra['rules'] as $rule ) {
1631                $css .= self::css_rule( $rule, (string) $color );
1632            }
1633        }
1634        return $css;
1635    }
1636
1637    /**
1638     * Function for making theme annotations.
1639     *
1640     * @param string      $category The color category. One of bg, txt, link, fg1, fg2.
1641     * @param string      $default_color The default color for this category.
1642     * @param array       $rules Array of rule arrays. $rule: array( selector, property, opacity );.
1643     * @param bool|string $label Optional. A UI helper label for identifying what a particular color will change in the theme.
1644     */
1645    public static function add_color_rule( $category, $default_color, $rules, $label = false ) {
1646        // extra rules
1647        if ( 'extra' === $category ) {
1648            self::$extra_colors[] = array(
1649                'color' => $default_color,
1650                'rules' => $rules,
1651            );
1652            return;
1653        }
1654        // prime it
1655        if ( ! isset( self::$colors[ $category ] ) ) {
1656            self::$colors[ $category ] = array();
1657        }
1658        self::$colors[ $category ] = array_merge( self::$colors[ $category ], $rules );
1659
1660        self::$default_colors[ $category ] = $default_color;
1661        if ( $label ) {
1662            self::$labels[ $category ] = $label;
1663        }
1664    }
1665
1666    /**
1667     * Allow a theme to declare its own color palettes.
1668     *
1669     * @param array       $palette An array with 5 colors.
1670     * @param bool|string $title optional title string.
1671     */
1672    public static function add_color_palette( $palette, $title = false ) {
1673        if ( ! $title ) {
1674            $theme = wp_get_theme();
1675            $title = sprintf(
1676                // translators: %1$s is a theme name, %2$s is its custom color scheme number.
1677                __( '%1$s Alternative Scheme %2$s', 'wpcomsh' ),
1678                $theme->display( 'Name' ),
1679                count( self::$color_palettes ) + 1
1680            );
1681        }
1682
1683        $id = sanitize_title_with_dashes( $title );
1684
1685        self::$color_palettes[ $id ] = compact( 'title', 'palette' );
1686    }
1687
1688    /**
1689     * Loads theme annotations, and filter them if loaded.
1690     *
1691     * @param  boolean $theme Which theme to check for annotations on. Defaults to current theme.
1692     * @return boolean Theme has annotations.
1693     */
1694    protected static function load_annotations( $theme = false ) {
1695        $theme_name       = 'pub/' . self::pick_theme( $theme );
1696        $annotations_file = get_stylesheet_directory() . '/inc/wpcom-colors.php';
1697        self::prime_color_labels();
1698        if ( is_readable( $annotations_file ) ) {
1699            require_once $annotations_file;
1700            self::$colors = apply_filters( 'custom_colors_rules', self::$colors, $theme_name );
1701            self::handle_unset_colors();
1702            return true;
1703        }
1704        return false;
1705    }
1706
1707    /**
1708     * Unset colors that need to be unset.
1709     */
1710    protected static function handle_unset_colors() {
1711        foreach ( self::$colors as $key => $value ) {
1712            if ( empty( $value ) ) {
1713                // set Label to Unused
1714                self::$labels[ $key ] = __( 'Unused', 'wpcomsh' );
1715                unset( self::$colors[ $key ] );
1716            }
1717        }
1718    }
1719
1720    /**
1721     * Sets default, i10n-ized default color labels that can be overridden in annotations.
1722     */
1723    protected static function prime_color_labels() {
1724        if ( ! empty( self::$labels ) ) {
1725            return;
1726        }
1727
1728        self::$labels = array(
1729            'bg'   => __( 'Background', 'wpcomsh' ),
1730            'txt'  => __( 'Headings', 'wpcomsh' ),
1731            'link' => __( 'Links', 'wpcomsh' ),
1732            'fg1'  => __( 'Accent #1', 'wpcomsh' ),
1733            'fg2'  => __( 'Accent #2', 'wpcomsh' ),
1734        );
1735    }
1736
1737    /**
1738     * Generate color suggestions for a given role from a set of colors.
1739     *
1740     * @param array  $colors color array.
1741     * @param string $role (bg|fg1|fg2|txt|link).
1742     * @return array
1743     */
1744    public static function color_suggestions( $colors, $role ) {
1745        if ( ! class_exists( 'Jetpack_color' ) ) {
1746            require_lib( 'class.color' );
1747        }
1748
1749        $suggestions = array();
1750
1751        $suggestions = array_merge( $suggestions, self::color_suggestions_from_palette( $colors, $role ) );
1752        $suggestions = array_merge( $suggestions, self::color_suggestions_from_math( $colors, $role ) );
1753
1754        shuffle( $suggestions );
1755
1756        return $suggestions;
1757    }
1758
1759    /**
1760     * Generate color suggestions by grabbing a popular palette and applying
1761     * it as a transformation to the colors we're using as a guide.
1762     *
1763     * @param array  $colors color array.
1764     * @param string $role (bg|fg1|fg2|txt|link).
1765     * @return array
1766     */
1767    public static function color_suggestions_from_palette( $colors, $role ) {
1768        $suggestions = array();
1769
1770        $top_palette = self::get_color_palettes(
1771            array(
1772                'limit'  => 1,
1773                'offset' => wp_rand(
1774                    0,
1775                    100
1776                ),
1777            )
1778        );
1779
1780        if ( ! $top_palette ) {
1781            return array();
1782        }
1783
1784        $top_palette = $top_palette[0];
1785
1786        $equivalent_color_hex = $top_palette['colors'][ $role ];
1787
1788        foreach ( $top_palette['colors'] as $palette_role => $palette_color_hex ) {
1789            $base_color_hex = $colors[ $palette_role ];
1790            try {
1791                // phpcs:ignore -- $base_color:$new_color :: $palette_color:$equivalent_color
1792                $base_color       = new Jetpack_Color( $base_color_hex );
1793                $palette_color    = new Jetpack_Color( $palette_color_hex );
1794                $equivalent_color = new Jetpack_Color( $equivalent_color_hex );
1795
1796                $palette_hsl    = $palette_color->toHsl();
1797                $equivalent_hsl = $equivalent_color->toHsl();
1798
1799                $base_color->incrementHue( $equivalent_hsl['h'] - $palette_hsl['h'] );
1800                $base_color->saturate( $equivalent_hsl['s'] - $palette_hsl['s'] );
1801                $base_color->lighten( $equivalent_hsl['l'] - $palette_hsl['l'] );
1802
1803                $suggestions[] = self::normalize_color( $base_color->toHex() );
1804            } catch ( RangeException $e ) {
1805                $message  = "Color exception!\n\n";
1806                $message .= "role: $role\n";
1807                $message .= "base: $base_color_hex\n";
1808                $message .= "palette: $palette_color_hex\n";
1809                $message .= "equiv: $equivalent_color_hex\n";
1810                $message .= 'colors arg: ' . print_r( $colors, 1 ); // phpcs:ignore
1811                self::exception_mailer( $message );
1812                continue;
1813            }
1814        }
1815
1816        return $suggestions;
1817    }
1818
1819    // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1820    /**
1821     * Mail the exception.
1822     *
1823     * @param string $message the exception private.
1824     */
1825    public static function exception_mailer( $message = 'Needs a message' ) {
1826        $message .= "\n\nblog: " . home_url() . "\n";
1827        $message .= 'backtrace: ' . wp_debug_backtrace_summary() . "\n"; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
1828        // phpcs:ignore -- wp_mail( 'wiebe@automattic.com', 'Color Exception on WordPress.com', $message );
1829    }
1830    // phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1831
1832    /**
1833     * Use a set of predefined transformations to generate color suggestions
1834     * based on roles.
1835     *
1836     * @param array  $colors color array.
1837     * @param string $role (bg|fg1|fg2|txt|link).
1838     * @return array
1839     */
1840    public static function color_suggestions_from_math( $colors, $role ) {
1841        $suggestions = array();
1842
1843        // These are the result of a couple of hours of playing around.
1844        // Nothing here is set in stone.
1845        $relations = array(
1846            'bg:fg1'   => array( 'brighter', 'saturate' ),
1847            'bg:fg2'   => array( 'darker', 'desaturate' ),
1848            'bg:txt'   => array( '+triad' ),
1849            'bg:link'  => array( '-triad' ),
1850            'fg1:bg'   => array( 'desaturate', 'darker' ),
1851            'fg1:fg2'  => array( '+analog' ),
1852            'fg1:txt'  => array( '-tetrad' ),
1853            'fg1:link' => array( 'darker', 'saturate' ),
1854            'fg2:bg'   => array( 'saturate', 'brighter' ),
1855            'fg2:fg1'  => array( '-analog' ),
1856            'fg2:txt'  => array( '-tetrad' ),
1857            'fg2:link' => array( 'darker', 'saturate' ),
1858            'txt:bg'   => array( '+triad' ),
1859            'txt:fg1'  => array( '+tetrad' ),
1860            'txt:fg2'  => array( '+tetrad' ),
1861            'txt:link' => array( '-split-complement', 'saturate' ),
1862            'link:bg'  => array( '-triad' ),
1863            'link:fg1' => array( 'desaturate', 'brighter' ),
1864            'link:fg2' => array( 'desaturate', 'brighter' ),
1865            'link:txt' => array( 'darker', 'saturate' ),
1866        );
1867
1868        foreach ( $colors as $known_role => $color_code ) {
1869            if ( $known_role === $role ) {
1870                continue;
1871            }
1872
1873            $transforms = $relations[ $known_role . ':' . $role ];
1874            try {
1875                $new_color = new Jetpack_Color( self::normalize_color( $color_code ) );
1876            } catch ( RangeException $e ) {
1877                $message  = 'function: ' . __FUNCTION__ . "\n";
1878                $message .= 'call: $new_color = new Jetpack_Color( self::normalize_color( ' . $color_code . ' ) );' . "\n";
1879                $message .= 'normalized color: ' . self::normalize_color( $color_code );
1880                self::exception_mailer( $message );
1881                continue;
1882            }
1883
1884            foreach ( $transforms as $transform ) {
1885                switch ( $transform ) {
1886                    case 'complement':
1887                        $new_color->getComplement();
1888                        break;
1889                    case 'brighter':
1890                        $new_color->lighten( 25 );
1891                        break;
1892                    case 'darker':
1893                        $new_color->darken( 25 );
1894                        break;
1895                    case 'grayscale':
1896                        $new_color->toGrayscale();
1897                        break;
1898                    case '+split-complement':
1899                        $new_color->getSplitComplement( 1 );
1900                        break;
1901                    case '-split-complement':
1902                        $new_color->getSplitComplement( -1 );
1903                        break;
1904                    case '+triad':
1905                        $new_color->getTriad( 1 );
1906                        break;
1907                    case '-triad':
1908                        $new_color->getTriad( -1 );
1909                        break;
1910                    case 'saturate':
1911                        $new_color->saturate( 25 );
1912                        break;
1913                    case 'desaturate':
1914                        $new_color->desaturate( 25 );
1915                        break;
1916                    case '+analog':
1917                        $new_color->getAnalog( 1 );
1918                        break;
1919                    case '-analog':
1920                        $new_color->getAnalog( -1 );
1921                        break;
1922                    case '+tetrad':
1923                        $new_color->getTetrad( 1 );
1924                        break;
1925                    case '-tetrad':
1926                        $new_color->getTetrad( -1 );
1927                        break;
1928                }
1929            }
1930
1931            $suggestions[] = self::normalize_color( $new_color->toHex() );
1932        }
1933
1934        return $suggestions;
1935    }
1936}
1937
1938/**
1939 * Nothing to override
1940 */
1941class Colors_Manager extends Colors_Manager_Common {}
1942
1943/**
1944 * Adds a color rule.
1945 *
1946 * @param string      $category The color category. One of bg, txt, link, fg1, fg2.
1947 * @param string      $default_color The default color for this category.
1948 * @param array       $rules Array of rule arrays. $rule: array( selector, property, opacity );.
1949 * @param bool|string $label Optional. A UI helper label for identifying what a particular color will change in the theme.
1950 */
1951function add_color_rule( $category, $default_color, $rules, $label = false ) {
1952    Colors_Manager::add_color_rule( $category, $default_color, $rules, $label );
1953}
1954
1955/**
1956 * Adds color palette.
1957 *
1958 * @param array       $palette An array with 5 colors.
1959 * @param bool|string $title optional title string.
1960 */
1961function add_color_palette( $palette, $title = false ) {
1962    return Colors_Manager::add_color_palette( $palette, $title );
1963}
1964
1965/**
1966 * Gutenberg color manager.
1967 */
1968class Colors_Manager_Gutenberg extends Colors_Manager_Common {
1969
1970    /**
1971     * Whether we're in Gutenberg.
1972     *
1973     * @var boolean
1974     */
1975    protected static $is_gutenberg = true;
1976
1977    /**
1978     * Annotations file path.
1979     *
1980     * @var string
1981     */
1982    protected static $annotations_file = 'wpcom-editor-colors.php';
1983}
1984
1985/**
1986 * Load Gutenberg's color manager.
1987 */
1988function colors_manager_gutenberg_load() {
1989    if ( get_current_screen()->is_block_editor() ) {
1990        Colors_Manager_Gutenberg::init(); // Gutenberg
1991    }
1992}
1993
1994/**
1995 * Load corresponding color manager.
1996 */
1997function load_corresponding_color_manager() {
1998    global $pagenow;
1999    if ( is_admin() && 'customize.php' !== $pagenow && ! defined( 'DOING_AJAX' ) ) {
2000        add_action( 'current_screen', 'colors_manager_gutenberg_load' );
2001    } else {
2002        Colors_Manager::init();
2003    }
2004}
2005
2006add_action( 'init', 'load_corresponding_color_manager' );