Files
roi-theme/wp-content/plugins/perfmatters/inc/classes/CSS.php
root a22573bf0b Commit inicial - WordPress Análisis de Precios Unitarios
- WordPress core y plugins
- Tema Twenty Twenty-Four configurado
- Plugin allow-unfiltered-html.php simplificado
- .gitignore configurado para excluir wp-config.php y uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 21:04:30 -06:00

666 lines
23 KiB
PHP
Executable File

<?php
namespace Perfmatters;
use Sabberworm\CSS\CSSList\AtRuleBlockList;
use Sabberworm\CSS\CSSList\CSSBlockList;
use Sabberworm\CSS\CSSList\Document;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parser as CSSParser;
use Sabberworm\CSS\Property\Charset;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\Settings;
use Sabberworm\CSS\Value\URL;
use WP_Admin_Bar;
class CSS
{
private static $used_selectors;
private static $excluded_selectors;
//initialize css functions
public static function init()
{
if(isset($_GET['perfmatterscssoff'])) {
return;
}
if(!empty(Config::$options['assets']['remove_unused_css'])) {
add_action('wp', array('Perfmatters\CSS', 'queue'));
add_action('wp_ajax_perfmatters_clear_post_used_css', array('Perfmatters\CSS', 'clear_post_used_css'));
add_action('admin_bar_menu', array('Perfmatters\CSS', 'admin_bar_menu'));
add_action('admin_notices', array('Perfmatters\CSS', 'admin_notices'));
add_action('admin_post_perfmatters_clear_used_css', array('Perfmatters\CSS', 'admin_bar_clear_used_css'));
}
}
//queue functions
public static function queue()
{
add_action('perfmatters_output_buffer_template_redirect', array('Perfmatters\CSS', 'remove_unused_css'));
}
//remove unused css
public static function remove_unused_css($html)
{
if(empty(apply_filters('perfmatters_remove_unused_css', true))) {
return $html;
}
if(Utilities::get_post_meta('perfmatters_exclude_unused_css')) {
return $html;
}
//only logged out
if(is_user_logged_in()) {
return $html;
}
//skip woocommerce
if(Utilities::is_woocommerce()) {
return $html;
}
//only known url types
$type = self::get_url_type();
if(empty($type)) {
return $html;
}
//setup file variables
$used_css_path = PERFMATTERS_CACHE_DIR . 'css/' . $type . '.used.css';
$used_css_url = PERFMATTERS_CACHE_URL . 'css/' . $type . '.used.css';
$used_css_exists = file_exists($used_css_path);
//match all stylesheets
preg_match_all('#<link\s[^>]*?href=[\'"]([^\'"]+?\.css.*?)[\'"][^>]*?\/?>#i', $html, $stylesheets, PREG_SET_ORDER);
if(!empty($stylesheets)) {
//create our css cache directory
if(!is_dir(PERFMATTERS_CACHE_DIR . 'css/')) {
@mkdir(PERFMATTERS_CACHE_DIR . 'css/', 0755, true);
}
//populate used selectors
self::get_used_selectors($html);
self::get_excluded_selectors();
$used_css_string = '';
//loop through stylesheets
foreach($stylesheets as $key => $stylesheet) {
//stylesheet check
if(!preg_match('#\srel=[\'"]stylesheet[\'"]#is', $stylesheet[0])) {
continue;
}
//ignore google fonts
if(stripos($stylesheet[1], '//fonts.googleapis.com/css') !== false || stripos($stylesheet[1], '.google-fonts.css') !== false) {
continue;
}
//exclude entire stylesheets
$stylesheet_exclusions = array(
'dashicons.min.css', //core
'/uploads/elementor/css/post-', //elementor
'animations.min.css',
'woocommerce-mobile.min.css', //woocommerce
'woocommerce-smallscreen.css',
'/uploads/oxygen/css/', //oxygen
'/uploads/bb-plugin/cache/', //beaver builder
'/uploads/generateblocks/', //generateblocks
'/et-cache/', //divi
'/widget-google-reviews/assets/css/public-main.css' //plugin for google reviews
);
if(!empty(Config::$options['assets']['rucss_excluded_stylesheets'])) {
$stylesheet_exclusions = array_merge($stylesheet_exclusions, Config::$options['assets']['rucss_excluded_stylesheets']);
}
$stylesheet_exclusions = apply_filters('perfmatters_rucss_excluded_stylesheets', $stylesheet_exclusions);
foreach($stylesheet_exclusions as $exclude) {
if(strpos($stylesheet[1], $exclude) !== false) {
unset($stylesheets[$key]);
continue 2;
}
}
//need to generate used css
if(!$used_css_exists) {
//get local stylesheet path
$url = str_replace(trailingslashit(apply_filters('perfmatters_local_stylesheet_url', (!empty(Config::$options['assets']['rucss_cdn_url']) ? Config::$options['assets']['rucss_cdn_url'] : site_url()))), '', explode('?', $stylesheet[1])[0]);
$file = str_replace('/wp-content', '/', WP_CONTENT_DIR) . $url;
//make sure local file exists
if(!file_exists($file)) {
continue;
}
//get used css from stylesheet
$used_css = self::clean_stylesheet($stylesheet[1], @file_get_contents($file));
//add used stylesheet css to total used
$used_css_string.= $used_css;
}
//delay stylesheets
if(empty(Config::$options['assets']['rucss_stylesheet_behavior'])) {
$new_link = preg_replace('#href=([\'"]).+?\1#', 'data-pmdelayedstyle="' . $stylesheet[1] . '"',$stylesheet[0]);
$html = str_replace($stylesheet[0], $new_link, $html);
}
//async stylesheets
elseif(Config::$options['assets']['rucss_stylesheet_behavior'] == 'async') {
$new_link = preg_replace(array('#media=([\'"]).+?\1#', '#onload=([\'"]).+?\1#'), '', $stylesheet[0]);
$new_link = str_replace('<link', '<link media="print" onload="this.media=\'all\';this.onload=null;"', $new_link);
$html = str_replace($stylesheet[0], $new_link, $html);
}
//remove stylesheets
elseif(Config::$options['assets']['rucss_stylesheet_behavior'] == 'remove') {
$html = str_replace($stylesheet[0], '', $html);
}
}
//store used css file
if(!empty($used_css_string)) {
if(file_put_contents($used_css_path, apply_filters('perfmatters_used_css', $used_css_string)) !== false) {
$time = get_option('perfmatters_used_css_time', array());
$time[$type] = time();
//update stored timestamp
update_option('perfmatters_used_css_time', $time);
}
}
//print used css inline after first title tag
$pos = strpos($html, '</title>');
if($pos !== false) {
//print file
if(!empty(Config::$options['assets']['rucss_method']) && Config::$options['assets']['rucss_method'] == 'file') {
//grab stored timestamp for query string
$time = get_option('perfmatters_used_css_time', array());
if(!empty($time[$type])) {
$used_css_url = add_query_arg('ver', $time[$type], $used_css_url);
}
$used_css_output = "<link rel='preload' href='" . $used_css_url . "' as='style' onload=\"this.rel='stylesheet';this.removeAttribute('onload');\">";
$used_css_output.= '<link rel="stylesheet" id="perfmatters-used-css" href="' . $used_css_url . '" media="all" />';
}
//print inline
else {
$used_css_output = '<style id="perfmatters-used-css">' . file_get_contents($used_css_path) . '</style>';
}
$html = substr_replace($html, '</title>' . $used_css_output, $pos, 8);
}
//delay stylesheet script
if(empty(Config::$options['assets']['rucss_stylesheet_behavior'])) {
$delay_check = !empty(apply_filters('perfmatters_delay_js', !empty(Config::$options['assets']['delay_js']))) && !Utilities::get_post_meta('perfmatters_exclude_delay_js');
if(!$delay_check || empty(Config::$options['assets']['delay_js_behavior']) || isset($_GET['perfmattersjsoff'])) {
$script = '<script type="text/javascript" id="perfmatters-delayed-styles-js">!function(){const e=["keydown","mousemove","wheel","touchmove","touchstart","touchend"];function t(){document.querySelectorAll("link[data-pmdelayedstyle]").forEach(function(e){e.setAttribute("href",e.getAttribute("data-pmdelayedstyle"))}),e.forEach(function(e){window.removeEventListener(e,t,{passive:!0})})}e.forEach(function(e){window.addEventListener(e,t,{passive:!0})})}();</script>';
$html = str_replace('</body>', $script . '</body>', $html);
}
}
}
return $html;
}
//get url type
private static function get_url_type()
{
global $wp_query;
$type = '';
if($wp_query->is_page) {
$type = is_front_page() ? 'front' : 'page-' . $wp_query->post->ID;
}
elseif($wp_query->is_home) {
$type = 'home';
}
elseif($wp_query->is_single) {
$type = get_post_type() !== false ? get_post_type() : 'single';
}
elseif($wp_query->is_category) {
$type = 'category';
}
elseif($wp_query->is_tag) {
$type = 'tag';
}
elseif($wp_query->is_tax) {
$type = 'tax';
}
elseif($wp_query->is_archive) {
$type = $wp_query->is_day ? 'day' : ($wp_query->is_month ? 'month' : ($wp_query->is_year ? 'year' : ($wp_query->is_author ? 'author' : 'archive')));
}
elseif($wp_query->is_search) {
$type = 'search';
}
elseif($wp_query->is_404) {
$type = '404';
}
return $type;
}
//get used selectors in html
private static function get_used_selectors($html) {
if(!$html) {
return;
}
//get dom document
$libxml_previous = libxml_use_internal_errors(true);
$dom = new \DOMDocument();
$result = $dom->loadHTML($html);
libxml_clear_errors();
libxml_use_internal_errors($libxml_previous);
if($result) {
$dom->xpath = new \DOMXPath($dom);
//setup used selectors array
self::$used_selectors = array('tags' => array(), 'ids' => array(), 'classes' => array());
//search for used selectors in dom
$classes = array();
foreach($dom->getElementsByTagName('*') as $tag) {
//add tag
self::$used_selectors['tags'][$tag->tagName] = 1;
//add add tag id
if($tag->hasAttribute('id')) {
self::$used_selectors['ids'][$tag->getAttribute('id')] = 1;
}
//store tag classes
if($tag->hasAttribute('class')) {
$class = $tag->getAttribute('class');
$tag_classes = preg_split('/\s+/', $class);
array_push($classes, ...$tag_classes);
}
}
//add classes
$classes = array_filter(array_unique($classes));
if($classes) {
self::$used_selectors['classes'] = array_fill_keys($classes, 1);
}
}
}
//get excluded selectors
private static function get_excluded_selectors() {
self::$excluded_selectors = array(
'.wp-embed-responsive', //core
'.wp-block-embed',
'.wp-block-embed__wrapper',
'.wp-caption',
'#elementor-device-mode', //elementor
'.elementor-nav-menu',
'.elementor-has-item-ratio',
'.elementor-popup-modal',
'.elementor-sticky--active',
'.dialog-type-lightbox',
'.dialog-widget-content',
'.lazyloaded',
'.elementor-motion-effects-container',
'.elementor-motion-effects-layer',
'.ast-header-break-point', //astra
'.dropdown-nav-special-toggle', //kadence
'rs-fw-forcer' //rev slider
);
if(!empty(Config::$options['assets']['rucss_excluded_selectors'])) {
self::$excluded_selectors = array_merge(self::$excluded_selectors, Config::$options['assets']['rucss_excluded_selectors']);
}
self::$excluded_selectors = apply_filters('perfmatters_rucss_excluded_selectors', self::$excluded_selectors);
}
//remove unusde css from stylesheet
private static function clean_stylesheet($url, $css)
{
//https://github.com/sabberworm/PHP-CSS-Parser/issues/150
$css = preg_replace('/^\xEF\xBB\xBF/', '', $css);
//setup css parser
$settings = Settings::create()->withMultibyteSupport(false);
$parser = new CSSParser($css, $settings);
$parsed_css = $parser->parse();
//convert relative urls to full urls
self::fix_relative_urls($url, $parsed_css);
$css_data = self::prep_css_data($parsed_css);
return self::remove_unused_selectors($css_data);
}
//convert relative urls to full urls
private static function fix_relative_urls($stylesheet_url, Document $data)
{
//get base url from stylesheet
$base_url = preg_replace('#[^/]+(\?.*)?$#', '', $stylesheet_url);
//search css for urls
$values = $data->getAllValues();
foreach($values as $value) {
if(!($value instanceof URL)) {
continue;
}
$url = $value->getURL()->getString();
//not relative
if(preg_match('/^(https?|data):/', $url)) {
continue;
}
$parsed_url = parse_url($url);
//final checks
if(!empty($parsed_url['host']) || empty($parsed_url['path']) || $parsed_url['path'][0] === '/') {
continue;
}
//create full url and replace
$new_url = $base_url . $url;
$value->getUrl()->setString($new_url);
}
}
//prep parsed css for cleaning
private static function prep_css_data(CSSBlockList $data)
{
$items = array();
foreach($data->getContents() as $content) {
//remove charset objects since were printing inline
if($content instanceof Charset) {
continue;
}
if($content instanceof AtRuleBlockList) {
$items[] = array(
'rulesets' => self::prep_css_data($content),
'at_rule' => "@{$content->atRuleName()} {$content->atRuleArgs()}",
);
}
else {
$item = array('css' => $content->render(OutputFormat::createCompact()));
if($content instanceof DeclarationBlock) {
$item['selectors'] = self::sort_selectors($content->getSelectors());
}
$items[] = $item;
}
}
return $items;
}
//sort selectors into different categories we need
private static function sort_selectors($selectors)
{
$selectors = array_map(
function($sel) {
return $sel->__toString();
},
$selectors
);
$selectors_data = array();
foreach($selectors as $selector) {
//setup selector data array
$data = array(
'selector' => trim($selector),
'classes' => array(),
'ids' => array(),
'tags' => array(),
'atts' => array()
);
//eliminate false negatives (:not(), pseudo, etc...)
$selector = preg_replace('/(?<!\\\\)::?[a-zA-Z0-9_-]+(\(.+?\))?/', '', $selector);
//atts
$selector = preg_replace_callback(
'/\[([A-Za-z0-9_:-]+)(\W?=[^\]]+)?\]/',
function($matches) use (&$data) {
$data['atts'][] = $matches[1];
return '';
},
$selector
);
//classes
$selector = preg_replace_callback(
'/\.((?:[a-zA-Z0-9_-]+|\\\\.)+)/',
function($matches) use (&$data) {
$data['classes'][] = stripslashes($matches[1]);
return '';
},
$selector
);
//ids
$selector = preg_replace_callback(
'/#([a-zA-Z0-9_-]+)/',
function($matches) use (&$data) {
$data['ids'][] = $matches[1];
return '';
},
$selector
);
//tags
$selector = preg_replace_callback(
'/[a-zA-Z0-9_-]+/',
function($matches) use (&$data) {
$data['tags'][] = $matches[0];
return '';
},
$selector
);
//add selector data to main array
$selectors_data[] = array_filter($data);
}
return array_filter($selectors_data);
}
//remove unused selectors from css data
private static function remove_unused_selectors($data)
{
$rendered = [];
foreach($data as $item) {
//has css
if(isset($item['css'])) {
//need at least one selector match
$should_render = !isset($item['selectors']) || 0 !== count(array_filter($item['selectors'],
function($selector) {
return self::is_selector_used($selector);
}
));
if($should_render) {
$rendered[] = $item['css'];
}
continue;
}
//nested rulesets
if(!empty($item['rulesets'])) {
$child_rulesets = self::remove_unused_selectors($item['rulesets']);
if($child_rulesets) {
$rendered[] = sprintf('%s{%s}', $item['at_rule'], $child_rulesets);
}
}
}
return implode("", $rendered);
}
//check if selector is used
private static function is_selector_used($selector)
{
//:root selector
if($selector['selector'] === ':root') {
return true;
}
//lone attribute selector
if(!empty($selector['atts']) && (empty($selector['classes']) && empty($selector['ids']) && empty($selector['tags']))) {
return true;
}
//search for excluded selector match
if(!empty(self::$excluded_selectors)) {
foreach(self::$excluded_selectors as $key => $value) {
if(preg_match('#(' . preg_quote($value) . ')(?=\s|\.|\:|,|\[|$)#', $selector['selector'])) {
return true;
}
}
}
//is selector used in the dom
foreach(array('classes', 'ids', 'tags') as $type) {
if(!empty($selector[$type])) {
//cast array if needed
$targets = (array)$selector[$type];
foreach($targets as $target) {
//bail if a target doesn't exist
if(!isset(self::$used_selectors[$type][$target])) {
return false;
}
}
}
}
return true;
}
//delete all files in the css cache directory
public static function clear_used_css()
{
$files = glob(PERFMATTERS_CACHE_DIR . 'css/*');
foreach($files as $file) {
if(is_file($file)) {
unlink($file);
}
}
delete_option('perfmatters_used_css_time');
}
//clear used css file for specific post or post type
public static function clear_post_used_css() {
if(empty($_POST['action']) || empty($_POST['nonce']) || empty($_POST['post_id'])) {
return;
}
if($_POST['action'] != 'perfmatters_clear_post_used_css') {
return;
}
if(!wp_verify_nonce($_POST['nonce'], 'perfmatters_clear_post_used_css')) {
return;
}
$post_id = (int)$_POST['post_id'];
$post_type = get_post_type($post_id);
$path = $post_type == 'page' ? 'page-' . $post_id : $post_type;
$file = PERFMATTERS_CACHE_DIR . 'css/' . $path . '.used.css';
if(is_file($file)) {
unlink($file);
}
wp_send_json_success();
exit;
}
//add admin bar menu item
public static function admin_bar_menu(WP_Admin_Bar $wp_admin_bar) {
if(!current_user_can('manage_options') || !perfmatters_network_access()) {
return;
}
$type = !is_admin() ? self::get_url_type() : '';
$menu_item = array(
'parent' => 'perfmatters',
'id' => 'perfmatters-clear-used-css',
'title' => __('Clear Used CSS', 'perfmatters') . ' (' . (!empty($type) ? __('Current', 'perfmatters') : __('All', 'perfmatters')) . ')',
'href' => add_query_arg(array(
'action' => 'perfmatters_clear_used_css',
'_wp_http_referer' => rawurlencode($_SERVER['REQUEST_URI']),
'_wpnonce' => wp_create_nonce('perfmatters_clear_used_css'),
'type' => $type
),
admin_url('admin-post.php'))
);
$wp_admin_bar->add_menu($menu_item);
}
//display admin notices
public static function admin_notices() {
if(get_transient('perfmatters_used_css_cleared') === false) {
return;
}
delete_transient('perfmatters_used_css_cleared');
echo '<div class="notice notice-success is-dismissible"><p><strong>' . __('Used CSS cleared.', 'perfmatters' ) . '</strong></p></div>';
}
//clear used css from admin bar
public static function admin_bar_clear_used_css() {
if(!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'perfmatters_clear_used_css')) {
wp_nonce_ays('');
}
if(!empty($_GET['type'])) {
//clear specific type
$file = PERFMATTERS_CACHE_DIR . 'css/' . $_GET['type'] . '.used.css';
if(is_file($file)) {
unlink($file);
}
}
else {
//clear all
self::clear_used_css();
if(is_admin()) {
set_transient('perfmatters_used_css_cleared', 1);
}
}
//go back to url where button was pressed
wp_safe_redirect(esc_url_raw(wp_get_referer()));
exit;
}
}