diff --git a/assets/scripts/Field.js b/assets/scripts/Field.js index 3a3551e..87bc400 100644 --- a/assets/scripts/Field.js +++ b/assets/scripts/Field.js @@ -1,16 +1,22 @@ +import { FieldInput } from "./FieldInput.js"; + /** - * Defines any key-value pair in any type of DNS-record - * Used for input fields on DNS creator pages + * Represents any key-value pair in any type of DNS-record + * Used for validation and to create input fields on DNS creator pages */ export class Field { displayName = null; description = null; categoryName = null; isHidden = false; + isDisabled = false; constructor(key) { this.key = key; - this.id = "field-" + key; + } + + createInput(parentElem) { + return new FieldInput(this, parentElem); } // Virtual methods @@ -44,4 +50,9 @@ export class Field { this.isHidden = true; return this; } + + disabled() { + this.isDisabled = true; + return this; + } } diff --git a/assets/scripts/FieldInput.js b/assets/scripts/FieldInput.js new file mode 100644 index 0000000..20ba004 --- /dev/null +++ b/assets/scripts/FieldInput.js @@ -0,0 +1,29 @@ +/** + * Represents the actual input element on the creator tool + * A field may have multiple inputs if it allows multiple values + */ +export class FieldInput { + constructor(field, parentElem) { + this.field = field; + this.id = "field-" + field.key; + + if (!field.isHidden) + parentElem.innerHTML += ` + +

${field.description ?? ""}

+ ${field.getInputHtml(this.id)} + `; + } + + isValid() { + return this.field.isValidInput(this.id); + } + + getValue() { + return this.field.getInputValue(this.id); + } + + toString() { + return this.field.inputToString(this.id); + } +} diff --git a/assets/scripts/records/DmarcRecord.js b/assets/scripts/records/DmarcRecord.js index 326a629..1c797a5 100644 --- a/assets/scripts/records/DmarcRecord.js +++ b/assets/scripts/records/DmarcRecord.js @@ -9,6 +9,7 @@ export class DmarcRecord extends TagListRecord { static fields = [ new ConstantTag("v", "DMARC1") .required() + .hidden() .pos(0), new EnumTag("p", ["none", "quarantine", "reject"]) @@ -77,7 +78,9 @@ export class DmarcRecord extends TagListRecord { .default(86400) .pos(2), - new Tag("rf").default("afrf"), // Other values not supported + new Tag("rf") + .disabled() + .default("afrf"), // Other values not supported ]; static categories = { diff --git a/assets/scripts/records/SpfRecord.js b/assets/scripts/records/SpfRecord.js index 6705997..fd17a4b 100644 --- a/assets/scripts/records/SpfRecord.js +++ b/assets/scripts/records/SpfRecord.js @@ -11,46 +11,47 @@ export class SpfRecord { static fields = [ new VersionTerm("v", "spf1") .required() + .hidden() .pos(0), - new DomainMechanism("include") - .label("Include") - .desc("Also apply the SPF records from these domains") - .multiple() - .pos(1), - new DomainMechanism("a") .label("Domains") - .desc("Select the IP address from these domains (or current domain if none specified)") + .desc("Match the IP addresses from the A records of these domains (or current domain if none specified)") .value(ValueRequirement.OPTIONAL) .multiple() .pos(1), new DomainMechanism("mx") .label("MX Records") - .desc("Select the IP addresses from the MX records of these domains (or current domain if none specified)") + .desc("Match the IP addresses from the MX records of these domains (or current domain if none specified)") .value(ValueRequirement.OPTIONAL) .multiple() .pos(2), new DomainMechanism("ptr") - .hidden() + .disabled() .value(ValueRequirement.OPTIONAL) .multiple() .pos(2), - new IPv4Mechanism("ipv4") + new IPv4Mechanism("ip4") .label("IPv4 addresses") - .desc("Select these IP addresses") + .desc("Match these IP addresses") .multiple() .pos(2), - new IPv6Mechanism("ipv6") + new IPv6Mechanism("ip6") .label("IPv6 addresses") - .desc("Select these IP addresses") + .desc("Match these IP addresses") .multiple() .pos(2), + new DomainMechanism("include") + .label("Include") + .desc("Check the SPF record of another domain. If it passes, return with the selected result") + .multiple() + .pos(1), + new DomainMechanism("exists") .label("Exists") .desc("Apply only if this domain exists (can be used with macro expansions)") @@ -66,7 +67,7 @@ export class SpfRecord { new Modifier("redirect") .label("Redirect") - .desc("Redirect to the SPF record of this domain if no IP addresses matched") + .desc("Redirect to the SPF record of this domain if no matches were found") .category("advanced") .pos(4), @@ -85,6 +86,16 @@ export class SpfRecord { this.text = text; } + static createFromFieldInputs(fieldInputs) { + const tokens = fieldInputs + .filter(input => !input.field.isDisabled && input.isValid()) + .map(input => input.toString()); + + const text = tokens.join(" "); + + return new this(text); + } + tokenize() { return this.text.trim().split(/\s+/); } @@ -150,17 +161,4 @@ export class SpfRecord { lastPos = term.position; } } - - static fieldsToString() { - const tokens = this.fields - .filter(field => !field.isHidden) - .filter(field => field.valueRequirement === ValueRequirement.REQUIRED ? field.getInputValue() : field.getInputQualifier()) - .map(field => - field.getInputQualifier() - + field.key - + (field.getInputValue() ? field.separator + field.getInputValue() : "") - ); - - return tokens.join(" "); - } } diff --git a/assets/scripts/records/TagListRecord.js b/assets/scripts/records/TagListRecord.js index 3a2b073..5dbb2cc 100644 --- a/assets/scripts/records/TagListRecord.js +++ b/assets/scripts/records/TagListRecord.js @@ -8,6 +8,17 @@ export class TagListRecord { this.text = text; } + static createFromFieldInputs(fieldInputs) { + const tokens = fieldInputs + .filter(input => !input.field.isDisabled && input.isValid()) + .filter(input => !input.field.defaultValue || input.getValue() !== input.field.defaultValue) + .map(input => input.toString()); + + const text = tokens.join("; "); + + return new this(text); + } + tokenize() { return this.text .replace(/;\s*$/, "") @@ -61,14 +72,4 @@ export class TagListRecord { return true; } - - static fieldsToString() { - const tokens = this.fields - .filter(field => !field.isHidden) - .filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue) - .filter(field => field.getInputValue()) - .map(field => field.key + "=" + field.getInputValue()); - - return tokens.join("; "); - } } diff --git a/assets/scripts/spf/IPv6Mechanism.js b/assets/scripts/spf/IPv6Mechanism.js index b794a7d..b34ef9b 100644 --- a/assets/scripts/spf/IPv6Mechanism.js +++ b/assets/scripts/spf/IPv6Mechanism.js @@ -9,6 +9,6 @@ export class IPv6Mechanism extends Mechanism { validate(value) { // TODO validate - return true; + return !!value; } } diff --git a/assets/scripts/spf/Mechanism.js b/assets/scripts/spf/Mechanism.js index 4ebd487..715707e 100644 --- a/assets/scripts/spf/Mechanism.js +++ b/assets/scripts/spf/Mechanism.js @@ -9,28 +9,28 @@ export class Mechanism extends Term { super(key); } - getInputQualifier() { - return document.getElementById(this.id + "-qualifier").value; + getInputQualifier(id) { + return document.getElementById(id + "-qualifier").value; } - getInputValue() { - return document.getElementById(this.id + "-value")?.value; + getInputValue(id) { + return document.getElementById(id + "-value")?.value; } - getInputHtml() { + getInputHtml(id) { const noValue = this.valueRequirement === ValueRequirement.PROHIBITED; const placeholder = this.placeholder + (this.valueRequirement === ValueRequirement.OPTIONAL ? " (Optional)" : ""); return ` - ${this.isRequired ? "" : ``} - ${noValue ? "" : ``} + ${noValue ? "" : ``} `; } } diff --git a/assets/scripts/spf/Modifier.js b/assets/scripts/spf/Modifier.js index 890f0c3..ef83aa9 100644 --- a/assets/scripts/spf/Modifier.js +++ b/assets/scripts/spf/Modifier.js @@ -15,11 +15,11 @@ export class Modifier extends Term { return true; } - getInputValue() { - return document.getElementById(this.id).value; + getInputValue(id) { + return document.getElementById(id).value; } - getInputHtml() { - return ``; + getInputHtml(id) { + return ``; } } diff --git a/assets/scripts/spf/Term.js b/assets/scripts/spf/Term.js index 7777b15..33ac449 100644 --- a/assets/scripts/spf/Term.js +++ b/assets/scripts/spf/Term.js @@ -12,9 +12,31 @@ export class Term extends Field { super(key) } + inputToString(fieldInputId) { + const input = this.getInputValue(fieldInputId); + const qualifier = this.getInputQualifier(fieldInputId); + + return qualifier + this.key + (input ? this.separator + input : ""); + } + + isValidInput(fieldInputId) { + const input = this.getInputValue(fieldInputId); + const qualifier = this.getInputQualifier(fieldInputId); + + + if (this.valueRequirement !== ValueRequirement.REQUIRED && qualifier) + return true; + + try { + return this.validate(input); + } catch(e) { + return false; + } + } + // Virtual methods - getInputQualifier() { + getInputQualifier(fieldId) { return ""; } diff --git a/assets/scripts/spf/VersionTerm.js b/assets/scripts/spf/VersionTerm.js index 0e1d134..3f4a6ea 100644 --- a/assets/scripts/spf/VersionTerm.js +++ b/assets/scripts/spf/VersionTerm.js @@ -15,7 +15,7 @@ export class VersionTerm extends Term { return true; } - getInputValue() { + getInputValue(id) { return this.version; } } diff --git a/assets/scripts/tags/ConstantTag.js b/assets/scripts/tags/ConstantTag.js index 1dec3a9..f81345a 100644 --- a/assets/scripts/tags/ConstantTag.js +++ b/assets/scripts/tags/ConstantTag.js @@ -8,10 +8,12 @@ export class ConstantTag extends Tag { } validate(value) { - if (this.value !== value) throw new ValidationError(`Field ${this.key} must be "${this.value}"`) + if (this.value !== value) throw new ValidationError(`Field ${this.key} must be "${this.value}"`); + + return true; } - getInputValue() { + getInputValue(id) { return this.value; } } diff --git a/assets/scripts/tags/DmarcUriListTag.js b/assets/scripts/tags/DmarcUriListTag.js index 0b4647f..d949f94 100644 --- a/assets/scripts/tags/DmarcUriListTag.js +++ b/assets/scripts/tags/DmarcUriListTag.js @@ -22,15 +22,13 @@ export class DmarcUriListTag extends Tag { return true; } - getInputHtml() { - return ``; + getInputHtml(id) { + return ``; } - getInputValue() { - if (!document.getElementById(this.id).value) { - return null; - } + getInputValue(id) { + if (!document.getElementById(id).value) return null; - return "mailto:" + document.getElementById(this.id).value; + return "mailto:" + document.getElementById(id).value; } } diff --git a/assets/scripts/tags/EnumTag.js b/assets/scripts/tags/EnumTag.js index 1f91db2..c4eb54f 100644 --- a/assets/scripts/tags/EnumTag.js +++ b/assets/scripts/tags/EnumTag.js @@ -15,8 +15,8 @@ export class EnumTag extends Tag { throw new ValidationError(`Invalid value for tag "${this.key}" - must be one of: ${this.values.join(", ")}`); } - getInputHtml() { - return `` + (this.isRequired || this.defaultValue ? "" : ``) + this.values.map((value, i) => `