Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.12% covered (success)
95.12%
39 / 41
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SafeHtml
95.12% covered (success)
95.12%
39 / 41
71.43% covered (warning)
71.43%
5 / 7
10
0.00% covered (danger)
0.00%
0 / 1
 getHtmlSanitizer
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 createRandom
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 provideIndexes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 convert
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 minStringLength
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maxStringLength
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2namespace Apie\CommonValueObjects;
3
4use Apie\CommonValueObjects\Bridge\Symfony\AllowedCssInSpanSanitizer;
5use Apie\CommonValueObjects\Bridge\Symfony\YoutubeNoCookieSanitizer;
6use Apie\Core\Attributes\CmsSingleInput;
7use Apie\Core\Attributes\Description;
8use Apie\Core\Attributes\FakeMethod;
9use Apie\Core\Attributes\ProvideIndex;
10use Apie\Core\ValueObjects\Exceptions\InvalidStringForValueObjectException;
11use Apie\Core\ValueObjects\Interfaces\LengthConstraintStringValueObjectInterface;
12use Apie\Core\ValueObjects\Interfaces\StringValueObjectInterface;
13use Apie\Core\ValueObjects\IsStringValueObject;
14use Apie\CountWords\WordCounter;
15use Faker\Generator;
16use ReflectionClass;
17use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
18use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
19
20#[FakeMethod('createRandom')]
21#[ProvideIndex('provideIndexes')]
22#[CmsSingleInput(['html'])]
23#[Description('a html input that removes unsafe HTML.')]
24final 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            '&nbsp;',
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}