*/ public static function provideMaliciousHtml(): Traversable { yield 'script tag' => ['Hello', ' ['', 'onerror']; yield 'JavaScript URL' => ['click', 'href="javascript:']; yield 'style tag' => ['Hello', ' ['click', 'href="data:']; yield 'data:text/html URL in iframe' => ['', 'src="data:']; yield 'data:text/html URL in image' => ['', 'src="data:']; yield 'data:image URL in link' => ['click', 'href="data:']; } public static function test_sanitizeHTML_whenSafeHtml_keepsAllowedTags(): void { $result = FreshRSS_SimplePieCustom::sanitizeHTML('

Hello world

'); self::assertSame('

Hello world

', $result); } public static function test_sanitizeHTML_whenUnsafeAttributeIsRemoved_keepsAllowedTag(): void { self::assertSame('Hello
', FreshRSS_SimplePieCustom::sanitizeHTML('Hello
')); self::assertSame('Hello
', FreshRSS_SimplePieCustom::sanitizeHTML('Hello
', maxLength: 100)); } public static function test_sanitizeHTML_whenMaxLengthIsZeroOrNegative_returnsEmptyString(): void { self::assertSame('', FreshRSS_SimplePieCustom::sanitizeHTML('

Hello world

', maxLength: 0)); self::assertSame('', FreshRSS_SimplePieCustom::sanitizeHTML('

Hello world

', maxLength: -1)); } public static function test_sanitizeHTML_whenResultFitsWithinMaxLength_isUnaffected(): void { $result = FreshRSS_SimplePieCustom::sanitizeHTML('

Hello world

', maxLength: 100); self::assertSame('

Hello world

', $result); } public static function test_sanitizeHTML_whenUnsafePrefixExceedsMaxLength_keepsSafeText(): void { self::assertSame('text', FreshRSS_SimplePieCustom::sanitizeHTML('

text', maxLength: 5)); } /** * Sanitizing can grow a truncated fragment (e.g. `

He` gets sanitized into `

He

`) */ #[DataProvider('provideMaxLengthInputs')] public static function test_sanitizeHTML_whenMaxLengthForcesReSanitizing_terminatesWithinBound(string $input, int $maxLength): void { $result = FreshRSS_SimplePieCustom::sanitizeHTML($input, maxLength: $maxLength); self::assertLessThanOrEqual($maxLength, strlen($result)); } /** @return Traversable */ public static function provideMaxLengthInputs(): Traversable { yield 'unclosed tag' => ['

Hello world

', 5]; yield 'repeated short tags' => [str_repeat('x ', 50), 20]; yield 'single-character budget' => ['

Hello world

', 1]; } #[DataProvider('provideIncompleteTagsOrEntities')] public static function test_sanitizeHTML_Cases(string $input, int $maxLength, string $expected): void { $result = FreshRSS_SimplePieCustom::sanitizeHTML($input, maxLength: $maxLength); self::assertLessThanOrEqual($maxLength, strlen($result)); self::assertSame(trim($expected), trim($result)); } /** @return Traversable */ public static function provideIncompleteTagsOrEntities(): Traversable { yield 'unclosed tag not fitting' => ['Hello World', 31, 'Hello']; yield 'unclosed entity' => ['Hello…', 9, 'Hello']; yield 'double unclosed tag' => [' x', 10, 'x']; yield 'triple unclosed tag' => [' y', 20, 'y']; } /** * Inline `data:image/...` URIs are inert in `` and widely used by feeds, so they must survive * while `data:` is blocked on navigational/framing attributes (see provideMaliciousHtml()). */ public static function test_sanitizeHTML_whenDataImageUri_keepsItOnImageElements(): void { $png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; self::assertSame('', FreshRSS_SimplePieCustom::sanitizeHTML('')); self::assertStringContainsString('poster="' . $png . '"', FreshRSS_SimplePieCustom::sanitizeHTML('')); } public static function test_sanitizeHTML_whenDisallowedScheme_prefixesUnsafe(): void { $result = FreshRSS_SimplePieCustom::sanitizeHTML('click'); self::assertStringContainsString('href="unsafe:data:text/html,x"', $result); } }