Skip to content

Commit

Permalink
Added relative internal link validation option for link fields
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed Sep 13, 2024
1 parent 5cd2c67 commit e8fca69
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config/schema/stanford_fields.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ field.widget.settings.localist_url:
select_distinct:
type: boolean
label: 'Select Distinct'

field.field.*.third_party.stanford_fields:
type: config_entity
label: "Stanford Fields third party settings"
mapping:
force_relative:
type: boolean
label: "Force relative internal links"
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Drupal\stanford_fields\Plugin\Validation\Constraint;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
use Drupal\Core\Validation\Attribute\Constraint;

/**
* Checks that the submitted value is a unique integer.
*/
#[Constraint(
id: 'relative_internal_link',
label: new TranslatableMarkup('Relative Internal Link', [], ['context' => 'Validation'])
)]
class RelativeLinkFieldItemConstraint extends SymfonyConstraint {

public $absoluteLink = 'Please use relative links that start with "/" for paths on this site.';

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Drupal\stanford_fields\Plugin\Validation\Constraint;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

/**
* Validates the Menu link item constraint.
*/
class RelativeLinkFieldItemConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {

/**
* Current request.
*
* @var \Drupal\path_alias\AliasManagerInterface
*/
protected $request;

/**
* {@inheritDoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('request_stack'));
}

/**
* Validation constructor.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* Current request stack.
*/
public function __construct(RequestStack $request_stack) {
$this->request = $request_stack->getCurrentRequest();
}

/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
/** @var \Drupal\Core\Field\FieldItemListInterface $value */
$link_uri = $value->get(0)?->get('uri')?->getString();
if ($link_uri && str_contains($link_uri, $this->request->getSchemeAndHttpHost())) {
$this->context->addViolation($constraint->absoluteLink);
}
}

}
54 changes: 54 additions & 0 deletions stanford_fields.module
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,61 @@
* stanford_fields.module
*/

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\FieldConfigInterface;

/**
* Implements hook_form_BASE_FORM_ID_alter().
*/
function stanford_fields_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
/** @var \Drupal\field\FieldConfigInterface $field_config */
$field_config = $form_state->get('field_config');
if ($field_config->getType() == 'link') {
$form['settings']['force_relative'] = [
'#type' => 'checkbox',
'#title' => t('Force relative internal links'),
'#default_value' => $field_config->getThirdPartySetting('stanford_fields', 'force_relative'),
'#states' => [
'invisible' => [
':input[name="settings[link_type]"]' => ['value' => '16'],
],
],
];
$form['#entity_builders'][] = 'stanford_fields_form_field_config_entity_builder';
}
}

/**
* Field config form submission to save the third party settings.
*
* @param string $entity_type
* Entity type ID.
* @param \Drupal\field\FieldConfigInterface $entity
* Submitted entity object.
* @param array $form
* Submitted form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Submitted form state.
*/
function stanford_fields_form_field_config_entity_builder(string $entity_type, FieldConfigInterface $entity, array &$form, FormStateInterface $form_state) {
if ($form_state->getValue(['settings', 'force_relative'])) {
$entity->setThirdPartySetting('stanford_fields', 'force_relative', TRUE);
return;
}
$entity->unsetThirdPartySetting('stanford_fields', 'force_relative');
}

/**
* Implements hook_entity_bundle_field_info_alter().
*/
function stanford_fields_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
foreach ($fields as $field) {
if ($field->getType() == 'link' && $field->getThirdPartySetting('stanford_fields', 'force_relative')) {
$field->addConstraint('relative_internal_link', []);
}
}
}

/**
* Implements hook_form_FORM_ID_alter().
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace Drupal\Tests\stanford_fields\Unit\Plugin\Validation\Constraint;

use Drupal\Core\DependencyInjection\Container;
use Drupal\Core\Validation\ExecutionContext;
use Drupal\Core\Validation\TranslatorInterface;
use Drupal\stanford_fields\Plugin\Validation\Constraint\RelativeLinkFieldItemConstraint;
use Drupal\stanford_fields\Plugin\Validation\Constraint\RelativeLinkFieldItemConstraintValidator;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
* @coversDefaultClass \Drupal\stanford_fields\Plugin\Validation\Constraint\RelativeLinkFieldItemConstraintValidator
*/
class LinkFieldItemConstraintValidatorTest extends UnitTestCase {

/**
* Data provider for validator.
*
* @return array[]
*/
public function dataProvider() {
return [
['http://localhost', 'http://localhost/foo/bar', TRUE],
['http://localhost', '/foo/bar', FALSE],
['http://localhost', 'http://hostlocal/foo/bar', FALSE],
];
}

/**
* Tests the validate method.
*
* @dataProvider dataProvider
*/
public function testValidation($currentDomain, $linkUrl, $shouldHaveViolations) {
// Create mocks for the services and dependencies.
$request_stack = $this->createMock(RequestStack::class);

$field_item_list = $this->createMock(FieldItemListInterface::class);
$field_item = $this->createMock(FieldItemInterface::class);
$request = $this->createMock(Request::class);

// Configure the request mock to return a specific scheme and host.
$request->method('getSchemeAndHttpHost')->willReturn($currentDomain);
$request_stack->method('getCurrentRequest')->willReturn($request);

// Configure the field item list mock to return a field item with a specific URI.
$field_item->method('get')->willReturnSelf();
$field_item->method('getString')->willReturn($linkUrl);
$field_item_list->method('get')->with(0)->willReturn($field_item);

$container = new Container();
$container->set('request_stack', $request_stack);

$validator = $this->createMock(ValidatorInterface::class);
$translator = $this->createMock(TranslatorInterface::class);
$translator->method('trans')->willReturnCallback(fn($message) => $message);
$context = new ExecutionContext($validator, NULL, $translator);

// Instantiate the validator and set the context.
$validator = TestRelativeLinkValidator::create($container);
$validator->initialize($context);

$constraint = new RelativeLinkFieldItemConstraint();
$context->setConstraint($constraint);

// Call the validate method.
$validator->validate($field_item_list, $constraint);
if ($shouldHaveViolations) {
$this->assertTrue($validator->hasViolation());
}
else {
$this->assertFalse($validator->hasViolation());
}
}

}

class TestRelativeLinkValidator extends RelativeLinkFieldItemConstraintValidator {

public function hasViolation() {
return count($this->context->getViolations()) > 0;
}

}

0 comments on commit e8fca69

Please sign in to comment.