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