Skip to content
Snippets Groups Projects
Commit 7431f5dd authored by Pierre Dureau's avatar Pierre Dureau
Browse files

Issue #3483508 by pdureau, just_like_good_vibes: Use a valid URI scheme and...

Issue #3483508 by pdureau, just_like_good_vibes: Use a valid URI scheme and rethink FieldValueSourceBase::extractPropertyValue
parent 023c7299
No related branches found
No related tags found
1 merge request!251Issue #3483508 by pdureau: Use a valid URI scheme and rethink...
Pipeline #324066 passed
......@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\ui_patterns\Plugin\UiPatterns\PropType;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\ui_patterns\Attribute\PropType;
use Drupal\ui_patterns\PropTypePluginBase;
......@@ -22,4 +23,48 @@ use Drupal\ui_patterns\PropTypePluginBase;
)]
class UrlPropType extends PropTypePluginBase {
/**
* {@inheritdoc}
*/
public static function normalize(mixed $value): mixed {
if (is_a($value, '\Drupal\Core\Url')) {
$value = $value->toString();
}
if (!is_string($value)) {
return "";
}
if ($value == "<front>") {
$value = "internal:/";
}
elseif ($value == "<none>") {
$value = "internal:";
}
// We don't use filter_var($value, FILTER_VALIDATE_URL) because too
// restrictive: catch only "scheme://foo".
if (preg_match("/^[a-z]+\:/", $value)) {
return self::resolveUrl($value);
}
// Any other string is considered as a valid path.
// This is not our work to do further checks and transformations.
return $value;
}
/**
* Normalize URL.
*/
protected static function resolveUrl(string $value): string {
// PHP_URL_SCHEME works with "scheme://foo" and "scheme:foo".
$scheme = parse_url($value, PHP_URL_SCHEME);
if (in_array($scheme, ['public', 'private', 'temp'])) {
$generator = \Drupal::service('file_url_generator');
$value = $generator->generateAbsoluteString($value);
return Url::fromUri($value)->toString();
}
if (in_array($scheme, ['internal', 'entity', 'route'])) {
return Url::fromUri($value)->toString();
}
return $value;
}
}
......@@ -25,7 +25,6 @@ class FieldPropertySource extends FieldValueSourceBase {
public function getPropValue(): mixed {
$items = $this->getEntityFieldItemList();
$delta = (isset($this->context['ui_patterns:field:index'])) ? $this->getContextValue('ui_patterns:field:index') : 0;
$lang_code = (isset($this->context['ui_patterns:lang_code'])) ? ($this->getContextValue("ui_patterns:lang_code") ?? "und") : "und";
$property = $this->getCustomPluginMetadata('property');
if (empty($property)) {
return NULL;
......@@ -35,7 +34,7 @@ class FieldPropertySource extends FieldValueSourceBase {
if (!$field_item_at_delta) {
return NULL;
}
$property_value = $this->extractPropertyValue($field_item_at_delta, $property, $lang_code);
$property_value = $field_item_at_delta->get($property)->getValue();
$prop_typ_types = [];
if (isset($this->propDefinition['type'])) {
// Type can be an array of types or a single type.
......
......@@ -6,14 +6,8 @@ namespace Drupal\ui_patterns\Plugin\UiPatterns\Source;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Plugin\DataType\EntityReference;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\TypedData\Plugin\DataType\Uri;
use Drupal\Core\Url;
use Drupal\text\TextProcessed;
use Drupal\ui_patterns\SourceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -114,86 +108,6 @@ abstract class FieldValueSourceBase extends FieldSourceBase implements SourceInt
return $entity->get($field_name);
}
/**
* Extract a property value from a field item.
*
* Entity references will be resolved to their label.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* The field item.
* @param string $property_name
* The source property.
* @param string $lang_code
* The language that should be used to render the field.
*
* @return mixed|null
* The extracted value or NULL if the field is referencing an entity that
* doesn't exist anymore.
*
* @throws \Drupal\Core\TypedData\Exception\MissingDataException
*/
protected function extractPropertyValue(FieldItemInterface $item, string $property_name, string $lang_code): mixed {
$property = $item->get($property_name);
if ($property instanceof TextProcessed) {
return $property->getValue();
}
if ($property instanceof EntityReference) {
return $this->getEntityReferencedLabel($property, $lang_code);
}
$value = $property->getValue();
if ($value && ($property instanceof Uri)) {
// $value is a non-empty string.
return $this->resolveInternalUri($item, $value);
}
return $value;
}
/**
* Resolve Uri datatype instead of directly returning the raw value.
*
* Raw value may be internal://, public://, private://, entity://...
*/
protected function resolveInternalUri(FieldItemInterface $item, string $value): string {
// Most of the time, Uri datatype is met in a "link" field type.
if ($item->getDataDefinition()->getDataType() === 'field_item:link') {
$options = (array) $item->get('options')->getValue();
return Url::fromUri($value, $options)->toString();
}
return Url::fromUri($value)->toString();
}
/**
* Returns the label of an entity referenced by a field property.
*/
protected function getEntityReferencedLabel(EntityReference $property, string $lang_code): mixed {
// Ensure the referenced entity still exists.
$referencedEntityTypeId = $property->getTargetDefinition()
->getEntityTypeId();
$referencedEntityId = $property->getTargetIdentifier();
if (!$referencedEntityId) {
return NULL;
}
$entity = $this->entityTypeManager
->getStorage($referencedEntityTypeId)
->load($referencedEntityId);
if (!$entity || !is_object($entity)) {
return NULL;
}
// Drupal loads the entity in its default language and should load
// the translated one if available.
$value = $entity->label();
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if (($entity instanceof TranslatableInterface)
&& $lang_code
&& $entity->hasTranslation($lang_code)) {
$translated_entity = $entity->getTranslation($lang_code);
$value = $translated_entity->label();
}
return $value;
}
/**
* Get the settings from the plugin configuration.
*
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\ui_patterns\Unit;
use Drupal\Tests\UnitTestCase;
use Drupal\ui_patterns\Plugin\UiPatterns\PropType\UrlPropType;
/**
* @coversDefaultClass \Drupal\ui_patterns\Plugin\UiPatterns\PropType\UrlPropType
*
* @group ui_patterns
*/
final class UrlPropTypeNormalizationTest extends UnitTestCase {
/**
* @covers ::normalize
*
* @dataProvider provideNormalizationData
*/
public function testNormalize(mixed $value, string $expected): void {
$normalized = UrlPropType::normalize($value);
self::assertEquals($normalized, $expected);
}
/**
* Provide data for testNormalize.
*/
public static function provideNormalizationData(): \Generator {
$data = self::notAnUrl() + self::validUrl();
foreach ($data as $label => $test) {
yield $label => [
$test['value'],
$test['expected'],
];
};
}
/**
* Not an URL.
*/
protected static function notAnUrl() {
return [
"Empty string" => [
"value" => "",
"expected" => "",
],
"Boolean" => [
"value" => TRUE,
"expected" => "",
],
"Integer" => [
"value" => 3,
"expected" => "",
],
"Array" => [
"value" => [],
"expected" => "",
],
];
}
/**
* Valid URL.
*/
protected static function validUrl() {
return [
"HTTP URL (domain only)" => [
"value" => "http://www.foo.com",
"expected" => "http://www.foo.com",
],
"HTTP URL" => [
"value" => "http://www.foo.com/path/to",
"expected" => "http://www.foo.com/path/to",
],
"HTTPS URL" => [
"value" => "https://www.foo.com/path/to",
"expected" => "https://www.foo.com/path/to",
],
"HTTP(S) URL" => [
"value" => "//www.foo.com/path/to",
"expected" => "//www.foo.com/path/to",
],
"SFTP URL" => [
"value" => "sftp://www.foo.com/path/to",
"expected" => "sftp://www.foo.com/path/to",
],
"Full path" => [
"value" => "/path/to",
"expected" => "/path/to",
],
"Relative path" => [
"value" => "path/to",
"expected" => "path/to",
],
"HTTPS IRI" => [
"value" => "https://en.tranché.org/bien-sûr",
"expected" => "https://en.tranché.org/bien-sûr",
],
"HTTPS IRI percent encoded" => [
"value" => "https://en.wiktionary.org/wiki/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82",
"expected" => "https://en.wiktionary.org/wiki/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82",
],
];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment