HEX
Server: Apache/2.4.65 (Debian)
System: Linux 88f31f35b0b8 6.1.0-38-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.147-1 (2025-08-02) x86_64
User: www-data (33)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/optimization-detective/optimization.php
<?php
/**
 * Optimizing for Optimization Detective.
 *
 * @package optimization-detective
 * @since 0.1.0
 */

// @codeCoverageIgnoreStart
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}
// @codeCoverageIgnoreEnd

/**
 * Starts output buffering at the end of the 'template_include' filter.
 *
 * This is to implement #43258 in core.
 *
 * This is a hack that would eventually be replaced with something like this in wp-includes/template-loader.php:
 *
 *          $template = apply_filters( 'template_include', $template );
 *     +    ob_start( 'wp_template_output_buffer_callback' );
 *          if ( $template ) {
 *              include $template;
 *          } elseif ( current_user_can( 'switch_themes' ) ) {
 *
 * @since 0.1.0
 * @access private
 * @link https://core.trac.wordpress.org/ticket/43258
 *
 * @param string|mixed $passthrough Value for the template_include filter which is passed through.
 * @return string|mixed Unmodified value of $passthrough.
 */
function od_buffer_output( $passthrough ) {
	/*
	 * Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for flags,
	 * we need to omit PHP_OUTPUT_HANDLER_FLUSHABLE. If the buffer were flushable, then each time that ob_flush() is
	 * called, it would send a fragment of the output into the output buffer callback. When buffering the entire
	 * response as an HTML document, this would result in broken HTML processing.
	 *
	 * If this ends up being problematic, then PHP_OUTPUT_HANDLER_FLUSHABLE could be added to the $flags and the
	 * output buffer callback could check if the phase is PHP_OUTPUT_HANDLER_FLUSH and abort any later
	 * processing while also emitting a _doing_it_wrong().
	 *
	 * The output buffer needs to be removable because WordPress calls wp_ob_end_flush_all() and then calls
	 * wp_cache_close(). If the buffers are not all flushed before wp_cache_close() is closed, then some output buffer
	 * handlers (e.g. for caching plugins) may fail to be able to store the page output in the object cache.
	 * See <https://github.com/WordPress/performance/pull/1317#issuecomment-2271955356>.
	 */
	$flags = PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE;

	ob_start(
		static function ( string $output, ?int $phase ): string {
			// When the output is being cleaned (e.g. the pending template is replaced with an error page), do not send it through the filter.
			if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) !== 0 ) {
				return $output;
			}

			/**
			 * Filters the template output buffer before sending it to the client.
			 *
			 * @since 0.1.0
			 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Filter%3A%20od_template_output_buffer
			 *
			 * @param string $output Output buffer.
			 * @return string Filtered output buffer.
			 */
			return (string) apply_filters( 'od_template_output_buffer', $output );
		},
		0, // Unlimited buffer size.
		$flags
	);
	return $passthrough;
}

/**
 * Adds template output buffer filter for optimization if eligible.
 *
 * @since 0.1.0
 * @access private
 */
function od_maybe_add_template_output_buffer_filter(): void {
	$disabled_reasons = od_get_disabled_reasons();
	if ( count( $disabled_reasons ) > 0 ) {
		if ( WP_DEBUG ) {
			add_action(
				'wp_print_footer_scripts',
				static function () use ( $disabled_reasons ): void {
					od_print_disabled_reasons( array_values( $disabled_reasons ) );
				}
			);
		}
		return;
	}

	$callback = 'od_optimize_template_output_buffer';
	if (
		function_exists( 'perflab_wrap_server_timing' )
		&&
		function_exists( 'perflab_server_timing_use_output_buffer' )
		&&
		perflab_server_timing_use_output_buffer()
	) {
		$callback = perflab_wrap_server_timing( $callback, 'optimization-detective', 'exist' );
	}
	add_filter( 'od_template_output_buffer', $callback );
}

/**
 * Prints the reasons why Optimization Detective is not optimizing the current page.
 *
 * This is only used when WP_DEBUG is enabled.
 *
 * @since 1.0.0
 * @access private
 *
 * @param string[] $reasons Reason messages.
 */
