Skip to content
74 changes: 74 additions & 0 deletions src/lib/Form/EventSubscriber/FixUrlProtocolListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\ContentForms\Form\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Extension\Core\EventListener\FixUrlProtocolListener as BaseFixUrlProtocolListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

/**
* @internal
*/
final class FixUrlProtocolListener implements EventSubscriberInterface
{
private ?string $defaultProtocol;

private BaseFixUrlProtocolListener $fixUrlProtocolListener;

/**
* @param string|null $defaultProtocol The URL scheme to add when there is none or null to not modify the data
*/
public function __construct(?string $defaultProtocol = 'http')
{
$this->defaultProtocol = $defaultProtocol;
$this->fixUrlProtocolListener = new BaseFixUrlProtocolListener($defaultProtocol);
}

public function onSubmit(FormEvent $event): void
{
$data = $event->getData();
if (null === $this->defaultProtocol || empty($data) || !\is_string($data)) {
return;
}

$protocol = explode(':', $data)[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$protocol = explode(':', $data)[0];
$protocol = explode(':', $data, 2)[0];

if ($this->hasAuthority($protocol) && $this->hasAuthority($this->defaultProtocol)) {
$this->fixUrlProtocolListener->onSubmit($event);

return;
}

if (!$this->hasAuthority($protocol) && preg_match('~^(?:[/.]|[\w+.-]+:|[^:/?@#]++@)~', $data)) {
return;
}

if ($this->hasAuthority($this->defaultProtocol)) {
$schemaSeparator = '://';
$regExp = '~^(?:[/.]|[\w+.-]+//|[^:/?@#]++@)~';
} else {
$schemaSeparator = ':';
$regExp = '~^[\w+.-]+:~'; // allowing emails for non-http/https/file
}

if (!preg_match($regExp, $data)) {
$event->setData($this->defaultProtocol . $schemaSeparator . $data);
}
}

private function hasAuthority(string $protocol): bool
{
return !in_array($protocol, ['mailto', 'tel'], true);
}

public static function getSubscribedEvents(): array
{
return [FormEvents::SUBMIT => 'onSubmit'];
}
}
4 changes: 4 additions & 0 deletions src/lib/Form/Type/FieldType/UrlFieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Ibexa\ContentForms\Form\Type\FieldType;

use Ibexa\ContentForms\FieldType\DataTransformer\FieldValueTransformer;
use Ibexa\ContentForms\Form\EventSubscriber\FixUrlProtocolListener;
use Ibexa\Contracts\Core\Repository\FieldTypeService;
use JMS\TranslationBundle\Annotation\Desc;
use Symfony\Component\Form\AbstractType;
Expand Down Expand Up @@ -49,6 +50,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
[
'label' => /** @Desc("URL") */ 'content.field_type.ezurl.link',
'required' => $options['required'],
'default_protocol' => null,
]
)
->add(
Expand All @@ -60,6 +62,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
]
)
->addModelTransformer(new FieldValueTransformer($this->fieldTypeService->getFieldType('ezurl')));

$builder->get('link')->addEventSubscriber(new FixUrlProtocolListener());
}

public function configureOptions(OptionsResolver $resolver)
Expand Down
104 changes: 104 additions & 0 deletions tests/lib/Form/EventSubscriber/FixUrlProtocolListenerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\ContentForms\Form\EventSubscriber;

use Ibexa\ContentForms\Form\EventSubscriber\FixUrlProtocolListener;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormInterface;

final class FixUrlProtocolListenerTest extends TestCase
{
private const DOMAIN = 'example.com';
private const MAIL = 'foo@' . self::DOMAIN;
private const TEL = '+123456';
private const URL_HTTP = 'http://' . self::DOMAIN;
private const URL_HTTPS = 'https://' . self::DOMAIN;
private const URL_MAILTO = 'mailto:' . self::MAIL;
private const URL_RELATIVE = '/foo/bar/';
private const URL_SFTP = 'sftp://' . self::DOMAIN;
private const URL_TEL = 'tel:' . self::TEL;

/**
* @dataProvider provideUrlCases
*
* @param string|null $inputData
* @param string|null $expectedData
* @param string $defaultProtocol
*/
public function testUrlProtocolHandling(?string $inputData, ?string $expectedData, ?string $defaultProtocol = 'http'): void
{
$form = $this->createMock(FormInterface::class);
$listener = new FixUrlProtocolListener($defaultProtocol);

$event = new FormEvent($form, $inputData);

$listener->onSubmit($event);

self::assertSame($expectedData, $event->getData());
}

/**
* @return iterable<string, array{
* 0: string|null,
* 1: string|null
* }>
*/
public static function provideUrlCases(): iterable
{
return [
'adds http when protocol missing' => [
self::DOMAIN,
self::URL_HTTP,
],
'does not modify https url' => [
self::URL_HTTPS,
self::URL_HTTPS,
],
'does not modify http url' => [
self::URL_HTTP,
self::URL_HTTP,
],
'keep relative url with leading / intact' => [
self::URL_RELATIVE,
self::URL_RELATIVE,
],
'keeps ftp intact' => [
self::URL_SFTP,
self::URL_SFTP,
],
'keeps tel intact' => [
self::URL_TEL,
self::URL_TEL,
],
'adds default tel' => [
self::TEL,
self::URL_TEL,
'tel',
],
'keeps mailto intact' => [
self::URL_MAILTO,
self::URL_MAILTO,
],
'adds default mailto' => [
self::MAIL,
self::URL_MAILTO,
'mailto',
],
'does nothing when link is empty string' => [
'',
'',
],
'does nothing when data is null' => [
null,
null,
],
];
}
}
Loading