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        if ( is_string( $url ) && (
663            false !== stripos( $url, 'colourlovers.com.s3.amazonaws.com' )
664            || false !== stripos( $url, 'colourlovers-static-replica.s3.amazonaws.com' )
665        ) ) {
666            return '';
667        }
668
669        return $url;
670    }
671
672    /**
673     * Ensure that COLOURLovers URLs are saved without any imgpress stuff.
674     *
675     * @param array $new_theme_mods new theme mods.
676     * @return array
677     */
678    public static function format_colourlovers_urls( $new_theme_mods ) {
679        if ( ! empty( $new_theme_mods['background_image'] ) && false !== strpos( $new_theme_mods['background_image'], '/imgpress?url=' . rawurlencode( self::COLOURLOVERS_HOST ) ) ) {
680            $parts                              = explode( '/imgpress?url=', $new_theme_mods['background_image'], 2 );
681            $new_theme_mods['background_image'] = urldecode( array_pop( $parts ) );
682        }
683
684        return $new_theme_mods;
685    }
686
687    /**
688     * When a user saves a COLOURLovers palette or pattern, save the CL metadata
689     * for attribution later on.
690     *
691     * Also used to track COLOURlovers asset usage.
692     * See https://mc.a8c.com/s/colourlovers-pattern/ and
693     * https://mc.a8c.com/s/colourlovers-palette/
694     *
695     * 1. Which color palettes are chosen, and overall number of times a pattern is switched to.
696     * 2. Which background patterns are chosen, and the overall number of times a pattern is switched to.
697     *
698     * @param array $oldvalue old metadata.
699     * @param array $newvalue new metadata value.
700     */
701    public static function save_colourlovers_metadata( $oldvalue, $newvalue ) {
702        $mods = $newvalue;
703
704        if ( isset( $oldvalue['background_image'] ) && isset( $newvalue['background_image'] ) && $oldvalue['background_image'] !== $newvalue['background_image'] ) {
705            $using_colourlovers_pattern = false;
706
707            if ( 0 === strpos( $mods['background_image'], self::COLOURLOVERS_HOST ) ) {
708                $matches = array();
709
710                if ( preg_match( '/\/([0-9]+)\.png$/i', $mods['background_image'], $matches ) ) {
711                    $using_colourlovers_pattern = true;
712
713                    $pattern_id = $matches[1];
714
715                    if ( empty( $mods['background_image_metadata'] ) || $pattern_id !== $mods['background_image_metadata']['pattern_id'] ) {
716                        $pattern = Colors_API::call( 'patterns', array(), (int) $pattern_id );
717                        if ( ! is_wp_error( $pattern ) && is_array( $pattern ) ) {
718                            set_theme_mod(
719                                'background_image_metadata',
720                                array(
721                                    'pattern_id' => $pattern_id,
722                                    'username'   => $pattern['username'],
723                                    'title'      => $pattern['title'],
724                                )
725                            );
726                        }
727                    }
728                }
729            }
730
731            if ( ! $using_colourlovers_pattern && ! empty( $mods['background_image_metadata'] ) ) {
732                remove_theme_mod( 'background_image_metadata' );
733            }
734        }
735
736        if ( isset( $newvalue['background_image'] ) && 0 === strpos( $newvalue['background_image'], self::COLOURLOVERS_HOST ) && $newvalue['background_image'] !== $newvalue['background_image_thumb'] ) {
737            /**
738             * Due to a bug with percent signs in background_image URLs, we need to make sure that
739             * our background image is also saved as the background_image_thumb value.  We need to
740             * do this any time theme_mods is updated, because there is other code aggressively
741             * trying to delete background_image_thumb completely.
742             */
743            set_theme_mod( 'background_image_thumb', $newvalue['background_image'] );
744        }
745
746        if ( isset( $oldvalue['colors_manager'] ) && isset( $newvalue['colors_manager'] ) && $newvalue['colors_manager']['colors'] !== $oldvalue['colors_manager']['colors'] ) {
747            if ( empty( $newvalue['colors_manager']['colors'] ) && $newvalue['color_palette_metadata'] ) {
748                remove_theme_mod( 'color_palette_metadata' );
749            } else {
750                require_once __DIR__ . '/class-palette.php';
751
752                $palette = Palette::get( array( 'colors' => $newvalue['colors_manager']['colors'] ) );
753
754                if ( $palette ) {
755                    if ( empty( $newvalue['color_palette_metadata'] ) || $palette->id !== $newvalue['color_palette_metadata']['palette_id'] ) {
756                        set_theme_mod(
757                            'color_palette_metadata',
758                            array(
759                                'palette_id' => $palette->id,
760                                'username'   => $palette->username,
761                                'title'      => $palette->title,
762                            )
763                        );
764                    }
765                } else {
766                    remove_theme_mod( 'color_palette_metadata' );
767                }
768            }
769        }
770    }
771
772    /**
773     * Are colors the same?
774     *
775     * @param string $a color A.
776     * @param string $b color B.
777     * @return boolean
778     */
779    public static function is_same_color( $a, $b ) {
780        $a = trim( strtolower( $a ), ' #' );
781        $b = trim( strtolower( $b ), ' #' );
782        return $a === $b;
783    }
784
785    /**
786     * Are we on the default pallette?
787     *
788     * @param array $colors tested colors.
789     * @return boolean
790     */
791    public static function is_default_palette( $colors ) {
792        // a saved palette may have more colors than the default palette. So,
793        // iterate over the default palette
794        foreach ( self::$default_colors as $id => $default_color ) {
795            if ( ! isset( $colors[ $id ] ) ) {
796                return false;
797            }
798            if ( ! self::is_same_color( $default_color, $colors[ $id ] ) ) {
799                return false;
800            }
801        }
802        return true;
803    }
804
805    /**
806     * Are we on the featured pallette?
807     *
808     * @param array $colors tested colors.
809     * @return boolean
810     */
811    public static function is_featured_palette( $colors ) {
812
813        $featured_palettes = self::$color_palettes;
814
815        foreach ( $colors as $c ) {
816            $c = strtolower( $c );
817        }
818
819        // look for our palette in featured palettes
820        foreach ( $featured_palettes as $p ) {
821            $p     = $p['palette'];
822            $found = true;
823            // for each color of the featured palette
824            foreach ( $p as $i => $c ) {
825                // we don't care about the background color; non-CD users are
826                // free to change it
827                if ( 0 === $i ) {
828                    continue;
829                }
830
831                $c = strtolower( $c );
832                // if that color isn't in our palette
833                if ( ! empty( $c ) && ! in_array( $c, $colors, true ) ) {
834                    // try another featured palette
835                    $found = false;
836                    break;
837                }
838            }
839            if ( $found ) {
840                return true;
841            }
842        }
843        return false;
844    }
845
846    /**
847     * Should we enable custom colors?
848     */
849    public static function should_enable_colors() {
850        $opts = get_theme_mod( 'colors_manager', array( 'colors' => false ) );
851        if ( ! $opts['colors'] ) {
852            return false;
853        }
854
855        $colors = $opts['colors'];
856
857        // If we managed to save the default palette, bail. It does not actually render
858        // the same thing as the theme's default style
859        if ( self::is_default_palette( $colors ) ) {
860            return false;
861        }
862
863        return apply_filters( 'custom_colors_enable', true );
864    }
865
866    /**
867     * Query and return palette data.
868     *
869     * @param array{color?:string,limit?:int,offset?:int} $args initial color settings.
870     * @return array An array of color palettes.
871     */
872    public static function get_color_palettes( $args = array() ) {
873        $defaults = array(
874            'color'  => false,
875            'limit'  => 6,
876            'offset' => 0,
877        );
878
879        $args = wp_parse_args( $args, $defaults );
880
881        if ( $args['color'] ) {
882            $args['color'] = self::normalize_color( $args['color'] );
883
884            $palettes = wp_cache_get( 'color-palettes-from-' . $args['color'], 'colors' );
885
886            if ( false === $palettes ) {
887                $palettes = Colors_API::call( 'palettes', array( 'color' => $args['color'] ) );
888                if ( ! is_wp_error( $palettes ) ) {
889                    wp_cache_set( 'color-palettes-from-' . $args['color'], $palettes, 'colors', MONTH_IN_SECONDS );
890                }
891            }
892        } else {
893            $palettes = wp_cache_get( 'color-palettes-top', 'colors' );
894
895            if ( false === $palettes ) {
896                $palettes = Colors_API::call( 'palettes' );
897                if ( ! is_wp_error( $palettes ) ) {
898                    wp_cache_set( 'color-palettes-top', $palettes, 'colors', MONTH_IN_SECONDS );
899                }
900            }
901        }
902
903        $palettes = array_slice( $palettes, $args['offset'], $args['limit'] );
904
905        if ( ! empty( $palettes ) ) {
906            foreach ( $palettes as $palette_index => $palette ) {
907                $colors = array();
908
909                foreach ( self::get_color_slots() as $color_index => $color_key ) {
910                    if ( count( $palette['colors'] ) === $color_index ) {
911                        break;
912                    }
913
914                    $colors[ $color_key ] = $palette['colors'][ $color_index ]['hex'];
915                }
916
917                $palettes[ $palette_index ]['colors'] = $colors;
918            }
919        }
920
921        // Shuffle palettes to make them less repetitive
922        shuffle( $palettes );
923
924        // Prepend theme-defined palettes to the first set of palettes
925        if ( 0 === (int) $args['offset'] ) {
926            $palettes = array_merge( self::get_theme_color_palettes(), $palettes );
927            $palettes = array_slice( $palettes, 0, (int) $args['limit'] );
928        }
929
930        return $palettes;
931    }
932
933    /**
934     * Return an image URL based on Gravatar URL.
935     *
936     * @param string $image_url URL to be transformed.
937     * @return string
938     */
939    public static function gravatar_image_url( $image_url ) {
940        $prefix_http     = preg_quote( 'http://www.gravatar.com/avatar/', '/' );
941        $prefix_https    = preg_quote( 'https://secure.gravatar.com/avatar/', '/' );
942        $gravatar_prefix = sprintf( '/^(%s|%s)/', $prefix_http, $prefix_https );
943        $is_gravatar_url = preg_match( $gravatar_prefix, $image_url );
944
945        if ( $is_gravatar_url ) {
946            $image_url = preg_replace( '#/([0-9a-f]+)/#', '/$1.jpg', $image_url );
947        }
948
949        return $image_url;
950    }
951
952    /**
953     * Returns a color palette matching a given image thanks to the Tonesque
954     * lib.
955     *
956     * @param array{image?:string} $args an image URL in the form of an array.
957     * @return array A single color palette
958     */
959    public static function get_generated_palette( $args = array() ) {
960        // Some themes, like Ryu, include an older version of Tonesque, which is loaded instead of the version in `/wp-content/lib/`.
961        // For now, only load the shared library if Tonesque isn't already present. See #5557.
962        if ( ! class_exists( 'Tonesque' ) ) {
963            require_lib( 'tonesque' );
964        }
965
966        // If the loaded version doesn't have the method needed to support palette generation, abort for now until the themes are updated. See #5557.
967        if ( ! method_exists( 'Tonesque', 'grab_points' ) ) {
968            return array();
969        }
970
971        $defaults = array(
972            'image' => false,
973        );
974
975        $args  = wp_parse_args( $args, $defaults );
976        $image = $args['image'] ?? '';
977
978        if ( ! $image ) {
979            return array();
980        }
981
982        $tonesque = new Tonesque( $image );
983        $points   = $tonesque->grab_points( 'hex' );
984
985        $roles = self::get_color_slots();
986        shuffle( $roles );
987
988        if ( ! is_array( $points ) ) {
989            return array();
990        }
991
992        $colors = array_combine( $roles, $points );
993
994        $palette = array(
995            'id'     => 'generated-palette',
996            'colors' => $colors,
997        );
998
999        return $palette;
1000    }
1001
1002    /**
1003     * Returns theme color pallettes.
1004     */
1005    public static function get_theme_color_palettes() {
1006        if ( empty( self::$color_palettes ) ) {
1007            return array();
1008        }
1009
1010        $map                = self::get_color_slots();
1011        $formatted_palettes = array();
1012        foreach ( self::$color_palettes as $id => $palette ) {
1013            $formatted_palette = array(
1014                'id'     => $id,
1015                'colors' => array(),
1016            );
1017            foreach ( $map as $index => $key ) {
1018                if ( ! isset( $palette['palette'][ $index ] ) ) {
1019                    continue;
1020                }
1021                $formatted_palette['colors'][ $key ] = str_replace( '#', '', $palette['palette'][ $index ] );
1022            }
1023
1024            $formatted_palettes[] = $formatted_palette;
1025        }
1026
1027        return $formatted_palettes;
1028    }
1029
1030    /**
1031     * Query and return pattern data.
1032     *
1033     * @param array{color?:string,limit?:int,offset?:int} $args initial settings.
1034     * @return array An array of patterns.
1035     */
1036    public static function get_patterns( $args = array() ) {
1037        $defaults = array(
1038            'color'  => false,
1039            'limit'  => 4,
1040            'offset' => 0,
1041        );
1042
1043        $args = wp_parse_args( $args, $defaults );
1044
1045        if ( $args['color'] ) {
1046            $args['color'] = self::normalize_color( $args['color'] );
1047
1048            $patterns = wp_cache_get( 'patterns-from-' . $args['color'], 'colors' );
1049
1050            if ( false === $patterns ) {
1051                $patterns = Colors_API::call( 'patterns', array( 'color' => $args['color'] ) );
1052                if ( ! is_wp_error( $patterns ) ) {
1053                    wp_cache_set( 'patterns-from-' . $args['color'], $patterns, 'colors', MONTH_IN_SECONDS );
1054                }
1055            }
1056        } else {
1057            $patterns = wp_cache_get( 'patterns-top', 'colors' );
1058
1059            if ( false === $patterns ) {
1060                $patterns = Colors_API::call( 'patterns' );
1061                if ( ! is_wp_error( $patterns ) ) {
1062                    wp_cache_set( 'patterns-top', $patterns, 'colors', MONTH_IN_SECONDS );
1063                }
1064            }
1065        }
1066
1067        $patterns = array_slice( $patterns, $args['offset'], $args['limit'] );
1068
1069        if ( ! empty( $patterns ) ) {
1070            foreach ( $patterns as $pattern_index => $pattern ) {
1071                $colors = array();
1072
1073                foreach ( self::get_color_slots() as $color_index => $color_key ) {
1074                    if ( count( $pattern['colors'] ) === $color_index ) {
1075                        break;
1076                    }
1077
1078                    $colors[ $color_key ] = $pattern['colors'][ $color_index ]['hex'];
1079                }
1080
1081                $patterns[ $pattern_index ]['colors']            = $colors;
1082                $patterns[ $pattern_index ]['preview_image_url'] = apply_filters( 'jetpack_photon_url', $pattern['preview_image_url'], array(), 'network_path' );
1083            }
1084        }
1085
1086        return $patterns;
1087    }
1088
1089    /**
1090     * Converts rgb() or hex color codes to the AABBCC format:
1091     *
1092     * @param string $color An rgb or hex color code.
1093     * @return string
1094     */
1095    public static function normalize_color( $color ) {
1096        if ( false !== strpos( $color, 'rgb' ) ) {
1097            $color_data       = preg_replace( '/[^0-9\.,]/', '', $color );
1098            $color_components = explode( ',', $color_data );
1099
1100            $hex_color = '';
1101
1102            for ( $i = 0; $i < 3; $i++ ) {
1103                $hex_equivalent = dechex( intval( $color_components[ $i ] ) );
1104                if ( strlen( $hex_equivalent ) < 2 ) {
1105                    $hex_color .= '0';
1106                }
1107                $hex_color .= $hex_equivalent;
1108            }
1109
1110            return strtoupper( $hex_color );
1111        } else {
1112            $hex = strtoupper( substr( preg_replace( '/[^0-9A-Z]/i', '', $color ), 0, 6 ) );
1113
1114            if ( strlen( $hex ) === 3 ) {
1115                $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
1116            } else {
1117                for ( $i = strlen( $hex ); $i < 6; $i++ ) {
1118                    $hex = '0' . $hex;
1119                }
1120            }
1121
1122            return $hex;
1123        }
1124    }
1125
1126    /**
1127     * Finds colors that could be suitable complement to a given set of colors.
1128     *
1129     * @param array{color?:string,role?:string,colors?:array,limit?:int} $args initial settings.
1130     * @return array An array of color codes.
1131     */
1132    public static function get_color_recommendations( $args ) {
1133        $defaults = array(
1134            'color'  => false,
1135            'role'   => false,
1136            'colors' => false,
1137            'limit'  => 8,
1138        );
1139
1140        $args = wp_parse_args( $args, $defaults );
1141
1142        if ( $args['color'] ) {
1143            $args['color'] = self::normalize_color( $args['color'] );
1144        }
1145
1146        $colors = array();
1147
1148        foreach ( $args['colors'] as $role => $color ) {
1149            $color                   = self::normalize_color( $color );
1150            $args['colors'][ $role ] = $color;
1151
1152            $palettes = Colors_API::call(
1153                'palettes',
1154                array(
1155                    'color' => $color,
1156                    'limit' => 8,
1157                )
1158            );
1159
1160            if ( is_array( $palettes ) ) {
1161                foreach ( $palettes as $palette ) {
1162                    $multiplier = 0;
1163
1164                    foreach ( $palette['colors'] as $_color ) {
1165                        if ( ! $_color ) {
1166                            continue;
1167                        }
1168
1169                        // If this palette contains more than one of the guide colors,
1170                        // give it more weight.
1171                        if ( in_array( $_color, $args['colors'], true ) ) {
1172                            ++$multiplier;
1173                        }
1174                    }
1175
1176                    foreach ( $palette['colors'] as $palette_role => $_color ) {
1177                        if ( ! $_color ) {
1178                            continue;
1179                        }
1180
1181                        $colors[ $_color ] += ( 1 * $multiplier );
1182
1183                        if ( $palette_role === $args['role'] ) {
1184                            $colors[ $_color ] += ( 1 * $multiplier );
1185                        }
1186                    }
1187                }
1188            }
1189        }
1190
1191        foreach ( $args['colors'] as $color ) {
1192            unset( $colors[ $color ] );
1193        }
1194
1195        if ( $args['color'] ) {
1196            unset( $colors[ $args['color'] ] );
1197        }
1198
1199        arsort( $colors );
1200        $colors = array_keys( $colors );
1201
1202        if ( count( $colors ) < 8 ) {
1203            $more_suggestions = self::color_suggestions( $args['colors'], $args['role'] );
1204            $colors           = array_merge( $colors, $more_suggestions );
1205
1206            foreach ( $args['colors'] as $color ) {
1207                unset( $colors[ $color ] );
1208            }
1209
1210            if ( $args['color'] ) {
1211                unset( $colors[ $args['color'] ] );
1212            }
1213        }
1214
1215        $colors = array_slice( $colors, 0, $args['limit'] );
1216
1217        return $colors;
1218    }
1219
1220    /**
1221     * Finds patterns that could be suitable complement to a given set of colors.
1222     *
1223     * @param array{colors?:array,limit?:int} $args initial settings.
1224     * @return array An array of patterns.
1225     */
1226    public static function get_pattern_recommendations( $args ) {
1227        $defaults = array(
1228            'colors' => false,
1229            'limit'  => 4,
1230        );
1231
1232        $args = wp_parse_args( $args, $defaults );
1233
1234        $patterns_by_id = array();
1235        $pattern_ids    = array();
1236
1237        foreach ( $args['colors'] as $role => $color ) {
1238            $color                   = self::normalize_color( $color );
1239            $args['colors'][ $role ] = $color;
1240
1241            $color_patterns = Colors_API::call(
1242                'patterns',
1243                array(
1244                    'color' => $color,
1245                    'limit' => 5,
1246                )
1247            );
1248
1249            if ( is_array( $color_patterns ) ) {
1250                foreach ( $color_patterns as $pattern ) {
1251                    $patterns_by_id[ $pattern['id'] ] = $pattern;
1252
1253                    if ( ! isset( $pattern_ids[ $pattern['id'] ] ) ) {
1254                        $pattern_ids[ $pattern['id'] ] = 0;
1255                    }
1256                    $pattern_ids[ $pattern['id'] ] += 1;
1257
1258                    foreach ( $pattern['colors'] as $value ) {
1259                        if ( in_array( $value, $args['colors'], true ) ) {
1260                            $pattern_ids[ $pattern['id'] ] += 1;
1261                        }
1262                    }
1263                }
1264            }
1265        }
1266
1267        arsort( $pattern_ids );
1268        $pattern_ids = array_keys( $pattern_ids );
1269        $pattern_ids = array_slice( $pattern_ids, 0, $args['limit'] );
1270
1271        $patterns = array();
1272
1273        foreach ( $pattern_ids as $pattern_id ) {
1274            unset( $patterns_by_id[ $pattern_id ]['colors'] );
1275            $patterns[] = $patterns_by_id[ $pattern_id ];
1276        }
1277
1278        return $patterns;
1279    }
1280
1281    /**
1282     * Make this work inside the Customizer.
1283     *
1284     * @param WP_Customize_Manager $wp_customize the customizer manager instance.
1285     */
1286    public static function in_customizer( $wp_customize ) {
1287        // Include controller class
1288        require_once __DIR__ . '/class-colors-controller.php';
1289
1290        $wp_customize->add_section(
1291            'colors_manager_tool',
1292            array(
1293                'title'    => __( 'Colors & Backgrounds', 'wpcomsh' ),
1294                'priority' => 35,
1295            )
1296        );
1297
1298        $setting_opts = array(
1299            'default'    => self::get_colors(),
1300            'capability' => 'edit_theme_options',
1301            'transport'  => 'postMessage',
1302            'type'       => 'theme_mod',
1303        );
1304
1305        if ( is_admin() ) {
1306            $setting_opts = array_merge(
1307                $setting_opts,
1308                array(
1309                    'sanitize_callback'    => array( __CLASS__, 'sanitize_colors_on_save' ),
1310                    'sanitize_js_callback' => array( __CLASS__, 'sanitize_colors' ),
1311                )
1312            );
1313        }
1314
1315        $wp_customize->add_setting( 'colors_manager[colors]', $setting_opts );
1316
1317        $wp_customize->add_control(
1318            new Colors_Manager_Control(
1319                $wp_customize,
1320                'colors-tool',
1321                array(
1322                    'label'    => __( 'Colors', 'wpcomsh' ),
1323                    'section'  => 'colors_manager_tool',
1324                    'settings' => 'colors_manager[colors]',
1325                )
1326            )
1327        );
1328    }
1329
1330    /**
1331     * Sanitizes colors on save.
1332     *
1333     * @param array $set_colors saved colors.
1334     * @return array
1335     */
1336    public static function sanitize_colors_on_save( $set_colors ) {
1337        return self::sanitize_colors( $set_colors );
1338    }
1339
1340    /**
1341     * Sanitizes colors.
1342     *
1343     * @param array $set_colors saved colors.
1344     * @return array
1345     */
1346    public static function sanitize_colors( $set_colors ) {
1347        if ( ! is_array( $set_colors ) && ! is_object( $set_colors ) ) {
1348            return array();
1349        }
1350        // let's make sure all of our keys/values are proper
1351        $colors_wanted = array();
1352        $cats          = self::get_color_slots();
1353        if ( ! class_exists( 'Jetpack_color' ) ) {
1354            require_lib( 'class.color' );
1355        }
1356        foreach ( $set_colors as $key => $color ) {
1357            if ( ! in_array( $key, $cats, true ) || ! $color ) {
1358                continue;
1359            }
1360            try {
1361                $color_object          = new Jetpack_Color( $color );
1362                $colors_wanted[ $key ] = '#' . $color_object->toHex();
1363            } catch ( Exception $e ) { // phpcs:ignore
1364                // Exception not handled to avoid it propagating further, apparently.
1365            }
1366        }
1367        return $colors_wanted;
1368    }
1369
1370    /**
1371     * Overriding theme colors.
1372     */
1373    public static function override_themecolors() {
1374        global $themecolors;
1375
1376        if ( ! self::should_enable_colors() ) {
1377            return;
1378        }
1379
1380        $opts = get_theme_mod( 'colors_manager', array( 'colors' => false ) );
1381        if ( ! isset( $opts ) ) {
1382            return;
1383        }
1384
1385        $colors = $opts['colors'];
1386
1387        if ( isset( $colors['fg1'] ) ) {
1388            $colors['border'] = $colors['fg1'];
1389        }
1390        if ( isset( $colors['link'] ) ) {
1391            $colors['url'] = $colors['link'];
1392        }
1393        if ( isset( $colors['txt'] ) ) {
1394            $colors['text'] = $colors['txt'];
1395        }
1396
1397        unset( $colors['fg1'] );
1398        unset( $colors['fg2'] );
1399        unset( $colors['txt'] );
1400
1401        foreach ( $colors as $role => $color ) {
1402            if ( $color ) {
1403                $themecolors[ $role ] = substr( $color, 1 );
1404            }
1405        }
1406    }
1407
1408    /**
1409     * Injects our postMessage listener scripts into the theme
1410     *
1411     * @param WP_Customize_Manager $wp_customize the customizer manager instance.
1412     */
1413    public static function theme_colors_js( $wp_customize ) {
1414        if ( $wp_customize->is_preview() && ! is_admin() ) {
1415            wp_enqueue_script( 'colors-instapreview' );
1416            $js_data = array(
1417                'colors'        => self::$colors,
1418                'defaultColors' => self::get_default_colors(),
1419                'extraCss'      => self::get_extra_css( true ),
1420                'extraColors'   => self::$extra_colors,
1421            );
1422            wp_localize_script( 'colors-instapreview', 'ColorsTool', $js_data );
1423        }
1424    }
1425
1426    /**
1427     * Prints theme CSS.
1428     */
1429    public static function print_theme_css() {
1430        if ( ! self::should_enable_colors() ) {
1431            return;
1432        }
1433        $css = self::get_theme_css();
1434        printf(
1435            '<style type="text/css" id="custom-colors-css">%s</style>%s',
1436            wp_strip_all_tags( $css ), // phpcs:ignore -- CSS can't be properly escaped with esc_html
1437            "\n"
1438        );
1439    }
1440
1441    /**
1442     * Return theme CSS.
1443     */
1444    public static function get_theme_css() {
1445        $opts   = get_theme_mod(
1446            'colors_manager',
1447            array(
1448                'colors' => false,
1449            )
1450        );
1451        $colors = $opts['colors'];
1452
1453        // extra colors/CSS: always on
1454        $css = self::get_extra_css();
1455
1456        // user colors
1457        foreach ( self::$colors as $cat => $rules ) {
1458            if ( ! isset( $colors[ $cat ] ) ) {
1459                continue;
1460            }
1461
1462            $color = $colors[ $cat ];
1463            foreach ( $rules as $rule ) {
1464                $css .= self::css_rule( $rule, $color );
1465            }
1466        }
1467
1468        // Minify & cache for future use.
1469        $minifier = new tubalmartin\CssMin\Minifier();
1470        $css      = $minifier->run( $css );
1471
1472        return $css;
1473    }
1474
1475    /**
1476     * Get CSS rule.
1477     *
1478     * @todo possibly combine all of this into a keyed array to prevent selector duplication bloat
1479     * @param array  $rule the CSS rule.
1480     * @param string $color the color string.
1481     * @return string
1482     */
1483    public static function css_rule( $rule, $color ) {
1484        $css = '';
1485
1486        if ( ! isset( $rule[0] ) || ! isset( $rule[1] ) ) {
1487            return $css;
1488        }
1489
1490        if ( isset( $rule[2] ) ) {
1491            // we'll need it in either case
1492            if ( ! class_exists( 'Jetpack_color' ) ) {
1493                require_lib( 'class.color' );
1494            }
1495
1496            try {
1497                $working_color = new Jetpack_Color( $color );
1498            } catch ( RangeException $e ) {
1499                $message  = 'rule: ' . print_r( $rule, 1 ) . "\n"; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
1500                $message .= 'call: $working_color = new Jetpack_Color( ' . $color . ' );' . "\n";
1501                self::exception_mailer( $message );
1502                return '';
1503            }
1504
1505            $number = (float) $rule[2];
1506            // ensure contrast or darken/lighten
1507            if ( is_string( $rule[2] ) ) {
1508                $first_char = substr( $rule[2], 0, 1 );
1509                // darken/lighten
1510                if ( '+' === $first_char || '-' === $first_char ) {
1511                    $modify = 10 * $number;
1512                    $color  = $working_color->incrementLightness( intval( $modify ) )->toString();
1513                } else {
1514                    // hex bg for contrast
1515                    if ( '#' === $first_char ) {
1516                        try {
1517                            $bg_color = new Jetpack_Color( $rule[2] );
1518                        } catch ( RangeException $e ) {
1519                            $message  = 'function: ' . __FUNCTION__ . "\n";
1520                            $message .= 'call: $bg_color = new Jetpack_Color( ' . $rule[2] . ' );' . "\n";
1521                            self::exception_mailer( $message );
1522                            return '';
1523                        }
1524                    } elseif ( isset( self::$colors[ $rule[2] ] ) ) { // set color bg for contrast
1525
1526                        $set_colors = self::get_colors();
1527                        try {
1528                            $bg_color = new Jetpack_Color( $set_colors[ $rule[2] ] ?? null );
1529                        } catch ( RangeException $e ) {
1530                            $message  = 'function: ' . __FUNCTION__ . "\n";
1531                            $message .= 'call: $bg_color = new Jetpack_Color( ' . $set_colors[ $rule[2] ] . ' );' . "\n";
1532                            self::exception_mailer( $message );
1533                            return '';
1534                        }
1535                    }
1536
1537                    // we have a bg color to contrast
1538                    if ( isset( $bg_color ) && is_a( $bg_color, 'Jetpack_Color' ) ) {
1539                        // default contrast of 5, can be overridden with 4th arg.
1540                        $contrast = $rule[3] ?? 5;
1541                        $color    = $working_color->getReadableContrastingColor( $bg_color, $contrast )->toString();
1542                    }
1543                }
1544            } elseif ( $rule[2] < 1 ) { // alpha
1545                unset( $rule[2] );
1546                // back compat for non-rgba browsers
1547                $css  .= self::css_rule( $rule, $color );
1548                $color = $working_color->toCSS( 'rgba', intval( $number ) );
1549            }
1550        }
1551
1552        $css .= "{$rule[0]} { {$rule[1]}{$color};}\n";
1553        return $css;
1554    }
1555
1556    /**
1557     * Get extra CSS.
1558     *
1559     * @param boolean $only_callback no processing, just callback.
1560     */
1561    public static function get_extra_css( $only_callback = false ) {
1562        $css      = '';
1563        $extra_cb = get_theme_support( 'custom_colors_extra_css' );
1564
1565        if ( is_array( $extra_cb ) && is_callable( $extra_cb[0] ) ) {
1566            // will work with return values or straight printing
1567            ob_start();
1568            $css  = call_user_func( $extra_cb[0] );
1569            $css .= ob_get_clean();
1570        }
1571
1572        if ( $only_callback ) {
1573            return $css;
1574        }
1575
1576        foreach ( self::$extra_colors as $extra ) {
1577            if ( ! isset( $extra['rules'] ) || ! is_array( $extra['rules'] ) ) {
1578                continue;
1579            }
1580            $color = $extra['color'];
1581            foreach ( $extra['rules'] as $rule ) {
1582                $css .= self::css_rule( $rule, (string) $color );
1583            }
1584        }
1585        return $css;
1586    }
1587
1588    /**
1589     * Function for making theme annotations.
1590     *
1591     * @param string      $category The color category. One of bg, txt, link, fg1, fg2.
1592     * @param string      $default_color The default color for this category.
1593     * @param array       $rules Array of rule arrays. $rule: array( selector, property, opacity );.
1594     * @param bool|string $label Optional. A UI helper label for identifying what a particular color will change in the theme.
1595     */
1596    public static function add_color_rule( $category, $default_color, $rules, $label = false ) {
1597        // extra rules
1598        if ( 'extra' === $category ) {
1599            self::$extra_colors[] = array(
1600                'color' => $default_color,
1601                'rules' => $rules,
1602            );
1603            return;
1604        }
1605        // prime it
1606        if ( ! isset( self::$colors[ $category ] ) ) {
1607            self::$colors[ $category ] = array();
1608        }
1609        self::$colors[ $category ] = array_merge( self::$colors[ $category ], $rules );
1610
1611        self::$default_colors[ $category ] = $default_color;
1612        if ( $label ) {
1613            self::$labels[ $category ] = $label;
1614        }
1615    }
1616
1617    /**
1618     * Allow a theme to declare its own color palettes.
1619     *
1620     * @param array       $palette An array with 5 colors.
1621     * @param bool|string $title optional title string.
1622     */
1623    public static function add_color_palette( $palette, $title = false ) {
1624        if ( ! $title ) {
1625            $theme = wp_get_theme();
1626            $title = sprintf(
1627                // translators: %1$s is a theme name, %2$s is its custom color scheme number.
1628                __( '%1$s Alternative Scheme %2$s', 'wpcomsh' ),
1629                $theme->display( 'Name' ),
1630                count( self::$color_palettes ) + 1
1631            );
1632        }
1633
1634        $id = sanitize_title_with_dashes( $title );
1635
1636        self::$color_palettes[ $id ] = compact( 'title', 'palette' );
1637    }
1638
1639    /**
1640     * Loads theme annotations, and filter them if loaded.
1641     *
1642     * @param  boolean $theme Which theme to check for annotations on. Defaults to current theme.
1643     * @return boolean Theme has annotations.
1644     */
1645    protected static function load_annotations( $theme = false ) {
1646        $theme_name       = 'pub/' . self::pick_theme( $theme );
1647        $annotations_file = get_stylesheet_directory() . '/inc/wpcom-colors.php';
1648        self::prime_color_labels();
1649        if ( is_readable( $annotations_file ) ) {
1650            require_once $annotations_file;
1651            self::$colors = apply_filters( 'custom_colors_rules', self::$colors, $theme_name );
1652            self::handle_unset_colors();
1653            return true;
1654        }
1655        return false;
1656    }
1657
1658    /**
1659     * Unset colors that need to be unset.
1660     */
1661    protected static function handle_unset_colors() {
1662        foreach ( self::$colors as $key => $value ) {
1663            if ( empty( $value ) ) {
1664                // set Label to Unused
1665                self::$labels[ $key ] = __( 'Unused', 'wpcomsh' );
1666                unset( self::$colors[ $key ] );
1667            }
1668        }
1669    }
1670
1671    /**
1672     * Sets default, i10n-ized default color labels that can be overridden in annotations.
1673     */
1674    protected static function prime_color_labels() {
1675        if ( ! empty( self::$labels ) ) {
1676            return;
1677        }
1678
1679        self::$labels = array(
1680            'bg'   => __( 'Background', 'wpcomsh' ),
1681            'txt'  => __( 'Headings', 'wpcomsh' ),
1682            'link' => __( 'Links', 'wpcomsh' ),
1683            'fg1'  => __( 'Accent #1', 'wpcomsh' ),
1684            'fg2'  => __( 'Accent #2', 'wpcomsh' ),
1685        );
1686    }
1687
1688    /**
1689     * Generate color suggestions for a given role from a set of colors.
1690     *
1691     * @param array  $colors color array.
1692     * @param string $role (bg|fg1|fg2|txt|link).
1693     * @return array
1694     */
1695    public static function color_suggestions( $colors, $role ) {
1696        if ( ! class_exists( 'Jetpack_color' ) ) {
1697            require_lib( 'class.color' );
1698        }
1699
1700        $suggestions = array();
1701
1702        $suggestions = array_merge( $suggestions, self::color_suggestions_from_palette( $colors, $role ) );
1703        $suggestions = array_merge( $suggestions, self::color_suggestions_from_math( $colors, $role ) );
1704
1705        shuffle( $suggestions );
1706
1707        return $suggestions;
1708    }
1709
1710    /**
1711     * Generate color suggestions by grabbing a popular palette and applying
1712     * it as a transformation to the colors we're using as a guide.
1713     *
1714     * @param array  $colors color array.
1715     * @param string $role (bg|fg1|fg2|txt|link).
1716     * @return array
1717     */
1718    public static function color_suggestions_from_palette( $colors, $role ) {
1719        $suggestions = array();
1720
1721        $top_palette = self::get_color_palettes(
1722            array(
1723                'limit'  => 1,
1724                'offset' => wp_rand(
1725                    0,
1726                    100
1727                ),
1728            )
1729        );
1730
1731        if ( ! $top_palette ) {
1732            return array();
1733        }
1734
1735        $top_palette = $top_palette[0];
1736
1737        $equivalent_color_hex = $top_palette['colors'][ $role ];
1738
1739        foreach ( $top_palette['colors'] as $palette_role => $palette_color_hex ) {
1740            $base_color_hex = $colors[ $palette_role ];
1741            try {
1742                // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1743                // $base_color:$new_color :: $palette_color:$equivalent_color
1744                $base_color       = new Jetpack_Color( $base_color_hex );
1745                $palette_color    = new Jetpack_Color( $palette_color_hex );
1746                $equivalent_color = new Jetpack_Color( $equivalent_color_hex );
1747
1748                $palette_hsl    = $palette_color->toHsl();
1749                $equivalent_hsl = $equivalent_color->toHsl();
1750
1751                $base_color->incrementHue( $equivalent_hsl['h'] - $palette_hsl['h'] );
1752                $base_color->saturate( $equivalent_hsl['s'] - $palette_hsl['s'] );
1753                $base_color->lighten( $equivalent_hsl['l'] - $palette_hsl['l'] );
1754
1755                $suggestions[] = self::normalize_color( $base_color->toHex() );
1756            } catch ( RangeException $e ) {
1757                $message  = "Color exception!\n\n";
1758                $message .= "role: $role\n";
1759                $message .= "base: $base_color_hex\n";
1760                $message .= "palette: $palette_color_hex\n";
1761                $message .= "equiv: $equivalent_color_hex\n";
1762                $message .= 'colors arg: ' . print_r( $colors, 1 ); // phpcs:ignore
1763                self::exception_mailer( $message );
1764                continue;
1765            }
1766        }
1767
1768        return $suggestions;
1769    }
1770
1771    // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1772    /**
1773     * Mail the exception.
1774     *
1775     * @param string $message the exception private.
1776     */
1777    public static function exception_mailer( $message = 'Needs a message' ) {
1778        $message .= "\n\nblog: " . home_url() . "\n";
1779        $message .= 'backtrace: ' . wp_debug_backtrace_summary() . "\n"; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
1780        // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1781        // wp_mail( 'someone@example.com', 'Color Exception on WordPress.com', $message );
1782    }
1783    // phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1784
1785    /**
1786     * Use a set of predefined transformations to generate color suggestions
1787     * based on roles.
1788     *
1789     * @param array  $colors color array.
1790     * @param string $role (bg|fg1|fg2|txt|link).
1791     * @return array
1792     */
1793    public static function color_suggestions_from_math( $colors, $role ) {
1794        $suggestions = array();
1795
1796        // These are the result of a couple of hours of playing around.
1797        // Nothing here is set in stone.
1798        $relations = array(
1799            'bg:fg1'   => array( 'brighter', 'saturate' ),
1800            'bg:fg2'   => array( 'darker', 'desaturate' ),
1801            'bg:txt'   => array( '+triad' ),
1802            'bg:link'  => array( '-triad' ),
1803            'fg1:bg'   => array( 'desaturate', 'darker' ),
1804            'fg1:fg2'  => array( '+analog' ),
1805            'fg1:txt'  => array( '-tetrad' ),
1806            'fg1:link' => array( 'darker', 'saturate' ),
1807            'fg2:bg'   => array( 'saturate', 'brighter' ),
1808            'fg2:fg1'  => array( '-analog' ),
1809            'fg2:txt'  => array( '-tetrad' ),
1810            'fg2:link' => array( 'darker', 'saturate' ),
1811            'txt:bg'   => array( '+triad' ),
1812            'txt:fg1'  => array( '+tetrad' ),
1813            'txt:fg2'  => array( '+tetrad' ),
1814            'txt:link' => array( '-split-complement', 'saturate' ),
1815            'link:bg'  => array( '-triad' ),
1816            'link:fg1' => array( 'desaturate', 'brighter' ),
1817            'link:fg2' => array( 'desaturate', 'brighter' ),
1818            'link:txt' => array( 'darker', 'saturate' ),
1819        );
1820
1821        foreach ( $colors as $known_role => $color_code ) {
1822            if ( $known_role === $role ) {
1823                continue;
1824            }
1825
1826            $transforms = $relations[ $known_role . ':' . $role ];
1827            try {
1828                $new_color = new Jetpack_Color( self::normalize_color( $color_code ) );
1829            } catch ( RangeException $e ) {
1830                $message  = 'function: ' . __FUNCTION__ . "\n";
1831                $message .= 'call: $new_color = new Jetpack_Color( self::normalize_color( ' . $color_code . ' ) );' . "\n";
1832                $message .= 'normalized color: ' . self::normalize_color( $color_code );
1833                self::exception_mailer( $message );
1834                continue;
1835            }
1836
1837            foreach ( $transforms as $transform ) {
1838                switch ( $transform ) {
1839                    case 'complement':
1840                        $new_color->getComplement();
1841                        break;
1842                    case 'brighter':
1843                        $new_color->lighten( 25 );
1844                        break;
1845                    case 'darker':
1846                        $new_color->darken( 25 );
1847                        break;
1848                    case 'grayscale':
1849                        $new_color->toGrayscale();
1850                        break;
1851                    case '+split-complement':
1852                        $new_color->getSplitComplement( 1 );
1853                        break;
1854                    case '-split-complement':
1855                        $new_color->getSplitComplement( -1 );
1856                        break;
1857                    case '+triad':
1858                        $new_color->getTriad( 1 );
1859                        break;
1860                    case '-triad':
1861                        $new_color->getTriad( -1 );
1862                        break;
1863                    case 'saturate':
1864                        $new_color->saturate( 25 );
1865                        break;
1866                    case 'desaturate':
1867                        $new_color->desaturate( 25 );
1868                        break;
1869                    case '+analog':
1870                        $new_color->getAnalog( 1 );
1871                        break;
1872                    case '-analog':
1873                        $new_color->getAnalog( -1 );
1874                        break;
1875                    case '+tetrad':
1876                        $new_color->getTetrad( 1 );
1877                        break;
1878                    case '-tetrad':
1879                        $new_color->getTetrad( -1 );
1880                        break;
1881                }
1882            }
1883
1884            $suggestions[] = self::normalize_color( $new_color->toHex() );
1885        }
1886
1887        return $suggestions;
1888    }
1889}
1890
1891/**
1892 * Nothing to override
1893 */
1894class Colors_Manager extends Colors_Manager_Common {}
1895
1896/**
1897 * Adds a color rule.
1898 *
1899 * @param string      $category The color category. One of bg, txt, link, fg1, fg2.
1900 * @param string      $default_color The default color for this category.
1901 * @param array       $rules Array of rule arrays. $rule: array( selector, property, opacity );.
1902 * @param bool|string $label Optional. A UI helper label for identifying what a particular color will change in the theme.
1903 */
1904function add_color_rule( $category, $default_color, $rules, $label = false ) {
1905    Colors_Manager::add_color_rule( $category, $default_color, $rules, $label );
1906}
1907
1908/**
1909 * Adds color palette.
1910 *
1911 * @param array       $palette An array with 5 colors.
1912 * @param bool|string $title optional title string.
1913 */
1914function add_color_palette( $palette, $title = false ) {
1915    return Colors_Manager::add_color_palette( $palette, $title );
1916}
1917
1918/**
1919 * Load the color manager.
1920 *
1921 * Customizer colors apply on the frontend, in the Customizer preview, and via
1922 * the AJAX palette/pattern tools. The block editor intentionally has no color
1923 * manager: injecting Customizer colors leaked into the editor UI, so we leave
1924 * those admin screens untouched and let the editor render with default colors,
1925 * matching Core.
1926 */
1927function load_corresponding_color_manager() {
1928    global $pagenow;
1929    if ( is_admin() && 'customize.php' !== $pagenow && ! defined( 'DOING_AJAX' ) ) {
1930        return;
1931    }
1932
1933    Colors_Manager::init();
1934}
1935
1936add_action( 'init', 'load_corresponding_color_manager' );