Issue #3091478 by lauriii, Tim Bozeman, malcomio, tim.plunkett, adeshsharma, longwave, EclipseGc, bnjmnm, larowlan, alexpott, amateescu, dpi, quietone: Improve StringItem::generateSampleValue()

merge-requests/3783/head
Lee Rowlands 2023-04-05 16:33:38 +10:00
parent ca0a26d23b
commit 036bf79c32
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
8 changed files with 270 additions and 20 deletions

View File

@ -72,7 +72,47 @@ class StringItem extends StringItemBase {
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$random = new Random();
$values['value'] = $random->word(mt_rand(1, $field_definition->getSetting('max_length')));
$max_length = $field_definition->getSetting('max_length');
// When the maximum length is less than 15, or the field needs to be unique,
// generate a random word using the maximum length.
if ($max_length <= 15 || $field_definition->getConstraint('UniqueField')) {
$values['value'] = ucfirst($random->word($max_length));
return $values;
}
// The minimum length is either 10% of the maximum length, or 15 characters
// long, whichever is greater.
$min_length = max(ceil($max_length * 0.10), 15);
// Reduce the max length to allow us to add a period.
$max_length -= 1;
// The random value is generated multiple times to create a slight
// preference towards values that are closer to the minimum length of the
// string. For values larger than 255 (which is the default maximum value),
// the bias towards minimum length is increased. This is because the default
// maximum length of 255 is often used for fields that include shorter
// values (i.e. title).
$length = mt_rand($min_length, mt_rand($min_length, $max_length >= 255 ? mt_rand($min_length, $max_length) : $max_length));
$string = $random->sentences(1);
while (mb_strlen($string) < $length) {
$string .= " {$random->sentences(1)}";
}
if (mb_strlen($string) > $max_length) {
$string = substr($string, 0, $length);
$string = substr($string, 0, strrpos($string, ' '));
}
$string = rtrim($string, ' .');
// Ensure that the string ends with a full stop if there are multiple
// sentences.
$values['value'] = $string . (str_contains($string, '.') ? '.' : '');
return $values;
}

View File

