| @@ -1,6 +1,10 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | +use OptimoleWP\BgOptimizer\Lazyload; | |
| 4 | +use OptimoleWP\PageProfiler\Profile; | |
| 5 | +use OptimoleWP\Preload\Links; | |
| 6 | + | |
| 3 | 7 | /** |
| 4 | 8 | * Class Optml_Manager. Adds hooks for processing tags and urls. |
| 5 | 9 | * |
| 6 | 10 | * @package \Optml\Inc |
| @@ -44,15 +48,15 @@ | ||
| 44 | 48 | */ |
| 45 | 49 | public $lazyload_replacer; |
| 46 | 50 | |
| 47 | 51 | /** |
| 48 | - * Holds the hero preloader class. | |
| 52 | + * Holds the page profiler class. | |
| 49 | 53 | * |
| 50 | 54 | * @access public |
| 51 | - * @since 3.9.0 | |
| 52 | - * @var Optml_Hero_Preloader Preloader instance. | |
| 55 | + * @since 1.0.0 | |
| 56 | + * @var Profile Page profiler instance. | |
| 53 | 57 | */ |
| 54 | - public $hero_preloader; | |
| 58 | + public $page_profiler; | |
| 55 | 59 | |
| 56 | 60 | /** |
| 57 | 61 | * Holds plugin settings. |
| 58 | 62 | * |
| @@ -64,8 +68,10 @@ | ||
| 64 | 68 | * |
| 65 | 69 | * @var array Integrations classes. |
| 66 | 70 | */ |
| 67 | 71 | private $possible_compatibilities = [ |
| 72 | + 'kadence_blocks', | |
| 73 | + 'essential_blocks', | |
| 68 | 74 | 'shortcode_ultimate', |
| 69 | 75 | 'foogallery', |
| 70 | 76 | 'envira', |
| 71 | 77 | 'beaver_builder', |
| @@ -94,8 +100,24 @@ | ||
| 94 | 100 | 'elementor_builder_late', |
| 95 | 101 | 'wpml', |
| 96 | 102 | 'otter_blocks', |
| 97 | 103 | 'spectra', |
| 104 | + 'wpsp', | |
| 105 | + 'jetengine', | |
| 106 | + 'jetpack', | |
| 107 | + 'jetpack_photon_compatibility', | |
| 108 | + 'wp_rocket', | |
| 109 | + 'wp_super_cache', | |
| 110 | + 'breeze', | |
| 111 | + 'litespeed_cache', | |
| 112 | + 'autoptimize_cache', | |
| 113 | + 'endurance_cache', | |
| 114 | + 'rocketnet', | |
| 115 | + 'speedycache', | |
| 116 | + 'hummingbird', | |
| 117 | + 'aruba_hsc', | |
| 118 | + 'spc', | |
| 119 | + 'groovy_menu', | |
| 98 | 120 | ]; |
| 99 | 121 | /** |
| 100 | 122 | * The current state of the buffer. |
| 101 | 123 | * |
| @@ -101,8 +123,25 @@ | ||
| 101 | 123 | * |
| 102 | 124 | * @var boolean Buffer state. |
| 103 | 125 | */ |
| 104 | 126 | private static $ob_started = false; |
| 127 | + /** | |
| 128 | + * The output-buffer nesting level of our capture buffer. | |
| 129 | + * | |
| 130 | + * Used to make sure we only ever capture or close our own buffer and not | |
| 131 | + * one started by a third party. | |
| 132 | + * | |
| 133 | + * @var int Buffer nesting level, 0 when no capture buffer is armed. | |
| 134 | + */ | |
| 135 | + private static $ob_level = 0; | |
| 136 | + /** | |
| 137 | + * Whether the captured buffer was already processed at shutdown. | |
| 138 | + * | |
| 139 | + * When true, the fallback output handler passes content through untouched. | |
| 140 | + * | |
| 141 | + * @var boolean Processed state. | |
| 142 | + */ | |
| 143 | + private static $ob_processed = false; | |
| 105 | 144 | |
| 106 | 145 | /** |
| 107 | 146 | * Class instance method. |
| 108 | 147 | * |
| @@ -117,10 +156,9 @@ | ||
| 117 | 156 | self::$instance = new self(); |
| 118 | 157 | self::$instance->url_replacer = Optml_Url_Replacer::instance(); |
| 119 | 158 | self::$instance->tag_replacer = Optml_Tag_Replacer::instance(); |
| 120 | 159 | self::$instance->lazyload_replacer = Optml_Lazyload_Replacer::instance(); |
| 121 | - self::$instance->hero_preloader = Optml_Hero_Preloader::instance(); | |
| 122 | - | |
| 160 | + self::$instance->page_profiler = new Profile(); | |
| 123 | 161 | add_action( 'after_setup_theme', [ self::$instance, 'init' ] ); |
| 124 | 162 | add_action( 'wp_footer', [ self::$instance, 'banner' ] ); |
| 125 | 163 | } |
| 126 | 164 | |
| @@ -138,9 +176,9 @@ | ||
| 138 | 176 | add_image_size( $key, $value['width'], $value['height'], true ); |
| 139 | 177 | } |
| 140 | 178 | foreach ( $this->possible_compatibilities as $compatibility_class ) { |
| 141 | 179 | $compatibility_class = 'Optml_' . $compatibility_class; |
| 142 | - $compatibility = new $compatibility_class; | |
| 180 | + $compatibility = new $compatibility_class(); | |
| 143 | 181 | |
| 144 | 182 | /** |
| 145 | 183 | * Check if we should load compatibility. |
| 146 | 184 | * |
| @@ -176,8 +214,11 @@ | ||
| 176 | 214 | if ( ! $has_banner ) { |
| 177 | 215 | return; |
| 178 | 216 | } |
| 179 | 217 | |
| 218 | + $position = $this->settings->get( 'badge_position' ) ?? 'right'; | |
| 219 | + $show_icon_only = $this->settings->get( 'show_badge_icon' ) === 'enabled'; | |
| 220 | + | |
| 180 | 221 | $string = __( 'Optimized by Optimole', 'optimole-wp' ); |
| 181 | 222 | $div_style = [ |
| 182 | 223 | 'display' => 'flex', |
| 183 | 224 | 'position' => 'fixed', |
| @@ -182,9 +223,8 @@ | ||
| 182 | 223 | 'display' => 'flex', |
| 183 | 224 | 'position' => 'fixed', |
| 184 | 225 | 'align-items' => 'center', |
| 185 | 226 | 'bottom' => '15px', |
| 186 | - 'right' => '15px', | |
| 187 | 227 | 'background-color' => '#fff', |
| 188 | 228 | 'padding' => '8px 6px', |
| 189 | 229 | 'font-size' => '12px', |
| 190 | 230 | 'font-weight' => '600', |
| @@ -195,11 +235,13 @@ | ||
| 195 | 235 | 'text-decoration' => 'none', |
| 196 | 236 | 'font-family' => 'Arial, Helvetica, sans-serif', |
| 197 | 237 | ]; |
| 198 | 238 | |
| 239 | + $div_style[ $position ] = '15px'; | |
| 240 | + | |
| 199 | 241 | $logo = OPTML_URL . 'assets/img/logo.svg'; |
| 200 | 242 | |
| 201 | - $link = 'https://optimole.com/wordpress/?from=badgeOn'; | |
| 243 | + $link = tsdk_translate_link( 'https://optimole.com/wordpress/?from=badgeOn' ); | |
| 202 | 244 | |
| 203 | 245 | $css = ''; |
| 204 | 246 | |
| 205 | 247 | foreach ( $div_style as $key => $value ) { |
| @@ -205,9 +247,9 @@ | ||
| 205 | 247 | foreach ( $div_style as $key => $value ) { |
| 206 | 248 | $css .= $key . ':' . $value . ';'; |
| 207 | 249 | } |
| 208 | 250 | |
| 209 | - $output = '<a style="' . esc_attr( $css ) . '" href="' . esc_url( $link ) . '" rel="nofollow" target="_blank">'; | |
| 251 | + $output = '<a style="' . esc_attr( $css ) . '" href="' . esc_url( $link ) . '" aria-label="' . esc_html( $string ) . '" rel="nofollow" target="_blank">'; | |
| 210 | 252 | $output .= ' |
| 211 | 253 | <svg style="margin-right:6px" width="20" height="20" viewBox="0 0 251 250" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 212 | 254 | <path d="M217.696 99.0926C240.492 152.857 211.284 215.314 153.249 239.999C95.2141 264.683 30.4436 242.239 7.64869 188.474C-15.1462 134.71 14.8767 75.1972 72.9116 50.4991C130.947 25.8007 194.902 45.3142 217.696 99.0926Z" fill="#EDF0FF"/> |
| 213 | 255 | <path d="M217.696 99.0926C240.492 152.857 211.284 215.314 153.249 239.999C95.2141 264.683 30.4436 242.239 7.64869 188.474C-15.1462 134.71 14.8767 75.1972 72.9116 50.4991C130.947 25.8007 194.902 45.3142 217.696 99.0926Z" fill="#D7EEFC"/> |
| @@ -238,9 +280,11 @@ | ||
| 238 | 280 | <path d="M100.727 110.97C100.642 111.097 100.487 111.167 100.333 111.238C97.3938 112.506 94.4407 113.802 91.305 114.62C90.7002 114.803 90.0958 114.929 89.4771 115.056C88.4642 115.267 87.4236 115.408 86.3691 115.479C86.4676 113.534 86.4814 111.604 86.4394 109.66C86.524 109.674 86.6224 109.688 86.7064 109.702C88.2535 109.928 89.8005 110.012 91.3471 110.026C94.047 110.069 96.7613 109.913 99.4612 109.956C99.7846 109.956 100.136 109.97 100.417 110.139C100.712 110.308 100.895 110.702 100.727 110.97Z" fill="#1D445C"/> |
| 239 | 281 | <path d="M182.837 193.871C181.5 195.322 180.136 196.731 178.702 198.07C177.071 197.351 175.524 196.519 174.005 195.618C172.74 194.871 171.502 194.082 170.251 193.293C170.124 193.209 169.97 193.11 169.913 192.969C169.787 192.687 170.026 192.349 170.349 192.222C170.659 192.11 170.996 192.138 171.348 192.194C173.12 192.462 174.905 192.786 176.663 193.096C178.702 193.448 180.756 193.772 182.837 193.871Z" fill="#1D445C"/> |
| 240 | 282 | <path d="M175.074 201.282C173.147 202.888 171.151 204.396 169.055 205.818C167.902 205.283 166.777 204.649 165.737 203.945C165.287 203.649 164.837 203.325 164.415 203.015C161.743 201 159.409 198.619 157.117 196.252C157.004 196.139 156.892 196.026 156.85 195.858C156.807 195.561 157.13 195.266 157.454 195.224C157.792 195.195 158.129 195.308 158.439 195.435C161.996 196.816 165.413 198.619 169.112 199.718C169.534 199.845 169.955 199.957 170.406 200.07C171.966 200.465 173.598 200.352 174.92 201.169C174.948 201.197 175.018 201.226 175.074 201.282Z" fill="#1D445C"/> |
| 241 | 283 | </svg>'; |
| 242 | - $output .= '<span>' . esc_html( $string ) . '</span>'; | |
| 284 | + if ( ! $show_icon_only ) { | |
| 285 | + $output .= '<span>' . esc_html( $string ) . '</span>'; | |
| 286 | + } | |
| 243 | 287 | $output .= '</a>'; |
| 244 | 288 | |
| 245 | 289 | echo $output; |
| 246 | 290 | } |
| @@ -265,13 +309,14 @@ | ||
| 265 | 309 | |
| 266 | 310 | if ( ( is_admin() && ! self::is_ajax_request() ) || ! $this->settings->is_connected() || ! $this->settings->is_enabled() ) { |
| 267 | 311 | return false; // @codeCoverageIgnore |
| 268 | 312 | } |
| 269 | - if ( array_key_exists( 'preview', $_GET ) && ! empty( $_GET['preview'] ) ) { | |
| 313 | + | |
| 314 | + if ( array_key_exists( 'preview', $_GET ) && ! empty( $_GET['preview'] ) && ! $this->settings->is_offload_enabled() ) { | |
| 270 | 315 | return false; // @codeCoverageIgnore |
| 271 | 316 | } |
| 272 | 317 | |
| 273 | - if ( array_key_exists( 'optml_off', $_GET ) && 'true' == $_GET['optml_off'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison | |
| 318 | + if ( array_key_exists( 'optml_off', $_GET ) && 'true' === $_GET['optml_off'] ) { | |
| 274 | 319 | return false; // @codeCoverageIgnore |
| 275 | 320 | } |
| 276 | 321 | if ( array_key_exists( 'elementor-preview', $_GET ) && ! empty( $_GET['elementor-preview'] ) ) { |
| 277 | 322 | return false; // @codeCoverageIgnore |
| @@ -281,15 +326,15 @@ | ||
| 281 | 326 | } |
| 282 | 327 | if ( array_key_exists( 'et_fb', $_GET ) && ! empty( $_GET['et_fb'] ) ) { |
| 283 | 328 | return false; // @codeCoverageIgnore |
| 284 | 329 | } |
| 285 | - if ( array_key_exists( 'tve', $_GET ) && $_GET['tve'] == 'true' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison | |
| 330 | + if ( array_key_exists( 'tve', $_GET ) && $_GET['tve'] === 'true' ) { | |
| 286 | 331 | return false; // @codeCoverageIgnore |
| 287 | 332 | } |
| 288 | - if ( array_key_exists( 'trp-edit-translation', $_GET ) && ( $_GET['trp-edit-translation'] == 'true' || $_GET['trp-edit-translation'] == 'preview' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison | |
| 333 | + if ( array_key_exists( 'trp-edit-translation', $_GET ) && ( $_GET['trp-edit-translation'] === 'true' || $_GET['trp-edit-translation'] === 'preview' ) ) { | |
| 289 | 334 | return false; // @codeCoverageIgnore |
| 290 | 335 | } |
| 291 | - if ( array_key_exists( 'context', $_GET ) && $_GET['context'] == 'edit' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison | |
| 336 | + if ( array_key_exists( 'context', $_GET ) && $_GET['context'] === 'edit' ) { | |
| 292 | 337 | return false; // @codeCoverageIgnore |
| 293 | 338 | } |
| 294 | 339 | // avada |
| 295 | 340 | if ( array_key_exists( 'fb-edit', $_GET ) && ! empty( $_GET['fb-edit'] ) ) { |
| @@ -299,9 +344,9 @@ | ||
| 299 | 344 | && array_key_exists( 'builder_id', $_GET ) && ! empty( $_GET['builder_id'] ) ) { |
| 300 | 345 | return false; // @codeCoverageIgnore |
| 301 | 346 | } |
| 302 | 347 | // Motion.page iFrame & builder |
| 303 | - if ( ( array_key_exists( 'motionpage_iframe', $_GET ) && $_GET['motionpage_iframe'] == 'true' ) || ( array_key_exists( 'page', $_GET ) && $_GET['page'] == 'motionpage' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison | |
| 348 | + if ( ( array_key_exists( 'motionpage_iframe', $_GET ) && $_GET['motionpage_iframe'] === 'true' ) || ( array_key_exists( 'page', $_GET ) && $_GET['page'] === 'motionpage' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison | |
| 304 | 349 | return false; // @codeCoverageIgnore |
| 305 | 350 | } |
| 306 | 351 | /** |
| 307 | 352 | * Disable replacement on POST request and when user is logged in, but allows for sample image call widget in dashboard |
| @@ -307,9 +352,9 @@ | ||
| 307 | 352 | * Disable replacement on POST request and when user is logged in, but allows for sample image call widget in dashboard |
| 308 | 353 | */ |
| 309 | 354 | if ( |
| 310 | 355 | isset( $_SERVER['REQUEST_METHOD'] ) && |
| 311 | - $_SERVER['REQUEST_METHOD'] == 'POST' && // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison | |
| 356 | + $_SERVER['REQUEST_METHOD'] === 'POST' && | |
| 312 | 357 | is_user_logged_in() |
| 313 | 358 | && ( ! isset( $_GET['quality'] ) || ! current_user_can( 'manage_options' ) ) |
| 314 | 359 | ) { |
| 315 | 360 | return false; // @codeCoverageIgnore |
| @@ -348,9 +393,9 @@ | ||
| 348 | 393 | } |
| 349 | 394 | if ( ! wp_doing_ajax() ) { |
| 350 | 395 | return false; |
| 351 | 396 | } |
| 352 | - if ( isset( $_REQUEST['action'] ) && strpos( $_REQUEST['action'], 'wpmdb' ) !== false ) { | |
| 397 | + if ( isset( $_REQUEST['action'] ) && is_string( $_REQUEST['action'] ) && strpos( $_REQUEST['action'], 'wpmdb' ) !== false ) { | |
| 353 | 398 | return false; |
| 354 | 399 | } |
| 355 | 400 | |
| 356 | 401 | return true; |
| @@ -380,9 +425,10 @@ | ||
| 380 | 425 | defined( 'OPTML_SITE_MIRROR' ) ? PHP_INT_MAX : PHP_INT_MIN |
| 381 | 426 | ); |
| 382 | 427 | add_action( 'template_redirect', [ $this, 'register_after_setup' ] ); |
| 383 | 428 | add_action( 'rest_api_init', [ $this, 'process_template_redirect_content' ], PHP_INT_MIN ); |
| 384 | - | |
| 429 | + add_action( 'shutdown', [ $this, 'close_buffer' ], PHP_INT_MIN ); | |
| 430 | + add_action( 'shutdown', [ $this, 'close_final_buffer' ], PHP_INT_MAX ); | |
| 385 | 431 | foreach ( self::$loaded_compatibilities as $registered_compatibility ) { |
| 386 | 432 | $registered_compatibility->register(); |
| 387 | 433 | } |
| 388 | 434 | } |
| @@ -394,22 +440,108 @@ | ||
| 394 | 440 | do_action( 'optml_after_setup' ); |
| 395 | 441 | } |
| 396 | 442 | |
| 397 | 443 | /** |
| 444 | + * Check if we should load the profiler. | |
| 445 | + * | |
| 446 | + * @param bool $default_value Default value. | |
| 447 | + * | |
| 448 | + * @return bool Should load the profiler. | |
| 449 | + */ | |
| 450 | + public static function should_load_profiler( $default_value = false ) { | |
| 451 | + return ! $default_value && apply_filters( 'optml_page_profiler_disable', false ) === false; | |
| 452 | + } | |
| 453 | + | |
| 454 | + /** | |
| 455 | + * Decide if the temporary Cache-Control header can be sent while page profiling is pending. | |
| 456 | + * | |
| 457 | + * The header must never be sent on non-cacheable pages: it would replace a | |
| 458 | + * Cache-Control header already set by WordPress or another plugin (PHP's | |
| 459 | + * header() replaces same-name headers by default), e.g. WooCommerce's | |
| 460 | + * no-cache header on cart and checkout, letting proxies cache user-specific | |
| 461 | + * pages. We back off when DONOTCACHEPAGE is set or when any Cache-Control | |
| 462 | + * header exists already, and let developers override the decision. | |
| 463 | + * | |
| 464 | + * @param array<int, string>|null $sent_headers Headers already set for the response; defaults to headers_list(). | |
| 465 | + * @param bool|null $do_not_cache Whether the page is flagged as non-cacheable; defaults to the DONOTCACHEPAGE constant. | |
| 466 | + * | |
| 467 | + * @return bool Whether the header can be sent. | |
| 468 | + */ | |
| 469 | + public function should_send_temporary_cache_header( $sent_headers = null, $do_not_cache = null ) { | |
| 470 | + if ( null === $do_not_cache ) { | |
| 471 | + $do_not_cache = defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE; | |
| 472 | + } | |
| 473 | + $send = ! $do_not_cache; | |
| 474 | + | |
| 475 | + if ( $send ) { | |
| 476 | + if ( null === $sent_headers ) { | |
| 477 | + $sent_headers = headers_list(); | |
| 478 | + } | |
| 479 | + foreach ( $sent_headers as $header ) { | |
| 480 | + if ( stripos( $header, 'cache-control:' ) === 0 ) { | |
| 481 | + $send = false; | |
| 482 | + break; | |
| 483 | + } | |
| 484 | + } | |
| 485 | + } | |
| 486 | + | |
| 487 | + /** | |
| 488 | + * Filters whether the temporary `Cache-Control: max-age=300` header is sent | |
| 489 | + * while page profiling is pending for the current page. | |
| 490 | + * | |
| 491 | + * @param bool $send Computed decision: false when DONOTCACHEPAGE is set or a Cache-Control header exists already. | |
| 492 | + */ | |
| 493 | + return apply_filters( 'optml_send_temporary_cache_header', $send ) === true; | |
| 494 | + } | |
| 495 | + /** | |
| 398 | 496 | * Filter raw HTML content for urls. |
| 399 | 497 | * |
| 400 | 498 | * @param string $html HTML to filter. |
| 499 | + * @param bool $partial If this is a partial content replacement and not a full page. It matters when we are are doing full page optimization like viewport lazyload. | |
| 401 | 500 | * |
| 402 | 501 | * @return mixed Filtered content. |
| 403 | 502 | */ |
| 404 | - public function replace_content( $html ) { | |
| 503 | + public function replace_content( $html, $partial = false ) { | |
| 405 | 504 | |
| 406 | 505 | if ( defined( 'REST_REQUEST' ) && REST_REQUEST && is_user_logged_in() && ( apply_filters( 'optml_force_replacement', false ) !== true ) ) { |
| 407 | 506 | return $html; |
| 408 | 507 | } |
| 508 | + if ( self::should_load_profiler( $partial ) ) { | |
| 509 | + $profile_id = Profile::generate_id( $html ); | |
| 510 | + $should_show_comment = false; | |
| 511 | + // We disable the optimizer for logged in users. | |
| 512 | + if ( ! is_user_logged_in() || ! apply_filters( 'optml_force_page_profiler', false ) !== true ) { | |
| 513 | + $js_optimizer = Optml_Admin::get_optimizer_script( false ); | |
| 409 | 514 | |
| 410 | - $html = $this->add_html_class( $html ); | |
| 515 | + if ( ! $this->page_profiler->exists_all( $profile_id ) ) { | |
| 516 | + $missing = $this->page_profiler->missing_devices( $profile_id ); | |
| 517 | + $time = time(); | |
| 518 | + $url = esc_url( esc_js( $this->get_current_url() ) ); | |
| 519 | + $hmac = wp_hash( $profile_id . $time . $url, 'nonce' ); | |
| 520 | + $js_optimizer = str_replace( | |
| 521 | + [ Profile::PLACEHOLDER, Profile::PLACEHOLDER_MISSING, Profile::PLACEHOLDER_TIME, Profile::PLACEHOLDER_HMAC, Profile::PLACEHOLDER_URL ], | |
| 522 | + [ $profile_id, implode( ',', $missing ), strval( $time ), $hmac, $url ], | |
| 523 | + $js_optimizer | |
| 524 | + ); | |
| 525 | + $html = str_replace( Optml_Admin::get_optimizer_script( true ), $js_optimizer, $html ); | |
| 526 | + if ( ! headers_sent() && $this->should_send_temporary_cache_header() ) { | |
| 527 | + header( 'Cache-Control: max-age=300' ); // Attempt to cache the page just for 5 mins until the optimizer is done. Once the optimizer is done, the page will load optimized. | |
| 528 | + } | |
| 529 | + } else { | |
| 530 | + $should_show_comment = isset( $_GET['optml_debug'] ) && $_GET['optml_debug'] === 'true'; | |
| 531 | + } | |
| 532 | + } | |
| 411 | 533 | |
| 534 | + Profile::set_current_profile_id( $profile_id ); | |
| 535 | + $this->page_profiler->set_current_profile_data(); | |
| 536 | + if ( $should_show_comment ) { | |
| 537 | + $html = str_replace( '</html>', '</html>' . $this->page_profiler->get_current_profile_html_comment(), $html ); | |
| 538 | + } | |
| 539 | + } | |
| 540 | + if ( ! $partial ) { | |
| 541 | + $html = $this->add_html_class( $html ); | |
| 542 | + } | |
| 543 | + | |
| 412 | 544 | $html = $this->process_images_from_content( $html ); |
| 413 | 545 | |
| 414 | 546 | if ( $this->settings->get( 'video_lazyload' ) === 'enabled' ) { |
| 415 | 547 | $html = apply_filters( 'optml_video_replace', $html ); |
| @@ -420,18 +552,60 @@ | ||
| 420 | 552 | $html = preg_replace( '/<head>(.*)<\/head>/ism', '<head> $1' . Optml_Lazyload_Replacer::IFRAME_PLACEHOLDER_STYLE . '</head>', $html ); |
| 421 | 553 | } |
| 422 | 554 | } |
| 423 | 555 | } |
| 556 | + | |
| 557 | + if ( $this->settings->is_lazyload_type_viewport() && ! $partial ) { | |
| 558 | + $personalized_bg_css = Lazyload::get_current_personalized_css(); | |
| 559 | + if ( OPTML_DEBUG ) { | |
| 560 | + do_action( 'optml_log', 'viewport_bgselectorsdata: ' . print_r( $personalized_bg_css, true ) ); | |
| 561 | + } | |
| 562 | + | |
| 563 | + if ( ! empty( $personalized_bg_css ) && ( $start_pos = strpos( $html, Lazyload::MARKER ) ) !== false ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.Found | |
| 564 | + // We replace the general bg css with the personalized one. | |
| 565 | + if ( ( $end_pos = strpos( $html, Lazyload::MARKER, $start_pos + strlen( Lazyload::MARKER ) ) ) !== false ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.Found | |
| 566 | + $html = substr_replace( | |
| 567 | + $html, | |
| 568 | + $personalized_bg_css, | |
| 569 | + $start_pos, | |
| 570 | + $end_pos + strlen( Lazyload::MARKER ) - $start_pos | |
| 571 | + ); | |
| 572 | + } | |
| 573 | + } | |
| 574 | + } | |
| 575 | + if ( ! $partial ) { | |
| 576 | + // WE need this last since during bg personalized CSS we collect preload urls | |
| 577 | + if ( Links::get_links_count() > 0 ) { | |
| 578 | + if ( OPTML_DEBUG ) { | |
| 579 | + do_action( 'optml_log', 'preload_links: ' . print_r( Links::get_links(), true ) ); | |
| 580 | + } | |
| 581 | + $html = str_replace( Optml_Admin::get_preload_links_placeholder(), Links::get_links_html(), $html ); | |
| 582 | + } else { | |
| 583 | + $html = str_replace( Optml_Admin::get_preload_links_placeholder(), '', $html ); | |
| 584 | + } | |
| 585 | + } | |
| 586 | + | |
| 424 | 587 | $html = apply_filters( 'optml_url_pre_process', $html ); |
| 425 | 588 | |
| 426 | 589 | $html = $this->process_urls_from_content( $html ); |
| 427 | 590 | |
| 428 | 591 | $html = apply_filters( 'optml_url_post_process', $html ); |
| 429 | - | |
| 592 | + if ( self::should_load_profiler( $partial ) ) { | |
| 593 | + Profile::reset_current_profile(); | |
| 594 | + } | |
| 430 | 595 | return $html; |
| 431 | 596 | } |
| 432 | 597 | |
| 433 | 598 | /** |
| 599 | + * Get the current url. | |
| 600 | + * | |
| 601 | + * @return string The current url. | |
| 602 | + */ | |
| 603 | + private function get_current_url() { | |
| 604 | + global $wp; | |
| 605 | + return home_url( add_query_arg( [], $wp->request ) ); | |
| 606 | + } | |
| 607 | + /** | |
| 434 | 608 | * Adds a filter that allows adding classes to the HTML tag. |
| 435 | 609 | * |
| 436 | 610 | * @param string $content The HTML content. |
| 437 | 611 | * |
| @@ -452,9 +626,9 @@ | ||
| 452 | 626 | |
| 453 | 627 | $add_classes = implode( ' ', $additional_html_classes ); |
| 454 | 628 | foreach ( $matches as $match ) { |
| 455 | 629 | if ( strpos( $match[0], 'class' ) !== false ) { |
| 456 | - $new_tag = str_replace( [ 'class="', "class='" ], [ 'class="' . $add_classes, "class='" . $add_classes ], $match[0] ); | |
| 630 | + $new_tag = str_replace( [ 'class="', "class='" ], [ 'class="' . $add_classes, "class='" . $add_classes ], $match[0] ); | |
| 457 | 631 | } else { |
| 458 | 632 | $new_tag = str_replace( 'html ', 'html class="' . $add_classes . '" ', $match[0] ); |
| 459 | 633 | } |
| 460 | 634 | |
| @@ -479,9 +653,8 @@ | ||
| 479 | 653 | $images = self::parse_images_from_html( $content ); |
| 480 | 654 | if ( empty( $images ) ) { |
| 481 | 655 | return $content; |
| 482 | 656 | } |
| 483 | - | |
| 484 | 657 | return apply_filters( 'optml_content_images_tags', $content, $images ); |
| 485 | 658 | } |
| 486 | 659 | |
| 487 | 660 | /** |
| @@ -521,20 +694,16 @@ | ||
| 521 | 694 | */ |
| 522 | 695 | public static function parse_images_from_html( $content ) { |
| 523 | 696 | $images = []; |
| 524 | 697 | |
| 525 | - $header_start = null; | |
| 526 | - $header_end = null; | |
| 527 | - | |
| 528 | - if ( preg_match( '/<header.*<\/header>/ismU', $content, $matches, PREG_OFFSET_CAPTURE ) === 1 ) { | |
| 529 | - $header_start = $matches[0][1]; | |
| 530 | - $header_end = $header_start + strlen( $matches[0][0] ); | |
| 698 | + if ( OPTML_DEBUG ) { | |
| 699 | + do_action( 'optml_log', 'Content to parse images from: ' . $content ); | |
| 531 | 700 | } |
| 532 | 701 | $regex = '/(?:<a[^>]+?href=["|\'](?P<link_url>[^\s]+?)["|\'][^>]*?>\s*)?(?P<img_tag>(?:<noscript\s*>\s*)?<img[^>]*?\s?(?:' . implode( '|', array_merge( [ 'src' ], Optml_Tag_Replacer::possible_src_attributes() ) ) . ')=["\'\\\\]*?(?P<img_url>[' . Optml_Config::$chars . ']{10,}).*?>(?:\s*<\/noscript\s*>)?){1}(?:\s*<\/a>)?/ismu'; |
| 533 | 702 | |
| 534 | 703 | if ( preg_match_all( $regex, $content, $images, PREG_OFFSET_CAPTURE ) ) { |
| 535 | 704 | if ( OPTML_DEBUG ) { |
| 536 | - do_action( 'optml_log', $images ); | |
| 705 | + do_action( 'optml_log', 'Image tags parsed: ' . print_r( $images, true ) ); | |
| 537 | 706 | } |
| 538 | 707 | foreach ( $images as $key => $unused ) { |
| 539 | 708 | // Simplify the output as much as possible, mostly for confirming test results. |
| 540 | 709 | if ( is_numeric( $key ) && $key > 0 ) { |
| @@ -550,9 +719,9 @@ | ||
| 550 | 719 | } |
| 551 | 720 | $images[ $key ][ $url_key ] = $url_value[0]; |
| 552 | 721 | |
| 553 | 722 | if ( $key === 0 ) { |
| 554 | - $images['in_header'][ $url_key ] = $header_start !== null ? ( $url_value[1] > $header_start && $url_value[1] < $header_end ) : false; | |
| 723 | + $images['in_no_script'][ $url_key ] = false; | |
| 555 | 724 | |
| 556 | 725 | // Check if we are in the noscript context. |
| 557 | 726 | if ( $is_no_script === false ) { |
| 558 | 727 | $is_no_script = strpos( $images[0][ $url_key ], '<noscript' ) !== false ? true : false; |
| @@ -557,9 +726,9 @@ | ||
| 557 | 726 | if ( $is_no_script === false ) { |
| 558 | 727 | $is_no_script = strpos( $images[0][ $url_key ], '<noscript' ) !== false ? true : false; |
| 559 | 728 | } |
| 560 | 729 | if ( $is_no_script ) { |
| 561 | - $images['in_header'][ $url_key ] = true; | |
| 730 | + $images['in_no_script'][ $url_key ] = true; | |
| 562 | 731 | $is_no_script = strpos( $images[0][ $url_key ], '</noscript' ) !== false ? false : true; |
| 563 | 732 | } |
| 564 | 733 | } |
| 565 | 734 | } |
| @@ -580,13 +749,11 @@ | ||
| 580 | 749 | */ |
| 581 | 750 | public function process_urls_from_content( $html ) { |
| 582 | 751 | $extracted_urls = $this->extract_urls_from_content( $html ); |
| 583 | 752 | if ( OPTML_DEBUG ) { |
| 584 | - do_action( 'optml_log', 'matched urls' ); | |
| 585 | - do_action( 'optml_log', $extracted_urls ); | |
| 753 | + do_action( 'optml_log', 'Extracted image urls from content: ' . print_r( $extracted_urls, true ) ); | |
| 586 | 754 | } |
| 587 | 755 | return $this->do_url_replacement( $html, $extracted_urls ); |
| 588 | - | |
| 589 | 756 | } |
| 590 | 757 | |
| 591 | 758 | /** |
| 592 | 759 | * Method to extract assets from content. |
| @@ -662,10 +829,10 @@ | ||
| 662 | 829 | $is_slashed = strpos( $url, '\/' ) !== false; |
| 663 | 830 | $is_relative = strpos( |
| 664 | 831 | $url, |
| 665 | 832 | $is_slashed ? |
| 666 | - addcslashes( $upload_resource['content_path'], '/' ) : | |
| 667 | - $upload_resource['content_path'] | |
| 833 | + addcslashes( $upload_resource['content_path'], '/' ) : | |
| 834 | + $upload_resource['content_path'] | |
| 668 | 835 | ) === 0; |
| 669 | 836 | if ( $is_relative ) { |
| 670 | 837 | $url = $upload_resource['content_host'] . $url; |
| 671 | 838 | } |
| @@ -674,16 +841,64 @@ | ||
| 674 | 841 | }, |
| 675 | 842 | $urls |
| 676 | 843 | ); |
| 677 | 844 | |
| 845 | + /* | |
| 846 | + * Replace all URLs in a single pass per chunk instead of one full-page | |
| 847 | + * preg_replace() per URL, which scanned and rebuilt the whole page for | |
| 848 | + * every replaced URL. Chunks are bounded by pattern size, not only | |
| 849 | + * count, so the compiled regex stays within PCRE's ~64KB limit even | |
| 850 | + * for very long URLs (e.g. signed CDN URLs with kilobyte-sized query | |
| 851 | + * strings). Each chunk is applied as soon as it fills, so only one | |
| 852 | + * chunk's bookkeeping is in memory at a time. | |
| 853 | + */ | |
| 854 | + $chunk = []; | |
| 855 | + $quoted = []; | |
| 856 | + $quoted_size = 0; | |
| 678 | 857 | foreach ( $urls as $origin => $replace ) { |
| 679 | - $html = preg_replace( '/(?<![\/|:|\\w])' . preg_quote( $origin, '/' ) . '/m', $replace, $html ); | |
| 858 | + $quoted_origin = preg_quote( $origin, '/' ); | |
| 859 | + if ( ! empty( $chunk ) && ( count( $chunk ) >= 200 || $quoted_size + strlen( $quoted_origin ) > 24000 ) ) { | |
| 860 | + $html = $this->replace_urls_chunk( $html, $chunk, $quoted ); | |
| 861 | + $chunk = []; | |
| 862 | + $quoted = []; | |
| 863 | + $quoted_size = 0; | |
| 864 | + } | |
| 865 | + $chunk[ $origin ] = $replace; | |
| 866 | + $quoted[] = $quoted_origin; | |
| 867 | + $quoted_size += strlen( $quoted_origin ) + 1; | |
| 680 | 868 | } |
| 869 | + if ( ! empty( $chunk ) ) { | |
| 870 | + $html = $this->replace_urls_chunk( $html, $chunk, $quoted ); | |
| 871 | + } | |
| 681 | 872 | |
| 682 | 873 | return $html; |
| 683 | 874 | } |
| 684 | 875 | |
| 685 | 876 | /** |
| 877 | + * Replace one chunk of URLs in the content with a single combined pattern. | |
| 878 | + * | |
| 879 | + * @param string $html Content to process. | |
| 880 | + * @param array<string, string> $chunk Map of origin => replacement URLs. | |
| 881 | + * @param string[] $quoted The preg_quote()d origins, in the same order. | |
| 882 | + * | |
| 883 | + * @return string Processed content, unchanged when the pattern fails. | |
| 884 | + */ | |
| 885 | + private function replace_urls_chunk( $html, $chunk, $quoted ) { | |
| 886 | + $result = preg_replace_callback( | |
| 887 | + '/(?<![\/|:|\\w])(?:' . implode( '|', $quoted ) . ')/m', | |
| 888 | + function ( $matches ) use ( $chunk ) { | |
| 889 | + return $chunk[ $matches[0] ]; | |
| 890 | + }, | |
| 891 | + $html | |
| 892 | + ); | |
| 893 | + if ( $result === null ) { | |
| 894 | + do_action( 'optml_log', 'URL replacement failed for a chunk of ' . count( $chunk ) . ' URLs, PCRE error ' . preg_last_error() ); | |
| 895 | + return $html; | |
| 896 | + } | |
| 897 | + return $result; | |
| 898 | + } | |
| 899 | + | |
| 900 | + /** | |
| 686 | 901 | * Init html replacer handler. |
| 687 | 902 | */ |
| 688 | 903 | public function process_template_redirect_content() { |
| 689 | 904 | // Early exit if function was already called, we don't want duplicate ob_start |
| @@ -693,13 +908,188 @@ | ||
| 693 | 908 | self::$ob_started = true; |
| 694 | 909 | // We no longer need this if the handler was started. |
| 695 | 910 | remove_filter( 'the_content', [ $this, 'process_images_from_content' ], PHP_INT_MAX ); |
| 696 | 911 | |
| 697 | - ob_start( | |
| 698 | - [ &$this, 'replace_content' ] | |
| 699 | - ); | |
| 912 | + $this->start_capture_buffer(); | |
| 700 | 913 | } |
| 701 | 914 | |
| 915 | + /** | |
| 916 | + * Start the output buffer that holds the page HTML. | |
| 917 | + * | |
| 918 | + * By default the page is processed by the attached handler when the buffer | |
| 919 | + * is flushed, the way it worked up to 4.2.11. We never flush other buffers | |
| 920 | + * and open no buffer at shutdown, so code that opens a buffer early and reads | |
| 921 | + * it back with ob_get_clean() at shutdown (FacetWP, Groovy Menu) keeps working. | |
| 922 | + * | |
| 923 | + * When the optml_capture_at_shutdown filter returns true, close_buffer() | |
| 924 | + * captures and processes the buffer at shutdown instead, outside of PHP's | |
| 925 | + * display-handler context. Callbacks hooked into our filters can then use | |
| 926 | + * output buffering themselves, and fatal errors raised during processing | |
| 927 | + * keep their real message instead of being masked by "Cannot use output | |
| 928 | + * buffering in output buffering display handlers". | |
| 929 | + * | |
| 930 | + * A named method is used instead of a closure so the buffer can be | |
| 931 | + * identified as ours via ob_get_status()['name']. | |
| 932 | + * | |
| 933 | + * @return void | |
| 934 | + */ | |
| 935 | + private function start_capture_buffer() { | |
| 936 | + self::$ob_processed = false; | |
| 937 | + ob_start( [ $this, 'handle_buffer_fallback' ] ); | |
| 938 | + self::$ob_level = ob_get_level(); | |
| 939 | + } | |
| 940 | + | |
| 941 | + /** | |
| 942 | + * The handler name PHP reports for our capture buffer in ob_get_status(). | |
| 943 | + */ | |
| 944 | + const OB_HANDLER_NAME = 'Optml_Manager::handle_buffer_fallback'; | |
| 945 | + | |
| 946 | + /** | |
| 947 | + * Whether the page is captured and processed at shutdown, outside of PHP's display-handler context. | |
| 948 | + * | |
| 949 | + * @return bool | |
| 950 | + */ | |
| 951 | + private function captures_at_shutdown() { | |
| 952 | + /** | |
| 953 | + * Filters whether the page is captured and processed at shutdown, outside | |
| 954 | + * of PHP's display-handler context, instead of inside the output-buffer | |
| 955 | + * handler. | |
| 956 | + * | |
| 957 | + * Off by default: the shutdown capture flushes the buffers | |
| 958 | + * stacked above ours and opens a new buffer afterwards, which breaks code | |
| 959 | + * that reads its own buffer back at shutdown. Return true to opt in. | |
| 960 | + * | |
| 961 | + * @param bool $capture_at_shutdown Whether to process the buffer at shutdown. | |
| 962 | + */ | |
| 963 | + return apply_filters( 'optml_capture_at_shutdown', false ) === true; | |
| 964 | + } | |
| 965 | + | |
| 966 | + /** | |
| 967 | + * Output-buffer handler attached to our buffer. | |
| 968 | + * | |
| 969 | + * By default this is where the page is processed. An exception thrown by the | |
| 970 | + * replacement never breaks the page: the content is returned untouched. | |
| 971 | + * | |
| 972 | + * With the optml_capture_at_shutdown opt-in the handler runs only when the | |
| 973 | + * buffer is flushed outside of close_buffer(), and the content is passed | |
| 974 | + * through unprocessed, because running the replacement filter graph inside a | |
| 975 | + * display handler is what that mode exists to avoid. | |
| 976 | + * | |
| 977 | + * @param string $content The buffered content. | |
| 978 | + * @param int $phase PHP's output-handler phase bitmask (unused; keeps replace_content()'s $partial parameter shielded from it). | |
| 979 | + * | |
| 980 | + * @return string The content to output. | |
| 981 | + */ | |
| 982 | + public function handle_buffer_fallback( $content, $phase = 0 ) { | |
| 983 | + if ( self::$ob_processed || $content === '' ) { | |
| 984 | + return $content; | |
| 985 | + } | |
| 986 | + if ( ! $this->captures_at_shutdown() ) { | |
| 987 | + try { | |
| 988 | + return $this->replace_content( $content, self::is_ajax_request() ); | |
| 989 | + } catch ( Throwable $t ) { | |
| 990 | + // Never break the page from inside a display handler. | |
| 991 | + do_action( 'optml_log', 'replace_content failed inside the output handler: ' . $t->getMessage() ); | |
| 992 | + return $content; | |
| 993 | + } | |
| 994 | + } | |
| 995 | + do_action( 'optml_log', 'Optimole buffer was flushed outside close_buffer(); content passed through unprocessed.' ); | |
| 996 | + | |
| 997 | + return $content; | |
| 998 | + } | |
| 999 | + | |
| 1000 | + /** | |
| 1001 | + * Close the buffer and flush the content. | |
| 1002 | + */ | |
| 1003 | + public function close_buffer() { | |
| 1004 | + if ( ! self::$ob_started ) { | |
| 1005 | + return; | |
| 1006 | + } | |
| 1007 | + | |
| 1008 | + if ( ! $this->captures_at_shutdown() ) { | |
| 1009 | + if ( ob_get_length() ) { | |
| 1010 | + ob_end_flush(); | |
| 1011 | + } | |
| 1012 | + return; | |
| 1013 | + } | |
| 1014 | + | |
| 1015 | + /* | |
| 1016 | + * Flush the buffers other plugins stacked on top of ours so their | |
| 1017 | + * handlers still transform the page before we process it, preserving | |
| 1018 | + * the same order as a full top-down flush at request shutdown. | |
| 1019 | + */ | |
| 1020 | + while ( ob_get_level() > self::$ob_level ) { | |
| 1021 | + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a non-flushable buffer must not raise a notice; we stop on failure. | |
| 1022 | + if ( ! @ob_end_flush() ) { | |
| 1023 | + break; | |
| 1024 | + } | |
| 1025 | + } | |
| 1026 | + | |
| 1027 | + if ( ! $this->capture_and_process_buffer() ) { | |
| 1028 | + do_action( 'optml_log', 'Optimole buffer was closed earlier by third-party code.' ); | |
| 1029 | + return; | |
| 1030 | + } | |
| 1031 | + | |
| 1032 | + /* | |
| 1033 | + * Re-arm the capture so output echoed by later shutdown callbacks is | |
| 1034 | + * still processed and unguarded third-party flush calls find a buffer | |
| 1035 | + * to close instead of raising a notice. | |
| 1036 | + */ | |
| 1037 | + $this->start_capture_buffer(); | |
| 1038 | + } | |
| 1039 | + | |
| 1040 | + /** | |
| 1041 | + * Close the re-armed buffer at the very end of shutdown. | |
| 1042 | + * | |
| 1043 | + * @return void | |
| 1044 | + */ | |
| 1045 | + public function close_final_buffer() { | |
| 1046 | + if ( ! self::$ob_started || ! $this->captures_at_shutdown() ) { | |
| 1047 | + return; | |
| 1048 | + } | |
| 1049 | + $this->capture_and_process_buffer( false ); | |
| 1050 | + } | |
| 1051 | + | |
| 1052 | + /** | |
| 1053 | + * Capture our buffer, process it outside the display-handler context and echo the result. | |
| 1054 | + * | |
| 1055 | + * Ownership is verified by both nesting level and handler identity, so a | |
| 1056 | + * buffer another plugin opened at the same level after ours was closed is | |
| 1057 | + * never captured or closed by us. | |
| 1058 | + * | |
| 1059 | + * @param bool $is_page Whether this is the page capture (true) or the late shutdown output (false). | |
| 1060 | + * | |
| 1061 | + * @return bool Whether our buffer was found and consumed. | |
| 1062 | + */ | |
| 1063 | + private function capture_and_process_buffer( $is_page = true ) { | |
| 1064 | + if ( self::$ob_level === 0 || ob_get_level() !== self::$ob_level ) { | |
| 1065 | + return false; | |
| 1066 | + } | |
| 1067 | + $status = ob_get_status(); | |
| 1068 | + if ( ( $status['name'] ?? '' ) !== self::OB_HANDLER_NAME ) { | |
| 1069 | + return false; | |
| 1070 | + } | |
| 1071 | + $html = ob_get_contents(); | |
| 1072 | + // Set before ob_end_clean() so our handler no-ops during buffer cleanup. | |
| 1073 | + self::$ob_processed = true; | |
| 1074 | + ob_end_clean(); | |
| 1075 | + if ( $html !== false && $html !== '' ) { | |
| 1076 | + if ( $is_page ) { | |
| 1077 | + /** | |
| 1078 | + * Filters the captured page HTML before Optimole processes it. | |
| 1079 | + * | |
| 1080 | + * Runs once per request, on the buffer captured at shutdown, outside of | |
| 1081 | + * PHP's display-handler context. Late output echoed by other shutdown | |
| 1082 | + * callbacks is not passed through this filter. | |
| 1083 | + * | |
| 1084 | + * @param string $html The full page HTML. | |
| 1085 | + */ | |
| 1086 | + $html = apply_filters( 'optml_captured_page_html', $html ); | |
| 1087 | + } | |
| 1088 | + echo $this->replace_content( $html, self::is_ajax_request() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- full page HTML, escaping would break the page. | |
| 1089 | + } | |
| 1090 | + return true; | |
| 1091 | + } | |
| 702 | 1092 | /** |
| 703 | 1093 | * Throw error on object clone |
| 704 | 1094 | * |
| 705 | 1095 | * The whole idea of the singleton design pattern is that there is a single |