aristath/kirki

View on GitHub
packages/kirki-framework/module-css/src/CSS.php

Summary

Maintainability
D
1 day
Test Coverage
<?php
/**
 * Handles the CSS Output of fields.
 *
 * @package     Kirki
 * @category    Modules
 * @author      Ari Stathopoulos (@aristath)
 * @copyright   Copyright (c) 2019, Ari Stathopoulos (@aristath)
 * @license    https://opensource.org/licenses/MIT
 * @since       3.0.0
 */

namespace Kirki\Module;

use Kirki\Compatibility\Kirki;
use Kirki\Util\Helper;
use Kirki\Compatibility\Values;
use Kirki\Module\CSS\Generator;

/**
 * The Module object.
 */
class CSS {

    /**
     * The CSS array
     *
     * @access public
     * @var array
     */
    public static $css_array = array();

    /**
     * An array of fields to be processed.
     *
     * @static
     * @access protected
     * @since 1.0
     * @var array
     */
    protected static $fields = array();

    /**
     * Field option types.
     *
     * @static
     * @access protected
     * @since 1.0
     * @var array
     */
    protected static $field_option_types = array();

    /**
     * The default handle for kirki's styles enqueue.
     *
     * @since 4.0
     * @access private
     * @static
     *
     * @var string
     */
    private static $css_handle = 'kirki-styles';

    /**
     * Constructor
     *
     * @access public
     */
    public function __construct() {
        add_action( 'kirki_field_init', array( $this, 'field_init' ), 10, 2 );
        add_action( 'init', array( $this, 'init' ) );
    }

