Skip to content

Commit

Permalink
ENH Improved validation
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Jan 30, 2024
1 parent a46c745 commit d19ee2e
Show file tree
Hide file tree
Showing 12 changed files with 304 additions and 2 deletions.
Empty file added lang/_manifest_exclude
Empty file.
6 changes: 6 additions & 0 deletions lang/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ en:
CREATE_LINK: 'Create link'
MENUTITLE: 'Link fields'
UPDATE_LINK: 'Update link'
SilverStripe\LinkField\Form\ExternalLinkField:
INVALID: 'Please enter a valid url'
SilverStripe\LinkField\Form\PhoneField:
INVALID: 'Please enter a valid phone number'
SilverStripe\LinkField\Form\Traits\AllowedLinkClassesTrait:
INVALID_TYPECLASS: '"{class}": {typeclass} is not a valid Link Type'
INVALID_TYPECLASS_EMPTY: '"{class}": Allowed types cannot be empty'
Expand Down Expand Up @@ -81,3 +85,5 @@ en:
has_one_Page: Page
SilverStripe\LinkField\Tasks\LinkableMigrationTask:
VERSIONED_STATUS_MISMATCH: 'Linkable and LinkField do not have matching Versioned applications. Make sure that both are either un-Versioned or Versioned'
SilverStripe\LinkField\Validators\HasOneCanViewValidator:
CANNOTBEVIEWED: '{name} cannot be viewed'
62 changes: 62 additions & 0 deletions src/Form/ExternalLinkField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace SilverStripe\LinkField\Form;

use SilverStripe\Forms\TextField;
use SilverStripe\Forms\Validator;

/**
* Text input field with validation for a url
*/
class ExternalLinkField extends TextField
{
/**
* This needs to be not surronded by regex delimiters so that it works on the frontend
*/
private const RX = '^[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$';

/**
* This is used for the <input> element type="text" attribute
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/tel
*/
protected $inputType = 'url';

/**
* This is added as a classname to the <input> element
*/
public function Type()
{
return 'url text';
}

/**
* @param Validator $validator
*
* @return string
*/
public function validate($validator)
{
$result = true;
$this->value = trim($this->value ?? '');
if ($this->value && !preg_match('#' . self::RX . '#', $this->value)) {
$validator->validationError(
$this->name,
_t(__CLASS__ . '.INVALID', 'Please enter a valid url'),
'validation'
);
$result = false;
}
return $this->extendValidationResult($result, $validator);
}

/**
* This is passed to the frontent via FormField::getSchemaValidation()
* and used in Validator.js
*/
public function getSchemaValidation()
{
$rules = parent::getSchemaValidation();
$rules['regex'] = ['pattern' => self::RX];
return $rules;
}
}
46 changes: 46 additions & 0 deletions src/Form/FormFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace SilverStripe\LinkField\Form;

use LogicException;
use SilverStripe\Admin\Forms\LinkFormFactory;
use SilverStripe\Forms\HiddenField;
use SilverStripe\LinkField\Type\Type;
use SilverStripe\ORM\DataObject;
use SilverStripe\Core\Injector\Injector;

/**
* Create Form schema for the LinkField based on a key provided by the request.
*/
class FormFactory extends LinkFormFactory
{
protected function getFormFields($controller, $name, $context)
{
/** @var Type $type */
$type = $context['LinkType'];

if (!$type instanceof Type) {
throw new LogicException(sprintf('%s: LinkType must be provided and must be an instance of Type', static::class));
}

// Pass on any available link data
$linkData = array_key_exists('LinkData', $context)
? $context['LinkData']
: [];
$fields = $type->scaffoldLinkFields($linkData);
$fields->push(HiddenField::create('typeKey')->setValue($context['LinkTypeKey']));
$this->extend('updateFormFields', $fields, $controller, $name, $context);

return $fields;
}

protected function getValidator($controller, $name, $context)
{
if (!array_key_exists('LinkType', $context)) {
return null;
}
/** @var DataObject|Type $type */
$type = $context['LinkType'];
return Injector::inst()->get($type)->getCMSCompositeValidator();
}
}
27 changes: 27 additions & 0 deletions src/Form/LinkFieldTreeDropdownField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace SilverStripe\LinkField\Form;

use SilverStripe\Forms\TreeDropdownField;

/**
* This class exists as workaround to an issue where RequiredFields is not respected
* when selecting an empty value using TreeDropdownField because RequiredFields views
* a value of 0 (ie no relation selected) as as non-emtpy value, as RequiredFields only counts
* an emtpy string as a missing value
*
* This class in only intended to be used in SiteTreeLink which also has a RequiredFields of ['PageID']
*/
class LinkFieldTreeDropdownField extends TreeDropdownField
{
/**
* This is passed to the frontent via FormField::getSchemaValidation()
* and used in Validator.js
*/
public function getSchemaValidation()
{
$rules = parent::getSchemaValidation();
$rules['regex'] = ['pattern' => '[^0]'];
return $rules;
}
}
62 changes: 62 additions & 0 deletions src/Form/PhoneField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace SilverStripe\LinkField\Form;

use SilverStripe\Forms\TextField;
use SilverStripe\Forms\Validator;