function od_print_disabled_reasons( array $reasons ): void {
	foreach ( $reasons as $i => $reason ) {
		wp_print_inline_script_tag(
			sprintf(
				"console.info( %s );\n//# sourceURL=od-print-disabled-reasons-%d",
				wp_json_encode( '[Optimization Detective] ' . $reason, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
				$i + 1
			),
			array( 'type' => 'module' )
		);
	}
}

/**
 * Determines whether the current response can be optimized.
 *
 * @since 0.1.0
 * @since 0.9.0 Response is optimized for admin users as well when in 'plugin' development mode.
 *
 * @access private
 *
 * @return bool Whether response can be optimized.
 */
function od_can_optimize_response(): bool {
	return count( od_get_disabled_reasons() ) === 0;
}

/**
 * Determines whether the response has an HTML Content-Type.
 *
 * @since 0.2.0
 * @private
 *
 * @return bool Whether Content-Type is HTML.
 */
function od_is_response_html_content_type(): bool {
	$is_html_content_type = false;

	$headers_list = array_merge(
		array( 'Content-Type: ' . ini_get( 'default_mimetype' ) ),
		headers_list()
	);
	foreach ( $headers_list as $header ) {
		$header_parts = preg_split( '/\s*[:;]\s*/', strtolower( $header ) );
		if ( is_array( $header_parts ) && count( $header_parts ) >= 2 && 'content-type' === $header_parts[0] ) {
			$is_html_content_type = in_array( $header_parts[1], array( 'text/html', 'application/xhtml+xml' ), true );
		}
	}

	return $is_html_content_type;
}

/**
 * Optimizes template output buffer.
 *
 * @since 0.1.0
 * @access private
 *
 * @global WP_Query $wp_the_query WP_Query object.
 *
 * @param string $buffer Template output buffer.
 * @return string Filtered template output buffer.
 */
function od_optimize_template_output_buffer( string $buffer ): string {
	global $wp_the_query;

	// If the content-type is not HTML or the output does not start with '<', then abort since the buffer is definitely not HTML.
	if (
		! od_is_response_html_content_type() ||
		! str_starts_with( ltrim( $buffer ), '<' )
	) {
		return $buffer;
	}

	// If the initial tag is not an open HTML tag, then abort since the buffer is not a complete HTML document.
	$processor = new OD_HTML_Tag_Processor( $buffer );
	if ( ! (
		$processor->next_tag( array( 'tag_closers' => 'visit' ) ) &&
		! $processor->is_tag_closer() &&
		'HTML' === $processor->get_tag()
	) ) {
		return $buffer;
	}

	$query_vars = od_get_normalized_query_vars();
	$slug       = od_get_url_metrics_slug( $query_vars );
	$post       = OD_URL_Metrics_Post_Type::get_post( $slug );

	/**
	 * Post ID.
	 *
	 * @var positive-int|null $post_id
	 */
	$post_id = $post instanceof WP_Post ? $post->ID : null;

	$tag_visitor_registry = new OD_Tag_Visitor_Registry();

	/**
	 * Fires to register tag visitors before walking over the document to perform optimizations.
	 *
	 * @since 0.3.0
	 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Action%3A%20od_register_tag_visitors
	 *
	 * @param OD_Tag_Visitor_Registry $tag_visitor_registry Tag visitor registry.
	 */
	do_action( 'od_register_tag_visitors', $tag_visitor_registry );

	$current_etag     = od_get_current_url_metrics_etag( $tag_visitor_registry, $wp_the_query, od_get_current_theme_template() );
	$group_collection = new OD_URL_Metric_Group_Collection(
		$post instanceof WP_Post ? OD_URL_Metrics_Post_Type::get_url_metrics_from_post( $post ) : array(),
		$current_etag,
		od_get_breakpoint_max_widths(),
		od_get_url_metrics_breakpoint_sample_size(),
		od_get_url_metric_freshness_ttl()
	);
	$link_collection  = new OD_Link_Collection();

	$template_optimization_context = new OD_Template_Optimization_Context(
		$group_collection,
		$link_collection,
		$query_vars,
		$slug,
		$post_id
	);

	/**
	 * Fires before Optimization Detective starts iterating over the document in the output buffer.
	 *
	 * This is before any of the registered tag visitors have been invoked.
	 *
	 * @since 1.0.0
	 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Action%3A%20od_start_template_optimization
	 *
	 * @param OD_Template_Optimization_Context $template_optimization_context Template optimization context.
	 */
	do_action( 'od_start_template_optimization', $template_optimization_context );

	$visited_tag_state    = new OD_Visited_Tag_State();
	$tag_visitor_context  = new OD_Tag_Visitor_Context(
		$processor,
		$group_collection,
		$link_collection,
		$visited_tag_state,
		$post_id
	);
	$current_tag_bookmark = 'optimization_detective_current_tag';
	$visitors             = iterator_to_array( $tag_visitor_registry );

	// Whether we need to add the data-od-xpath attribute to elements and whether the detection script should be injected.
	$needs_detection          = ! $group_collection->is_every_group_complete();
	$did_amend_meta_generator = false;
	do {
		// Never process anything inside NOSCRIPT since it will never show up in the DOM when scripting is enabled, and thus it can never be detected nor measured.
		// Similarly, elements in the Admin Bar are not relevant for optimization, so this loop ensures that no tags in the Admin Bar are visited.
		if (
			in_array( 'NOSCRIPT', $processor->get_breadcrumbs(), true )
			||
			$processor->is_admin_bar()
		) {
			continue;
		}

		// Amend the META generator tag if it's the right one and hasn't been amended already.
		if (
			! $did_amend_meta_generator && // @phpstan-ignore booleanNot.alwaysTrue, booleanAnd.alwaysFalse, booleanAnd.alwaysFalse, booleanAnd.alwaysFalse (False positives in PHPStan due to the following line.)
			'META' === $processor->get_tag() && // @phpstan-ignore identical.alwaysFalse (False positive in PHPStan since it isn't aware of the do/while loop apparently.)
			'generator' === $processor->get_attribute( 'name' ) &&
			str_starts_with( (string) $processor->get_attribute( 'content' ), 'optimization-detective ' )
		) {
			$content               = (string) $processor->get_attribute( 'content' );
			$viewport_group_status = array();
			foreach ( $group_collection as $group ) {
				$min_width = $group->get_minimum_viewport_width();

				$status = 'empty';
				if ( $group->is_complete() ) {
					$status = 'complete';
				} elseif ( $group->count() > 0 ) {
					$status = 'populated';
				}

				$viewport_group_status[] = sprintf( '%s:%s', $min_width, $status );
			}
			$content .= '; url_metric_groups={' . implode( ', ', $viewport_group_status ) . '}';
			$processor->set_attribute( 'content', $content );
			$did_amend_meta_generator = true;
		}

		$tracked_in_url_metrics = false;
		$processor->set_bookmark( $current_tag_bookmark ); // TODO: Should we break if this returns false?

		foreach ( $visitors as $visitor ) {
			$cursor_move_count    = $processor->get_cursor_move_count();
			$visitor_return_value = $visitor( $tag_visitor_context );
			if ( true === $visitor_return_value ) {
				$tracked_in_url_metrics = true;
			}

			// If the visitor traversed HTML tags, we need to go back to this tag so that in the next iteration any
			// relevant tag visitors may apply, in addition to properly setting the data-od-xpath on this tag below.
			if ( $cursor_move_count !== $processor->get_cursor_move_count() ) {
				$processor->seek( $current_tag_bookmark ); // TODO: Should this break out of the optimization loop if it returns false?
			}
		}
		$processor->release_bookmark( $current_tag_bookmark );

		if ( $visited_tag_state->is_tag_tracked() ) {
			$tracked_in_url_metrics = true;
		}

		if ( $tracked_in_url_metrics && $needs_detection ) {
			$processor->set_meta_attribute( 'xpath', $processor->get_xpath() );
		}

		$visited_tag_state->reset();
	} while ( $processor->next_tag( array( 'tag_closers' => 'skip' ) ) );

	// Inject detection script.
	// TODO: When optimizing above, if we find that there is a stored LCP element but it fails to match, it should perhaps set $needs_detection to true and send the request with an override nonce. However, this would require backtracking and adding the data-od-xpath attributes.
	if ( $needs_detection ) {
		$processor->append_body_html( od_get_detection_scripts( $slug, $group_collection ) );
	}

	/**
	 * Fires after Optimization Detective has finished iterating over the document in the output buffer.
	 *
	 * This is after all the registered tag visitors have been invoked.
	 *
	 * @since 1.0.0
	 * @link https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/docs/hooks.md#:~:text=Action%3A-,od_finish_template_optimization
	 *
	 * @param OD_Template_Optimization_Context $template_optimization_context Template optimization context.
	 */
	do_action( 'od_finish_template_optimization', $template_optimization_context );

	// Send any preload links in a Link response header and in a LINK tag injected at the end of the HEAD.
	// Additional links may have been added at the od_finish_template_optimization action, so this must come after.
	if ( count( $link_collection ) > 0 ) {
		$response_header_links = $link_collection->get_response_header();
		if ( ! is_null( $response_header_links ) && ! headers_sent() ) {
			header( $response_header_links, false );
		}
		$processor->append_head_html( $link_collection->get_html() );
	}

	return $processor->get_updated_html();
}