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
|
* Represents any key-value pair in any type of DNS-record
|
||||||
* Used for input fields on DNS creator pages
|
* Used for validation and to create input fields on DNS creator pages
|
||||||
*/
|
*/
|
||||||
export class Field {
|
export class Field {
|
||||||
displayName = null;
|
displayName = null;
|
||||||
description = null;
|
description = null;
|
||||||
categoryName = null;
|
categoryName = null;
|
||||||
isHidden = false;
|
isHidden = false;
|
||||||
|
isDisabled = false;
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.id = "field-" + key;
|
}
|
||||||
|
|
||||||
|
createInput(parentElem) {
|
||||||
|
return new FieldInput(this, parentElem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Virtual methods
|
// Virtual methods
|
||||||
@ -44,4 +50,9 @@ export class Field {
|
|||||||
this.isHidden = true;
|
this.isHidden = true;
|
||||||
return this;
|
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 = [
|
static fields = [
|
||||||
new ConstantTag("v", "DMARC1")
|
new ConstantTag("v", "DMARC1")
|
||||||
.required()
|
.required()
|
||||||
|
.hidden()
|
||||||
.pos(0),
|
.pos(0),
|
||||||
|
|
||||||
new EnumTag("p", ["none", "quarantine", "reject"])
|
new EnumTag("p", ["none", "quarantine", "reject"])
|
||||||
@ -77,7 +78,9 @@ export class DmarcRecord extends TagListRecord {
|
|||||||
.default(86400)
|
.default(86400)
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new Tag("rf").default("afrf"), // Other values not supported
|
new Tag("rf")
|
||||||
|
.disabled()
|
||||||
|
.default("afrf"), // Other values not supported
|
||||||
];
|
];
|
||||||
|
|
||||||
static categories = {
|
static categories = {
|
||||||
|
|||||||
@ -11,46 +11,47 @@ export class SpfRecord {
|
|||||||
static fields = [
|
static fields = [
|
||||||
new VersionTerm("v", "spf1")
|
new VersionTerm("v", "spf1")
|
||||||
.required()
|
.required()
|
||||||
|
.hidden()
|
||||||
.pos(0),
|
.pos(0),
|
||||||
|
|
||||||
new DomainMechanism("include")
|
|
||||||
.label("Include")
|
|
||||||
.desc("Also apply the SPF records from these domains")
|
|
||||||
.multiple()
|
|
||||||
.pos(1),
|
|
||||||
|
|
||||||
new DomainMechanism("a")
|
new DomainMechanism("a")
|
||||||
.label("Domains")
|
.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)
|
.value(ValueRequirement.OPTIONAL)
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(1),
|
.pos(1),
|
||||||
|
|
||||||
new DomainMechanism("mx")
|
new DomainMechanism("mx")
|
||||||
.label("MX Records")
|
.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)
|
.value(ValueRequirement.OPTIONAL)
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new DomainMechanism("ptr")
|
new DomainMechanism("ptr")
|
||||||
.hidden()
|
.disabled()
|
||||||
.value(ValueRequirement.OPTIONAL)
|
.value(ValueRequirement.OPTIONAL)
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new IPv4Mechanism("ipv4")
|
new IPv4Mechanism("ip4")
|
||||||
.label("IPv4 addresses")
|
.label("IPv4 addresses")
|
||||||
.desc("Select these IP addresses")
|
.desc("Match these IP addresses")
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new IPv6Mechanism("ipv6")
|
new IPv6Mechanism("ip6")
|
||||||
.label("IPv6 addresses")
|
.label("IPv6 addresses")
|
||||||
.desc("Select these IP addresses")
|
.desc("Match these IP addresses")
|
||||||
.multiple()
|
.multiple()
|
||||||
.pos(2),
|
.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")
|
new DomainMechanism("exists")
|
||||||
.label("Exists")
|
.label("Exists")
|
||||||
.desc("Apply only if this domain exists (can be used with macro expansions)")
|
.desc("Apply only if this domain exists (can be used with macro expansions)")
|
||||||
@ -66,7 +67,7 @@ export class SpfRecord {
|
|||||||
|
|
||||||
new Modifier("redirect")
|
new Modifier("redirect")
|
||||||
.label("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")
|
.category("advanced")
|
||||||
.pos(4),
|
.pos(4),
|
||||||
|
|
||||||
@ -85,6 +86,16 @@ export class SpfRecord {
|
|||||||
this.text = text;
|
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() {
|
tokenize() {
|
||||||
return this.text.trim().split(/\s+/);
|
return this.text.trim().split(/\s+/);
|
||||||
}
|
}
|
||||||
@ -150,17 +161,4 @@ export class SpfRecord {
|
|||||||
lastPos = term.position;
|
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;
|
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() {
|
tokenize() {
|
||||||
return this.text
|
return this.text
|
||||||
.replace(/;\s*$/, "")
|
.replace(/;\s*$/, "")
|
||||||
@ -61,14 +72,4 @@ export class TagListRecord {
|
|||||||
|
|
||||||
return true;
|
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) {
|
validate(value) {
|
||||||
// TODO validate
|
// TODO validate
|
||||||
return true;
|
return !!value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,28 +9,28 @@ export class Mechanism extends Term {
|
|||||||
super(key);
|
super(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputQualifier() {
|
getInputQualifier(id) {
|
||||||
return document.getElementById(this.id + "-qualifier").value;
|
return document.getElementById(id + "-qualifier").value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputValue() {
|
getInputValue(id) {
|
||||||
return document.getElementById(this.id + "-value")?.value;
|
return document.getElementById(id + "-value")?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputHtml() {
|
getInputHtml(id) {
|
||||||
const noValue = this.valueRequirement === ValueRequirement.PROHIBITED;
|
const noValue = this.valueRequirement === ValueRequirement.PROHIBITED;
|
||||||
const placeholder = this.placeholder
|
const placeholder = this.placeholder
|
||||||
+ (this.valueRequirement === ValueRequirement.OPTIONAL ? " (Optional)" : "");
|
+ (this.valueRequirement === ValueRequirement.OPTIONAL ? " (Optional)" : "");
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<select id="${this.id}-qualifier">
|
<select id="${id}-qualifier">
|
||||||
${this.isRequired ? "" : `<option value=""><not set></option>`}
|
${this.isRequired ? "" : `<option value=""><not set></option>`}
|
||||||
<option value="+">Pass</option>
|
<option value="+">Pass</option>
|
||||||
<option value="-">Fail</option>
|
<option value="-">Fail</option>
|
||||||
<option value="~">Soft fail</option>
|
<option value="~">Soft fail</option>
|
||||||
<option value="?">Neutral</option>
|
<option value="?">Neutral</option>
|
||||||
</select>
|
</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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputValue() {
|
getInputValue(id) {
|
||||||
return document.getElementById(this.id).value;
|
return document.getElementById(id).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputHtml() {
|
getInputHtml(id) {
|
||||||
return `<input id="${this.id}" type="text" placeholder="example.com">`;
|
return `<input id="${id}" type="text" placeholder="example.com">`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,31 @@ export class Term extends Field {
|
|||||||
super(key)
|
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
|
// Virtual methods
|
||||||
|
|
||||||
getInputQualifier() {
|
getInputQualifier(fieldId) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export class VersionTerm extends Term {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputValue() {
|
getInputValue(id) {
|
||||||
return this.version;
|
return this.version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,10 +8,12 @@ export class ConstantTag extends Tag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate(value) {
|
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;
|
return this.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,15 +22,13 @@ export class DmarcUriListTag extends Tag {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputHtml() {
|
getInputHtml(id) {
|
||||||
return `<input id="${this.id}" type="email" name="${this.key}" placeholder="mail@example.com">`;
|
return `<input id="${id}" type="email" name="${this.key}" placeholder="mail@example.com">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputValue() {
|
getInputValue(id) {
|
||||||
if (!document.getElementById(this.id).value) {
|
if (!document.getElementById(id).value) return null;
|
||||||
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(", ")}`);
|
throw new ValidationError(`Invalid value for tag "${this.key}" - must be one of: ${this.values.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputHtml() {
|
getInputHtml(id) {
|
||||||
return `<select id="${this.id}" name="${this.key}" ${this.isRequired ? "required" : ""}>` +
|
return `<select id="${id}" name="${this.key}" ${this.isRequired ? "required" : ""}>` +
|
||||||
(this.isRequired || this.defaultValue ? "" : `<option value="" selected><not set></option>`) +
|
(this.isRequired || this.defaultValue ? "" : `<option value="" selected><not set></option>`) +
|
||||||
this.values.map((value, i) =>
|
this.values.map((value, i) =>
|
||||||
`<option value="${value}" ${this.defaultValue === value ? "selected" : ""}>
|
`<option value="${value}" ${this.defaultValue === value ? "selected" : ""}>
|
||||||
@ -26,8 +26,8 @@ export class EnumTag extends Tag {
|
|||||||
`</select>`;
|
`</select>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputValue() {
|
getInputValue(id) {
|
||||||
return document.getElementById(this.id).value;
|
return document.getElementById(id).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
options(options) {
|
options(options) {
|
||||||
|
|||||||
@ -22,11 +22,11 @@ export class IntTag extends Tag {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputHtml() {
|
getInputHtml(id) {
|
||||||
return `<input id="${this.id}" type="number" name="${this.key}" min="${this.min}" max="${this.max}" ${this.isRequired ? "required" : ""} placeholder="${this.defaultValue ?? ""}">`;
|
return `<input id="${id}" type="number" name="${this.key}" min="${this.min}" max="${this.max}" ${this.isRequired ? "required" : ""} placeholder="${this.defaultValue ?? ""}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputValue() {
|
getInputValue(id) {
|
||||||
return document.getElementById(this.id).value;
|
return document.getElementById(id).value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,22 @@ export class Tag extends Field {
|
|||||||
super(key);
|
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
|
// Virtual methods
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
|
|||||||
@ -8,7 +8,9 @@ const records = {
|
|||||||
|
|
||||||
const Record = records[location.pathname];
|
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);
|
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);
|
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) {
|
for (const field of fields) {
|
||||||
if (field.isHidden || !field.getInputHtml()) continue;
|
if (field.isDisabled) continue;
|
||||||
|
|
||||||
elem.innerHTML += `
|
const input = field.createInput(elem);
|
||||||
<label for="${field.key}">${field.displayName}</label>
|
inputs.push(input);
|
||||||
<p class="description">${field.description ?? ""}</p>
|
|
||||||
${field.getInputHtml()}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("form").onchange = generate;
|
document.getElementById("form").onchange = generate;
|
||||||
generate();
|
generate();
|
||||||
|
|
||||||
function generate(event) {
|
function generate() {
|
||||||
document.getElementById("record").value = Record.fieldsToString();
|
const record = Record.createFromFieldInputs(inputs);
|
||||||
|
|
||||||
|
document.getElementById("record").value = record.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("record").onclick = (e) => {
|
document.getElementById("record").onclick = (e) => {
|
||||||
|
|||||||
@ -22,9 +22,9 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b>Pass</b> (Default): Selected IP addresses are authorized to send emails from this domain<br>
|
<b>Pass</b> (Default): Matching 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>Fail</b>: Matching 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>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)
|
<b>Neutral</b>: Do not explicitly state whether the IP addresses are authorized or not (can be used for overriding other qualifiers)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user