'; echo '

Asterion read-only canonical truth surface

'; echo '

Source: ' . esc_html( $root ) . '  ·  Repo is authoritative. No edits from WP.

'; echo self::inline_styles(); echo '
'; // ---- Tree ---- echo ''; // ---- Content ---- echo '
'; if ( $requested !== '' && $resolved === null ) { echo '
Invalid file path. The requested file is outside the spec root, has a non-.md extension, contains path traversal, or does not exist.
'; } elseif ( $resolved === null ) { echo '
Select a file from the tree to view its contents.
'; } else { $rendered = self::render_markdown_file( $resolved['absolute'] ); if ( $rendered === null ) { echo '
Read failed. The file resolved but could not be read.
'; } else { echo '
' . esc_html( $resolved['relative'] ) . '
'; echo '
' . $rendered . '
'; } } echo '
'; echo '
'; // .sa-asterion-explorer echo ''; // .wrap } /** * Build a nested array tree of the spec pack. * Returns: [ 'dirs' => [ name => sub-tree ], 'files' => [ name => relative_path ] ] */ private static function build_tree( string $root ): array { if ( ! is_dir( $root ) ) { return []; } return self::walk( $root, '' ); } private static function walk( string $abs, string $rel_prefix ): array { $dirs = []; $files = []; $entries = @scandir( $abs ); if ( $entries === false ) { return [ 'dirs' => [], 'files' => [] ]; } foreach ( $entries as $entry ) { if ( $entry === '.' || $entry === '..' ) continue; $abs_path = $abs . '/' . $entry; $rel_path = $rel_prefix === '' ? $entry : $rel_prefix . '/' . $entry; if ( is_dir( $abs_path ) ) { $dirs[ $entry ] = self::walk( $abs_path, $rel_path ); } elseif ( is_file( $abs_path ) && substr( strtolower( $entry ), -3 ) === '.md' ) { $files[ $entry ] = $rel_path; } } ksort( $dirs ); ksort( $files ); return [ 'dirs' => $dirs, 'files' => $files ]; } private static function render_tree_html( array $tree, string $current_dir_rel, string $selected ) { echo ''; } /** * Validate a relative path against the spec root. Returns * [ 'relative' => normalized, 'absolute' => realpath ] * or null on any failure (path traversal, wrong extension, outside root, * not a file). * * Public so other surfaces (Board #28 Sara file binding; * Board #30 Asterion projection — both reuse) can validate without * re-implementing the rules. */ public static function validate_relative_path( string $relative ): ?array { // Normalize separators $relative = str_replace( '\\', '/', $relative ); // Reject any obvious traversal or absolute paths if ( strpos( $relative, '..' ) !== false ) return null; if ( $relative === '' ) return null; if ( $relative[0] === '/' ) return null; if ( preg_match( '#^[A-Za-z]:#', $relative ) ) return null; // Whitelist .md extension only if ( substr( strtolower( $relative ), -3 ) !== '.md' ) return null; $root_real = realpath( self::spec_root() ); if ( $root_real === false ) return null; $root_real = str_replace( '\\', '/', $root_real ); $candidate = realpath( self::spec_root() . '/' . $relative ); if ( $candidate === false ) return null; $candidate = str_replace( '\\', '/', $candidate ); // Final containment check: candidate must be inside root if ( strpos( $candidate, $root_real . '/' ) !== 0 ) return null; if ( ! is_file( $candidate ) ) return null; // Re-derive normalized relative from the realpath for display $normalized_rel = ltrim( substr( $candidate, strlen( $root_real ) ), '/' ); return [ 'relative' => $normalized_rel, 'absolute' => $candidate, ]; } /** * Read the file from disk and render via Parsedown in safe mode. * Returns rendered HTML, or null on read failure. */ private static function render_markdown_file( string $absolute ): ?string { $content = @file_get_contents( $absolute ); if ( $content === false ) return null; if ( ! class_exists( 'Parsedown' ) ) { $vendor = dirname( __DIR__ ) . '/vendor/Parsedown.php'; if ( file_exists( $vendor ) ) { require_once $vendor; } } if ( ! class_exists( 'Parsedown' ) ) { // Last-resort fallback: HTML-escape and render in
. Honest if ugly.
            return '
' . esc_html( $content ) . '
'; } $pd = new Parsedown(); if ( method_exists( $pd, 'setSafeMode' ) ) { $pd->setSafeMode( true ); } return $pd->text( $content ); } private static function inline_styles(): string { // Scoped styles. Keeping it inline so the Board ships in one file. return ''; } }