> */ public static function allowed_markup() { $common = [ 'id' => true, 'class' => true, 'style' => true, 'title' => true, 'role' => true, 'hidden' => true, 'data-*' => true, 'aria-*' => true, ]; $allowed = wp_kses_allowed_html( 'post' ); $allowed['form'] = array_merge( $common, [ 'method' => true, 'action' => true, 'target' => true ] ); $allowed['input'] = array_merge( $common, [ 'type' => true, 'name' => true, 'value' => true, 'checked' => true, 'placeholder' => true, 'min' => true, 'max' => true, 'step' => true, 'required' => true, 'readonly' => true, 'disabled' => true, 'autocomplete' => true, 'pattern' => true, 'inputmode' => true, ] ); $allowed['select'] = array_merge( $common, [ 'name' => true, 'multiple' => true, 'disabled' => true ] ); $allowed['option'] = array_merge( $common, [ 'value' => true, 'selected' => true, 'disabled' => true ] ); $allowed['textarea'] = array_merge( $common, [ 'name' => true, 'rows' => true, 'cols' => true, 'placeholder' => true ] ); $allowed['button'] = array_merge( $common, [ 'type' => true, 'name' => true, 'value' => true, 'disabled' => true ] ); $allowed['label'] = array_merge( $common, [ 'for' => true ] ); $allowed['details'] = array_merge( $common, [ 'open' => true ] ); $allowed['summary'] = $common; $allowed['template'] = $common; return array_merge( $allowed, self::allowed_svg() ); } /** * The same, for SVG drawn INSIDE an HTML page — the QR code and the logo. * * Not for an SVG served as its own image/svg+xml document: there the file * is parsed as XML, where kses lowercasing viewBox would break it. The one * such document this plugin serves (the score badge) is built from a band * letter and a YYYY-MM string and is sent with its own restrictive CSP. * * @return array> */ public static function allowed_svg() { // aria-label by name as well as by wildcard: core's kses honours the // data-* wildcard but drops aria-* unless the attribute is named, and // the QR code's label is the only thing a screen reader has of it. $common = [ 'id' => true, 'class' => true, 'style' => true, 'role' => true, 'aria-label' => true, 'aria-hidden' => true, 'aria-*' => true, 'data-*' => true, ]; return [ 'svg' => array_merge( $common, [ 'xmlns' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'preserveaspectratio' => true, 'focusable' => true, 'shape-rendering' => true, 'image-rendering' => true, 'transform' => true, ] ), 'g' => array_merge( $common, [ 'fill' => true, 'stroke' => true, 'transform' => true ] ), 'path' => array_merge( $common, [ 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'fill-rule' => true, 'clip-rule' => true ] ), 'rect' => array_merge( $common, [ 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true, 'fill' => true, 'stroke' => true, 'shape-rendering' => true ] ), 'text' => array_merge( $common, [ 'x' => true, 'y' => true, 'fill' => true, 'font-family' => true, 'font-size' => true, 'font-weight' => true, 'text-anchor' => true, 'dominant-baseline' => true ] ), 'title' => $common, 'desc' => $common, 'img' => array_merge( $common, [ 'src' => true, 'alt' => true, 'width' => true, 'height' => true, 'loading' => true ] ), ]; } public static function pager( $paged, $pages, $query_arg ) { $pages = (int) $pages; if ( $pages < 2 ) { return ''; } // A hand-typed ?lpaged=999 must still be able to walk back, so the // link targets are computed from a clamped page number, not the raw one. $paged = min( max( 1, (int) $paged ), $pages ); $out = '
'; if ( $paged > 1 ) { $out .= '' . esc_html__( 'Previous page', 'guardforge' ) . ''; } /* translators: 1: page currently shown, 2: total number of pages. */ $out .= '' . esc_html( sprintf( __( 'Page %1$d of %2$d', 'guardforge' ), $paged, $pages ) ) . ''; if ( $paged < $pages ) { $out .= '' . esc_html__( 'Next page', 'guardforge' ) . ''; } return $out . '
'; } /** * Render the header for one admin screen. * * @param string $title Screen title — the page's one top-level heading. * @param string $subtitle Optional one-line description under the title. */ public static function render( $title, $subtitle = '' ) { echo '
'; echo '
'; echo wp_kses( self::logo_markup(), self::allowed_markup() ); echo '
'; echo '
' . esc_html( self::product_name() ) . ' v' . esc_html( defined( 'GUARDFORGE_VERSION' ) ? GUARDFORGE_VERSION : '' ) . '
'; echo '

' . esc_html( $title ); if ( $subtitle !== '' ) { echo ' ' . esc_html( $subtitle ) . ''; } echo '

'; echo '
'; // titles echo '
'; // brand echo '
'; // The Pro add-on prints its plan and credits here; this package has neither. do_action( 'guardforge/admin_header_meta' ); self::render_docs_link(); // The add-on's trial / renew button goes last in the row, after Docs, // as it did before the licence layer moved out; a second hook, because // the first one sits ahead of the link. do_action( 'guardforge/admin_header_actions' ); echo '
'; // meta echo '
'; echo '
'; } /** * "Guard Forge" — from the shared suite registry (call only, never * edit — includes/class-forge-suite-registry.php is byte-identical * across all eight products) when it happens to be loaded, else the * literal every other GuardForge string already carries. */ private static function product_name() { if ( class_exists( 'Forge_Suite_Registry' ) ) { $tool = Forge_Suite_Registry::tool( self::SLUG ); if ( is_array( $tool ) && ! empty( $tool['name'] ) ) { return (string) $tool['name']; } } return __( 'Guard Forge', 'guardforge' ); } /** * The real product mark, in priority order: * 1. Forge_Suite_Dashboard::LOGOS_B64[slug] via ::logo_img — the * shared dashboard class (call only, never edit). * 2. This plugin's own assets/logo.svg. * 3. A letter fallback, same visual slot either way. */ private static function logo_markup() { if ( class_exists( 'Forge_Suite_Dashboard' ) ) { $html = Forge_Suite_Dashboard::logo_img( self::SLUG, 'forge-admin-header__mark' ); if ( $html !== '' ) { return method_exists( 'Forge_Suite_Dashboard', 'kses_logo' ) ? Forge_Suite_Dashboard::kses_logo( $html ) : $html; } } if ( defined( 'GUARDFORGE_DIR' ) && defined( 'GUARDFORGE_URL' ) && is_readable( GUARDFORGE_DIR . 'assets/logo.svg' ) ) { return ''; } return ''; } /** Docs link — the plugin's own existing docs URL constant, no new one. */ private static function render_docs_link() { $url = defined( 'FORGE_DOCS_GUARDFORGE' ) ? FORGE_DOCS_GUARDFORGE : ''; if ( $url === '' ) { return; } echo '' . esc_html__( 'Docs', 'guardforge' ) . ''; } }