function add_used_css_to_html( string $html, string $used_css ): string { $replace = preg_replace( '##iU', '' . $this->get_used_css_markup( $used_css ), $html, 1 ); if ( null === $replace ) { return $html; } return $replace; } /** * Return Markup for used_css into the page. * * @param string $used_css Used CSS content. * * @return string */ private function get_used_css_markup( string $used_css ): string { /** * Filters Used CSS content before output. * * @since 3.9.0.2 * * @param string $used_css Used CSS content. */ $used_css = apply_filters( 'rocket_usedcss_content', $used_css ); $used_css = str_replace( '\\', '\\\\', $used_css );// Guard the backslashes before passing the content to preg_replace. $used_css = $this->handle_charsets( $used_css, false ); return sprintf( '', $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 ); } }