diff --git a/components/Formatter.php b/components/Formatter.php index 68e6d617..5ae6e240 100644 --- a/components/Formatter.php +++ b/components/Formatter.php @@ -28,6 +28,12 @@ class Formatter extends \yii\i18n\Formatter 'ul', 'ol', 'li', 'img' ], + 'AllowedAttributes' => [ + 'a' => ['href', 'title', 'target', 'rel'], + 'img' => ['src', 'alt', 'width', 'height'], + '*' => ['id', 'class'] + ], + 'TargetNoopener' => true, ], 'Attr' => [ 'EnableID' => true, diff --git a/tests/unit/FormatterTest.php b/tests/unit/FormatterTest.php new file mode 100644 index 00000000..5fc42ef0 --- /dev/null +++ b/tests/unit/FormatterTest.php @@ -0,0 +1,173 @@ +formatter = new Formatter(); + } + + /** + * Test that the HTML.TargetNoopener configuration is properly set + */ + public function testTargetNoopenerConfigurationExists() + { + $config = $this->formatter->purifierConfig; + + // Verify that TargetNoopener is enabled in the HTML configuration + $this->assertArrayHasKey('HTML', $config); + $this->assertArrayHasKey('TargetNoopener', $config['HTML']); + $this->assertTrue($config['HTML']['TargetNoopener']); + + // Verify that anchor tags allow target and rel attributes + $this->assertArrayHasKey('AllowedAttributes', $config['HTML']); + $this->assertArrayHasKey('a', $config['HTML']['AllowedAttributes']); + $this->assertContains('target', $config['HTML']['AllowedAttributes']['a']); + $this->assertContains('rel', $config['HTML']['AllowedAttributes']['a']); + } + + /** + * Test that the purifier configuration includes all expected security settings + */ + public function testSecurityConfiguration() + { + $config = $this->formatter->purifierConfig; + + // Verify HTML configuration + $this->assertArrayHasKey('HTML', $config); + $htmlConfig = $config['HTML']; + + // Should have allowed elements + $this->assertArrayHasKey('AllowedElements', $htmlConfig); + $this->assertIsArray($htmlConfig['AllowedElements']); + + // Should include anchor tags for links + $this->assertContains('a', $htmlConfig['AllowedElements']); + + // Should have allowed attributes configured + $this->assertArrayHasKey('AllowedAttributes', $htmlConfig); + $this->assertArrayHasKey('a', $htmlConfig['AllowedAttributes']); + + // Anchor tags should allow href, target, and rel attributes + $this->assertContains('href', $htmlConfig['AllowedAttributes']['a']); + $this->assertContains('target', $htmlConfig['AllowedAttributes']['a']); + $this->assertContains('rel', $htmlConfig['AllowedAttributes']['a']); + + // Should have TargetNoopener enabled for security + $this->assertArrayHasKey('TargetNoopener', $htmlConfig); + $this->assertTrue($htmlConfig['TargetNoopener']); + + // Verify Attr configuration + $this->assertArrayHasKey('Attr', $config); + $this->assertArrayHasKey('EnableID', $config['Attr']); + $this->assertTrue($config['Attr']['EnableID']); + } + + /** + * Test that HTMLPurifier adds rel="noopener noreferrer" to links with target="_blank" + * This test verifies the security feature works as expected + */ + public function testTargetBlankLinksGetNoopenerRel() + { + // Skip this test if HTMLPurifier is not available (e.g., in CI without full dependencies) + if (!class_exists('\HTMLPurifier')) { + $this->markTestSkipped('HTMLPurifier is not available'); + } + + // Test HTML with a link that has target="_blank" + $htmlInput = 'External Link'; + + try { + // Process through HTMLPurifier with the formatter's configuration + $result = \yii\helpers\HtmlPurifier::process($htmlInput, $this->formatter->purifierConfig); + + // Should contain rel="noopener noreferrer" + $this->assertStringContainsString('rel="noopener noreferrer"', $result); + $this->assertStringContainsString('target="_blank"', $result); + $this->assertStringContainsString('href="https://example.com"', $result); + } catch (\Error $e) { + // If there's an error due to missing dependencies, mark test as skipped + $this->markTestSkipped('HTMLPurifier dependencies not available: ' . $e->getMessage()); + } + } + + /** + * Test that links without target="_blank" don't get rel attributes added + */ + public function testLinksWithoutTargetBlankUnaffected() + { + // Skip this test if HTMLPurifier is not available + if (!class_exists('\HTMLPurifier')) { + $this->markTestSkipped('HTMLPurifier is not available'); + } + + // Test HTML with a normal link (no target attribute) + $htmlInput = 'Normal Link'; + + try { + // Process through HTMLPurifier with the formatter's configuration + $result = \yii\helpers\HtmlPurifier::process($htmlInput, $this->formatter->purifierConfig); + + // Should NOT contain rel="noopener noreferrer" + $this->assertStringNotContainsString('rel="noopener noreferrer"', $result); + $this->assertStringContainsString('href="https://example.com"', $result); + } catch (\Error $e) { + $this->markTestSkipped('HTMLPurifier dependencies not available: ' . $e->getMessage()); + } + } + + /** + * Test that asMarkdown method properly processes links with target="_blank" + */ + public function testMarkdownProcessingWithTargetBlank() + { + try { + // Test a simple markdown to ensure the method works + $markdown = 'This is a [test link](https://example.com)'; + $result = $this->formatter->asMarkdown($markdown); + + // Should be wrapped in markdown div + $this->assertStringContainsString('