adhoc-commands.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import "./autocomplete.js"
  2. import log from "@converse/headless/log";
  3. import sizzle from "sizzle";
  4. import { CustomElement } from './element.js';
  5. import { __ } from '@converse/headless/i18n';
  6. import { api, converse } from "@converse/headless/converse-core";
  7. import { html } from "lit-html";
  8. import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
  9. const { Strophe, $iq } = converse.env;
  10. const u = converse.env.utils;
  11. const tpl_command_form = (o, command) => {
  12. const i18n_hide = __('Hide');
  13. const i18n_run = __('Execute');
  14. return html`
  15. <form @submit=${o.runCommand}>
  16. ${ command.alert ? html`<div class="alert alert-${command.alert_type}" role="alert">${command.alert}</div>` : '' }
  17. <fieldset class="form-group">
  18. <input type="hidden" name="command_node" value="${command.node}"/>
  19. <input type="hidden" name="command_jid" value="${command.jid}"/>
  20. <p class="form-help">${command.instructions}</p>
  21. <!-- Fields are generated internally, with xForm2webForm -->
  22. ${ command.fields.map(field => unsafeHTML(field)) }
  23. </fieldset>
  24. <fieldset>
  25. <input type="submit" class="btn btn-primary" value="${i18n_run}">
  26. <input type="button" class="btn btn-secondary button-cancel" value="${i18n_hide}" @click=${o.hideCommandForm}>
  27. </fieldset>
  28. </form>
  29. `;
  30. }
  31. const tpl_command = (o, command) => html`
  32. <li class="room-item list-group-item">
  33. <div class="available-chatroom d-flex flex-row">
  34. <a class="open-room available-room w-100"
  35. @click=${o.toggleCommandForm}
  36. data-command-node="${command.node}"
  37. data-command-jid="${command.jid}"
  38. data-command-name="${command.name}"
  39. title="${command.name}"
  40. href="#">${command.name || command.jid}</a>
  41. </div>
  42. ${ command.node === o.showform ? tpl_command_form(o, command) : '' }
  43. </li>
  44. `;
  45. async function getAutoCompleteList () {
  46. const models = [...(await api.rooms.get()), ...(await api.contacts.get())];
  47. const jids = [...new Set(models.map(o => Strophe.getDomainFromJid(o.get('jid'))))];
  48. return jids;
  49. }
  50. const tpl_adhoc = (o) => {
  51. const i18n_choose_service = __('On which entity do you want to run commands?');
  52. const i18n_choose_service_instructions = __(
  53. 'Certain XMPP services and entities allow privileged users to execute ad-hoc commands on them.');
  54. const i18n_commands_found = __('Commands found');
  55. const i18n_fetch_commands = __('List available commands');
  56. const i18n_jid_placeholder = __('XMPP Address');
  57. const i18n_no_commands_found = __('No commands found');
  58. return html`
  59. ${ o.alert ? html`<div class="alert alert-${o.alert_type}" role="alert">${o.alert}</div>` : '' }
  60. <form class="converse-form" @submit=${o.fetchCommands}>
  61. <fieldset class="form-group">
  62. <label>
  63. ${i18n_choose_service}
  64. <p class="form-help">${i18n_choose_service_instructions}</p>
  65. <converse-autocomplete
  66. .getAutoCompleteList="${getAutoCompleteList}"
  67. placeholder="${i18n_jid_placeholder}"
  68. name="jid"/>
  69. </label>
  70. </fieldset>
  71. <fieldset class="form-group">
  72. <input type="submit" class="btn btn-primary" value="${i18n_fetch_commands}">
  73. </fieldset>
  74. ${ o.view === 'list-commands' ? html`
  75. <fieldset class="form-group">
  76. <ul class="list-group">
  77. <li class="list-group-item active">${ o.commands.length ? i18n_commands_found : i18n_no_commands_found }:</li>
  78. ${ o.commands.map(cmd => tpl_command(o, cmd)) }
  79. </ul>
  80. </fieldset>`
  81. : '' }
  82. </form>
  83. `;
  84. }
  85. async function fetchCommandForm (command) {
  86. const node = command.node;
  87. const jid = command.jid;
  88. const stanza = $iq({
  89. 'type': 'set',
  90. 'to': jid
  91. }).c('command', {
  92. 'xmlns': Strophe.NS.ADHOC,
  93. 'node': node,
  94. 'action': 'execute'
  95. });
  96. try {
  97. const iq = await api.sendIQ(stanza);
  98. const cmd_el = sizzle(`command[xmlns="${Strophe.NS.ADHOC}"]`, iq).pop();
  99. command.sessionid = cmd_el.getAttribute('sessionid');
  100. command.instructions = sizzle('x[type="form"][xmlns="jabber:x:data"] instructions', cmd_el).pop()?.textContent;
  101. command.fields = sizzle('x[type="form"][xmlns="jabber:x:data"] field', cmd_el)
  102. .map(f => u.xForm2webForm(f, cmd_el));
  103. } catch (e) {
  104. if (e === null) {
  105. log.error(`Error: timeout while trying to execute command for ${jid}`);
  106. } else {
  107. log.error(`Error while trying to execute command for ${jid}`);
  108. log.error(e);
  109. }
  110. command.fields = [];
  111. }
  112. }
  113. export default class AdHocCommands extends CustomElement {
  114. static get properties () {
  115. return {
  116. 'alert': { type: String },
  117. 'alert_type': { type: String },
  118. 'nonce': { type: String }, // Used to force re-rendering
  119. 'showform': { type: String },
  120. 'view': { type: String },
  121. }
  122. }
  123. constructor () {
  124. super();
  125. this.view = 'choose-service';
  126. this.showform = '';
  127. this.commands = [];
  128. }
  129. render () {
  130. return tpl_adhoc({
  131. 'alert': this.alert,
  132. 'alert_type': this.alert_type,
  133. 'commands': this.commands,
  134. 'fetchCommands': ev => this.fetchCommands(ev),
  135. 'hideCommandForm': ev => this.hideCommandForm(ev),
  136. 'runCommand': ev => this.runCommand(ev),
  137. 'showform': this.showform,
  138. 'toggleCommandForm': ev => this.toggleCommandForm(ev),
  139. 'view': this.view,
  140. });
  141. }
  142. async fetchCommands (ev) {
  143. ev.preventDefault();
  144. delete this.alert_type;
  145. delete this.alert;
  146. const form_data = new FormData(ev.target);
  147. const jid = form_data.get('jid').trim();
  148. let supported;
  149. try {
  150. supported = await api.disco.supports(Strophe.NS.ADHOC, jid)
  151. } catch (e) {
  152. log.error(e);
  153. }
  154. if (supported) {
  155. try {
  156. this.commands = await api.adhoc.getCommands(jid);
  157. this.view = 'list-commands';
  158. } catch (e) {
  159. log.error(e);
  160. this.alert_type = 'danger';
  161. this.alert = __('Sorry, an error occurred while looking for commands on that entity.');
  162. this.commands = [];
  163. log.error(e);
  164. return;
  165. }
  166. } else {
  167. this.alert_type = 'danger';
  168. this.alert = __("The specified entity doesn't support ad-hoc commands");
  169. }
  170. }
  171. async toggleCommandForm (ev) {
  172. ev.preventDefault();
  173. const node = ev.target.getAttribute('data-command-node');
  174. const cmd = this.commands.filter(c => c.node === node)[0];
  175. this.showform !== node && await fetchCommandForm(cmd);
  176. this.showform = node;
  177. }
  178. hideCommandForm (ev) {
  179. ev.preventDefault();
  180. this.showform = ''
  181. }
  182. async runCommand (ev) {
  183. ev.preventDefault();
  184. const form_data = new FormData(ev.target);
  185. const jid = form_data.get('command_jid').trim();
  186. const node = form_data.get('command_node').trim();
  187. const cmd = this.commands.filter(c => c.node === node)[0];
  188. const inputs = sizzle(':input:not([type=button]):not([type=submit])', ev.target);
  189. const configArray = inputs
  190. .filter(i => !['command_jid', 'command_node'].includes(i.getAttribute('name')))
  191. .map(u.webForm2xForm);
  192. const iq = $iq({to: jid, type: "set"})
  193. .c("command", {
  194. 'sessionid': cmd.sessionid,
  195. 'node': cmd.node,
  196. 'xmlns': Strophe.NS.ADHOC
  197. }).c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
  198. configArray.forEach(node => iq.cnode(node).up());
  199. let result;
  200. try {
  201. result = await api.sendIQ(iq);
  202. } catch (e) {
  203. cmd.alert_type = 'danger';
  204. cmd.alert = __('Sorry, an error occurred while trying to execute the command. See the developer console for details');
  205. log.error('Error while trying to execute an ad-hoc command');
  206. log.error(e);
  207. }
  208. if (result) {
  209. cmd.alert = result.querySelector('note')?.textContent;
  210. } else {
  211. cmd.alert = 'Done';
  212. }
  213. cmd.alert_type = 'primary';
  214. this.nonce = u.getUniqueId();
  215. }
  216. }
  217. api.elements.define('converse-adhoc-commands', AdHocCommands);