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