dashboards = $dashboards; } /** * Hooks the route into WordPress. */ public function register(): void { add_action( 'init', array( self::class, 'add_rules' ) ); add_action( 'init', array( self::class, 'maybe_flush' ), 20 ); add_filter( 'query_vars', array( $this, 'add_query_var' ) ); add_action( 'template_redirect', array( $this, 'handle' ) ); } /** * Adds the pretty URL rule. */ public static function add_rules(): void { add_rewrite_rule( self::RULE_REGEX, 'index.php?' . self::QUERY_VAR . '=$matches[1]', 'top' ); } /** * Removes the pretty URL rule from the rules WordPress holds in memory. */ public static function remove_rules(): void { global $wp_rewrite; unset( $wp_rewrite->extra_rules_top[ self::RULE_REGEX ] ); } /** * Flushes rewrite rules once per plugin version, not on every request. */ public static function maybe_flush(): void { if ( PULSECAST_VERSION !== get_option( self::FLUSH_OPTION ) ) { flush_rewrite_rules( false ); update_option( self::FLUSH_OPTION, PULSECAST_VERSION, false ); } } /** * Registers the query variable. * * @param array $vars Query variables. * @return array */ public function add_query_var( array $vars ): array { $vars[] = self::QUERY_VAR; return $vars; } /** * Serves the dashboard, or a generic 404 for an unknown or disabled one. */ public function handle(): void { $key = get_query_var( self::QUERY_VAR ); if ( ! is_string( $key ) || '' === $key ) { return; } $dashboard = $this->dashboards->find_by_key( $key ); if ( null === $dashboard ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); self::send_header( 'X-Robots-Tag', 'noindex' ); return; } foreach ( self::headers() as $name => $value ) { self::send_header( $name, $value ); } echo self::render( $dashboard ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built from escaped parts in render(). exit; } /** * Sends a header unless output has already started. * * @param string $name Header name. * @param string $value Header value. */ private static function send_header( string $name, string $value ): void { if ( ! headers_sent() ) { header( $name . ': ' . $value ); } } /** * Security and caching headers for the page. * * @return array */ public static function headers(): array { return array( 'Content-Type' => 'text/html; charset=utf-8', 'Cache-Control' => 'no-cache', 'X-Robots-Tag' => 'noindex, nofollow', 'Referrer-Policy' => 'no-referrer', 'X-Content-Type-Options' => 'nosniff', 'Content-Security-Policy' => "default-src 'none'; script-src 'self'; style-src 'self'; img-src https:; connect-src 'self'; frame-ancestors 'self'; base-uri 'none'; form-action 'none'", ); } /** * The dashboard's public address. * * @param string $key Dashboard key. */ public static function url( string $key ): string { return get_option( 'permalink_structure' ) ? home_url( '/feedmarquee/' . $key . '/' ) : add_query_arg( self::QUERY_VAR, $key, home_url( '/' ) ); } /** * Translated strings the display script uses. * * @return array */ public static function strings(): array { return array( 'unavailable' => __( 'This dashboard is unavailable', 'feedmarquee' ), 'offline' => __( 'Offline. Showing the last update', 'feedmarquee' ), 'loading' => __( 'Loading feeds', 'feedmarquee' ), 'stale' => __( 'Content may be out of date', 'feedmarquee' ), 'liveUnavailable' => __( 'Unavailable', 'feedmarquee' ), 'liveOffline' => __( 'Offline', 'feedmarquee' ), 'liveLoading' => __( 'Loading', 'feedmarquee' ), 'liveDelayed' => __( 'Delayed', 'feedmarquee' ), 'live' => __( 'Live', 'feedmarquee' ), 'latest' => __( 'Latest Headlines', 'feedmarquee' ), /* translators: %d: number of feeds (one). */ 'feedOne' => __( '%d feed', 'feedmarquee' ), /* translators: %d: number of feeds. */ 'feedOther' => __( '%d feeds', 'feedmarquee' ), /* translators: %d: number of stories (one). */ 'storyOne' => __( '%d story', 'feedmarquee' ), /* translators: %d: number of stories. */ 'storyOther' => __( '%d stories', 'feedmarquee' ), ); } /** * Builds the HTML document. * * @param Dashboard $dashboard Dashboard. */ public static function render( Dashboard $dashboard ): string { $settings = $dashboard->settings; wp_register_style( 'pulsecast-display', plugins_url( 'assets/css/display.css', PULSECAST_FILE ), array(), PULSECAST_VERSION ); wp_register_script( 'pulsecast-display', plugins_url( 'assets/js/display.js', PULSECAST_FILE ), array(), PULSECAST_VERSION, true ); ob_start(); wp_print_styles( 'pulsecast-display' ); $styles = (string) ob_get_clean(); ob_start(); wp_print_scripts( 'pulsecast-display' ); $scripts = (string) ob_get_clean(); $flag = static fn( string $name ): string => $settings[ $name ] ? '1' : '0'; return '' . "\n" . '' . "\n" . '' . '' . '' . '' . esc_html__( 'FeedMarquee', 'feedmarquee' ) . '' . $styles . '' . "\n" . '
' . '

' . esc_html__( 'FeedMarquee dashboard', 'feedmarquee' ) . '

' . '
' . '' . $scripts . ''; } }