Separate field inputs from fields + other small fixes
This commit is contained in:
parent
f8e10f32d0
commit
8730b7c199
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
29
assets/scripts/FieldInput.js
Normal file
29
assets/scripts/FieldInput.js
Normal file
@ -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 += `
|
||||
<label for="${field.key}">${field.displayName}</label>
|
||||
<p class="description">${field.description ?? ""}</p>
|
||||
${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);
|
||||
}
|
||||
}
|
||||
@ -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 = {
|
||||
|
||||
@ -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(" ");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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("; ");
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,6 @@ export class IPv6Mechanism extends Mechanism {
|
||||
|
||||
validate(value) {
|
||||
// TODO validate
|
||||
return true;
|
||||
return !!value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 `
|
||||
<select id="${this.id}-qualifier">
|
||||
<select id="${id}-qualifier">
|
||||
${this.isRequired ? "" : `<option value=""><not set></option>`}
|
||||
<option value="+">Pass</option>
|
||||
<option value="-">Fail</option>
|
||||
<option value="~">Soft fail</option>
|
||||
<option value="?">Neutral</option>
|
||||
</select>
|
||||
${noValue ? "" : `<input id="${this.id}-value" type="text" placeholder="${placeholder}">`}
|
||||
${noValue ? "" : `<input id="${id}-value" type="text" placeholder="${placeholder}">`}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 `<input id="${this.id}" type="text" placeholder="example.com">`;
|
||||
getInputHtml(id) {
|
||||
return `<input id="${id}" type="text" placeholder="example.com">`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 "";
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ export class VersionTerm extends Term {
|
||||
return true;
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
getInputValue(id) {
|
||||
return this.version;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,15 +22,13 @@ export class DmarcUriListTag extends Tag {
|
||||
return true;
|
||||
}
|
||||
|
||||
getInputHtml() {
|
||||
return `<input id="${this.id}" type="email" name="${this.key}" placeholder="mail@example.com">`;
|
||||
getInputHtml(id) {
|
||||
return `<input id="${id}" type="email" name="${this.key}" placeholder="mail@example.com">`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 `<select id="${this.id}" name="${this.key}" ${this.isRequired ? "required" : ""}>` +
|
||||
getInputHtml(id) {
|
||||
return `<select id="${id}" name="${this.key}" ${this.isRequired ? "required" : ""}>` +
|
||||
(this.isRequired || this.defaultValue ? "" : `<option value="" selected><not set></option>`) +
|
||||
this.values.map((value, i) =>
|
||||
`<option value="${value}" ${this.defaultValue === value ? "selected" : ""}>
|
||||
@ -26,8 +26,8 @@ export class EnumTag extends Tag {
|
||||
`</select>`;
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return document.getElementById(this.id).value;
|
||||
getInputValue(id) {
|
||||
return document.getElementById(id).value;
|
||||
}
|
||||
|
||||
options(options) {
|
||||
|
||||
@ -22,11 +22,11 @@ export class IntTag extends Tag {
|
||||
return true;
|
||||
}
|
||||
|
||||
getInputHtml() {
|
||||
return `<input id="${this.id}" type="number" name="${this.key}" min="${this.min}" max="${this.max}" ${this.isRequired ? "required" : ""} placeholder="${this.defaultValue ?? ""}">`;
|
||||
getInputHtml(id) {
|
||||
return `<input id="${id}" type="number" name="${this.key}" min="${this.min}" max="${this.max}" ${this.isRequired ? "required" : ""} placeholder="${this.defaultValue ?? ""}">`;
|
||||
}
|
||||
|
||||
getInputValue() {
|
||||
return document.getElementById(this.id).value;
|
||||
getInputValue(id) {
|
||||
return document.getElementById(id).value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,22 @@ export class Tag extends Field {
|
||||
super(key);
|
||||
}
|
||||
|
||||
inputToString(fieldInputId) {
|
||||
const input = this.getInputValue(fieldInputId);
|
||||
|
||||
return `${this.key}=${input}`;
|
||||
}
|
||||
|
||||
isValidInput(fieldInputId) {
|
||||
const input = this.getInputValue(fieldInputId);
|
||||
|
||||
try {
|
||||
return this.validate(input);
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual methods
|
||||
|
||||
validate() {
|
||||
|
||||
@ -8,7 +8,9 @@ const records = {
|
||||
|
||||
const Record = records[location.pathname];
|
||||
|
||||
addFields(document.getElementById("form"), Record.fields.filter(field => !field.categoryName));
|
||||
const inputs = [];
|
||||
|
||||
addInputs(document.getElementById("form"), Record.fields.filter(field => !field.categoryName));
|
||||
|
||||
const categories = Record.fields.map(field => field.categoryName).filter(isUnique).filter(val => val);
|
||||
|
||||
@ -18,26 +20,25 @@ for (const category of categories) {
|
||||
|
||||
document.getElementById("form").appendChild(details);
|
||||
|
||||
addFields(details, Record.fields.filter(field => field.categoryName === category));
|
||||
addInputs(details, Record.fields.filter(field => field.categoryName === category));
|
||||
}
|
||||
|
||||
function addFields(elem, fields) {
|
||||
function addInputs(elem, fields) {
|
||||
for (const field of fields) {
|
||||
if (field.isHidden || !field.getInputHtml()) continue;
|
||||
if (field.isDisabled) continue;
|
||||
|
||||
elem.innerHTML += `
|
||||
<label for="${field.key}">${field.displayName}</label>
|
||||
<p class="description">${field.description ?? ""}</p>
|
||||
${field.getInputHtml()}
|
||||
`;
|
||||
const input = field.createInput(elem);
|
||||
inputs.push(input);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("form").onchange = generate;
|
||||
generate();
|
||||
|
||||
function generate(event) {
|
||||
document.getElementById("record").value = Record.fieldsToString();
|
||||
function generate() {
|
||||
const record = Record.createFromFieldInputs(inputs);
|
||||
|
||||
document.getElementById("record").value = record.text;
|
||||
}
|
||||
|
||||
document.getElementById("record").onclick = (e) => {
|
||||
|
||||
@ -22,9 +22,9 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>Pass</b> (Default): Selected IP addresses are authorized to send emails from this domain<br>
|
||||
<b>Fail</b>: Selected IP addresses are NOT authorized to send emails from this domain<br>
|
||||
<b>Soft fail</b>: Selected IP addresses might not be authorized to send emails from this domain (actual behavior differs)<br>
|
||||
<b>Pass</b> (Default): Matching IP addresses are authorized to send emails from this domain<br>
|
||||
<b>Fail</b>: Matching IP addresses are NOT authorized to send emails from this domain<br>
|
||||
<b>Soft fail</b>: Matching IP addresses might not be authorized to send emails from this domain (actual behavior differs)<br>
|
||||
<b>Neutral</b>: Do not explicitly state whether the IP addresses are authorized or not (can be used for overriding other qualifiers)
|
||||
</p>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user