<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HtmlSanitizer\Tests;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use Symfony\Component\HtmlSanitizer\Visitor\AttributeSanitizer\AttributeSanitizerInterface;

class HtmlSanitizerCustomTest extends TestCase
{
    public function testSanitizeForHead()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
        ;

        $this->assertSame(
            '',
            (new HtmlSanitizer($config))->sanitizeFor('head', '<div style="width: 100px">Hello world</div>')
        );
    }

    public function testSanitizeForTextarea()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
        ;

        $this->assertSame(
            '&lt;div style&#61;&#34;width: 100px&#34;&gt;Hello&lt;/div&gt; world',
            (new HtmlSanitizer($config))->sanitizeFor('textarea', '<div style="width: 100px">Hello</div> world')
        );
    }

    public function testSanitizeForTitle()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
        ;

        $this->assertSame(
            '&lt;div style&#61;&#34;width: 100px&#34;&gt;Hello&lt;/div&gt; world',
            (new HtmlSanitizer($config))->sanitizeFor('title', '<div style="width: 100px">Hello</div> world')
        );
    }

    public function testSanitizeDeepNestedString()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
        ;

        $this->assertNotEmpty($this->sanitize($config, str_repeat('<div>T', 10000)));
    }

    public function testSanitizeNullByte()
    {
        $this->assertSame('Null byte�', $this->sanitize(new HtmlSanitizerConfig(), "Null byte\0"));
        $this->assertSame('Null byte�', $this->sanitize(new HtmlSanitizerConfig(), 'Null byte&#0;'));
    }

    public function testSanitizeDefaultBody()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
        ;

        $this->assertSame(
            '<div>Hello</div> world',
            (new HtmlSanitizer($config))->sanitize('<div style="width: 100px">Hello</div> world')
        );
    }

    public function testAllowElement()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
        ;

        $this->assertSame(
            '<div>Hello</div> world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            ' world',
            $this->sanitize($config, '<span style="width: 100px">Hello</span> world')
        );
    }

    public function testAllowElementWithAttribute()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div', ['style'])
        ;

        $this->assertSame(
            '<div style="width: 100px">Hello</div> world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            ' world',
            $this->sanitize($config, '<span style="width: 100px">Hello</span> world')
        );
    }

    public function testBlockElement()
    {
        $config = (new HtmlSanitizerConfig())
            ->blockElement('div')
        ;

        $this->assertSame(
            'Hello world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            ' world',
            $this->sanitize($config, '<span style="width: 100px">Hello</span> world')
        );
    }

    public function testDropElement()
    {
        $config = (new HtmlSanitizerConfig())
            ->blockElement('div')
            ->dropElement('div')
        ;

        $this->assertSame(
            ' world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            ' world',
            $this->sanitize($config, '<span style="width: 100px">Hello</span> world')
        );
    }

    public function testAllowAttributeOnElement()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
            ->allowElement('span')
            ->allowAttribute('style', ['div'])
        ;

        $this->assertSame(
            '<div style="width: 100px">Hello</div> world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            '<span>Hello</span> world',
            $this->sanitize($config, '<span style="width: 100px">Hello</span> world')
        );
    }

    public function testAllowAttributeEverywhere()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
            ->allowElement('span')
            ->allowAttribute('style', '*')
        ;

        $this->assertSame(
            '<div style="width: 100px">Hello</div> world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            '<span style="width: 100px">Hello</span> world',
            $this->sanitize($config, '<span style="width: 100px">Hello</span> world')
        );
    }

    public function testDropAttributeOnElement()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
            ->allowElement('span')
            ->allowAttribute('style', '*')
            ->dropAttribute('style', 'span')
        ;

        $this->assertSame(
            '<div style="width: 100px">Hello</div> world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            '<span>Hello</span> world',
            $this->sanitize($config, '<span style="width: 100px">Hello</span> world')
        );
    }

    public function testDropAttributeEverywhere()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
            ->allowElement('span')
            ->allowAttribute('style', '*')
            ->dropAttribute('style', '*')
        ;

        $this->assertSame(
            '<div>Hello</div> world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            '<span>Hello</span> world',
            $this->sanitize($config, '<span style="width: 100px">Hello</span> world')
        );
    }

    public function testForceAttribute()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div')
            ->allowElement('img', '*')
            ->allowElement('a', ['href'])
            ->forceAttribute('a', 'rel', 'noopener noreferrer')
            ->forceAttribute('img', 'loading', 'lazy')
        ;

        $this->assertSame(
            '<img title="My image" src="https://example.com/image.png" loading="lazy" />',
            $this->sanitize($config, '<img title="My image" src="https://example.com/image.png" loading="eager" onerror="alert(\'1234\')" />')
        );

        $this->assertSame(
            '<a rel="noopener noreferrer">Hello</a> world',
            $this->sanitize($config, '<a>Hello</a> world')
        );

        $this->assertSame(
            '<a href="https://symfony.com" rel="noopener noreferrer">Hello</a> world',
            $this->sanitize($config, '<a href="https://symfony.com">Hello</a> world')
        );

        $this->assertSame(
            '<div>Hello</div> world',
            $this->sanitize($config, '<div style="width: 100px">Hello</div> world')
        );

        $this->assertSame(
            '<a href="https://symfony.com" rel="noopener noreferrer">Hello</a> world',
            $this->sanitize($config, '<a href="https://symfony.com" rel="noopener">Hello</a> world')
        );
    }

    public function testForceHttps()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('a', ['href'])
            ->forceHttpsUrls()
        ;

        $this->assertSame(
            '<a href="https://symfony.com">Hello world</a>',
            $this->sanitize($config, '<a href="http://symfony.com">Hello world</a>')
        );

        $this->assertSame(
            '<a href="https://symfony.com">Hello world</a>',
            $this->sanitize($config, '<a href="https://symfony.com">Hello world</a>')
        );

        $this->assertSame(
            '<a>Hello world</a>',
            $this->sanitize($config, '<a href="/index.php">Hello world</a>')
        );
    }

    public function testAllowLinksSchemes()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('a', ['href'])
            ->allowLinkSchemes(['https'])
        ;

        $this->assertSame(
            '<a href="https://trusted.com">Hello world</a>',
            $this->sanitize($config, '<a href="https://trusted.com">Hello world</a>')
        );

        $this->assertSame(
            '<a>Hello world</a>',
            $this->sanitize($config, '<a href="mailto:galopintitouan@gmail.com">Hello world</a>')
        );
    }

    public function testAllowLinksHosts()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('a', ['href'])
            ->allowLinkHosts(['trusted.com'])
        ;

        $this->assertSame(
            '<a href="https://trusted.com">Hello world</a>',
            $this->sanitize($config, '<a href="https://trusted.com">Hello world</a>')
        );

        $this->assertSame(
            '<a>Hello world</a>',
            $this->sanitize($config, '<a href="https://untrusted.com">Hello world</a>')
        );
    }

    public function testAreaUsesLinkPolicy()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('area', ['href'])
            ->allowLinkHosts(['trusted.com'])
        ;

        $this->assertSame(
            '<area href="https://trusted.com" />',
            $this->sanitize($config, '<area href="https://trusted.com">')
        );

        $this->assertSame(
            '<area />',
            $this->sanitize($config, '<area href="https://untrusted.com">')
        );
    }

    public function testAllowLinksRelative()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('a', ['href'])
            ->allowRelativeLinks()
        ;

        $this->assertSame(
            '<a href="/index.php">Hello world</a>',
            $this->sanitize($config, '<a href="/index.php">Hello world</a>')
        );

        $this->assertSame(
            '<a href="https://symfony.com">Hello world</a>',
            $this->sanitize($config, '<a href="https://symfony.com">Hello world</a>')
        );
    }

    public function testAllowMediaSchemes()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('img', ['src'])
            ->allowMediaSchemes(['https'])
        ;

        $this->assertSame(
            '<img src="https://trusted.com" />',
            $this->sanitize($config, '<img src="https://trusted.com" />')
        );

        $this->assertSame(
            '<img />',
            $this->sanitize($config, '<img src="http://trusted.com" />')
        );

        $this->assertSame(
            '<img />',
            $this->sanitize($config, '<img src="/image.png" />')
        );
    }

    public function testAllowMediasHosts()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('img', ['src'])
            ->allowMediaHosts(['trusted.com'])
        ;

        $this->assertSame(
            '<img src="https://trusted.com" />',
            $this->sanitize($config, '<img src="https://trusted.com" />')
        );

        $this->assertSame(
            '<img />',
            $this->sanitize($config, '<img src="https://untrusted.com" />')
        );

        $this->assertSame(
            '<img />',
            $this->sanitize($config, '<img src="/image.png" />')
        );
    }

    public function testAllowMediasRelative()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('img', ['src'])
            ->allowRelativeMedias()
        ;

        $this->assertSame(
            '<img src="/image.png" />',
            $this->sanitize($config, '<img src="/image.png" />')
        );

        $this->assertSame(
            '<img src="https://trusted.com" />',
            $this->sanitize($config, '<img src="https://trusted.com" />')
        );
    }

    public function testActionAttributeIsSanitized()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('form', ['action'])
        ;

        $this->assertSame(
            '<form>Hello world</form>',
            $this->sanitize($config, '<form action="javascript:alert(1)">Hello world</form>')
        );

        $this->assertSame(
            '<form>Hello world</form>',
            $this->sanitize($config, '<form action="data:text/html,foo">Hello world</form>')
        );

        $this->assertSame(
            '<form action="https://symfony.com">Hello world</form>',
            $this->sanitize($config, '<form action="https://symfony.com">Hello world</form>')
        );
    }

    public function testFormactionAttributeIsSanitized()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('button', ['formaction'])
            ->allowElement('input', ['type', 'formaction'])
        ;

        $this->assertSame(
            '<button>Submit</button>',
            $this->sanitize($config, '<button formaction="javascript:alert(1)">Submit</button>')
        );

        $this->assertSame(
            '<input type="image" />',
            $this->sanitize($config, '<input type="image" formaction="javascript:alert(1)">')
        );

        $this->assertSame(
            '<button formaction="https://symfony.com">Submit</button>',
            $this->sanitize($config, '<button formaction="https://symfony.com">Submit</button>')
        );
    }

    public function testPosterAttributeIsSanitized()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('video', ['poster'])
        ;

        $this->assertSame(
            '<video>Hello world</video>',
            $this->sanitize($config, '<video poster="javascript:alert(1)">Hello world</video>')
        );

        $this->assertSame(
            '<video poster="https://symfony.com/poster.jpg">Hello world</video>',
            $this->sanitize($config, '<video poster="https://symfony.com/poster.jpg">Hello world</video>')
        );
    }

    public function testCiteAttributeIsSanitized()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('blockquote', ['cite'])
            ->allowElement('q', ['cite'])
        ;

        $this->assertSame(
            '<blockquote>Hello world</blockquote>',
            $this->sanitize($config, '<blockquote cite="javascript:alert(1)">Hello world</blockquote>')
        );

        $this->assertSame(
            '<q cite="https://symfony.com">Hello world</q>',
            $this->sanitize($config, '<q cite="https://symfony.com">Hello world</q>')
        );
    }

    #[DataProvider('provideUrlAttributesSanitizedAcrossEmbeddingElements')]
    public function testUrlAttributesAreSanitizedAcrossEmbeddingElements(string $element, string $attribute, string $input, string $expected)
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement($element, [$attribute])
        ;

        $this->assertSame($expected, $this->sanitize($config, $input));
    }

    public static function provideUrlAttributesSanitizedAcrossEmbeddingElements(): iterable
    {
        yield 'object data javascript:' => [
            'object', 'data',
            '<object data="javascript:alert(1)">x</object>',
            '<object>x</object>',
        ];
        yield 'object data http kept' => [
            'object', 'data',
            '<object data="https://symfony.com/x">x</object>',
            '<object data="https://symfony.com/x">x</object>',
        ];
        yield 'applet codebase javascript:' => [
            'applet', 'codebase',
            '<applet codebase="javascript:alert(1)">x</applet>',
            '<applet>x</applet>',
        ];
        yield 'applet archive javascript:' => [
            'applet', 'archive',
            '<applet archive="javascript:alert(1)">x</applet>',
            '<applet>x</applet>',
        ];
        yield 'object codebase javascript:' => [
            'object', 'codebase',
            '<object codebase="javascript:alert(1)">x</object>',
            '<object>x</object>',
        ];
        yield 'iframe longdesc javascript:' => [
            'iframe', 'longdesc',
            '<iframe longdesc="javascript:alert(1)">x</iframe>',
            '<iframe>x</iframe>',
        ];
        yield 'img longdesc javascript:' => [
            'img', 'longdesc',
            '<img longdesc="javascript:alert(1)">',
            '<img />',
        ];
    }

    public function testMetaRefreshContentIsSanitized()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('meta', ['http-equiv', 'content'])
        ;

        $this->assertSame(
            '<meta http-equiv="refresh" />',
            (new HtmlSanitizer($config))->sanitizeFor('head', '<meta http-equiv="refresh" content="0; url=javascript:alert(1)">')
        );

        $this->assertSame(
            '<meta http-equiv="refresh" content="5; url&#61;https://symfony.com/" />',
            (new HtmlSanitizer($config))->sanitizeFor('head', '<meta http-equiv="refresh" content="5; url=https://symfony.com/">')
        );

        $this->assertSame(
            '<meta http-equiv="refresh" content="5" />',
            (new HtmlSanitizer($config))->sanitizeFor('head', '<meta http-equiv="refresh" content="5">')
        );
    }

    public function testCustomAttributeSanitizer()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div', ['data-attr'])
            ->withAttributeSanitizer(new class implements AttributeSanitizerInterface {
                public function getSupportedElements(): ?array
                {
                    return ['div'];
                }

                public function getSupportedAttributes(): ?array
                {
                    return ['data-attr'];
                }

                public function sanitizeAttribute(string $element, string $attribute, string $value, HtmlSanitizerConfig $config): ?string
                {
                    return 'new value';
                }
            })
        ;

        $this->assertSame(
            '<div data-attr="new value">Hello world</div>',
            $this->sanitize($config, '<div data-attr="old value">Hello world</div>')
        );
    }

    public function testWildcardAttributeSanitizerIsCalled()
    {
        $config = (new HtmlSanitizerConfig())
            ->allowElement('div', ['data-foo', 'data-bar'])
            ->withAttributeSanitizer(new class implements AttributeSanitizerInterface {
                public function getSupportedElements(): ?array
                {
                    return null;
                }

                public function getSupportedAttributes(): ?array
                {
                    return null;
                }

                public function sanitizeAttribute(string $element, string $attribute, string $value, HtmlSanitizerConfig $config): ?string
                {
                    return strrev($value);
                }
            })
        ;

        $this->assertSame(
            '<div data-foo="cba" data-bar="zyx">Hello world</div>',
            $this->sanitize($config, '<div data-foo="abc" data-bar="xyz">Hello world</div>')
        );
    }

    public function testMaxInputLengthIsAppliedToTextContext()
    {
        $config = (new HtmlSanitizerConfig());

        $input = str_repeat('A', $config->getMaxInputLength() + 100);
        $expected = str_repeat('A', $config->getMaxInputLength());

        $this->assertSame($expected, (new HtmlSanitizer($config))->sanitizeFor('textarea', $input));
        $this->assertSame($expected, (new HtmlSanitizer($config))->sanitizeFor('title', $input));
    }

    private function sanitize(HtmlSanitizerConfig $config, string $input): string
    {
        return (new HtmlSanitizer($config))->sanitize($input);
    }
}
