diff --git a/_config.php b/_config.php index 9e519d4a..71c7914c 100644 --- a/_config.php +++ b/_config.php @@ -5,5 +5,4 @@ // Avoid creating global variables call_user_func(function () { - }); diff --git a/lang/_manifest_exclude b/lang/_manifest_exclude new file mode 100644 index 00000000..e69de29b diff --git a/lang/en.yml b/lang/en.yml new file mode 100644 index 00000000..b4c16635 --- /dev/null +++ b/lang/en.yml @@ -0,0 +1,5 @@ +en: + SilverStripe\LinkField\Form\PhoneField: + INVALID: 'Please enter a valid phone number' + SilverStripe\LinkField\Validators\HasOneCanViewValidator: + CANNOTBEVIEWED: '{name} cannot be viewed' diff --git a/src/Form/LinkFieldTreeDropdownField.php b/src/Form/LinkFieldTreeDropdownField.php new file mode 100644 index 00000000..881edb57 --- /dev/null +++ b/src/Form/LinkFieldTreeDropdownField.php @@ -0,0 +1,27 @@ + '[^0]']; + return $rules; + } +} diff --git a/src/Form/PhoneField.php b/src/Form/PhoneField.php new file mode 100644 index 00000000..089e09df --- /dev/null +++ b/src/Form/PhoneField.php @@ -0,0 +1,62 @@ + 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 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; + } +} diff --git a/src/Models/EmailLink.php b/src/Models/EmailLink.php index 410455db..ea3d1410 100644 --- a/src/Models/EmailLink.php +++ b/src/Models/EmailLink.php @@ -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. @@ -28,7 +30,6 @@ public function getCMSFields(): FieldList $this->beforeUpdateCMSFields(static function (FieldList $fields) { $fields->replaceField('Email', EmailField::create('Email')); }); - return parent::getCMSFields(); } @@ -36,4 +37,11 @@ public function getURL(): string { return $this->Email ? sprintf('mailto:%s', $this->Email) : ''; } + + public function getCMSCompositeValidator(): CompositeValidator + { + $validator = parent::getCMSCompositeValidator(); + $validator->addValidator(RequiredFields::create(['Email'])); + return $validator; + } } diff --git a/src/Models/ExternalLink.php b/src/Models/ExternalLink.php index c51fb50c..b47d2fcc 100644 --- a/src/Models/ExternalLink.php +++ b/src/Models/ExternalLink.php @@ -2,6 +2,9 @@ namespace SilverStripe\LinkField\Models; +use SilverStripe\Forms\CompositeValidator; +use SilverStripe\Forms\RequiredFields; + /** * A link to an external URL. * @@ -24,4 +27,11 @@ public function getURL(): string { return $this->ExternalUrl ?? ''; } + + public function getCMSCompositeValidator(): CompositeValidator + { + $validator = parent::getCMSCompositeValidator(); + $validator->addValidator(RequiredFields::create(['ExternalUrl'])); + return $validator; + } } diff --git a/src/Models/FileLink.php b/src/Models/FileLink.php index 0d40b933..0dbbe34b 100644 --- a/src/Models/FileLink.php +++ b/src/Models/FileLink.php @@ -3,6 +3,8 @@ namespace SilverStripe\LinkField\Models; use SilverStripe\Assets\File; +use SilverStripe\Forms\CompositeValidator; +use SilverStripe\Forms\RequiredFields; /** * A link to a File track in asset-admin @@ -42,4 +44,11 @@ public function getURL(): string return $file->exists() ? (string) $file->getURL() : ''; } + + public function getCMSCompositeValidator(): CompositeValidator + { + $validator = parent::getCMSCompositeValidator(); + $validator->addValidator(RequiredFields::create(['File'])); + return $validator; + } } diff --git a/src/Models/Link.php b/src/Models/Link.php index 87d2de0f..5b3671ac 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -16,6 +16,9 @@ use SilverStripe\ORM\DataObject; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\View\Requirements; +use SilverStripe\Forms\Form; +use SilverStripe\ORM\ValidationResult; +use SilverStripe\Forms\TreeDropdownField; /** * A Link Data Object. This class should be a subclass, and you should never directly interact with a plain Link @@ -110,6 +113,36 @@ 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); + return $combinedResult; + } + /** * Form hook defined in @see Form::saveInto() * We use this to work with an in-memory only field diff --git a/src/Models/PhoneLink.php b/src/Models/PhoneLink.php index 855e3b1c..c0d20dd4 100644 --- a/src/Models/PhoneLink.php +++ b/src/Models/PhoneLink.php @@ -2,6 +2,11 @@ namespace SilverStripe\LinkField\Models; +use SilverStripe\Forms\CompositeValidator; +use SilverStripe\Forms\RequiredFields; +use SilverStripe\Forms\FieldList; +use SilverStripe\LinkField\Form\PhoneField; + /** * A link to a phone number * @@ -15,6 +20,14 @@ class PhoneLink extends Link 'Phone' => 'Varchar(255)', ]; + public function getCMSFields(): FieldList + { + $this->beforeUpdateCMSFields(function (FieldList $fields) { + $fields->replaceField('Phone', PhoneField::create('Phone')); + }); + return parent::getCMSFields(); + } + public function generateLinkDescription(array $data): string { return isset($data['Phone']) ? $data['Phone'] : ''; @@ -24,4 +37,11 @@ public function getURL(): string { return $this->Phone ? sprintf('tel:%s', $this->Phone) : ''; } + + public function getCMSCompositeValidator(): CompositeValidator + { + $validator = parent::getCMSCompositeValidator(); + $validator->addValidator(RequiredFields::create(['Phone'])); + return $validator; + } } diff --git a/src/Models/SiteTreeLink.php b/src/Models/SiteTreeLink.php index f3f611d8..c8b02572 100644 --- a/src/Models/SiteTreeLink.php +++ b/src/Models/SiteTreeLink.php @@ -7,7 +7,10 @@ use SilverStripe\Control\Controller; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\TextField; -use SilverStripe\Forms\TreeDropdownField; +use SilverStripe\Forms\CompositeValidator; +use SilverStripe\Forms\RequiredFields; +use SilverStripe\LinkField\Form\LinkFieldTreeDropdownField; +use SilverStripe\LinkField\Validators\HasOneCanViewValidator; /** * A link to a Page in the CMS @@ -63,7 +66,7 @@ public function getCMSFields(): FieldList $fields->insertAfter( 'Title', - TreeDropdownField::create( + LinkFieldTreeDropdownField::create( 'PageID', 'Page', SiteTree::class, @@ -128,4 +131,12 @@ public function getTitle(): ?string // Use page title as a default value in case CMS user didn't provide the title return $page->Title; } + + public function getCMSCompositeValidator(): CompositeValidator + { + $validator = parent::getCMSCompositeValidator(); + $validator->addValidator(RequiredFields::create(['PageID'])); + $validator->addValidator(HasOneCanViewValidator::create(['PageID'])); + return $validator; + } } diff --git a/src/Validators/HasOneCanViewValidator.php b/src/Validators/HasOneCanViewValidator.php new file mode 100644 index 00000000..d6fe6857 --- /dev/null +++ b/src/Validators/HasOneCanViewValidator.php @@ -0,0 +1,98 @@ + $relationField) { + if (!preg_match('#ID$#', $relationField)) { + $relationFields[$i] .= 'ID'; + } + } + $this->relationFields = $relationFields; + } + + /** + * Allows validation of fields via specification of a php function for + * validation which is executed after the form is submitted. + * + * @param array $data + * @return boolean + */ + public function php($data) + { + $valid = true; + $fields = $this->form->Fields(); + $dataObjectClassName = get_class($this->form->getRecord()); + $hasOnes = Config::inst()->get($dataObjectClassName, 'has_one'); + + foreach ($this->relationFields as $fieldName) { + if ($fieldName instanceof FormField) { + $formField = $fieldName; + $fieldName = $fieldName->getName(); + } else { + $formField = $fields->dataFieldByName($fieldName); + } + + if (!$formField) { + continue; + } + + $value ??= $data[$fieldName]; + if (!$value) { + continue; + } + + $relation = preg_replace('#ID$#', '', $fieldName); + $relationClassName = $hasOnes[$relation]; + $relationObject = $relationClassName::get()->byID($value); + + if ($relationObject && $relationObject->canView()) { + // It's valid + // Note if $relationObject is null then still fail this CanView validator the same + // way so that user cannot tell if the relation exists or not + continue; + } + + $errorMessage = _t( + __CLASS__ . '.CANNOTBEVIEWED', + '{name} cannot be viewed', + [ + 'name' => strip_tags( + '"' . ($formField->Title() ? $formField->Title() : $fieldName) . '"' + ) + ] + ); + if ($msg = $formField->getCustomValidationMessage()) { + $errorMessage = $msg; + } + $this->validationError($fieldName, $errorMessage, 'required'); + $valid = false; + } + + return $valid; + } +}