217 lines
7.6 KiB
JavaScript
217 lines
7.6 KiB
JavaScript
![]() |
import "./autocomplete.js"
|
||
|
import { __ } from '@converse/headless/i18n';
|
||
|
import { CustomElement } from './element.js';
|
||
|
import { api } from "@converse/headless/converse-core";
|
||
|
import { html } from "lit-html";
|
||
|
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
||
|
import log from "@converse/headless/log";
|
||
|
import sizzle from "sizzle";
|
||
|
|
||
|
const { Strophe, $iq } = window.converse.env;
|
||
|
const u = window.converse.env.utils;
|
||
|
|
||
|
const i18n_hide = __('Hide');
|
||
|
const i18n_choose_service = __('On which entity do you want to run commands?');
|
||
|
const i18n_choose_service_instructions = __(
|
||
|
'Certain XMPP services and entities allow privileged users to execute ad-hoc commands on them.');
|
||
|
const i18n_commands_found = __('Commands found');
|
||
|
const i18n_fetch_commands = __('List available commands');
|
||
|
const i18n_jid_placeholder = __('XMPP Address');
|
||
|
const i18n_no_commands_found = __('No commands found');
|
||
|
const i18n_run = __('Execute');
|
||
|
|
||
|
|
||
|
const tpl_command_form = (o, command) => html`
|
||
|
<form @submit=${o.runCommand}>
|
||
|
${ command.alert ? html`<div class="alert alert-${command.alert_type}" role="alert">${command.alert}</div>` : '' }
|
||
|
<fieldset class="form-group">
|
||
|
<input type="hidden" name="command_node" value="${command.node}"/>
|
||
|
<input type="hidden" name="command_jid" value="${command.jid}"/>
|
||
|
|
||
|
<p class="form-help">${command.instructions}</p>
|
||
|
<!-- Fields are generated internally, with xForm2webForm -->
|
||
|
${ command.fields.map(field => unsafeHTML(field)) }
|
||
|
</fieldset>
|
||
|
<fieldset>
|
||
|
<input type="submit" class="btn btn-primary" value="${i18n_run}">
|
||
|
<input type="button" class="btn btn-secondary button-cancel" value="${i18n_hide}" @click=${o.hideCommandForm}>
|
||
|
</fieldset>
|
||
|
</form>
|
||
|
`;
|
||
|
|
||
|
|
||
|
const tpl_command = (o, command) => html`
|
||
|
<li class="room-item list-group-item">
|
||
|
<div class="available-chatroom d-flex flex-row">
|
||
|
<a class="open-room available-room w-100"
|
||
|
@click=${o.toggleCommandForm}
|
||
|
data-command-node="${command.node}"
|
||
|
data-command-jid="${command.jid}"
|
||
|
data-command-name="${command.name}"
|
||
|
title="${command.name}"
|
||
|
href="#">${command.name || command.jid}</a>
|
||
|
</div>
|
||
|
${ command.node === o.showform ? tpl_command_form(o, command) : '' }
|
||
|
</li>
|
||
|
`;
|
||
|
|
||
|
|
||
|
async function getAutoCompleteList () {
|
||
|
const models = [...(await api.rooms.get()), ...(await api.contacts.get())];
|
||
|
const jids = [...new Set(models.map(o => Strophe.getDomainFromJid(o.get('jid'))))];
|
||
|
return jids;
|
||
|
}
|
||
|
|
||
|
const tpl_adhoc = (o) => html`
|
||
|
<form class="converse-form" @submit=${o.fetchCommands}>
|
||
|
<fieldset class="form-group">
|
||
|
<label>
|
||
|
${i18n_choose_service}
|
||
|
<p class="form-help">${i18n_choose_service_instructions}</p>
|
||
|
<converse-autocomplete
|
||
|
.getAutoCompleteList="${getAutoCompleteList}"
|
||
|
placeholder="${i18n_jid_placeholder}"
|
||
|
name="jid"/>
|
||
|
</label>
|
||
|
</fieldset>
|
||
|
<fieldset class="form-group">
|
||
|
<input type="submit" class="btn btn-primary" value="${i18n_fetch_commands}">
|
||
|
</fieldset>
|
||
|
${ o.view === 'list-commands' ? html`
|
||
|
<fieldset class="form-group">
|
||
|
<ul class="list-group">
|
||
|
<li class="list-group-item active">${ o.commands.length ? i18n_commands_found : i18n_no_commands_found }:</li>
|
||
|
${ o.commands.map(cmd => tpl_command(o, cmd)) }
|
||
|
</ul>
|
||
|
</fieldset>`
|
||
|
: '' }
|
||
|
|
||
|
</form>
|
||
|
`;
|
||
|
|
||
|
|
||
|
async function fetchCommandForm (command) {
|
||
|
const node = command.node;
|
||
|
const jid = command.jid;
|
||
|
const stanza = $iq({
|
||
|
'type': 'set',
|
||
|
'to': jid
|
||
|
}).c('command', {
|
||
|
'xmlns': Strophe.NS.ADHOC,
|
||
|
'node': node,
|
||
|
'action': 'execute'
|
||
|
});
|
||
|
try {
|
||
|
const iq = await api.sendIQ(stanza);
|
||
|
const cmd_el = sizzle(`command[xmlns="${Strophe.NS.ADHOC}"]`, iq).pop();
|
||
|
command.sessionid = cmd_el.getAttribute('sessionid');
|
||
|
command.instructions = sizzle('x[type="form"][xmlns="jabber:x:data"] instructions', cmd_el).pop()?.textContent;
|
||
|
command.fields = sizzle('x[type="form"][xmlns="jabber:x:data"] field', cmd_el)
|
||
|
.map(f => u.xForm2webForm(f, cmd_el));
|
||
|
|
||
|
} catch (e) {
|
||
|
if (e === null) {
|
||
|
log.error(`Error: timeout while trying to execute command for ${jid}`);
|
||
|
} else {
|
||
|
log.error(`Error while trying to execute command for ${jid}`);
|
||
|
log.error(e);
|
||
|
}
|
||
|
command.fields = [];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
export class AdHocCommands extends CustomElement {
|
||
|
|
||
|
static get properties () {
|
||
|
return {
|
||
|
'view': { type: String },
|
||
|
'showform': { type: String },
|
||
|
'nonce': { type: String } // Used to force re-rendering
|
||
|
}
|
||
|
}
|
||
|
|
||
|
constructor () {
|
||
|
super();
|
||
|
this.view = 'choose-service';
|
||
|
this.showform = '';
|
||
|
this.commands = [];
|
||
|
}
|
||
|
|
||
|
render () {
|
||
|
return tpl_adhoc({
|
||
|
'commands': this.commands,
|
||
|
'fetchCommands': ev => this.fetchCommands(ev),
|
||
|
'hideCommandForm': ev => this.hideCommandForm(ev),
|
||
|
'runCommand': ev => this.runCommand(ev),
|
||
|
'showform': this.showform,
|
||
|
'toggleCommandForm': ev => this.toggleCommandForm(ev),
|
||
|
'view': this.view,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async fetchCommands (ev) {
|
||
|
ev.preventDefault();
|
||
|
const form_data = new FormData(ev.target);
|
||
|
const jid = form_data.get('jid').trim();
|
||
|
if (await api.disco.supports(Strophe.NS.ADHOC, jid)) {
|
||
|
this.commands = await api.adhoc.getCommands(jid);
|
||
|
this.view = 'list-commands';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async toggleCommandForm (ev) {
|
||
|
ev.preventDefault();
|
||
|
const node = ev.target.getAttribute('data-command-node');
|
||
|
const cmd = this.commands.filter(c => c.node === node)[0];
|
||
|
this.showform !== node && await fetchCommandForm(cmd);
|
||
|
this.showform = node;
|
||
|
}
|
||
|
|
||
|
hideCommandForm (ev) {
|
||
|
ev.preventDefault();
|
||
|
this.showform = ''
|
||
|
}
|
||
|
|
||
|
async runCommand (ev) {
|
||
|
ev.preventDefault();
|
||
|
const form_data = new FormData(ev.target);
|
||
|
const jid = form_data.get('command_jid').trim();
|
||
|
const node = form_data.get('command_node').trim();
|
||
|
|
||
|
const cmd = this.commands.filter(c => c.node === node)[0];
|
||
|
const inputs = sizzle(':input:not([type=button]):not([type=submit])', ev.target);
|
||
|
const configArray = inputs
|
||
|
.filter(i => !['command_jid', 'command_node'].includes(i.getAttribute('name')))
|
||
|
.map(u.webForm2xForm);
|
||
|
|
||
|
const iq = $iq({to: jid, type: "set"})
|
||
|
.c("command", {
|
||
|
'sessionid': cmd.session,
|
||
|
'node': cmd.node,
|
||
|
'xmlns': Strophe.NS.ADHOC
|
||
|
}).c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
|
||
|
configArray.forEach(node => iq.cnode(node).up());
|
||
|
|
||
|
let result;
|
||
|
try {
|
||
|
result = await api.sendIQ(iq);
|
||
|
} catch (e) {
|
||
|
cmd.alert_type = 'danger';
|
||
|
cmd.alert = __('Sorry, an error occurred while trying to execute the command. See the developer console for details');
|
||
|
log.error('Error while trying to execute an ad-hoc command');
|
||
|
log.error(e);
|
||
|
}
|
||
|
|
||
|
if (result) {
|
||
|
cmd.alert = result.querySelector('note')?.textContent;
|
||
|
} else {
|
||
|
cmd.alert = 'Done';
|
||
|
}
|
||
|
cmd.alert_type = 'primary';
|
||
|
this.nonce = u.getUniqueId();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
window.customElements.define('converse-adhoc-commands', AdHocCommands);
|