// Sshwifty - A Web SSH client // // Copyright (C) 2019-2020 Rui NI // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import Exception from "./exception.js"; import * as subscribe from "../stream/subscribe.js"; export const NEXT_PROMPT = 1; export const NEXT_WAIT = 2; export const NEXT_DONE = 3; export class Result { /** * constructor * * @param {string} name Result type * @param {Info} info Result info * @param {object} control Result controller * @param {string} ui User interfact this command will use */ constructor(name, info, control, ui) { this.name = name; this.info = info; this.control = control; this.ui = ui; } } class Done { /** * constructor * * @param {object} data Step data * */ constructor(data) { this.s = !!data.success; this.d = data.successData; this.errorTitle = data.errorTitle; this.errorMessage = data.errorMessage; } /** * Return the error of current Done * * @returns {string} title * */ error() { return this.errorTitle; } /** * Return the error message of current Done * * @returns {string} message * */ message() { return this.errorMessage; } /** * Returns whether or not current Done is representing a success * * @returns {boolean} True when success, false otherwise */ success() { return this.s; } /** * Returns final data * * @returns {Result} Successful result */ data() { return this.d; } } class Wait { /** * constructor * * @param {object} data Step data * */ constructor(data) { this.t = data.title; this.m = data.message; } /** * Return the title of current Wait * * @returns {string} title * */ title() { return this.t; } /** * Return the message of current Wait * * @returns {string} message * */ message() { return this.m; } } const defField = { name: "", description: "", type: "", value: "", example: "", verify(v) { return "OK"; } }; /** * Create a Prompt field * * @param {object} def Field default value * @param {object} f Field value * * @returns {object} Field data * * @throws {Exception} When input field is invalid * */ export function field(def, f) { let n = {}; for (let i in def) { n[i] = def[i]; } for (let i in f) { if (typeof n[i] !== typeof f[i]) { throw new Exception( 'Field data type for "' + i + '" was not unmatched. Expecting "' + typeof def[i] + '", got "' + typeof f[i] + '" instead' ); } n[i] = f[i]; } if (!n["name"]) { throw new Exception('Field "name" must be specified'); } return n; } /** * Build a group of field value * * @param {object} definitions Definition of a group of fields * @param {array} fs Data of the field group * * @returns {array} Result fields * * @throws {Exception} When input field is invalid * */ export function fields(definitions, fs) { let fss = []; for (let i in fs) { if (!fs[i]["name"]) { throw new Exception('Field "name" must be specified'); } if (!definitions[fs[i].name]) { throw new Exception('Undefined field "' + fs[i].name + '"'); } fss.push(field(definitions[fs[i].name], fs[i])); } return fss; } class Prompt { /** * constructor * * @param {object} data Step data * * @throws {Exception} If the field verify is not a function while * not null */ constructor(data) { this.t = data.title; this.m = data.message; this.a = data.actionText; this.r = data.respond; this.c = data.cancel; this.i = []; this.f = {}; for (let i in data.inputs) { let f = field(defField, data.inputs[i]); this.i.push(f); this.f[data.inputs[i].name.toLowerCase()] = { value: f.value, verify: f.verify }; } } /** * Return the title of current Prompt * * @returns {string} title * */ title() { return this.t; } /** * Return the message of current Prompt * * @returns {string} message * */ message() { return this.m; } /** * Return the input field of current prompt * * @returns {array} Input fields * */ inputs() { let inputs = []; for (let i in this.i) { inputs.push(this.i[i]); } return inputs; } /** * Returns the name of the action * * @returns {string} Action name * */ actionText() { return this.a; } /** * Receive the submit of current prompt * * @param {object} inputs Input value * * @returns {any} The result of the step responder * * @throws {Exception} When the field is undefined or invalid * */ submit(inputs) { let fields = {}; for (let i in this.f) { fields[i] = this.f[i].value; } for (let i in inputs) { let k = i.toLowerCase(); if (typeof fields[k] === "undefined") { throw new Exception('Field "' + k + '" is undefined'); } try { this.f[k].verify(inputs[i]); } catch (e) { throw new Exception('Field "' + k + '" is invalid: ' + e); } fields[k] = inputs[i]; } return this.r(fields); } /** * Cancel current wait operation * */ cancel() { return this.c(); } } /** * Create a Wizard step * * @param {string} type Step type * @param {object} data Step data * * @returns {object} Step data * */ function next(type, data) { return { type() { return type; }, data() { return data; } }; } /** * Create data for a Done step of the wizard * * @param {boolean} success * @param {Success} successData * @param {string} errorTitle * @param {string} errorMessage * * @returns {object} Done step data * */ export function done(success, successData, errorTitle, errorMessage) { return next(NEXT_DONE, { success: success, successData: successData, errorTitle: errorTitle, errorMessage: errorMessage }); } /** * Create data for a Wait step of the wizard * * @param {string} title Waiter title * @param {message} message Waiter message * * @returns {object} Done step data * */ export function wait(title, message) { return next(NEXT_WAIT, { title: title, message: message }); } /** * Create data for a Prompt step of the wizard * * @param {string} title Title of the prompt * @param {string} message Message of the prompt * @param {string} actionText Text of the action (button) * @param {function} respond Respond callback * @param {function} cancel cancel handler * @param {object} inputs Input field objects * * @returns {object} Prompt step data * */ export function prompt(title, message, actionText, respond, cancel, inputs) { return next(NEXT_PROMPT, { title: title, message: message, actionText: actionText, inputs: inputs, respond: respond, cancel: cancel }); } class Next { /** * constructor * * @param {object} data Step data */ constructor(data) { this.t = data.type(); this.d = data.data(); } /** * Return step type * * @returns {string} Step type */ type() { return this.t; } /** * Return step data * * @returns {Done|Prompt} Step data * * @throws {Exception} When the step type is unknown * */ data() { switch (this.type()) { case NEXT_PROMPT: return new Prompt(this.d); case NEXT_WAIT: return new Wait(this.d); case NEXT_DONE: return new Done(this.d); default: throw new Exception("Unknown data type"); } } } class Wizard { /** * constructor * * @param {function} builder Command builder * @param {subscribe.Subscribe} subs Wizard step subscriber * @param {function} done Callback which will be called when the wizard * is done * */ constructor(built, subs, done) { this.built = built; this.subs = subs; this.done = done; this.closed = false; } /** * Return the Next step * * @returns {Next} Next step * * @throws {Exception} When wizard is closed * */ async next() { if (this.closed) { throw new Exception("Wizard already closed, no next step is available"); } let n = await this.subs.subscribe(); if (n.type() === NEXT_DONE) { this.close(); this.done(n); } return new Next(n); } /** * Return whether or not the command is started * * @returns {boolean} True when the command already started, false otherwise * */ started() { return this.built.started(); } /** * Return the name of the control info of current wizard * * @returns {object} * */ control() { return this.built.control(); } /** * Close current wizard * * @returns {any} Close result * */ close() { if (this.closed) { return; } this.closed = true; return this.built.close(); } } export class Info { /** * constructor * * @param {Builder} info Builder info * */ constructor(info) { this.type = info.name(); this.info = info.description(); this.tcolor = info.color(); } /** * Return command name * * @returns {string} Command name * */ name() { return this.type; } /** * Return command description * * @returns {string} Command description * */ description() { return this.info; } /** * Return the theme color of the command * * @returns {string} Command name * */ color() { return this.tcolor; } } class Builder { /** * constructor * * @param {object} command Command builder * */ constructor(command) { this.cid = command.id(); this.builder = (n, i, r, u, y, x, l) => { return command.builder(n, i, r, u, y, x, l); }; this.launchCmd = (n, i, r, u, y, x) => { return command.launch(n, i, r, u, y, x); }; this.launcherCmd = c => { return command.launcher(c); }; this.type = command.name(); this.info = command.description(); this.tcolor = command.color(); } /** * Return the command ID * * @returns {number} Command ID * */ id() { return this.cid; } /** * Return command name * * @returns {string} Command name * */ name() { return this.type; } /** * Return command description * * @returns {string} Command description * */ description() { return this.info; } /** * Return the theme color of the command * * @returns {string} Command name * */ color() { return this.tcolor; } /** * Build command wizard * * @param {stream.Streams} streams * @param {controls.Controls} controls * @param {history.History} history * @param {object} config * @param {object} session * @param {function} done Callback which will be called when wizard is done * * @returns {Wizard} Command wizard * */ build(streams, controls, history, config, session, done) { let subs = new subscribe.Subscribe(); return new Wizard( this.builder( new Info(this), config, session, streams, subs, controls, history ), subs, done ); } /** * Launch command wizard out of given launcher string * * @param {stream.Streams} streams * @param {controls.Controls} controls * @param {history.History} history * @param {string} launcher Launcher format * @param {function} done Callback which will be called when launching is done * * @returns {Wizard} Command wizard * */ launch(streams, controls, history, launcher, done) { let subs = new subscribe.Subscribe(); return new Wizard( this.launchCmd( new Info(this), decodeURI(launcher), streams, subs, controls, history ), subs, done ); } /** * Build launcher string out of given config * * @param {object} config Configuration object * * @return {string} Launcher string */ launcher(config) { return this.name() + ":" + encodeURI(this.launcherCmd(config)); } } export class Commands { /** * constructor * * @param {[]object} commands Command array * */ constructor(commands) { this.commands = []; for (let i in commands) { this.commands.push(new Builder(commands[i])); } } /** * Return all commands * * @returns {[]Builder} A group of command * */ all() { return this.commands; } /** * Select one command * * @param {number} id Command ID * * @returns {Builder} Command builder * */ select(id) { return this.commands[id]; } }