diff --git a/application/Espo/Classes/FieldValidators/UrlType.php b/application/Espo/Classes/FieldValidators/UrlType.php index 9e20b43b4e..05b5bb425f 100644 --- a/application/Espo/Classes/FieldValidators/UrlType.php +++ b/application/Espo/Classes/FieldValidators/UrlType.php @@ -30,19 +30,17 @@ namespace Espo\Classes\FieldValidators; use Espo\Core\Utils\Metadata; +use Espo\ORM\Defs; use Espo\ORM\Entity; class UrlType { - private Metadata $metadata; - private VarcharType $varcharType; - - public function __construct(Metadata $metadata, VarcharType $varcharType) - { - $this->metadata = $metadata; - $this->varcharType = $varcharType; - } + public function __construct( + private Metadata $metadata, + private VarcharType $varcharType, + private Defs $defs, + ) {} public function checkRequired(Entity $entity, string $field): bool { @@ -62,6 +60,15 @@ class UrlType return true; } + if ( + $this->defs + ->getEntity($entity->getEntityType()) + ->tryGetField($field) + ?->getParam('protocolRequired') + ) { + return filter_var($value, FILTER_VALIDATE_URL) !== false; + } + /** @var string $pattern */ $pattern = $this->metadata->get(['app', 'regExpPatterns', 'uriOptionalProtocol', 'pattern']); diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 70979703bf..c6245c5eb7 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -245,7 +245,8 @@ "notStorable": "Not Storable", "itemsEditable": "Items Editable", "openInNewTab": "Open in new tab", - "notLockable": "Not Lockable" + "notLockable": "Not Lockable", + "protocolRequired": "Protocol Required" }, "strings" : { "rebuildRequired": "Rebuild is required" diff --git a/application/Espo/Resources/metadata/fields/url.json b/application/Espo/Resources/metadata/fields/url.json index f078b892a4..f409f3fcd7 100644 --- a/application/Espo/Resources/metadata/fields/url.json +++ b/application/Espo/Resources/metadata/fields/url.json @@ -18,6 +18,10 @@ "type": "bool", "tooltip": "urlStrip" }, + { + "name": "protocolRequired", + "type": "bool" + }, { "name": "copyToClipboard", "type": "bool", diff --git a/client/src/views/fields/url.js b/client/src/views/fields/url.js index cb3c331d68..332d372ffe 100644 --- a/client/src/views/fields/url.js +++ b/client/src/views/fields/url.js @@ -52,6 +52,7 @@ class UrlFieldView extends VarcharFieldView { * @property {boolean} [required] Required. * @property {boolean} [copyToClipboard] To display a Copy-to-clipboard button. * @property {boolean} [strip] To strip. + * @property {boolean} [protocolRequired] Require protocol. As of v9.4. */ /** @@ -83,7 +84,7 @@ class UrlFieldView extends VarcharFieldView { noSpellCheck = true optionalProtocol = true - DEFAULT_MAX_LENGTH =255 + DEFAULT_MAX_LENGTH = 255 data() { const data = super.data(); @@ -93,6 +94,14 @@ class UrlFieldView extends VarcharFieldView { return data; } + setup() { + super.setup(); + + if (this.params.protocolRequired) { + this.optionalProtocol = false; + } + } + afterRender() { super.afterRender(); @@ -141,7 +150,7 @@ class UrlFieldView extends VarcharFieldView { parse(value) { value = value.trim(); - if (this.params.strip) { + if (this.params.strip && !this.params.protocolRequired) { value = this.strip(value); } @@ -194,14 +203,7 @@ class UrlFieldView extends VarcharFieldView { return false; } - const patternName = this.optionalProtocol ? 'uriOptionalProtocol' : 'uri'; - - /** @var {string} */ - const pattern = this.getMetadata().get(['app', 'regExpPatterns', patternName, 'pattern']); - - const regExp = new RegExp('^' + pattern + '$'); - - if (regExp.test(value)) { + if (this.isValid(value)) { return false; } @@ -213,6 +215,34 @@ class UrlFieldView extends VarcharFieldView { return true; } + /** + * @private + * @param {string} value + * @return {boolean} + */ + isValid(value) { + if (!this.optionalProtocol) { + try { + new URL(value); + + return true; + } catch (e) { + return false; + } + } + + /** @var {string} */ + const pattern = this.getMetadata().get(['app', 'regExpPatterns', 'uriOptionalProtocol', 'pattern']); + + const regExp = new RegExp('^' + pattern + '$'); + + if (regExp.test(value)) { + return true; + } + + return false; + } + // noinspection JSUnusedGlobalSymbols validateMaxLength() { const maxLength = this.params.maxLength || this.DEFAULT_MAX_LENGTH; diff --git a/schema/metadata/entityDefs.json b/schema/metadata/entityDefs.json index 70e55ecc60..22bcfdf5a7 100644 --- a/schema/metadata/entityDefs.json +++ b/schema/metadata/entityDefs.json @@ -495,6 +495,10 @@ "copyToClipboard": { "type": "boolean", "description": "Display 'Copy to Clipboard' button." + }, + "protocolRequired": { + "type": "boolean", + "description": "Require protocol. As of v9.4." } } }