import DataModel from "./abstract/data.mjs"; import * as fields from "./data/fields.mjs"; import {CSS_THEMES, SOFTWARE_UPDATE_CHANNELS} from "./constants.mjs"; import {isNewerVersion} from "./utils/helpers.mjs"; /** @namespace config */ /** * A data model definition which describes the application configuration options. * These options are persisted in the user data Config folder in the options.json file. * The server-side software extends this class and provides additional validations and * @extends {DataModel} * @memberof config * * @property {string|null} adminPassword The server administrator password (obscured) * @property {string|null} awsConfig The relative path (to Config) of an AWS configuration file * @property {boolean} compressStatic Whether to compress static files? True by default * @property {string} dataPath The absolute path of the user data directory (obscured) * @property {boolean} fullscreen Whether the application should automatically start in fullscreen mode? * @property {string|null} hostname A custom hostname applied to internet invitation addresses and URLs * @property {string} language The default language for the application * @property {string|null} localHostname A custom hostname applied to local invitation addresses * @property {string|null} passwordSalt A custom salt used for hashing user passwords (obscured) * @property {number} port The port on which the server is listening * @property {number} [protocol] The Internet Protocol version to use, either 4 or 6. * @property {number} proxyPort An external-facing proxied port used for invitation addresses and URLs * @property {boolean} proxySSL Is the application running in SSL mode at a reverse-proxy level? * @property {string|null} routePrefix A URL path part which prefixes normal application routing * @property {string|null} sslCert The relative path (to Config) of a used SSL certificate * @property {string|null} sslKey The relative path (to Config) of a used SSL key * @property {string} updateChannel The current application update channel * @property {boolean} upnp Is UPNP activated? * @property {number} upnpLeaseDuration The duration in seconds of a UPNP lease, if UPNP is active * @property {string} world A default world name which starts automatically on launch */ class ApplicationConfiguration extends DataModel { static defineSchema() { return { adminPassword: new fields.StringField({required: true, blank: false, nullable: true, initial: null, label: "SETUP.AdminPasswordLabel", hint: "SETUP.AdminPasswordHint"}), awsConfig: new fields.StringField({label: "SETUP.AWSLabel", hint: "SETUP.AWSHint", blank: false, nullable: true, initial: null}), compressStatic: new fields.BooleanField({initial: true, label: "SETUP.CompressStaticLabel", hint: "SETUP.CompressStaticHint"}), compressSocket: new fields.BooleanField({initial: true, label: "SETUP.CompressSocketLabel", hint: "SETUP.CompressSocketHint"}), cssTheme: new fields.StringField({blank: false, choices: CSS_THEMES, initial: "foundry", label: "SETUP.CSSTheme", hint: "SETUP.CSSThemeHint"}), dataPath: new fields.StringField({label: "SETUP.DataPathLabel", hint: "SETUP.DataPathHint"}), deleteNEDB: new fields.BooleanField({label: "SETUP.DeleteNEDBLabel", hint: "SETUP.DeleteNEDBHint"}), fullscreen: new fields.BooleanField({initial: false}), hostname: new fields.StringField({required: true, blank: false, nullable: true, initial: null}), hotReload: new fields.BooleanField({initial: false, label: "SETUP.HotReloadLabel", hint: "SETUP.HotReloadHint"}), language: new fields.StringField({required: true, blank: false, initial: "en.core", label: "SETUP.DefaultLanguageLabel", hint: "SETUP.DefaultLanguageHint"}), localHostname: new fields.StringField({required: true, blank: false, nullable: true, initial: null}), passwordSalt: new fields.StringField({required: true, blank: false, nullable: true, initial: null}), port: new fields.NumberField({required: true, nullable: false, integer: true, initial: 30000, validate: this._validatePort, label: "SETUP.PortLabel", hint: "SETUP.PortHint"}), protocol: new fields.NumberField({integer: true, choices: [4, 6], nullable: true}), proxyPort: new fields.NumberField({required: true, nullable: true, integer: true, initial: null}), proxySSL: new fields.BooleanField({initial: false}), routePrefix: new fields.StringField({required: true, blank: false, nullable: true, initial: null}), sslCert: new fields.StringField({label: "SETUP.SSLCertLabel", hint: "SETUP.SSLCertHint", blank: false, nullable: true, initial: null}), sslKey: new fields.StringField({label: "SETUP.SSLKeyLabel", blank: false, nullable: true, initial: null}), telemetry: new fields.BooleanField({required: false, initial: undefined, label: "SETUP.Telemetry", hint: "SETUP.TelemetryHint"}), updateChannel: new fields.StringField({required: true, choices: SOFTWARE_UPDATE_CHANNELS, initial: "stable"}), upnp: new fields.BooleanField({initial: true}), upnpLeaseDuration: new fields.NumberField(), world: new fields.StringField({required: true, blank: false, nullable: true, initial: null, label: "SETUP.WorldLabel", hint: "SETUP.WorldHint"}), noBackups: new fields.BooleanField({required: false}) } } /* ----------------------------------------- */ /** @override */ static migrateData(data) { // Backwards compatibility for -v9 update channels data.updateChannel = { "alpha": "prototype", "beta": "testing", "release": "stable" }[data.updateChannel] || data.updateChannel; // Backwards compatibility for awsConfig of true if ( data.awsConfig === true ) data.awsConfig = ""; return data; } /* ----------------------------------------- */ /** * Validate a port assignment. * @param {number} port The requested port * @throws An error if the requested port is invalid * @private */ static _validatePort(port) { if ( !Number.isNumeric(port) || ((port < 1024) && ![80, 443].includes(port)) || (port > 65535) ) { throw new Error(`The application port must be an integer, either 80, 443, or between 1024 and 65535`); } } } /* ----------------------------------------- */ /** * A data object which represents the details of this Release of Foundry VTT * @extends {DataModel} * @memberof config * * @property {number} generation The major generation of the Release * @property {number} [maxGeneration] The maximum available generation of the software. * @property {number} [maxStableGeneration] The maximum available stable generation of the software. * @property {string} channel The channel the Release belongs to, such as "stable" * @property {string} suffix An optional appended string display for the Release * @property {number} build The internal build number for the Release * @property {number} time When the Release was released * @property {number} [node_version] The minimum required Node.js major version * @property {string} [notes] Release notes for the update version * @property {string} [download] A temporary download URL where this version may be obtained */ class ReleaseData extends DataModel { /** @override */ static defineSchema() { return { generation: new fields.NumberField({required: true, nullable: false, integer: true, min: 1}), maxGeneration: new fields.NumberField({ required: false, nullable: false, integer: true, min: 1, initial: () => this.generation }), maxStableGeneration: new fields.NumberField({ required: false, nullable: false, integer: true, min: 1, initial: () => this.generation }), channel: new fields.StringField({choices: SOFTWARE_UPDATE_CHANNELS, blank: false}), suffix: new fields.StringField(), build: new fields.NumberField({required: true, nullable: false, integer: true}), time: new fields.NumberField({nullable: false, initial: Date.now}), node_version: new fields.NumberField({required: true, nullable: false, integer: true, min: 10}), notes: new fields.StringField(), download: new fields.StringField() } } /* ----------------------------------------- */ /** * A formatted string for shortened display, such as "Version 9" * @return {string} */ get shortDisplay() { return `Version ${this.generation} Build ${this.build}`; } /** * A formatted string for general display, such as "V9 Prototype 1" or "Version 9" * @return {string} */ get display() { return ["Version", this.generation, this.suffix].filterJoin(" "); } /** * A formatted string for Version compatibility checking, such as "9.150" * @return {string} */ get version() { return `${this.generation}.${this.build}`; } /* ----------------------------------------- */ /** @override */ toString() { return this.shortDisplay; } /* ----------------------------------------- */ /** * Is this ReleaseData object newer than some other version? * @param {string|ReleaseData} other Some other version to compare against * @returns {boolean} Is this ReleaseData a newer version? */ isNewer(other) { const version = other instanceof ReleaseData ? other.version : other; return isNewerVersion(this.version, version); } /* ----------------------------------------- */ /** * Is this ReleaseData object a newer generation than some other version? * @param {string|ReleaseData} other Some other version to compare against * @returns {boolean} Is this ReleaseData a newer generation? */ isGenerationalChange(other) { if ( !other ) return true; let generation; if ( other instanceof ReleaseData ) generation = other.generation.toString(); else { other = String(other); const parts = other.split("."); if ( parts[0] === "0" ) parts.shift() generation = parts[0]; } return isNewerVersion(this.generation, generation); } } // Module Exports export { ApplicationConfiguration, ReleaseData }