_from_html( string $clean_html, string $html ): string {
$this->set_inline_exclusions_lists();
$html = $this->remove_external_styles_from_html( $clean_html, $html );
return $this->remove_internal_styles_from_html( $clean_html, $html );
}
/**
* Remove external styles from the page's HTML.
*
* @param string $clean_html Cleaned HTML after removing comments, noscripts and scripts.
* @param string $html Actual page's HTML.
*
* @return string
*/
private function remove_external_styles_from_html( string $clean_html, string $html ) {
$link_styles = $this->find(
']+[\s"\'])?href\s*=\s*[\'"]\s*?(?[^\'"]+(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>',
$clean_html,
'Uis'
);
$preserve_google_font = apply_filters( 'rocket_rucss_preserve_google_font', false );
$external_exclusions = $this->validate_array_and_quote(
/**
* Filters the array of external exclusions.
*
* @since 3.11.4
*
* @param array $external_exclusions Array of patterns used to match against the external style tag.
*/
(array) apply_filters( 'rocket_rucss_external_exclusions', $this->external_exclusions )
);
foreach ( $link_styles as $style ) {
if (
! (bool) preg_match( '/rel=[\'"]?stylesheet[\'"]?/is', $style[0] )
&&
! ( (bool) preg_match( '/rel=[\'"]?preload[\'"]?/is', $style[0] ) && (bool) preg_match( '/as=[\'"]?style[\'"]?/is', $style[0] ) )
||
( $preserve_google_font && strstr( $style['url'], '//fonts.googleapis.com/css' ) )
) {
continue;
}
if ( ! empty( $external_exclusions ) && $this->find( implode( '|', $external_exclusions ), $style[0] ) ) {
continue;
}
$html = str_replace( $style[0], '', $html );
}
return (string) $html;
}
/**
* Remove internal styles from the page's HTML.
*
* @param string $clean_html Cleaned HTML after removing comments, noscripts and scripts.
* @param string $html Actual page's HTML.
*
* @return string
*/
private function remove_internal_styles_from_html( string $clean_html, string $html ) {
$inline_styles = $this->find(
'',
$used_css
);
}
/**
* Determines if the page is mobile and separate cache for mobile files is enabled.
*
* @return boolean
*/
private function is_mobile(): bool {
return $this->options->get( 'cache_mobile', 0 )
&& $this->options->get( 'do_caching_mobile_files', 0 )
&& wp_is_mobile();
}
/**
* Check if current page is the home page.
*
* @param string $url Current page url.
*
* @return bool
*/
private function is_home( string $url ): bool {
/**
* Filters the home url.
*
* @since 3.11.4
*
* @param string $home_url home url.
* @param string $url url of current page.
*/
$home_url = apply_filters( 'rocket_rucss_is_home_url', home_url(), $url );
return untrailingslashit( $url ) === untrailingslashit( $home_url );
}
/**
* Process pending jobs inside cron iteration.
*
* @return void
*/
public function process_pending_jobs() {
Logger::debug( 'RUCSS: Start processing pending jobs inside cron.' );
if ( ! $this->is_enabled() ) {
Logger::debug( 'RUCSS: Stop processing cron iteration because option is disabled.' );
return;
}
// Get some items from the DB with status=pending & job_id isn't empty.
/**
* Filters the pending jobs count.
*
* @since 3.11
*
* @param int $rows Number of rows to grab with each CRON iteration.
*/
$rows = apply_filters( 'rocket_rucss_pending_jobs_cron_rows_count', 100 );
Logger::debug( "RUCSS: Start getting number of {$rows} pending jobs." );
$pending_jobs = $this->used_css_query->get_pending_jobs( $rows );
if ( ! $pending_jobs ) {
Logger::debug( 'RUCSS: No pending jobs are there.' );
return;
}
foreach ( $pending_jobs as $used_css_row ) {
Logger::debug( "RUCSS: Send the job for url {$used_css_row->url} to Async task to check its job status." );
// Change status to in-progress.
$this->used_css_query->make_status_inprogress( (int) $used_css_row->id );
$this->queue->add_job_status_check_async( (int) $used_css_row->id );
}
}
/**
* Check job status by DB row ID.
*
* @param int $id DB Row ID.
*
* @return void
*/
public function check_job_status( int $id ) {
Logger::debug( 'RUCSS: Start checking job status for row ID: ' . $id );
$new_job_id = false;
$row_details = $this->used_css_query->get_item( $id );
if ( ! $row_details ) {
Logger::debug( 'RUCSS: Row ID not found ', compact( 'id' ) );
// Nothing in DB, bailout.
return;
}
// Send the request to get the job status from SaaS.
$job_details = $this->api->get_queue_job_status( $row_details->job_id, $row_details->queue_name, $this->is_home( $row_details->url ) );
/**
* Filters the rocket min rucss css result size.
*
* @since 3.13.3
*
* @param int min size.
*/
$min_rucss_size = apply_filters( 'rocket_min_rucss_size', 150 );
if ( ! is_numeric( $min_rucss_size ) ) {
$min_rucss_size = 150;
}
if ( isset( $job_details['contents']['shakedCSS_size'] ) && intval( $job_details['contents']['shakedCSS_size'] ) < $min_rucss_size ) {
$message = 'RUCSS: shakedCSS size is less than ' . $min_rucss_size;
Logger::error( $message );
$this->used_css_query->make_status_failed( $id, '500', $message );
return;
}
if (
200 !== $job_details['code']
||
empty( $job_details['contents'] )
||
! isset( $job_details['contents']['shakedCSS'] )
) {
Logger::debug( 'RUCSS: Job status failed for url: ' . $row_details->url, $job_details );
// Failure, check the retries number.
if ( $row_details->retries >= 3 ) {
Logger::debug( 'RUCSS: Job failed 3 times for url: ' . $row_details->url );
/**
* Unlock preload URL.
*
* @param string $url URL to unlock
*/
do_action( 'rocket_preload_unlock_url', $row_details->url );
$this->used_css_query->make_status_failed( $id, strval( $job_details['code'] ), $job_details['message'] );
return;
}
// on timeout errors with code 408 create new job.
switch ( $job_details['code'] ) {
case 408:
$add_to_queue_response = $this->add_url_to_the_queue( $row_details->url, (bool) $row_details->is_mobile );
if ( false !== $add_to_queue_response ) {
$new_job_id = $add_to_queue_response['contents']['jobId'];
$this->used_css_query->update_job_id( $id, $new_job_id );
}
break;
}
// Increment the retries number with 1 , Change status to pending again and change job id on timeout.
$this->used_css_query->increment_retries( $id, $row_details->retries );
// @Todo: Maybe we can add this row to the async job to get the status before the next cron
return;
}
/**
* Unlock preload URL.
*
* @param string $url URL to unlock
*/
do_action( 'rocket_preload_unlock_url', $row_details->url );
$css = $this->apply_font_display_swap( $job_details['contents']['shakedCSS'] );
$hash = md5( $css );
if ( ! $this->filesystem->write_used_css( $hash, $css ) ) {
$message = 'RUCSS: Could not write used CSS to the filesystem: ' . $row_details->url;
Logger::error( $message );
$this->used_css_query->make_status_failed( $id, '', $message );
return;
}
// Everything is fine, save the usedcss into DB, change status to completed and reset queue_name and job_id.
Logger::debug( 'RUCSS: Save used CSS for url: ' . $row_details->url );
$this->used_css_query->make_status_completed( $id, $hash );
/**
* Fires after successfully saving the used CSS for an URL
*
* @param string $url URL used to generated the used CSS.
* @param array $job_details Result of the request to get the job status from SaaS.
*/
do_action( 'rocket_rucss_complete_job_status', $row_details->url, $job_details );
}
/**
* Add clear UsedCSS adminbar item.
*
* @param WP_Admin_Bar $wp_admin_bar Adminbar object.
*
* @return void
*/
public function add_clear_usedcss_bar_item( WP_Admin_Bar $wp_admin_bar ) {
global $post;
if ( 'local' === wp_get_environment_type() ) {
return;
}
if ( ! current_user_can( 'rocket_remove_unused_css' ) ) {
return;
}
if ( is_admin() ) {
return;
}
if ( ! $this->can_optimize_url() ) {
return;
}
if ( ! rocket_can_display_options() ) {
return;
}
/**
* Filters the rocket `clear used css of this url` option on admin bar menu.
*
* @since 3.12.1
*
* @param bool $should_skip Should skip adding `clear used css of this url` option in admin bar.
* @param type $post Post object.
*/
if ( apply_filters( 'rocket_skip_admin_bar_clear_used_css_option', false, $post ) ) {
return;
}
$referer = '';
$action = 'rocket_clear_usedcss_url';
if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
$referer_url = filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_SANITIZE_URL );
$referer = '&_wp_http_referer=' . rawurlencode( remove_query_arg( 'fl_builder', $referer_url ) );
}
/**
* Clear usedCSS for this URL (frontend).
*/
$wp_admin_bar->add_menu(
[
'parent' => 'wp-rocket',
'id' => 'clear-usedcss-url',
'title' => __( 'Clear Used CSS of this URL', 'rocket' ),
'href' => wp_nonce_url( admin_url( 'admin-post.php?action=' . $action . $referer ), $action ),
]
);
}
/**
* Clear specific url.
*
* @param string $url Page url.
*
* @return void
*/
public function clear_url_usedcss( string $url ) {
$this->delete_used_css( $url );
/**
* Fires after clearing usedcss for specific url.
*
* @since 3.11
*
* @param string $url Current page URL.
*/
do_action( 'rocket_rucss_after_clearing_usedcss', $url );
}
/**
* Get the count of not completed rows.
*
* @return int
*/
public function get_not_completed_count() {
return $this->used_css_query->get_not_completed_count();
}
/**
* Clear failed urls.
*
* @return void
*/
public function clear_failed_urls() {
$rows = $this->used_css_query->get_failed_rows();
if ( empty( $rows ) ) {
return;
}
$failed_urls = [];
foreach ( $rows as $row ) {
$failed_urls[] = $row->url;
$id = (int) $row->id;
if ( empty( $id ) ) {
continue;
}
$this->used_css_query->revert_to_pending( $id );
}
/**
* Fires after clearing failed urls.
*
* @param array $urls Failed urls.
*/
do_action( 'rocket_rucss_after_clearing_failed_url', $failed_urls );
}
/**
* Add preload links for the fonts in the used CSS
*
* @param string $html HTML content.
* @param string $used_css Used CSS content.
*
* @return string
*/
private function add_used_fonts_preload( string $html, string $used_css ): string {
/**
* Filters the fonts preload from the used CSS
*
* @since 3.11
*
* @param bool $enable True to enable, false to disable.
*/
if ( ! apply_filters( 'rocket_enable_rucss_fonts_preload', true ) ) {
return $html;
}
if ( ! preg_match_all( '/@font-face\s*{\s*(?[^}]+)}/is', $used_css, $font_faces, PREG_SET_ORDER ) ) {
return $html;
}
if ( empty( $font_faces ) ) {
return $html;
}
$urls = [];
foreach ( $font_faces as $font_face ) {
if ( empty( $font_face['content'] ) ) {
continue;
}
$font_url = $this->extract_first_font( $font_face['content'] );
/**
* Filters font URL with CDN hostname
*
* @since 3.11.4
*
* @param type $url url to be rewritten.
*/
$font_url = apply_filters( 'rocket_font_url', $font_url );
if ( empty( $font_url ) ) {
continue;
}
$urls[] = $font_url;
}
if ( empty( $urls ) ) {
return $html;
}
$urls = array_unique( $urls );
$replace = preg_replace(
'##iU',
'' . $this->preload_links( $urls ),
$html,
1
);
if ( null === $replace ) {
return $html;
}
return $replace;
}
/**
* Remove preconnect tag for google api.
*
* @param string $html html content.
*
* @return string
*/
protected function remove_google_font_preconnect( string $html ): string {
$clean_html = $this->hide_comments( $html );
$clean_html = $this->hide_noscripts( $clean_html );
$clean_html = $this->hide_scripts( $clean_html );
$links = $this->find(
']+[\s"\'])?rel\s*=\s*[\'"]((preconnect)|(dns-prefetch))[\'"]([^>]+)?\/?>',
$clean_html,
'Uis'
);
foreach ( $links as $link ) {
if ( preg_match( '/href=[\'"](https:)?\/\/fonts.googleapis.com\/?[\'"]/', $link[0] ) ) {
$html = str_replace( $link[0], '', $html );
}
}
return $html;
}
/**
* Extracts the first font URL from the font-face declaration
*
* Skips .eot fonts if it exists
*
* @since 3.11
*
* @param string $font_face Font-face declaration content.
*
* @return string
*/
private function extract_first_font( string $font_face ): string {
if ( ! preg_match_all( '/src:\s*(?[^;}]*)/is', $font_face, $sources, PREG_SET_ORDER ) ) {
return '';
}
foreach ( $sources as $src ) {
if ( empty( $src['urls'] ) ) {
continue;
}
$urls = explode( ',', $src['urls'] );
foreach ( $urls as $url ) {
if ( false !== strpos( $url, '.eot' ) ) {
continue;
}
if ( ! preg_match( '/url\(\s*[\'"]?(?[^\'")]+)[\'"]?\)/is', $url, $matches ) ) {
continue;
}
return trim( $matches['url'] );
}
}
return '';
}
/**
* Converts an array of URLs to preload link tags
*
* @param array $urls An array of URLs.
*
* @return string
*/
private function preload_links( array $urls ): string {
$links = '';
foreach ( $urls as $url ) {
$links .= '';
}
return $links;
}
/**
* Set Rucss inline attr exclusions
*
* @return void
*/
private function set_inline_exclusions_lists() {
$wpr_dynamic_lists = $this->data_manager->get_lists();
$this->inline_atts_exclusions = isset( $wpr_dynamic_lists->rucss_inline_atts_exclusions ) ? $wpr_dynamic_lists->rucss_inline_atts_exclusions : [];
$this->inline_content_exclusions = isset( $wpr_dynamic_lists->rucss_inline_content_exclusions ) ? $wpr_dynamic_lists->rucss_inline_content_exclusions : [];
}
/**
* Displays a notice if the used CSS folder is not writable
*
* @since 3.11.4
*
* @return void
*/
public function notice_write_permissions() {
if ( ! current_user_can( 'rocket_manage_options' ) ) {
return;
}
if ( ! $this->is_enabled() ) {
return;
}
if ( $this->filesystem->is_writable_folder() ) {
return;
}
$message = rocket_notice_writing_permissions( trim( str_replace( rocket_get_constant( 'ABSPATH', '' ), '', rocket_get_constant( 'WP_ROCKET_USED_CSS_PATH', '' ) ), '/' ) );
rocket_notice_html(
[
'status' => 'error',
'dismissible' => '',
'message' => $message,
]
);
}
/**
* Validate the items in array to be strings only and preg_quote them.
*
* @param array $items Array to be validated and quoted.
*
* @return array|string[]
*/
private function validate_array_and_quote( array $items ) {
$items_array = array_filter( $items, 'is_string' );
return array_map(
static function ( $item ) {
return preg_quote( $item, '/' );
},
$items_array
);
}
}