Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
site_logo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
Site_Logo
0.00% covered (danger)
0.00%
0 / 141
0.00% covered (danger)
0.00%
0 / 17
1722
0.00% covered (danger)
0.00%
0 / 1
 instance
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register_hooks
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 customize_register
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 1
30
 preview_enqueue
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 header_text_classes
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 head_text_styles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 theme_size
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 media_manager_image_sizes
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 add_media_state
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 reset_on_attachment_delete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 has_site_logo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 remove_site_logo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 body_classes
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 sanitize_checkbox
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 sanitize_logo_setting
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 customizer_preview
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Site Logo class main class file.
4 *
5 * @package automattic/jetpack
6 */
7
8if ( ! defined( 'ABSPATH' ) ) {
9    exit( 0 );
10}
11
12/**
13 * Site Logo class for managing a theme-agnostic logo through the Customizer.
14 */
15class Site_Logo {
16    /**
17     * Stores our single instance.
18     *
19     * @var Site_Logo
20     */
21    private static $instance;
22
23    /**
24     * Stores the attachment ID of the site logo.
25     *
26     * @var int
27     */
28    public $logo;
29
30    /**
31     * Return our instance, creating a new one if necessary.
32     *
33     * @uses Site_Logo::$instance
34     * @return object Site_Logo
35     */
36    public static function instance() {
37        if ( ! isset( self::$instance ) ) {
38            self::$instance = new Site_Logo();
39            self::$instance->register_hooks();
40        }
41
42        return self::$instance;
43    }
44
45    /**
46     * Get our current logo settings stored in options.
47     *
48     * @uses get_option()
49     */
50    private function __construct() {
51        $this->logo = (int) get_option( 'site_logo', null );
52    }
53
54    /**
55     * Register our actions and filters.
56     *
57     * @uses Site_Logo::head_text_styles()
58     * @uses Site_Logo::customize_register()
59     * @uses Site_Logo::preview_enqueue()
60     * @uses Site_Logo::body_classes()
61     * @uses Site_Logo::media_manager_image_sizes()
62     * @uses add_action
63     * @uses add_filter
64     */
65    public function register_hooks() {
66        // This would only happen if a theme supports BOTH site-logo and custom-logo for some reason
67        if ( current_theme_supports( 'custom-logo' ) ) {
68            return;
69        }
70
71        add_action( 'wp_head', array( $this, 'head_text_styles' ) );
72        add_action( 'customize_register', array( $this, 'customize_register' ) );
73        add_action( 'customize_preview_init', array( $this, 'preview_enqueue' ) );
74        add_action( 'delete_attachment', array( $this, 'reset_on_attachment_delete' ) );
75        add_filter( 'body_class', array( $this, 'body_classes' ) );
76        add_filter( 'image_size_names_choose', array( $this, 'media_manager_image_sizes' ) );
77        add_filter( 'display_media_states', array( $this, 'add_media_state' ) );
78    }
79
80    /**
81     * Add our logo uploader to the Customizer.
82     *
83     * @param object $wp_customize Customizer object.
84     * @uses current_theme_supports()
85     * @uses current_theme_supports()
86     * @uses WP_Customize_Manager::add_setting()
87     * @uses WP_Customize_Manager::add_control()
88     * @uses Site_Logo::sanitize_checkbox()
89     */
90    public function customize_register( $wp_customize ) {
91        // Add a setting to hide header text if the theme isn't supporting the feature itself
92        if ( ! current_theme_supports( 'custom-header' ) ) {
93            $wp_customize->add_setting(
94                'site_logo_header_text',
95                array(
96                    'default'           => 1,
97                    'sanitize_callback' => array( $this, 'sanitize_checkbox' ),
98                    'transport'         => 'postMessage',
99                )
100            );
101
102            $wp_customize->add_control(
103                new WP_Customize_Control(
104                    $wp_customize,
105                    'site_logo_header_text',
106                    array(
107                        'label'    => __( 'Display Header Text', 'jetpack' ),
108                        'section'  => 'title_tagline',
109                        'settings' => 'site_logo_header_text',
110                        'type'     => 'checkbox',
111                    )
112                )
113            );
114        }
115
116        // Add the setting for our logo value.
117        $wp_customize->add_setting(
118            'site_logo',
119            array(
120                'capability'        => 'manage_options',
121                'default'           => 0,
122                'sanitize_callback' => array( $this, 'sanitize_logo_setting' ),
123                'transport'         => 'postMessage',
124                'type'              => 'option',
125            )
126        );
127
128        // By default, not setting width and height will suggest a square crop.
129        $width     = null;
130        $height    = null;
131        $logo_size = jetpack_get_site_logo_dimensions();
132
133        // Only suggested a different crop if the theme declares both dimensions.
134        if ( false !== $logo_size && $logo_size['width'] && $logo_size['height'] ) {
135            $width  = $logo_size['width'];
136            $height = $logo_size['height'];
137        }
138
139        // Add our image uploader.
140        $wp_customize->add_control(
141            new WP_Customize_Cropped_Image_Control(
142                $wp_customize,
143                'site_logo',
144                array(
145                    'label'         => __( 'Logo', 'jetpack' ),
146                    'section'       => 'title_tagline',
147                    'settings'      => 'site_logo',
148                    'width'         => $width,
149                    'height'        => $height,
150                    'flex_width'    => true,
151                    'flex_height'   => true,
152                    'button_labels' => array(
153                        'select'       => __( 'Add logo', 'jetpack' ),
154                        'change'       => __( 'Change logo', 'jetpack' ),
155                        'remove'       => __( 'Remove logo', 'jetpack' ),
156                        'placeholder'  => __( 'No logo set', 'jetpack' ),
157                        'frame_title'  => __( 'Set as logo', 'jetpack' ),
158                        'frame_button' => __( 'Choose logo', 'jetpack' ),
159                    ),
160                )
161            )
162        );
163
164        $wp_customize->selective_refresh->add_partial(
165            'site_logo',
166            array(
167                'settings'            => 'site_logo',
168                'selector'            => '.site-logo-link',
169                'render_callback'     => array( $this, 'customizer_preview' ),
170                'container_inclusive' => true,
171            )
172        );
173    }
174
175    /**
176     * Enqueue scripts for the Customizer live preview.
177     *
178     * @uses wp_enqueue_script()
179     * @uses plugins_url()
180     * @uses current_theme_supports()
181     * @uses Site_Logo::header_text_classes()
182     * @uses wp_localize_script()
183     */
184    public function preview_enqueue() {
185        // Don't bother passing in header text classes if the theme supports custom headers.
186        if ( ! current_theme_supports( 'custom-header' ) ) {
187            $classes = jetpack_sanitize_header_text_classes( $this->header_text_classes() );
188            wp_enqueue_script(
189                'site-logo-header-text',
190                plugins_url( '../js/site-logo-header-text.js', __FILE__ ),
191                array( 'jquery', 'media-views' ),
192                JETPACK__VERSION,
193                true
194            );
195            wp_localize_script( 'site-logo-header-text', 'site_logo_header_classes', array( 'classes' => $classes ) );
196        }
197    }
198
199    /**
200     * Get header text classes. If not defined in add_theme_support(), defaults from Underscores will be used.
201     *
202     * @uses get_theme_support
203     * @return string String of classes to hide
204     */
205    public function header_text_classes() {
206        $args = get_theme_support( 'site-logo' );
207
208        if ( isset( $args[0]['header-text'] ) ) {
209            // Use any classes defined in add_theme_support().
210            $classes = $args[0]['header-text'];
211        } else {
212            // Otherwise, use these defaults, which will work with any Underscores-based theme.
213            $classes = array(
214                'site-title',
215                'site-description',
216            );
217        }
218
219        // If we've got an array, reduce them to a string for output
220        if ( is_array( $classes ) ) {
221            $classes = '.' . implode( ', .', $classes );
222        } else {
223            $classes = '.' . $classes;
224        }
225
226        return $classes;
227    }
228
229    /**
230     * Hide header text on front-end if necessary.
231     *
232     * @uses current_theme_supports()
233     * @uses get_theme_mod()
234     * @uses Site_Logo::header_text_classes()
235     * @uses esc_html()
236     */
237    public function head_text_styles() {
238        // Bail if our theme supports custom headers.
239        if ( current_theme_supports( 'custom-header' ) ) {
240            return;
241        }
242
243        // Is Display Header Text unchecked? If so, we need to hide our header text.
244        if ( ! get_theme_mod( 'site_logo_header_text', 1 ) ) {
245            $classes = $this->header_text_classes();
246            ?>
247            <!-- Site Logo: hide header text -->
248            <style type="text/css">
249            <?php echo jetpack_sanitize_header_text_classes( $classes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> {
250                position: absolute;
251                clip: rect(1px, 1px, 1px, 1px);
252            }
253            </style>
254            <?php
255        }
256    }
257
258    /**
259     * Determine image size to use for the logo.
260     *
261     * @uses get_theme_support()
262     * @return string Size specified in add_theme_support declaration, or 'thumbnail' default
263     */
264    public function theme_size() {
265        $args        = get_theme_support( 'site-logo' );
266        $valid_sizes = get_intermediate_image_sizes();
267
268        // Add 'full' to the list of accepted values.
269        $valid_sizes[] = 'full';
270
271        // If the size declared in add_theme_support is valid, use it; otherwise, just go with 'thumbnail'.
272        $size = ( isset( $args[0]['size'] ) && in_array( $args[0]['size'], $valid_sizes, true ) ) ? $args[0]['size'] : 'thumbnail';
273
274        return $size;
275    }
276
277    /**
278     * Make custom image sizes available to the media manager.
279     *
280     * @param array $sizes List of image sizes.
281     * @uses get_intermediate_image_sizes()
282     * @return array All default and registered custom image sizes.
283     */
284    public function media_manager_image_sizes( $sizes ) {
285        // Get an array of all registered image sizes.
286        $intermediate = get_intermediate_image_sizes();
287
288        // Have we got anything fun to work with?
289        if ( is_array( $intermediate ) && ! empty( $intermediate ) ) {
290            foreach ( $intermediate as $size ) {
291                // If the size isn't already in the $sizes array, add it.
292                if ( ! array_key_exists( $size, $sizes ) ) {
293                    $sizes[ $size ] = $size;
294                }
295            }
296        }
297
298        return $sizes;
299    }
300
301    /**
302     * Add site logos to media states in the Media Manager.
303     *
304     * @param array $media_states An array of media states.
305     *
306     * @return array The current attachment's media states.
307     */
308    public function add_media_state( $media_states ) {
309        // Only bother testing if we have a site logo set.
310        if ( $this->has_site_logo() ) {
311            global $post;
312
313            // If our attachment ID and the site logo ID match, this image is the site logo.
314            if ( $post && $post->ID === $this->logo ) {
315                $media_states[] = __( 'Site Logo', 'jetpack' );
316            }
317        }
318
319        return $media_states;
320    }
321
322    /**
323     * Reset the site logo if the current logo is deleted in the media manager.
324     *
325     * @param int $post_id Attachment ID.
326     * @uses Site_Logo::remove_site_logo()
327     */
328    public function reset_on_attachment_delete( $post_id ) {
329        if ( $this->logo === $post_id ) {
330            $this->remove_site_logo();
331        }
332    }
333
334    /**
335     * Determine if a site logo is assigned or not.
336     *
337     * @uses Site_Logo::$logo
338     * @return boolean True if there is an active logo, false otherwise
339     */
340    public function has_site_logo() {
341        return (bool) $this->logo;
342    }
343
344    /**
345     * Reset the site logo option to zero (empty).
346     *
347     * @uses update_option()
348     */
349    public function remove_site_logo() {
350        update_option( 'site_logo', null );
351    }
352
353    /**
354     * Adds custom classes to the array of body classes.
355     *
356     * @uses Site_Logo::has_site_logo()
357     * @param array $classes Classes for the body element.
358     * @return array Array of <body> classes
359     */
360    public function body_classes( $classes ) {
361        // Add a class if a Site Logo is active
362        if ( $this->has_site_logo() ) {
363            $classes[] = 'has-site-logo';
364        }
365
366        return $classes;
367    }
368
369    /**
370     * Sanitize our header text Customizer setting.
371     *
372     * @param mixed $input The input value to sanitize.
373     * @return bool|string 1 if checked, empty string if not checked.
374     */
375    public function sanitize_checkbox( $input ) {
376        return ( 1 === (int) $input ) ? 1 : '';
377    }
378
379    /**
380     * Validate and sanitize a new site logo setting.
381     *
382     * @param mixed $input Logo setting value to sanitize.
383     * @return int Attachment post ID, or 0 if invalid.
384     */
385    public function sanitize_logo_setting( $input ) {
386        $input = absint( $input );
387
388        // If the new setting doesn't point to a valid attachment, just reset the whole thing.
389        if ( false === wp_get_attachment_image_src( $input ) ) {
390            $input = 0;
391        }
392
393        return $input;
394    }
395
396    /**
397     * This function returns the updated HTML in the Customizer preview when the logo is added, updated, or removed.
398     *
399     * @return string
400     */
401    public function customizer_preview() {
402        ob_start();
403        jetpack_the_site_logo();
404        return ob_get_clean();
405    }
406}
407
408// phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
409
410/**
411 * Allow themes and plugins to access Site_Logo methods and properties.
412 *
413 * @uses Site_Logo::instance()
414 * @return object Site_Logo
415 */
416function site_logo() {
417    return Site_Logo::instance();
418}
419
420/**
421 * One site logo, please.
422 */
423site_logo();