    /**
     * Init.
     *
     * @access public
     */
    public function init() {

        new \Kirki\Module\Webfonts();

        add_action( 'wp', array( $this, 'print_styles_action' ) );

        if ( ! apply_filters( 'kirki_output_inline_styles', true ) ) {
            $config   = apply_filters( 'kirki_config', array() );
            $priority = 999;

            if ( isset( $config['styles_priority'] ) ) {
                $priority = absint( $config['styles_priority'] );
            }

            add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ), $priority );
        } else {
            add_action( 'wp_head', array( $this, 'print_styles_inline' ), 999 );
        }
    }

    /**
     * Runs when a field gets added.
     * Adds fields to this object so their styles can later be generated.
     *
     * @access public
     * @since 1.0
     * @param array  $args   The field args.
     * @param Object $object The field object.
     * @return void
     */
    public function field_init( $args, $object ) {
        if ( ! isset( $args['output'] ) ) {
            $args['output'] = array();
        }

        self::$field_option_types[ $args['settings'] ] = isset( $args['option_type'] ) ? $args['option_type'] : 'theme_mod';

        if ( ! is_array( $args['output'] ) ) {
            /* translators: The field ID where the error occurs. */
            _doing_it_wrong( __METHOD__, sprintf( esc_html__( '"output" invalid format in field %s. The "output" argument should be defined as an array of arrays.', 'kirki' ), esc_html( $this->settings ) ), '3.0.10' );
            $args['output'] = array(
                array(
                    'element' => $args['output'],
                ),
            );
        }

        // Convert to array of arrays if needed.
        if ( isset( $args['output']['element'] ) ) {
            /* translators: The field ID where the error occurs. */
            _doing_it_wrong( __METHOD__, sprintf( esc_html__( '"output" invalid format in field %s. The "output" argument should be defined as an array of arrays.', 'kirki' ), esc_html( $this->settings ) ), '3.0.10' );
            $args['output'] = array( $args['output'] );
        }

        if ( empty( $args['output'] ) ) {
            return;
        }

        foreach ( $args['output'] as $key => $output ) {
            if ( empty( $output ) || ! isset( $output['element'] ) ) {
                unset( $args['output'][ $key ] );
                continue;
            }
            if ( ! isset( $output['sanitize_callback'] ) && isset( $output['callback'] ) ) {
                $args['output'][ $key ]['sanitize_callback'] = $output['callback'];
            }

            // Convert element arrays to strings.
            if ( isset( $output['element'] ) && is_array( $output['element'] ) ) {
                $args['output'][ $key ]['element'] = array_unique( $args['output'][ $key ]['element'] );
                sort( $args['output'][ $key ]['element'] );

                // Trim each element in the array.
                foreach ( $args['output'][ $key ]['element'] as $index => $element ) {
                    $args['output'][ $key ]['element'][ $index ] = trim( $element );
                }
                $args['output'][ $key ]['element'] = implode( ',', $args['output'][ $key ]['element'] );
            }

            // Fix for https://github.com/aristath/kirki/issues/1659#issuecomment-346229751.
            $args['output'][ $key ]['element'] = str_replace( array( "\t", "\n", "\r", "\0", "\x0B" ), ' ', $args['output'][ $key ]['element'] );
            $args['output'][ $key ]['element'] = trim( preg_replace( '/\s+/', ' ', $args['output'][ $key ]['element'] ) );
        }

        if ( ! isset( $args['type'] ) && isset( $object->type ) ) {
            $args['type'] = $object->type;
        }

        self::$fields[] = $args;
    }

    /**
     * Print styles inline.
     *
     * @access public
     * @since 3.0.36
     * @return void
     */
    public function print_styles_inline() {
        echo '<style id="kirki-inline-styles">';
        $this->print_styles();
        echo '</style>';
    }

    /**
     * Enqueue the styles.
     *
     * @access public
     * @since 3.0.36
     * @return void
     */
    public function enqueue_styles() {

        $args = array(
            'action' => apply_filters( 'kirki_styles_action_handle', self::$css_handle ),
        );

        if ( is_admin() ) {
            global $current_screen;

            /**
             * This `enqueue_styles` method is also hooked into `enqueue_block_editor_assets`.
             * It needs to be excluded from customize control page.
             *
             * Why not simply excluding all admin area except gutenberg editing interface?
             * Because it would be nice to let the possibility open
             * if a 3rd party plugin will output gutenberg syles somewhere in admin area.
             *
             * Example of possibility:
             * In the future, Ultimate Dashboard Pro's admin page feature might supports Gutenberg.
             */
            if ( is_object( $current_screen ) && property_exists( $current_screen, 'id' ) && 'customize' === $current_screen->id ) {
                return;
            }

            if ( property_exists( $current_screen, 'is_block_editor' ) && 1 === (int) $current_screen->is_block_editor ) {
                $args['editor'] = '1';
            }
        }

        // Enqueue the dynamic stylesheet.
        wp_enqueue_style(
            self::$css_handle,
            add_query_arg( $args, home_url() ),
            array(),
            '4.0'
        );
    }

    /**
     * Prints the styles as an enqueued file.
     *
     * @access public
     * @since 3.0.36
     * @return void
     */
    public function print_styles_action() {

        /**
         * Note to code reviewers:
         * There is no need for a nonce check here, we're only checking if this is a valid request or not.
         */

        // phpcs:ignore WordPress.Security.NonceVerification
        if ( empty( $_GET['action'] ) || apply_filters( 'kirki_styles_action_handle', self::$css_handle ) !== $_GET['action'] ) {
            return;
        }

        // This is a stylesheet.
        header( 'Content-type: text/css' );
        $this->print_styles();
        exit;
    }

    /**
     * Prints the styles.
     *
     * @access public
     */
    public function print_styles() {

        // Go through all configs.
        $configs = Kirki::$config;

        foreach ( $configs as $config_id => $args ) {
            if ( isset( $args['disable_output'] ) && true === $args['disable_output'] ) {
                continue;
            }

            $styles = self::loop_controls( $config_id );
            $styles = apply_filters( "kirki_{$config_id}_dynamic_css", $styles );

            if ( ! empty( $styles ) ) {
                /**
                 * Note to code reviewers:
                 *
                 * Though all output should be run through an escaping function, this is pure CSS.
                 *
                 * When used in the print_styles_action() method the PHP header() call makes the browser interpret it as such.
                 * No code, script or anything else can be executed from inside a stylesheet.
                 *
                 * When using in the print_styles_inline() method the wp_strip_all_tags call we use below
                 * strips anything that has the possibility to be malicious, and since this is inslide a <style> tag
                 * it can only be interpreted by the browser as such.
                 * wp_strip_all_tags() excludes the possibility of someone closing the <style> tag and then opening something else.
                 */
                echo wp_strip_all_tags( $styles ); // phpcs:ignore WordPress.Security.EscapeOutput
            }
        }

        do_action( 'kirki_dynamic_css' );
    }

    /**
     * Loop through all fields and create an array of style definitions.
     *
     * @static
     * @access public
     * @param string $config_id The configuration ID.
     */
    public static function loop_controls( $config_id ) {

        // Get an instance of the Generator class.
        // This will make sure google fonts and backup fonts are loaded.
        Generator::get_instance();

        $fields = self::get_fields_by_config( $config_id );

        // Compatibility with v3 API.
        if ( class_exists( '\Kirki\Compatibility\Kirki' ) ) {
            $fields = array_merge( \Kirki\Compatibility\Kirki::$fields, $fields );
        }

        $css = array();

        // Early exit if no fields are found.
        if ( empty( $fields ) ) {
            return;
        }

        foreach ( $fields as $field ) {

            // Only process fields that belong to $config_id.
            if ( isset( $field['kirki_config'] ) && $config_id !== $field['kirki_config'] ) {
                continue;
            }

            if ( true === apply_filters( "kirki_{$config_id}_css_skip_hidden", true ) ) {

                // Only continue if field dependencies are met.
                if ( ! empty( $field['required'] ) ) {
                    $valid = true;

                    foreach ( $field['required'] as $requirement ) {
                        if ( isset( $requirement['setting'] ) && isset( $requirement['value'] ) && isset( $requirement['operator'] ) && isset( self::$field_option_types[ $requirement['setting'] ] ) ) {
                            $value_method     = 'get_' . self::$field_option_types[ $requirement['setting'] ];
                            $controller_value = $value_method( $requirement['setting'] );

                            if ( ! Helper::compare_values( $controller_value, $requirement['value'], $requirement['operator'] ) ) {
                                $valid = false;
                            }
                        }
                    }

                    if ( ! $valid ) {
                        continue;
                    }
                }
            }

            // Only continue if $field['output'] is set.
            if ( isset( $field['output'] ) && ! empty( $field['output'] ) ) {
                $css = Helper::array_replace_recursive( $css, Generator::css( $field ) );

                // Add the globals.
                if ( isset( self::$css_array[ $config_id ] ) && ! empty( self::$css_array[ $config_id ] ) ) {
                    Helper::array_replace_recursive( $css, self::$css_array[ $config_id ] );
                }
            }
        }

        $css = apply_filters( "kirki_{$config_id}_styles", $css );

        if ( is_array( $css ) ) {
            return Generator::styles_parse( Generator::add_prefixes( $css ) );
        }
    }

    /**
     * Gets fields from self::$fields by config-id.
     *
     * @static
     * @access private
     * @since 1.0
     * @param string $config_id The config-ID.
     * @return array
     */
    private static function get_fields_by_config( $config_id ) {
        $fields = array();
        foreach ( self::$fields as $field ) {
            if (
                ( isset( $field['kirki_config'] ) && $config_id === $field['kirki_config'] ) ||
                (
                    ( 'global' === $config_id || ! $config_id ) &&
                    ( ! isset( $field['kirki_config'] ) || 'global' === $field['kirki_config'] || ! $field['kirki_config'] )
                )
            ) {
                $fields[] = $field;
            }
        }
        return $fields;
    }
}