These commands introduce security concerns because how they interact with encrypted messages. With MAM and Carbons installed on nearly every server these Adhoc commands became not useful anymore
433 lines
15 KiB
Python
433 lines
15 KiB
Python
# -*- coding:utf-8 -*-
|
|
## src/common/commands.py
|
|
##
|
|
## Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
|
|
## Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
|
|
## Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
|
|
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
|
|
## Stephan Erb <steve-e AT h3c.de>
|
|
##
|
|
## This file is part of Gajim.
|
|
##
|
|
## Gajim is free software; you can redistribute it and/or modify
|
|
## it under the terms of the GNU General Public License as published
|
|
## by the Free Software Foundation; version 3 only.
|
|
##
|
|
## Gajim 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 General Public License for more details.
|
|
##
|
|
## You should have received a copy of the GNU General Public License
|
|
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
|
##
|
|
|
|
import nbxmpp
|
|
from gajim.common import helpers
|
|
from gajim.common import dataforms
|
|
from gajim.common import app
|
|
|
|
import logging
|
|
log = logging.getLogger('gajim.c.commands')
|
|
|
|
class AdHocCommand:
|
|
commandnode = 'command'
|
|
commandname = 'The Command'
|
|
commandfeatures = (nbxmpp.NS_DATA,)
|
|
|
|
@staticmethod
|
|
def isVisibleFor(samejid):
|
|
"""
|
|
This returns True if that command should be visible and invokable for
|
|
others
|
|
|
|
samejid - True when command is invoked by an entity with the same bare
|
|
jid.
|
|
"""
|
|
return True
|
|
|
|
def __init__(self, conn, jid, sessionid):
|
|
self.connection = conn
|
|
self.jid = jid
|
|
self.sessionid = sessionid
|
|
|
|
def buildResponse(self, request, status = 'executing', defaultaction = None,
|
|
actions = None):
|
|
assert status in ('executing', 'completed', 'canceled')
|
|
|
|
response = request.buildReply('result')
|
|
cmd = response.getTag('command', namespace=nbxmpp.NS_COMMANDS)
|
|
cmd.setAttr('sessionid', self.sessionid)
|
|
cmd.setAttr('node', self.commandnode)
|
|
cmd.setAttr('status', status)
|
|
if defaultaction is not None or actions is not None:
|
|
if defaultaction is not None:
|
|
assert defaultaction in ('cancel', 'execute', 'prev', 'next',
|
|
'complete')
|
|
attrs = {'action': defaultaction}
|
|
else:
|
|
attrs = {}
|
|
|
|
cmd.addChild('actions', attrs, actions)
|
|
return response, cmd
|
|
|
|
def badRequest(self, stanza):
|
|
self.connection.connection.send(nbxmpp.Error(stanza,
|
|
nbxmpp.NS_STANZAS + ' bad-request'))
|
|
|
|
def cancel(self, request):
|
|
response = self.buildResponse(request, status = 'canceled')[0]
|
|
self.connection.connection.send(response)
|
|
return False # finish the session
|
|
|
|
class ChangeStatusCommand(AdHocCommand):
|
|
commandnode = 'change-status'
|
|
commandname = _('Change status information')
|
|
|
|
def __init__(self, conn, jid, sessionid):
|
|
AdHocCommand.__init__(self, conn, jid, sessionid)
|
|
self.cb = self.first_step
|
|
|
|
@staticmethod
|
|
def isVisibleFor(samejid):
|
|
"""
|
|
Change status is visible only if the entity has the same bare jid
|
|
"""
|
|
return samejid
|
|
|
|
def execute(self, request):
|
|
return self.cb(request)
|
|
|
|
def first_step(self, request):
|
|
# first query...
|
|
response, cmd = self.buildResponse(request, defaultaction = 'execute',
|
|
actions = ['execute'])
|
|
|
|
cmd.addChild(node = dataforms.SimpleDataForm(
|
|
title = _('Change status'),
|
|
instructions = _('Set the presence type and description'),
|
|
fields = [
|
|
dataforms.Field('list-single',
|
|
var = 'presence-type',
|
|
label = 'Type of presence:',
|
|
options = [
|
|
('chat', _('Free for chat')),
|
|
('online', _('Online')),
|
|
('away', _('Away')),
|
|
('xa', _('Extended away')),
|
|
('dnd', _('Do not disturb')),
|
|
('offline', _('Offline - disconnect'))],
|
|
value = 'online',
|
|
required = True),
|
|
dataforms.Field('text-multi',
|
|
var = 'presence-desc',
|
|
label = _('Presence description:'))]))
|
|
|
|
self.connection.connection.send(response)
|
|
|
|
# for next invocation
|
|
self.cb = self.second_step
|
|
|
|
return True # keep the session
|
|
|
|
def second_step(self, request):
|
|
# check if the data is correct
|
|
try:
|
|
form = dataforms.SimpleDataForm(extend = request.getTag('command').\
|
|
getTag('x'))
|
|
except Exception:
|
|
self.badRequest(request)
|
|
return False
|
|
|
|
try:
|
|
presencetype = form['presence-type'].value
|
|
if not presencetype in \
|
|
('chat', 'online', 'away', 'xa', 'dnd', 'offline'):
|
|
self.badRequest(request)
|
|
return False
|
|
except Exception: # KeyError if there's no presence-type field in form or
|
|
# AttributeError if that field is of wrong type
|
|
self.badRequest(request)
|
|
return False
|
|
|
|
try:
|
|
presencedesc = form['presence-desc'].value
|
|
except Exception: # same exceptions as in last comment
|
|
presencedesc = ''
|
|
|
|
response, cmd = self.buildResponse(request, status = 'completed')
|
|
cmd.addChild('note', {}, _('The status has been changed.'))
|
|
|
|
# if going offline, we need to push response so it won't go into
|
|
# queue and disappear
|
|
self.connection.connection.send(response, now = presencetype == 'offline')
|
|
|
|
# send new status
|
|
app.interface.roster.send_status(self.connection.name, presencetype,
|
|
presencedesc)
|
|
|
|
return False # finish the session
|
|
|
|
def find_current_groupchats(account):
|
|
from gajim import message_control
|
|
rooms = []
|
|
for gc_control in app.interface.msg_win_mgr.get_controls(
|
|
message_control.TYPE_GC) + \
|
|
app.interface.minimized_controls[account].values():
|
|
acct = gc_control.account
|
|
# check if account is the good one
|
|
if acct != account:
|
|
continue
|
|
room_jid = gc_control.room_jid
|
|
nick = gc_control.nick
|
|
if room_jid in app.gc_connected[acct] and \
|
|
app.gc_connected[acct][room_jid]:
|
|
rooms.append((room_jid, nick,))
|
|
return rooms
|
|
|
|
|
|
class LeaveGroupchatsCommand(AdHocCommand):
|
|
commandnode = 'leave-groupchats'
|
|
commandname = _('Leave Groupchats')
|
|
|
|
def __init__(self, conn, jid, sessionid):
|
|
AdHocCommand.__init__(self, conn, jid, sessionid)
|
|
self.cb = self.first_step
|
|
|
|
@staticmethod
|
|
def isVisibleFor(samejid):
|
|
"""
|
|
Leave groupchats is visible only if the entity has the same bare jid
|
|
"""
|
|
return samejid
|
|
|
|
def execute(self, request):
|
|
return self.cb(request)
|
|
|
|
def first_step(self, request):
|
|
# first query...
|
|
response, cmd = self.buildResponse(request, defaultaction = 'execute',
|
|
actions=['execute'])
|
|
options = []
|
|
account = self.connection.name
|
|
for gc in find_current_groupchats(account):
|
|
options.append(('%s' %(gc[0]), _('%(nickname)s on %(room_jid)s') % \
|
|
{'nickname': gc[1], 'room_jid': gc[0]}))
|
|
if not len(options):
|
|
response, cmd = self.buildResponse(request, status = 'completed')
|
|
cmd.addChild('note', {}, _('You have not joined a groupchat.'))
|
|
|
|
self.connection.connection.send(response)
|
|
return False
|
|
|
|
cmd.addChild(node=dataforms.SimpleDataForm(
|
|
title = _('Leave Groupchats'),
|
|
instructions = _('Choose the groupchats you want to leave'),
|
|
fields=[
|
|
dataforms.Field('list-multi',
|
|
var = 'groupchats',
|
|
label = _('Groupchats'),
|
|
options = options,
|
|
required = True)]))
|
|
|
|
self.connection.connection.send(response)
|
|
|
|
# for next invocation
|
|
self.cb = self.second_step
|
|
|
|
return True # keep the session
|
|
|
|
def second_step(self, request):
|
|
# check if the data is correct
|
|
try:
|
|
form = dataforms.SimpleDataForm(extend = request.getTag('command').\
|
|
getTag('x'))
|
|
except Exception:
|
|
self.badRequest(request)
|
|
return False
|
|
|
|
try:
|
|
gc = form['groupchats'].values
|
|
except Exception: # KeyError if there's no groupchats in form
|
|
self.badRequest(request)
|
|
return False
|
|
account = self.connection.name
|
|
try:
|
|
for room_jid in gc:
|
|
gc_control = app.interface.msg_win_mgr.get_gc_control(room_jid,
|
|
account)
|
|
if not gc_control:
|
|
gc_control = app.interface.minimized_controls[account]\
|
|
[room_jid]
|
|
gc_control.shutdown()
|
|
app.interface.roster.remove_groupchat(room_jid, account)
|
|
continue
|
|
gc_control.parent_win.remove_tab(gc_control, None, force = True)
|
|
except Exception: # KeyError if there's no such room opened
|
|
self.badRequest(request)
|
|
return False
|
|
response, cmd = self.buildResponse(request, status = 'completed')
|
|
note = _('You left the following groupchats:')
|
|
for room_jid in gc:
|
|
note += '\n\t' + room_jid
|
|
cmd.addChild('note', {}, note)
|
|
|
|
self.connection.connection.send(response)
|
|
return False
|
|
|
|
|
|
class ConnectionCommands:
|
|
"""
|
|
This class depends on that it is a part of Connection() class
|
|
"""
|
|
|
|
def __init__(self):
|
|
# a list of all commands exposed: node -> command class
|
|
self.__commands = {}
|
|
if app.config.get('remote_commands'):
|
|
for cmdobj in (ChangeStatusCommand, LeaveGroupchatsCommand):
|
|
self.__commands[cmdobj.commandnode] = cmdobj
|
|
|
|
# a list of sessions; keys are tuples (jid, sessionid, node)
|
|
self.__sessions = {}
|
|
|
|
def getOurBareJID(self):
|
|
return app.get_jid_from_account(self.name)
|
|
|
|
def isSameJID(self, jid):
|
|
"""
|
|
Test if the bare jid given is the same as our bare jid
|
|
"""
|
|
return nbxmpp.JID(jid).getStripped() == self.getOurBareJID()
|
|
|
|
def commandListQuery(self, con, iq_obj):
|
|
iq = iq_obj.buildReply('result')
|
|
jid = helpers.get_full_jid_from_iq(iq_obj)
|
|
q = iq.getTag('query')
|
|
# buildReply don't copy the node attribute. Re-add it
|
|
q.setAttr('node', nbxmpp.NS_COMMANDS)
|
|
|
|
for node, cmd in self.__commands.items():
|
|
if cmd.isVisibleFor(self.isSameJID(jid)):
|
|
q.addChild('item', {
|
|
# TODO: find the jid
|
|
'jid': self.getOurBareJID() + '/' + self.server_resource,
|
|
'node': node,
|
|
'name': cmd.commandname})
|
|
|
|
self.connection.send(iq)
|
|
|
|
def commandInfoQuery(self, con, iq_obj):
|
|
"""
|
|
Send disco#info result for query for command (JEP-0050, example 6.).
|
|
Return True if the result was sent, False if not
|
|
"""
|
|
try:
|
|
jid = helpers.get_full_jid_from_iq(iq_obj)
|
|
except helpers.InvalidFormat:
|
|
log.warning('Invalid JID: %s, ignoring it' % iq_obj.getFrom())
|
|
return
|
|
node = iq_obj.getTagAttr('query', 'node')
|
|
|
|
if node not in self.__commands: return False
|
|
|
|
cmd = self.__commands[node]
|
|
if cmd.isVisibleFor(self.isSameJID(jid)):
|
|
iq = iq_obj.buildReply('result')
|
|
q = iq.getTag('query')
|
|
q.addChild('identity', attrs = {'type': 'command-node',
|
|
'category': 'automation',
|
|
'name': cmd.commandname})
|
|
q.addChild('feature', attrs = {'var': nbxmpp.NS_COMMANDS})
|
|
for feature in cmd.commandfeatures:
|
|
q.addChild('feature', attrs = {'var': feature})
|
|
|
|
self.connection.send(iq)
|
|
return True
|
|
|
|
return False
|
|
|
|
def commandItemsQuery(self, con, iq_obj):
|
|
"""
|
|
Send disco#items result for query for command. Return True if the result
|
|
was sent, False if not.
|
|
"""
|
|
jid = helpers.get_full_jid_from_iq(iq_obj)
|
|
node = iq_obj.getTagAttr('query', 'node')
|
|
|
|
if node not in self.__commands: return False
|
|
|
|
cmd = self.__commands[node]
|
|
if cmd.isVisibleFor(self.isSameJID(jid)):
|
|
iq = iq_obj.buildReply('result')
|
|
self.connection.send(iq)
|
|
return True
|
|
|
|
return False
|
|
|
|
def _CommandExecuteCB(self, con, iq_obj):
|
|
jid = helpers.get_full_jid_from_iq(iq_obj)
|
|
|
|
cmd = iq_obj.getTag('command')
|
|
if cmd is None: return
|
|
|
|
node = cmd.getAttr('node')
|
|
if node is None: return
|
|
|
|
sessionid = cmd.getAttr('sessionid')
|
|
if sessionid is None:
|
|
# we start a new command session... only if we are visible for the jid
|
|
# and command exist
|
|
if node not in self.__commands.keys():
|
|
self.connection.send(
|
|
nbxmpp.Error(iq_obj, nbxmpp.NS_STANZAS + ' item-not-found'))
|
|
raise nbxmpp.NodeProcessed
|
|
|
|
newcmd = self.__commands[node]
|
|
if not newcmd.isVisibleFor(self.isSameJID(jid)):
|
|
return
|
|
|
|
# generate new sessionid
|
|
sessionid = self.connection.getAnID()
|
|
|
|
# create new instance and run it
|
|
obj = newcmd(conn = self, jid = jid, sessionid = sessionid)
|
|
rc = obj.execute(iq_obj)
|
|
if rc:
|
|
self.__sessions[(jid, sessionid, node)] = obj
|
|
raise nbxmpp.NodeProcessed
|
|
else:
|
|
# the command is already running, check for it
|
|
magictuple = (jid, sessionid, node)
|
|
if magictuple not in self.__sessions:
|
|
# we don't have this session... ha!
|
|
return
|
|
|
|
action = cmd.getAttr('action')
|
|
obj = self.__sessions[magictuple]
|
|
|
|
try:
|
|
if action == 'cancel':
|
|
rc = obj.cancel(iq_obj)
|
|
elif action == 'prev':
|
|
rc = obj.prev(iq_obj)
|
|
elif action == 'next':
|
|
rc = obj.next(iq_obj)
|
|
elif action == 'execute' or action is None:
|
|
rc = obj.execute(iq_obj)
|
|
elif action == 'complete':
|
|
rc = obj.complete(iq_obj)
|
|
else:
|
|
# action is wrong. stop the session, send error
|
|
raise AttributeError
|
|
except AttributeError:
|
|
# the command probably doesn't handle invoked action...
|
|
# stop the session, return error
|
|
del self.__sessions[magictuple]
|
|
return
|
|
|
|
# delete the session if rc is False
|
|
if not rc:
|
|
del self.__sessions[magictuple]
|
|
|
|
raise nbxmpp.NodeProcessed
|