Commit 2682581d authored by Marcos Cano's avatar Marcos Cano 💬 Committed by Marcos Cano
Browse files

Issue #3283620 by marcoscano, e0ipso, rabbitlair, deviantintegral: Initial implementation

parent 0eb88f9e
Loading
Loading
Loading
Loading
+33 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Hooks for the url_friendly_options module.
 */

/**
 * @addtogroup hooks
 * @{
 */

/**
 * Allows modules to bypass url-friendly validation for a particular field.
 *
 * Modules implementing this hook should return TRUE if the field being saved
 * should bypass URL-friendly validation.
 *
 * @param string $field_name
 *   The name of the field storage being saved.
 * @param string $entity_type_id
 *   The entity type that holds the field storage being saved.
 */
function hook_url_friendly_options_bypass_field_validation($field_name, $entity_type_id) {
  if ($field_name === 'field_foo_bar' && $entity_type_id === 'node') {
    return TRUE;
  }
  return FALSE;
}

/**
 * @} End of "addtogroup hooks".
 */
+7 −0
Original line number Diff line number Diff line
name: URL-friendly Options
type: module
description: Enforces that keys in option list fields are URL-friendly.
core_version_requirement: ^9
package: Other
dependencies:
  - drupal:options
+57 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Install, update and uninstall functions for url_friendly_options.
 */

use Drupal\field\Entity\FieldStorageConfig;

/**
 * Implements hook_requirements().
 */
function url_friendly_options_requirements($phase) {
  $requirements = [];
  if ($phase == 'runtime') {
    $errors = [];
    foreach (FieldStorageConfig::loadMultiple() as $config) {
      /** @var \Drupal\field\FieldStorageConfigInterface $config */
      if ($config->getType() === 'list_string') {
        $field_name = $config->getName();
        $entity_type_id = $config->getTargetEntityTypeId();
        $context = [
          'field_name' => $field_name,
          'entity_type_id' => $entity_type_id,
        ];
        $hooks_return = \Drupal::moduleHandler()->invokeAll('url_friendly_options_bypass_field_validation', $context);
        if (in_array(TRUE, $hooks_return, TRUE)) {
          continue;
        }
        $allowed_values = $config->getSetting('allowed_values');
        $failed_validation = _url_friendly_options_check_allowed_values($allowed_values);
        if (!empty($failed_validation)) {
          $errors[] = "{$entity_type_id}.{$field_name}";
        }
      }
    }

    if (!empty($errors)) {
      $requirements['url_friendly_options'] = [
        'title' => t('URL-friendly option list keys'),
        'value' => t('<b>Non URL-friendly keys found.</b> The following fields nave non-compliant option keys: %fields', [
          '%fields' => implode(", ", $errors),
        ]),
        'description' => t('Edit the field storage definition and make sure all keys are using only alpha-numeric characters and hyphens.'),
        'severity' => REQUIREMENT_ERROR,
      ];
    }
    else {
      $requirements['url_friendly_options'] = [
        'title' => t('URL-friendly option list keys'),
        'value' => t('All option list keys are URL-friendly.'),
        'severity' => REQUIREMENT_OK,
      ];
    }
  }
  return $requirements;
}
+71 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Hook implementations for url_friendly_options.module.
 */

declare(strict_types=1);

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

/**
 * Implements hook_form_FORM_ID_alter() for field_storage_config_edit_form().
 */
function url_friendly_options_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Add our custom validate function.
  $form['#validate'][] = 'url_friendly_options_validate';
}

/**
 * Custom validation helper.
 *
 * @param array $form
 *   The form array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state object.
 */
function url_friendly_options_validate(array &$form, FormStateInterface $form_state) {
  $config = $form_state->get('field_config');
  if ($config instanceof FieldConfigInterface) {
    $field_name = $config->getFieldStorageDefinition()->getName();
    $entity_type_id = $config->getTargetEntityTypeId();
    $context = [
      'field_name' => $field_name,
      'entity_type_id' => $entity_type_id,
    ];
    $hooks_return = \Drupal::moduleHandler()->invokeAll('url_friendly_options_bypass_field_validation', $context);
    // If at least one module wants to bypass validation for this field, do so.
    if (in_array(TRUE, $hooks_return, TRUE)) {
      return;
    }
  }
  $allowed_values = $form_state->getValue(['settings', 'allowed_values'], []);
  $failed_validation = _url_friendly_options_check_allowed_values($allowed_values);
  if (!empty($failed_validation)) {
    $form_state->setErrorByName('settings', t('The following keys are not URL-friendly: @keys. Make sure you use only alpha-numeric characters and hyphens.', [
      '@keys' => implode(", ", $failed_validation),
    ]));
  }
}

/**
 * Helper to check allowed_values keys against a URL-friendly pattern.
 *
 * @param array $allowed_values
 *   An associative array of allowed values, as stored in list_string field
 *   storages.
 *
 * @return array
 *   An indexed array of keys that failed URL-friendly validation.
 */
function _url_friendly_options_check_allowed_values(array $allowed_values): array {
  $failed_validation = [];
  foreach ($allowed_values as $key => $label) {
    if (!preg_match('/^[a-zA-Z0-9-]*[a-zA-Z0-9]+$/', $key)) {
      $failed_validation[] = $key;
    }
  }
  return $failed_validation;
}