Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.12% |
39 / 41 |
|
71.43% |
5 / 7 |
CRAP | |
0.00% |
0 / 1 |
SafeHtml | |
95.12% |
39 / 41 |
|
71.43% |
5 / 7 |
10 | |
0.00% |
0 / 1 |
getHtmlSanitizer | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
2 | |||
createRandom | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
provideIndexes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validate | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
convert | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
minStringLength | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
maxStringLength | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | namespace Apie\CommonValueObjects; |
3 | |
4 | use Apie\CommonValueObjects\Bridge\Symfony\AllowedCssInSpanSanitizer; |
5 | use Apie\CommonValueObjects\Bridge\Symfony\YoutubeNoCookieSanitizer; |
6 | use Apie\Core\Attributes\CmsSingleInput; |
7 | use Apie\Core\Attributes\Description; |
8 | use Apie\Core\Attributes\FakeMethod; |
9 | use Apie\Core\Attributes\ProvideIndex; |
10 | use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException; |
11 | use Apie\Core\ValueObjects\Interfaces\LengthConstraintStringValueObjectInterface; |
12 | use Apie\Core\ValueObjects\Interfaces\StringValueObjectInterface; |
13 | use Apie\Core\ValueObjects\IsStringValueObject; |
14 | use Apie\CountWords\WordCounter; |
15 | use Faker\Generator; |
16 | use ReflectionClass; |
17 | use Symfony\Component\HtmlSanitizer\HtmlSanitizer; |
18 | use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; |
19 | |
20 | #[FakeMethod('createRandom')] |
21 | #[ProvideIndex('provideIndexes')] |
22 | #[CmsSingleInput(['html'])] |
23 | #[Description('a html input that removes unsafe HTML.')] |
24 | final class SafeHtml implements StringValueObjectInterface, LengthConstraintStringValueObjectInterface |
25 | { |
26 | use IsStringValueObject; |
27 | |
28 | private static HtmlSanitizer $htmlSanitizer; |
29 | |
30 | private static function getHtmlSanitizer(): HtmlSanitizer |
31 | { |
32 | if (!isset(self::$htmlSanitizer)) { |
33 | $config = (new HtmlSanitizerConfig()) |
34 | ->allowSafeElements() |
35 | ->allowElement('b') |
36 | ->allowElement('i') |
37 | ->dropAttribute('id', []) |
38 | ->forceAttribute('a', 'rel', 'noopener noreferrer') |
39 | ->forceAttribute('a', 'target', '_blank') |
40 | ->allowLinkSchemes(['https', 'http', 'mailto']) |
41 | ->allowRelativeLinks(false) |
42 | ->withMaxInputLength(1024 * 1024 * 21) // character limit is 20mb, but could be truncated |
43 | ->dropElement('html') |
44 | ->dropElement('head') |
45 | ->dropElement('script') |
46 | ->dropElement('style') |
47 | ->allowAttribute('style', 'span') |
48 | ->withAttributeSanitizer(new YoutubeNoCookieSanitizer()) |
49 | ->allowElement('iframe', ['src', 'referrerpolicy', 'width', 'title', 'height', 'frameborder', 'allowfullscreen', 'allow']) |
50 | ->withAttributeSanitizer(new AllowedCssInSpanSanitizer()); |
51 | self::$htmlSanitizer = new HtmlSanitizer( |
52 | $config |
53 | ); |
54 | } |
55 | |
56 | return self::$htmlSanitizer; |
57 | } |
58 | |
59 | public static function createRandom(Generator $factory): self |
60 | { |
61 | $randomHtmls = ['div', 'h1', 'h2', 'h3', 'p']; |
62 | $string = '<div>'; |
63 | $counter = $factory->numberBetween(1, 3); |
64 | for ($i = 0; $i < $counter; $i++) { |
65 | $tag = $factory->randomElement($randomHtmls); |
66 | $string .= '<' . $tag . '>' . $factory->text(50) . '</' . $tag . '>'; |
67 | } |
68 | $string .= '</div>'; |
69 | return new self($string); |
70 | } |
71 | |
72 | /** |
73 | * @return array<string, int> |
74 | */ |
75 | public function provideIndexes(): array |
76 | { |
77 | return WordCounter::countFromString(strip_tags(str_replace('<', ' <', $this->internal))); |
78 | } |
79 | |
80 | public static function validate(string $input): void |
81 | { |
82 | if (strlen($input) > (1024 * 1024 * 20)) { |
83 | throw new InvalidStringForValueObjectException($input, new ReflectionClass(__CLASS__)); |
84 | } |
85 | } |
86 | |
87 | protected function convert(string $input): string |
88 | { |
89 | return str_replace( |
90 | ' ', |
91 | ' ', |
92 | self::getHtmlSanitizer()->sanitizeFor('body', $input) |
93 | ); |
94 | } |
95 | |
96 | public static function minStringLength(): int |
97 | { |
98 | return 0; |
99 | } |
100 | |
101 | public static function maxStringLength(): int |
102 | { |
103 | return 1024 * 1024 * 20; |
104 | } |
105 | } |