// raw-html-ok: reason * * Typical justified uses: pre-escaped ARIA attributes from navActive(), hardcoded * HTML-safe ternaries (e.g. 'aria-current="page"'), or pre-built HTML fragments * from shared helpers. Each raw echo MUST carry the "raw-html-ok" marker. */ class ViewLayerOutputEscapingContractTest extends TestCase { use ProjectFileAssertionSupport; /** * Regex matching scanForRawEcho('pages'); self::assertSame( [], $violations, "Raw scanForRawEcho('templates'); self::assertSame( [], $violations, "Raw projectRootPath(); $modulesDir = $root . '/modules'; if (!is_dir($modulesDir)) { self::markTestSkipped('modules/ directory missing.'); } $allViolations = []; foreach (scandir($modulesDir) ?: [] as $entry) { if ($entry === '.' || $entry === '..' || !is_dir($modulesDir . '/' . $entry)) { continue; } foreach (['pages', 'templates'] as $subDir) { $rel = 'modules/' . $entry . '/' . $subDir; if (is_dir($root . '/' . $rel)) { $allViolations = array_merge($allViolations, $this->scanForRawEcho($rel)); } } } sort($allViolations); self::assertSame( [], $allViolations, "Raw */ private function scanForRawEcho(string $relativeDirectory): array { $root = $this->projectRootPath(); $basePath = $root . '/' . $relativeDirectory; $this->assertDirectoryExists($basePath, 'Directory not found: ' . $relativeDirectory); $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($basePath), ); $violations = []; /** @var \SplFileInfo $file */ foreach ($iterator as $file) { if (!$file->isFile() || $file->getExtension() !== 'phtml') { continue; } $content = file_get_contents($file->getPathname()); $this->assertNotFalse($content, 'Could not read file: ' . $file->getPathname()); $lines = explode("\n", $content); foreach ($lines as $lineNumber => $line) { if (!preg_match(self::ECHO_PATTERN, $line)) { continue; } if (str_contains($line, self::EXCEPTION_MARKER)) { continue; } $relativePath = str_replace($root . '/', '', $file->getPathname()); $violations[] = sprintf('%s:%d: %s', $relativePath, $lineNumber + 1, trim($line)); } } sort($violations); return $violations; } }