/**
* Text input field with validation for a phone number
*/
class PhoneField extends TextField
{
/**
* This needs to be not surronded by regex delimiters so that it works on the frontend
*/
private const RX = '^[()\- 0-9]+$';

/**
* This is used for the <input> element type="tel" attribute
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/tel
*/
protected $inputType = 'tel';

/**
* This is added as a classname to the <input> element
*/
public function Type()
{
return 'phone text';
}

/**
* @param Validator $validator
*
* @return string
*/
public function validate($validator)
{
$result = true;
$this->value = trim($this->value ?? '');
if ($this->value && !preg_match('#' . self::RX . '#', $this->value)) {
$validator->validationError(
$this->name,
_t(__CLASS__ . '.INVALID', 'Please enter a valid phone number'),
'validation'
);
$result = false;
}
return $this->extendValidationResult($result, $validator);
}

/**
* This is passed to the frontent via FormField::getSchemaValidation()
* and used in Validator.js
*/
public function getSchemaValidation()
{
$rules = parent::getSchemaValidation();
$rules['regex'] = ['pattern' => self::RX];
return $rules;
}
}
9 changes: 9 additions & 0 deletions src/Models/EmailLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\RequiredFields;

/**
* A link to an Email address.
Expand Down Expand Up @@ -55,4 +57,11 @@ public function getMenuTitle(): string
{
return _t(__CLASS__ . '.LINKLABEL', 'Link to email address');
}

public function getCMSCompositeValidator(): CompositeValidator
{
$validator = parent::getCMSCompositeValidator();
$validator->addValidator(RequiredFields::create(['Email']));
return $validator;
}
}
19 changes: 19 additions & 0 deletions src/Models/ExternalLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
namespace SilverStripe\LinkField\Models;

use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\FieldList;
use SilverStripe\LinkField\Form\ExternalLinkField;

/**
* A link to an external URL.
Expand All @@ -25,6 +29,14 @@ class ExternalLink extends Link
private static $icon = 'font-icon-external-link';

public function getCMSFields(): FieldList
{
$this->beforeUpdateCMSFields(function (FieldList $fields) {
$fields->replaceField('ExternalUrl', ExternalLinkField::create('ExternalUrl'));
});
return parent::getCMSFields();
}

public function generateLinkDescription(array $data): string
{
$this->beforeUpdateCMSFields(function (FieldList $fields) {
$linkField = $fields->dataFieldByName('ExternalUrl');
Expand All @@ -51,4 +63,11 @@ public function getMenuTitle(): string
{
return _t(__CLASS__ . '.LINKLABEL', 'Link to external URL');
}

public function getCMSCompositeValidator(): CompositeValidator
{
$validator = parent::getCMSCompositeValidator();
$validator->addValidator(RequiredFields::create(['ExternalUrl']));
return $validator;
}
}
9 changes: 9 additions & 0 deletions src/Models/FileLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use SilverStripe\Assets\File;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\RequiredFields;

/**
* A link to a File in the CMS
Expand Down Expand Up @@ -71,4 +73,11 @@ public function getMenuTitle(): string
{
return _t(__CLASS__ . '.LINKLABEL', 'Link to a file');
}

public function getCMSCompositeValidator(): CompositeValidator
{
$validator = parent::getCMSCompositeValidator();
$validator->addValidator(RequiredFields::create(['File']));
return $validator;
}
}
34 changes: 34 additions & 0 deletions src/Models/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
use SilverStripe\ORM\DataObjectSchema;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Forms\TreeDropdownField;

/**
* A Link Data Object. This class should be treated as abstract. You should never directly interact with a plain Link
Expand Down Expand Up @@ -153,6 +156,37 @@ public function getCMSCompositeValidator(): CompositeValidator
return $validator;
}

/**
* This method is used to validate the dataobject before saving as part of DataObject::write()
* Linkfield works a bit differently from normal forms, because where the modal data is turned
* into JSON and saved into a single JsonField and in saveInto() the JSON is loaded into the
* dataobject using DataObject::setData($data)
* Because of this alternate method of saving, the getCMSCompositeValidator() isn't called
* so we need to call it manually here by loading it into a temporary Form
*
* @return ValidationResult
*/
public function validate()
{
$parentResult = parent::validate();
$validator = $this->getCMSCompositeValidator();
$form = Form::create(null, Form::DEFAULT_NAME, $this->getCMSFields());
$form->setValidator($validator);
$form->loadDataFrom($this);
// Workaround an issue where RequiredFields does not treat RelationID's of 0 as missing
// by changing the value of any TreeDropdowns on the field with a value of 0 to empty string
/** @var FormField $field */
foreach ($form->Fields()->flattenFields() as $field) {
if (is_a($field, TreeDropdownField::class) && $field->Value() === 0) {
$field->setValue('');
}
}
$formResult = $form->validationResult();
$combinedResult = $parentResult->combineAnd($formResult);
$this->extend('updateValidate', $combinedResult);
return $combinedResult;
}

/**
* Form hook defined in @see Form::saveInto()
* We use this to work with an in-memory only field
Expand Down
19 changes: 19 additions & 0 deletions src/Models/PhoneLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
namespace SilverStripe\LinkField\Models;

use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\FieldList;
use SilverStripe\LinkField\Form\PhoneField;

/**
* A link to a phone number
Expand All @@ -23,6 +27,14 @@ class PhoneLink extends Link
private static $icon = 'font-icon-mobile';

public function getCMSFields(): FieldList
{
$this->beforeUpdateCMSFields(function (FieldList $fields) {
$fields->replaceField('Phone', PhoneField::create('Phone'));
});
return parent::getCMSFields();
}

public function generateLinkDescription(array $data): string
{
$this->beforeUpdateCMSFields(function (FieldList $fields) {
$linkField = $fields->dataFieldByName('Phone');
Expand Down Expand Up @@ -50,4 +62,11 @@ public function getMenuTitle(): string
{
return _t(__CLASS__ . '.LINKLABEL', 'Phone number');
}

public function getCMSCompositeValidator(): CompositeValidator
{
$validator = parent::getCMSCompositeValidator();
$validator->addValidator(RequiredFields::create(['Phone']));
return $validator;
}
}
Loading

0 comments on commit d19ee2e

Please sign in to comment.