policy = $policy; $this->elementor = $elementor; $this->scan = $scan; } /** * Haakt aan. */ public function register(): void { add_action( 'template_redirect', array( $this, 'maybe_activate' ), 0 ); // Zonder de paginascan (Pro) is er niemand die de pagina aan het einde // van het request door het page_output-filter haalt — en dat filter is // wat de sleuven in de balk invult. Dan doen we het zelf, minimaal. if ( null === $this->scan ) { add_action( 'template_redirect', array( $this, 'maybe_buffer' ), 2 ); } add_action( 'admin_bar_menu', array( $this, 'menu' ), 100 ); // Het rapport als állerlaatste regel, ná de bewerkknop van EditorMode // (prioriteit 101): eerst de acties, dan de uitleg. add_action( 'admin_bar_menu', array( $this, 'rapport_node' ), 102 ); // De vaste opmaak van het paneel gaat mee met de stylesheet van de // adminbalk zelf, via de enqueue-keten. De kleur van het stipje kan // daar niet bij — die is pas na het renderen bekend — en wordt door // finalize() als attribuut gezet, in dezelfde ronde als het rapport. add_action( 'wp_enqueue_scripts', array( $this, 'styles' ), 20 ); // De invulronde: de paginascan geeft aan het einde van het request de // complete pagina door dit filter, en dan pas bestaan alle tellingen. add_filter( 'yinga_focus/page_output', array( $this, 'finalize' ) ); } /** * De minimale outputbuffer voor de invulronde, alleen als de paginascan * er niet is om dat werk te doen. * * ── Waarom er een buffer nodig is ────────────────────────────────────── * * De adminbalk wordt bovenaan de pagina gerenderd, vóór de afbeeldingen. * Wat er met die afbeeldingen gebeurt, weten we pas als de pagina klaar * is. Daarom schrijft menu() sleuven en vult finalize() ze aan het einde * in — en daarvoor moet de hele pagina even door onze handen. * * ── Waarom hij expliciet dicht gaat ──────────────────────────────────── * * Een buffer die open blijft tot PHP hem bij het afsluiten opruimt, werkt * meestal, maar in een gedeelde omgeving als WordPress is "meestal" niet * goed genoeg: opent of sluit een ander onderdeel een buffer die niet met * de onze spoort, dan raakt de stapel scheef. Dus onthouden we op welk * niveau we openden en sluiten we precies die buffer zelf, op `shutdown`, * vóór WordPress zijn eigen opruimronde (wp_ob_end_flush_all, prioriteit 1) * draait. * * De nettere route — de template-outputbuffer die WordPress 6.9 biedt — * kan hier nog niet: de plugin ondersteunt 6.5. Zodra het minimum omhoog * gaat, hoort dit daarheen. */ public function maybe_buffer(): void { if ( ! $this->showing() || $this->buffer_level > 0 ) { return; } $started = ob_start( static function ( string $html ): string { try { return (string) apply_filters( 'yinga_focus/page_output', $html ); } catch ( \Throwable $error ) { unset( $error ); return $html; } } ); if ( ! $started ) { return; } $this->buffer_level = ob_get_level(); add_action( 'shutdown', array( $this, 'close_buffer' ), 0 ); } /** * Sluit de buffer die maybe_buffer() opende — en alleen die. * * Buffers sluiten van buiten naar binnen, dus eerst gaat alles weg wat * ná ons is geopend en nog open staat; dat zou PHP bij het afsluiten toch * doen, alleen nu gebeurt het op een voorspelbaar moment. Ligt onze * buffer er niet meer (iemand anders heeft hem al gesloten), dan doen we * niets: een buffer van een ander sluiten is precies de fout die we hier * willen voorkomen. */ public function close_buffer(): void { if ( $this->buffer_level <= 0 ) { return; } while ( ob_get_level() >= $this->buffer_level ) { if ( ! ob_end_flush() ) { break; } } $this->buffer_level = 0; } /** * Zet het journaal aan zodra er iemand meekijkt die er iets aan heeft. * * Dit gebeurt vóór het renderen, want daarna is het te laat: de beslissingen * die we willen vastleggen worden tijdens het renderen genomen. */ public function maybe_activate(): void { if ( $this->showing() ) { $this->policy->journal()->activate(); } } /** * Mag deze bezoeker het rapport zien? * * @return bool */ private function showing(): bool { return ! is_admin() && is_user_logged_in() && current_user_can( 'upload_files' ) && is_admin_bar_showing(); } /** * Bouwt het menu. * * @param mixed $bar De WP_Admin_Bar. */ public function menu( $bar = null ): void { if ( ! is_object( $bar ) || ! method_exists( $bar, 'add_node' ) || ! $this->showing() ) { return; } // Sleuven, geen cijfers: de balk rendert bovenaan de pagina, vóór de // afbeeldingen. finalize() vult ze aan het einde van het request in. $bar->add_node( array( 'id' => 'yinga-focus', 'title' => '' . '' . esc_html( Naming::product() ) . '', 'href' => admin_url( 'options-media.php#yinga_focus' ), 'meta' => array( 'title' => Naming::product() ), ) ); // De vergelijkingsknop staat er altijd, en is puur request-afhankelijk — // die kan dus wél meteen. Twee tabbladen naast elkaar, één met en één // zonder: het enige echte bewijs. $this->compare_link( $bar ); } /** * Het rapport-item, als laatste regel in het submenu. * * @param mixed $bar De WP_Admin_Bar. */ public function rapport_node( $bar = null ): void { if ( ! is_object( $bar ) || ! method_exists( $bar, 'add_node' ) || ! $this->showing() ) { return; } $bar->add_node( array( 'id' => 'yinga-focus-rapport', 'parent' => 'yinga-focus', 'title' => '' . esc_html__( 'Building report…', 'yinga-focus' ) . '', ) ); } /** * Vult de sleuven in de al gerenderde adminbalk met het echte rapport. * * Draait aan het einde van het request, in de outputbuffer van de * paginascan, wanneer alle routes hun werk hebben gedaan en het journaal * compleet is. * * @param mixed $html De complete pagina. * @return mixed */ public function finalize( $html ) { if ( ! is_string( $html ) || ! $this->showing() ) { return $html; } if ( false === strpos( $html, 'data-yf-slot=' ) ) { return $html; } $totals = $this->policy->journal()->totals(); // Eerst het stipje: een style-attribuut op de sleuf, geen