hasher = $hasher; $this->store = $store; $this->rules = $rules; $this->background_processor = $background_processor; $this->hash_store = $hash_store; $this->path_factory = $path_factory; $this->logger = $logger; } /** * Begin buffering the request. * * @action template_redirect * * @return void */ public function start_buffering(): void { ob_start( [ $this, 'end_buffering' ] ); } /** * Manage the current HTML hash state for the request. * * Compares the freshly generated hash of the final output buffer * against the previously stored hash. If the hash differs, this indicates * that the page content has changed since the last optimization pass. * * This method is intended to be called AS LATE AS POSSIBLE in the `shutdown` phase, after * output buffering has captured the full HTML content. * * If running under fastcgi or litespeed, the request will be returned immediately and the logic * processed in the background. * * @action shutdown * * @return void */ public function check_hash(): void { if ( SG::get_get_var( Request::QUERY_OPTIMIZER_PREVIEW ) ) { $this->logger->debug( 'Skipping hash check due to optimizer preview query variable.' ); return; } if ( ! $this->html ) { if ( ! is_admin() ) { $this->logger->debug( 'Bypassing Optimizer: No HTML found to check', [ 'request_uri' => SG::get_server_var( 'REQUEST_URI', 'unknown' ), ] ); } return; } // Don't check hashes on 404 requests. if ( is_404() ) { $this->logger->debug( 'Bypassing Optimizer: 404 not found', [ 'request_uri' => SG::get_server_var( 'REQUEST_URI', 'unknown' ), ] ); return; } // Return request early, if possible, so we can process this in the background. $this->background_processor->try_finish(); $viewport = Viewport::current( $this->is_mobile() ); // Process skip rules and bail if required. foreach ( $this->rules->all() as $rule ) { if ( $rule->should_skip() ) { $this->logger->debug( 'Bypassing Optimizer: skip rule', [ 'rule' => get_class( $rule ), 'viewport' => $viewport->value(), ] ); return; } } try { $path = $this->path_factory->make(); } catch ( InvalidArgumentException $e ) { $this->logger->error( 'Hash handler unable to determine the path', [ 'viewport' => $viewport->value(), 'exception' => $e, ] ); return; } // Generate a hash based on the final HTML markup, note this differs for mobile vs desktop. $hash = $this->hasher->build_hash( $this->html ); $stored_hash = $this->hash_store->get( $path, $viewport ); // The frontend script will pass this get var as a hash set request. $maybe_set_hash = (bool) SG::get_get_var( 'kadence_set_optimizer_hash', false ); if ( $maybe_set_hash ) { $this->logger->debug( 'Attempting to store new optimizer hash', [ 'path' => $path->path(), 'viewport' => $viewport->value(), 'hash' => $hash, ] ); // Store the hash for the current viewport. $this->hash_store->set( $path, $viewport, $hash ); do_action( 'kadence_blocks_optimizer_set_hash', $hash, $path, $viewport ); return; } // The HTML has been changed somehow, invalidate the optimization data, so that the next request will not have the data. if ( $stored_hash && $stored_hash !== $hash ) { $changes = $this->hasher->get_changed_components( $stored_hash, $hash ); $this->logger->debug( 'Optimizer hash does not match...deleting', [ 'path' => $path->path(), 'viewport' => $viewport->value(), 'changes' => $changes, ] ); // Delete the viewport hash. $this->hash_store->delete( $path, $viewport ); $analysis = $this->store->get( $path ); // This page isn't optimized or the data is already invalidated. if ( ! $analysis ) { return; } // Set data to stale to force invalidate data for all viewports. try { $this->logger->debug( 'Marking optimizer path as stale to remove on next page load', [ 'path' => $path->path(), ] ); $analysis->isStale = true; $this->store->set( $path, $analysis ); do_action( 'kadence_blocks_optimizer_data_invalidated', $analysis->isStale, $path ); } catch ( Throwable $e ) { // Our DateTimeImmutable should never throw an exception, but this is here just in case. return; } } $this->html = ''; do_action( 'kadence_blocks_hash_check_complete' ); } /** * Get the HTML, which will differ as the request proceeds. * * @return string */ public function html(): string { return $this->html; } /** * Callback that receives the buffer's contents. Captures the full page HTML * in our property for use when we manage the hash state down the line. * * @param string $html The final HTML. * @param int $phase The bitmask of the PHP_OUTPUT_HANDLER_* constants. * * @return string */ private function end_buffering( string $html, int $phase ): string { if ( $phase & PHP_OUTPUT_HANDLER_FINAL ) { $this->html = $html; } return $html; } }