692 lines
16 KiB
PHP
692 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* Handle critical CSS generating and rendering.
|
|
*
|
|
* @package fusion-builder
|
|
* @since 3.4
|
|
*/
|
|
|
|
/**
|
|
* Critical CSS.
|
|
*
|
|
* @since 3.4
|
|
*/
|
|
class AWB_Critical_CSS {
|
|
|
|
/**
|
|
* The one, true instance of this object.
|
|
*
|
|
* @static
|
|
* @access private
|
|
* @since 3.4
|
|
* @var object
|
|
*/
|
|
private static $instance;
|
|
|
|
/**
|
|
* Whether or not the filter or action events are initialized.
|
|
*
|
|
* @since 3.9.2
|
|
* @var bool
|
|
*/
|
|
private static $events_initialized = false;
|
|
|
|
/**
|
|
* Table version.
|
|
*
|
|
* @access protected
|
|
* @since 3.4
|
|
* @var string.
|
|
*/
|
|
protected $table_version;
|
|
|
|
/**
|
|
* Table collation.
|
|
*
|
|
* @access protected
|
|
* @since 3.4
|
|
* @var string.
|
|
*/
|
|
protected $table_collation;
|
|
|
|
/**
|
|
* Table name.
|
|
*
|
|
* @access protected
|
|
* @since 3.4
|
|
* @var string.
|
|
*/
|
|
protected $table_name = 'awb_critical_css';
|
|
|
|
/**
|
|
* Table structure.
|
|
*
|
|
* @access protected
|
|
* @since 3.4
|
|
* @var array.
|
|
*/
|
|
protected $table = [];
|
|
|
|
/**
|
|
* Page
|
|
*
|
|
* @access protected
|
|
* @since 3.4
|
|
* @var object.
|
|
*/
|
|
protected $page;
|
|
|
|
/**
|
|
* CSS for current page.
|
|
*
|
|
* @access protected
|
|
* @since 3.4
|
|
* @var mixed.
|
|
*/
|
|
protected $css = [];
|
|
|
|
/**
|
|
* Creates or returns an instance of this class.
|
|
*
|
|
* @static
|
|
* @access public
|
|
* @since 2.2
|
|
*/
|
|
public static function get_instance() {
|
|
|
|
// If an instance hasn't been created and set to $instance create an instance and set it to $instance.
|
|
if ( null === self::$instance ) {
|
|
self::$instance = new AWB_Critical_CSS();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @access public
|
|
*/
|
|
private function __construct() {
|
|
$option_name = class_exists( 'Fusion_Settings' ) ? Fusion_Settings::get_option_name() : 'fusion_options';
|
|
$option = get_option( $option_name, [] );
|
|
$enable_critical = isset( $option['critical_css'] ) && '1' === $option['critical_css'];
|
|
if ( ! apply_filters( 'enable_awb_critical_css', $enable_critical ) ) {
|
|
return;
|
|
}
|
|
|
|
$this->table_version = '3.4';
|
|
$this->table_collation = 'utf8mb4_unicode_ci';
|
|
|
|
if ( ! self::$events_initialized ) {
|
|
// Create tables if needed.
|
|
if ( ! get_option( 'awb_critical_table' ) || ( isset( $_GET['create_tables'] ) && $_GET['create_tables'] ) ) { // phpcs:ignore WordPress.Security
|
|
$this->create_table();
|
|
}
|
|
|
|
// Critical CSS request.
|
|
if ( ! empty( $_GET['mcritical'] ) || ! empty( $_GET['dcritical'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
|
$this->generating_css();
|
|
|
|
add_filter( 'body_class', [ $this, 'body_classes' ] );
|
|
} else {
|
|
|
|
// Check if we critical CSS.
|
|
add_action( 'wp', [ $this, 'check_for_critical' ] );
|
|
}
|
|
|
|
// Load admin page.
|
|
if ( is_admin() ) {
|
|
add_action( 'init', [ $this, 'admin_init' ] );
|
|
}
|
|
|
|
add_action( 'wp_ajax_awb_enable_critical_css', 'AWB_Critical_CSS::ajax_enable_critical_css' );
|
|
|
|
self::$events_initialized = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Admin init.
|
|
*/
|
|
public function admin_init() {
|
|
require_once FUSION_BUILDER_PLUGIN_DIR . 'inc/critical-css/class-awb-critical-css-table.php';
|
|
require_once FUSION_BUILDER_PLUGIN_DIR . 'inc/critical-css/class-awb-critical-css-page.php';
|
|
$this->page = new AWB_Critical_CSS_Page();
|
|
}
|
|
|
|
/**
|
|
* Generating CSS.
|
|
*
|
|
* @since 3.4
|
|
* @return void
|
|
*/
|
|
public function generating_css() {
|
|
|
|
// Disable admin bar.
|
|
show_admin_bar( false );
|
|
|
|
// Disable QM if it exists.
|
|
if ( class_exists( 'QM' ) ) {
|
|
add_filter( 'user_has_cap', [ $this, 'disable_qm' ], 10, 1 );
|
|
}
|
|
|
|
// Disable animations.
|
|
add_action(
|
|
'wp_head',
|
|
function () {
|
|
echo '<style id="test-critical-css">.fusion-animated{ visibility: visible !important;}.awb-menu.loading { opacity: 1 !important; } .fusion-megamenu-wrapper, .awb-menu__mega-wrap { display: none !important; }</style>';
|
|
}
|
|
);
|
|
|
|
// Emulate load as if mobile.
|
|
if ( ! empty( $_GET['mcritical'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
|
add_filter(
|
|
'wp_is_mobile',
|
|
function () {
|
|
return true;
|
|
}
|
|
);
|
|
|
|
add_filter(
|
|
'awb_device_detection_is_mobile',
|
|
function () {
|
|
return true;
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disables query monitor in front-end builder mode.
|
|
*
|
|
* @param array $user_caps Array of user capabilities.
|
|
*
|
|
* @since 6.0
|
|
* @return array
|
|
*/
|
|
public function disable_qm( $user_caps ) {
|
|
$user_caps['view_query_monitor'] = false;
|
|
return $user_caps;
|
|
}
|
|
|
|
/**
|
|
* Check if mobile or not.
|
|
*
|
|
* @access public
|
|
* @since 3.4
|
|
*/
|
|
public function is_mobile() {
|
|
return apply_filters( 'awb_critical_css_is_mobile', wp_is_mobile() );
|
|
}
|
|
|
|
/**
|
|
* Check if page load has critical CSS saved.
|
|
*
|
|
* @access public
|
|
* @since 3.4
|
|
*/
|
|
public function check_for_critical() {
|
|
|
|
$this->css = apply_filters( 'awb_critical_css_block', $this->find_css() );
|
|
|
|
if ( (int) get_option( 'awb_disable_critical_css', 0 ) ) {
|
|
return;
|
|
}
|
|
|
|
// Check we have critical CSS entry.
|
|
if ( ! empty( $this->css ) && ! fusion_is_preview_frame() ) {
|
|
// Check we have CSS for the current device.
|
|
if ( ( $this->is_mobile() && ! empty( $this->css[0]->mobile_css ) ) || ( ! $this->is_mobile() && ! empty( $this->css[0]->desktop_css ) ) ) {
|
|
add_filter( 'awb_defer_styles', '__return_true' );
|
|
add_action( 'wp_head', [ $this, 'output_critical_css' ], -10 );
|
|
}
|
|
|
|
// Check we have preload for the current device and its not a global critical set.
|
|
if ( false === strpos( $this->css[0]->css_key, 'global' ) && ( ( $this->is_mobile() && ! empty( $this->css[0]->mobile_preloads ) ) || ( ! $this->is_mobile() && ! empty( $this->css[0]->desktop_preloads ) ) ) ) {
|
|
add_action( 'wp_head', [ $this, 'output_critical_preloads' ], -11 );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find critical CSS for current page load.
|
|
*
|
|
* @access public
|
|
* @since 3.4
|
|
*/
|
|
public function find_css() {
|
|
$post_id = fusion_library()->get_page_id();
|
|
|
|
// If homepage, check if homepage critical is set.
|
|
if ( is_front_page() ) {
|
|
$home = $this->get(
|
|
[
|
|
'where' => [
|
|
'css_key' => '"homepage"',
|
|
],
|
|
]
|
|
);
|
|
return $home;
|
|
}
|
|
|
|
// Check if we have a targeted critical CSS, takes priority over global/generic.
|
|
if ( $post_id ) {
|
|
$specific = $this->get(
|
|
[
|
|
'where' => [
|
|
'css_key' => '"' . $post_id . '"',
|
|
],
|
|
]
|
|
);
|
|
if ( $specific ) {
|
|
return $specific;
|
|
}
|
|
}
|
|
|
|
// Single post type check if global is set.
|
|
if ( is_singular() ) {
|
|
$global = 'global_' . get_post_type();
|
|
$post = $this->get(
|
|
[
|
|
'where' => [
|
|
'css_key' => '"' . $global . '"',
|
|
],
|
|
]
|
|
);
|
|
return $post;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sets table structure.
|
|
*
|
|
* @access public
|
|
* @since 3.4
|
|
*/
|
|
public function set_table() {
|
|
$this->table = [
|
|
'unique_key' => [ 'id' ],
|
|
'primary_key' => 'id',
|
|
'columns' => [
|
|
|
|
// CSS ID.
|
|
[
|
|
'name' => 'id',
|
|
'type' => 'bigint(20)',
|
|
'auto_increment' => true,
|
|
'not_null' => true,
|
|
],
|
|
|
|
// The post ID string.
|
|
[
|
|
'name' => 'css_key',
|
|
'type' => 'varchar(255)',
|
|
'not_null' => true,
|
|
'collation' => $this->table_collation,
|
|
],
|
|
|
|
// The mobile critical CSS.
|
|
[
|
|
'name' => 'mobile_css',
|
|
'type' => 'longtext',
|
|
'not_null' => true,
|
|
'collation' => $this->table_collation,
|
|
],
|
|
|
|
// The desktop critical CSS.
|
|
[
|
|
'name' => 'desktop_css',
|
|
'type' => 'longtext',
|
|
'not_null' => true,
|
|
'collation' => $this->table_collation,
|
|
],
|
|
|
|
// The mobile preload tags.
|
|
[
|
|
'name' => 'mobile_preloads',
|
|
'type' => 'longtext',
|
|
'not_null' => true,
|
|
'collation' => $this->table_collation,
|
|
],
|
|
|
|
// The desktop preload tags.
|
|
[
|
|
'name' => 'desktop_preloads',
|
|
'type' => 'longtext',
|
|
'not_null' => true,
|
|
'collation' => $this->table_collation,
|
|
],
|
|
|
|
// Time critical CSS was created at.
|
|
[
|
|
'name' => 'updated_at',
|
|
'type' => 'VARCHAR(25)',
|
|
'not_null' => true,
|
|
'collation' => $this->table_collation,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create the critical CSS table.
|
|
*
|
|
* @access public
|
|
* @since 1.0.0
|
|
*/
|
|
public function create_table() {
|
|
global $wpdb;
|
|
|
|
$this->set_table();
|
|
|
|
// Save version of table construction.
|
|
add_option( 'awb_critical_table', $this->table_version, '', false );
|
|
|
|
// Include file from wp-core if not already loaded.
|
|
if ( ! function_exists( 'dbDelta' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
}
|
|
|
|
$table_name = $this->table_name;
|
|
$table = $this->table;
|
|
|
|
// Get collation.
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
$query_array = [];
|
|
|
|
/**
|
|
* Loop columns for this table.
|
|
*
|
|
* Generates the query fragment for this column
|
|
* which will be them used to build the final query.
|
|
*/
|
|
foreach ( $table['columns'] as $column ) {
|
|
|
|
// Basic row properties.
|
|
$query_fragment = [
|
|
$column['name'],
|
|
$column['type'],
|
|
];
|
|
|
|
// Add "NOT NULL" if needed.
|
|
if ( isset( $column['not_null'] ) && $column['not_null'] ) {
|
|
$query_fragment[] = 'NOT NULL';
|
|
}
|
|
|
|
// Add "AUTO_INCREMENT" if needed.
|
|
if ( isset( $column['auto_increment'] ) && $column['auto_increment'] ) {
|
|
$query_fragment[] = 'AUTO_INCREMENT';
|
|
}
|
|
|
|
// Add "DEFAULT" if needed.
|
|
if ( isset( $column['default'] ) ) {
|
|
$query_fragment[] = "DEFAULT {$column['default']}";
|
|
}
|
|
|
|
// Add our row to the query array.
|
|
$query_array[] = implode( ' ', $query_fragment );
|
|
}
|
|
|
|
// Add "UNIQUE KEY" if needed.
|
|
if ( isset( $table['unique_key'] ) ) {
|
|
foreach ( $table['unique_key'] as $unique_key ) {
|
|
$query_array[] = "UNIQUE KEY $unique_key ($unique_key)";
|
|
}
|
|
}
|
|
|
|
// Add "PRIMARY KEY" if needed.
|
|
if ( isset( $table['primary_key'] ) ) {
|
|
$query_array[] = "PRIMARY KEY {$table['primary_key']} ({$table['primary_key']})";
|
|
}
|
|
|
|
// Build the query string.
|
|
$columns_query_string = implode( ', ', $query_array );
|
|
|
|
// Run the SQL query.
|
|
dbDelta( "CREATE TABLE IF NOT EXISTS `{$wpdb->prefix}$table_name` ($columns_query_string) $charset_collate" );
|
|
}
|
|
|
|
/**
|
|
* Get total number of CSS posts.
|
|
*
|
|
* @since 1.0
|
|
* @access public
|
|
* @return int
|
|
*/
|
|
public function get_total() {
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . $this->table_name;
|
|
$count_query = "select count(*) from $table_name";
|
|
return (int) $wpdb->get_var( $count_query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL
|
|
}
|
|
|
|
/**
|
|
* Get critical CSS entry.
|
|
*
|
|
* @param array $args An array of arguments for the query.
|
|
* @return array An array of submissions.
|
|
*/
|
|
public function get( $args = [] ) {
|
|
global $wpdb;
|
|
|
|
$defaults = [
|
|
'what' => '*',
|
|
'where' => [],
|
|
'order_by' => '',
|
|
'order' => 'ASC',
|
|
'limit' => '',
|
|
'offset' => 0,
|
|
];
|
|
$args = wp_parse_args( $args, $defaults );
|
|
|
|
// The table name.
|
|
$table_name = $wpdb->prefix . $this->table_name;
|
|
|
|
// The query basics.
|
|
$query = 'SELECT ' . $args['what'] . " FROM `$table_name`";
|
|
|
|
// Build the WHERE fragment of the query.
|
|
if ( ! empty( $args['where'] ) ) {
|
|
$where = [];
|
|
foreach ( $args['where'] as $where_fragment_key => $where_fragment_val ) {
|
|
if ( false === strpos( $where_fragment_val, 'LIKE' ) ) {
|
|
$where[] = "$where_fragment_key = $where_fragment_val";
|
|
} else {
|
|
$where[] = "$where_fragment_key $where_fragment_val";
|
|
}
|
|
}
|
|
|
|
$query .= ' WHERE ' . implode( ' AND ', $where );
|
|
}
|
|
|
|
// Build the ORDER BY fragment of the query.
|
|
if ( '' !== $args['order_by'] ) {
|
|
$order = 'ASC' !== strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
|
|
$orderby_sql = sanitize_sql_orderby( "{$args['order_by']} {$order}" );
|
|
$query .= ' ORDER BY ' . $orderby_sql;
|
|
}
|
|
|
|
// Build the LIMIT fragment of the query.
|
|
if ( '' !== $args['limit'] ) {
|
|
$query .= ' LIMIT ' . absint( $args['limit'] );
|
|
}
|
|
|
|
// Build the OFFSET fragment of the query.
|
|
if ( 0 !== $args['offset'] ) {
|
|
$query .= ' OFFSET ' . absint( $args['offset'] );
|
|
}
|
|
|
|
return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB
|
|
}
|
|
|
|
/**
|
|
* Insert critical css code.
|
|
*
|
|
* @param array $args An array of arguments for the query.
|
|
* @return array An array of submissions.
|
|
*/
|
|
public function insert( $args = [] ) {
|
|
global $wpdb;
|
|
|
|
return $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prefix . $this->table_name,
|
|
apply_filters( $this->table_name . '_insert_query_args', $args )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update item.
|
|
*
|
|
* @since 3.1
|
|
* @access public
|
|
* @param array $data Data to update (in column => value pairs).
|
|
* @param array $where A named array of WHERE clauses (in column => value pairs).
|
|
* @param array|string $format An array of formats to be mapped to each of the values in $data.
|
|
* @param array|string $where_format An array of formats to be mapped to each of the values in $where.
|
|
* @return mixed
|
|
*/
|
|
public function update( $data, $where, $format = null, $where_format = null ) {
|
|
global $wpdb;
|
|
|
|
return $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prefix . $this->table_name,
|
|
$data,
|
|
$where,
|
|
$format,
|
|
$where_format
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete entries.
|
|
*
|
|
* @access public
|
|
* @since 3.1
|
|
* @param int|array $ids The submission ID(s).
|
|
* @return void
|
|
*/
|
|
public function bulk_delete( $ids ) {
|
|
global $wpdb;
|
|
|
|
$ids = implode( ',', array_map( 'absint', $ids ) );
|
|
$wpdb->query( 'DELETE FROM ' . $wpdb->prefix . $this->table_name . " WHERE id IN ($ids)" ); // phpcs:ignore WordPress.DB
|
|
}
|
|
|
|
/**
|
|
* Get css_keys in bulk.
|
|
*
|
|
* @access public
|
|
* @since 3.1
|
|
* @param int|array $ids The submission ID(s).
|
|
* @return array
|
|
*/
|
|
public function get_bulk_css_keys( $ids ) {
|
|
global $wpdb;
|
|
|
|
$ids = implode( ',', array_map( 'absint', $ids ) );
|
|
$result = $wpdb->get_results( 'SELECT css_key FROM ' . $wpdb->prefix . $this->table_name . " WHERE id IN ($ids)", OBJECT_K ); // phpcs:ignore WordPress.DB
|
|
|
|
return array_keys( $result );
|
|
}
|
|
|
|
/**
|
|
* Delete an entry.
|
|
*
|
|
* @access public
|
|
* @since 3.1
|
|
* @param int|array $where The where param.
|
|
* @param string $where_format The format of where clause.
|
|
* @return void
|
|
*/
|
|
public function delete( $where, $where_format = null ) {
|
|
global $wpdb;
|
|
|
|
$wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prefix . $this->table_name,
|
|
$where,
|
|
$where_format
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Output the critical CSS.
|
|
*
|
|
* @access public
|
|
* @since 3.4
|
|
* @return void
|
|
*/
|
|
public function output_critical_css() {
|
|
if ( $this->is_mobile() ) {
|
|
echo '<style id="awb-critical-css">' . $this->css[0]->mobile_css . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
} else {
|
|
echo '<style id="awb-critical-css">' . $this->css[0]->desktop_css . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Output the preload tags for images skipping lazy load.
|
|
*
|
|
* @access public
|
|
* @since 3.4
|
|
* @return void
|
|
*/
|
|
public function output_critical_preloads() {
|
|
if ( $this->is_mobile() ) {
|
|
echo wp_unslash( $this->css[0]->mobile_preloads ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
} else {
|
|
echo wp_unslash( $this->css[0]->desktop_preloads ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate any extra classes for the <body> element.
|
|
*
|
|
* @param array $classes CSS classes.
|
|
* @return array The needed body classes.
|
|
*/
|
|
public function body_classes( $classes ) {
|
|
|
|
$classes[] = 'awb-generating-critical-css';
|
|
|
|
return $classes;
|
|
}
|
|
|
|
/**
|
|
* Enable critical CSS via ajax call.
|
|
*
|
|
* @since 3.9.2
|
|
* @return void
|
|
*/
|
|
public static function ajax_enable_critical_css() {
|
|
if (
|
|
! isset( $_POST['critical_nonce'] ) ||
|
|
wp_verify_nonce( sanitize_key( $_POST['critical_nonce'] ), 'fusion-page-options-nonce' ) !== 1 ||
|
|
! current_user_can( 'manage_options' )
|
|
) {
|
|
echo '0';
|
|
die();
|
|
}
|
|
|
|
update_option( 'awb_disable_critical_css', 0 );
|
|
|
|
echo '1';
|
|
die();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instantiates the AWB_Critical_CSS class.
|
|
* Make sure the class is properly set-up.
|
|
*
|
|
* @since object 3.4
|
|
* @return AWB_Critical_CSS
|
|
*/
|
|
function AWB_Critical_CSS() { // phpcs:ignore WordPress.NamingConventions
|
|
return AWB_Critical_CSS::get_instance();
|
|
}
|
|
AWB_Critical_CSS();
|
|
/* Omit closing PHP tag to avoid "Headers already sent" issues. */
|