@ -2,6 +2,7 @@
namespace Drupal\Core\Field\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
@ -77,8 +78,18 @@ class UriItem extends StringItem {
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$values = parent::generateSampleValue($field_definition);
$suffix_length = $field_definition->getSetting('max_length') - 7;
$random = new Random();
$max_length = $field_definition->getSetting('max_length');
$min_length = min(10, $max_length);
// The random value is generated multiple times to create a slight
// preference towards values that are closer to the minimum length of the
// string.
$length = mt_rand($min_length, mt_rand($min_length, mt_rand($min_length, $max_length)));
$values['value'] = $random->word($length);
$suffix_length = $max_length - 7;
foreach ($values as $key => $value) {
$values[$key] = 'http://' . mb_substr($value, 0, $suffix_length);
}

View File

@ -17,7 +17,9 @@ class UniqueFieldConstraint extends Constraint {
public $message = 'A @entity_type with @field_name %value already exists.';
/**
* {@inheritdoc}
* Returns the name of the class that validates this constraint.
*
* @return string
*/
public function validatedBy() {
return '\Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldValueValidator';

View File

@ -12,7 +12,7 @@ use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Row;
use Drupal\user\UserInterface;
use Drupal\user\UserNameItem;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -158,23 +158,22 @@ class EntityUser extends EntityContentBase {
*/
protected function processStubRow(Row $row) {
parent::processStubRow($row);
// Email address is not defined as required in the base field definition but
// is effectively required by the UserMailRequired constraint. This means
// that Entity::processStubRow() did not populate it - we do it here.
$field_definitions = $this->entityFieldManager
->getFieldDefinitions($this->storage->getEntityTypeId(),
$this->getKey('bundle'));
// Name is generated using a dedicated sample value generator to ensure
// uniqueness and a valid length.
// @todo Remove this as part of https://www.drupal.org/node/3352288.
$name = UserNameItem::generateSampleValue($field_definitions['name']);
$row->setDestinationProperty('name', reset($name));
// Email address is not defined as required in the base field definition but
// is effectively required by the UserMailRequired constraint. This means
// that Entity::processStubRow() did not populate it - we do it here.
$mail = EmailItem::generateSampleValue($field_definitions['mail']);
$row->setDestinationProperty('mail', reset($mail));
// @todo Work-around for https://www.drupal.org/node/2602066.
$name = $row->getDestinationProperty('name');
if (is_array($name)) {
$name = reset($name);
}
if (mb_strlen($name) > UserInterface::USERNAME_MAX_LENGTH) {
$row->setDestinationProperty('name', mb_substr($name, 0, UserInterface::USERNAME_MAX_LENGTH));
}
}
/**

View File

@ -2,6 +2,7 @@
namespace Drupal\user;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
@ -28,9 +29,36 @@ class UserNameItem extends StringItem {
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$values = parent::generateSampleValue($field_definition);
// User names larger than 60 characters won't pass validation.
$values['value'] = substr($values['value'], 0, UserInterface::USERNAME_MAX_LENGTH);
$random = new Random();
$max_length = min(UserInterface::USERNAME_MAX_LENGTH, $field_definition->getSetting('max_length'));
// Generate a list of words, which can be used to generate a string.
$words = explode(' ', $random->sentences(8));
// Begin with a username that is either 2 or 3 words.
$count = mt_rand(2, 3);
// Capitalize the words used in usernames 50% of the time.
$words = mt_rand(0, 1) ? array_map('ucfirst', $words) : $words;
// Username is a single long word 50% of the time. In the case of a single
// long word, sometimes the generated username may also contain periods in
// the middle of the username.
$separator = ' ';
if (mt_rand(0, 1)) {
$separator = '';
$count = mt_rand(2, 8);
// The username will start with a capital letter 50% of the time.
$words = mt_rand(0, 1) ? array_map('strtolower', $words) : $words;
}
$string = implode($separator, array_splice($words, 0, $count));
// Normalize the string to not be longer than the maximum length, and to not
// end with a space or a period.
$values['value'] = rtrim(mb_substr($string, 0, $max_length), ' .');
return $values;
}

View File

@ -0,0 +1,53 @@
<?php
namespace Drupal\Tests\user\Unit;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\user\UserNameItem;
/**
* Defines a test for the UserNameItem field-type.
*
* @group Field
* @coversDefaultClass \Drupal\user\UserNameItem
*/
class UserNameItemTest extends UnitTestCase {
/**
* Tests generating sample values.
*
* @param int $max_length
* Maximum field length.
*
* @covers ::generateSampleValue
* @dataProvider providerMaxLength
*/
public function testGenerateSampleValue(int $max_length): void {
$definition = $this->prophesize(FieldDefinitionInterface::class);
$definition->getSetting('max_length')->willReturn($max_length);
for ($i = 0; $i < 1000; $i++) {
$sample_value = UserNameItem::generateSampleValue($definition->reveal());
$this->assertLessThanOrEqual($max_length, mb_strlen($sample_value['value']));
$this->assertEquals(trim($sample_value['value'], ' '), $sample_value['value']);
}
}
/**
* Data provider for maximum-lengths.
*
* @return array
* Test cases.
*/
public function providerMaxLength(): array {
return [
'32' => [32],
'255' => [255],
'500' => [500],
'15' => [15],
'64' => [64],
];
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Drupal\Tests\Core\Field;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
use Drupal\Core\Validation\Plugin\Validation\Constraint\UniqueFieldConstraint;
use Drupal\Tests\UnitTestCase;
/**
* Defines a test for the StringItem field-type.
*
* @group Field
* @coversDefaultClass \Drupal\Core\Field\Plugin\Field\FieldType\StringItem
*/
class StringItemTest extends UnitTestCase {
/**
* Tests generating sample values.
*
* @param int $max_length
* Maximum field length.
*
* @covers ::generateSampleValue
* @dataProvider providerMaxLength
*/
public function testGenerateSampleValue(int $max_length): void {
foreach ([TRUE, FALSE] as $unique) {
$definition = $this->prophesize(FieldDefinitionInterface::class);
$constraints = $unique ? [$this->prophesize(UniqueFieldConstraint::class)] : [];
$definition->getConstraint('UniqueField')->willReturn($constraints);
$definition->getSetting('max_length')->willReturn($max_length);
for ($i = 0; $i < 1000; $i++) {
$sample_value = StringItem::generateSampleValue($definition->reveal());
// When the field value needs to be unique, the generated sample value
// should match the maximum length to ensure sufficient entropy.
if ($unique) {
$this->assertEquals($max_length, mb_strlen($sample_value['value']));
}
else {
$this->assertLessThanOrEqual($max_length, mb_strlen($sample_value['value']));
}
}
}
}
/**
* Data provider for maximum-lengths.
*
* @return array
* Test cases.
*/
public function providerMaxLength(): array {
return [
'32' => [32],
'255' => [255],
'500' => [500],
'15' => [15],
'4' => [4],
'64' => [64],
];
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Drupal\Tests\Core\Field;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
use Drupal\Tests\UnitTestCase;
/**
* Defines a test for the UriItem field-type.
*
* @group Field
* @coversDefaultClass \Drupal\Core\Field\Plugin\Field\FieldType\UriItem
*/
class UriItemTest extends UnitTestCase {
/**
* Tests generating sample values.
*
* @param int $max_length
* Maximum field length.
*
* @covers ::generateSampleValue
* @dataProvider providerMaxLength
*/
public function testGenerateSampleValue(int $max_length): void {
$definition = $this->prophesize(FieldDefinitionInterface::class);
$definition->getSetting('max_length')->willReturn($max_length);
for ($i = 0; $i < 1000; $i++) {
$sample_value = UriItem::generateSampleValue($definition->reveal());
$this->assertLessThanOrEqual($max_length, mb_strlen($sample_value['value']));
$this->assertStringNotContainsString(' ', $sample_value['value']);
}
}
/**
* Data provider for maximum-lengths.
*
* @return array
* Test cases.
*/
public function providerMaxLength(): array {
return [
'32' => [32],
'255' => [255],
'500' => [500],
'15' => [15],
'64' => [64],
];
}
}