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