core (memory), plugin XEP-0329, plugin invitation: minor style improvments

This commit is contained in:
Goffi 2019-04-14 08:21:51 +02:00
commit 5e2426c065
3 changed files with 126 additions and 98 deletions

View file

@ -1417,27 +1417,11 @@ class Memory(object):
@defer.inlineCallbacks
def setFile(
self,
client,
name,
file_id=None,
version=u"",
parent=None,
path=None,
type_=C.FILE_TYPE_FILE,
file_hash=None,
hash_algo=None,
size=None,
namespace=None,
mime_type=None,
created=None,
modified=None,
owner=None,
access=None,
extra=None,
peer_jid=None,
perms_to_check=(C.ACCESS_PERM_WRITE,),
):
self, client, name, file_id=None, version=u"", parent=None, path=None,
type_=C.FILE_TYPE_FILE, file_hash=None, hash_algo=None, size=None,
namespace=None, mime_type=None, created=None, modified=None, owner=None,
access=None, extra=None, peer_jid=None, perms_to_check=(C.ACCESS_PERM_WRITE,)
):
"""set a file metadata
@param name(unicode): basename of the file
@ -1446,32 +1430,38 @@ class Memory(object):
empty string for current version or when there is no versioning
@param parent(unicode, None): id of the directory containing the files
@param path(unicode, None): virtual path of the file in the namespace
if set, parent must be None. All intermediate directories will be created if needed,
using current access.
if set, parent must be None. All intermediate directories will be created
if needed, using current access.
@param file_hash(unicode): unique hash of the payload
@param hash_algo(unicode): algorithm used for hashing the file (usually sha-256)
@param size(int): size in bytes
@param namespace(unicode, None): identifier (human readable is better) to group files
for instance, namespace could be used to group files in a specific photo album
@param namespace(unicode, None): identifier (human readable is better) to group
files
For instance, namespace could be used to group files in a specific photo album
@param mime_type(unicode): MIME type of the file, or None if not known/guessed
@param created(int): UNIX time of creation
@param modified(int,None): UNIX time of last modification, or None to use created date
@param owner(jid.JID, None): jid of the owner of the file (mainly useful for component)
@param modified(int,None): UNIX time of last modification, or None to use
created date
@param owner(jid.JID, None): jid of the owner of the file (mainly useful for
component)
will be used to check permission (only bare jid is used, don't use with MUC).
Use None to ignore permission (perms_to_check must be None too)
@param access(dict, None): serialisable dictionary with access rules.
None (or empty dict) to use private access, i.e. allow only profile's jid to access the file
None (or empty dict) to use private access, i.e. allow only profile's jid to
access the file
key can be on on C.ACCESS_PERM_*,
then a sub dictionary with a type key is used (one of C.ACCESS_TYPE_*).
According to type, extra keys can be used:
- C.ACCESS_TYPE_PUBLIC: the permission is granted for everybody
- C.ACCESS_TYPE_WHITELIST: the permission is granted for jids (as unicode) in the 'jids' key
- C.ACCESS_TYPE_WHITELIST: the permission is granted for jids (as unicode)
in the 'jids' key
will be encoded to json in database
@param extra(dict, None): serialisable dictionary of any extra data
will be encoded to json in database
@param perms_to_check(tuple[unicode],None): permission to check
must be a tuple of C.ACCESS_PERM_* or None
if None, permission will no be checked (peer_jid must be None too in this case)
if None, permission will no be checked (peer_jid must be None too in this
case)
@param profile(unicode): profile owning the file
"""
if "/" in name:

View file

@ -17,20 +17,21 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import shortuuid
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber import error
from sat.core.i18n import _, D_
from sat.core.constants import Const as C
from sat.core import exceptions
from sat.core.log import getLogger
log = getLogger(__name__)
import shortuuid
from sat.tools import utils
from sat.tools.common import data_format
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber import error
from sat.memory import persistent
from sat.tools import email as sat_email
log = getLogger(__name__)
PLUGIN_INFO = {
C.PI_NAME: "Invitations",
@ -53,7 +54,8 @@ KEY_LAST_CONNECTION = u'last_connection'
KEY_GUEST_PROFILE = u'guest_profile'
KEY_PASSWORD = u'password'
KEY_EMAILS_EXTRA = u'emails_extra'
EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION, KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA}
EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION,
KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA}
DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}")
DEFAULT_BODY = D_(u"""Hello {name}!
@ -74,26 +76,33 @@ class InvitationsPlugin(object):
log.info(_(u"plugin Invitations initialization"))
self.host = host
self.invitations = persistent.LazyPersistentBinaryDict(u'invitations')
host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s', out_sign='a{ss}',
host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s',
out_sign='a{ss}',
method=self._create,
async=True)
host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}',
method=self.get,
async=True)
host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b', out_sign='',
host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b',
out_sign='',
method=self._modify,
async=True)
host.bridge.addMethod("invitationList", ".plugin", in_sign='s', out_sign='a{sa{ss}}',
host.bridge.addMethod("invitationList", ".plugin", in_sign='s',
out_sign='a{sa{ss}}',
method=self._list,
async=True)
def checkExtra(self, extra):
if EXTRA_RESERVED.intersection(extra):
raise ValueError(_(u"You can't use following key(s) in extra, they are reserved: {}").format(
u', '.join(EXTRA_RESERVED.intersection(extra))))
raise ValueError(
_(u"You can't use following key(s) in extra, they are reserved: {}")
.format(u', '.join(EXTRA_RESERVED.intersection(extra))))
def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'', host_name=u'', language=u'', url_template=u'', message_subject=u'', message_body=u'', extra=None, profile=u''):
# XXX: we don't use **kwargs here to keep arguments name for introspection with D-Bus bridge
def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'',
host_name=u'', language=u'', url_template=u'', message_subject=u'',
message_body=u'', extra=None, profile=u''):
# XXX: we don't use **kwargs here to keep arguments name for introspection with
# D-Bus bridge
if emails_extra is None:
emails_extra = []
@ -106,8 +115,10 @@ class InvitationsPlugin(object):
KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra]
}
# we need to be sure that values are unicode, else they won't be pickled correctly with D-Bus
for key in ("jid_", "password", "name", "host_name", "email", "language", "url_template", "message_subject", "message_body", "profile"):
# we need to be sure that values are unicode, else they won't be pickled correctly
# with D-Bus
for key in ("jid_", "password", "name", "host_name", "email", "language",
"url_template", "message_subject", "message_body", "profile"):
value = locals()[key]
if value:
kwargs[key] = unicode(value)
@ -120,32 +131,45 @@ class InvitationsPlugin(object):
@defer.inlineCallbacks
def create(self, **kwargs):
ur"""create an invitation
ur"""Create an invitation
this will create an XMPP account and a profile, and use a UUID to retrieve them.
the profile is automatically generated in the form guest@@[UUID], this way they can be retrieved easily
**kwargs: keywords arguments which can have the following keys, unset values are equivalent to None:
jid_(jid.JID, None): jid to use for invitation, the jid will be created using XEP-0077
if the jid has no user part, an anonymous account will be used (no XMPP account created in this case)
if None, automatically generate an account name (in the form "invitation-[random UUID]@domain.tld") (note that this UUID is not the
same as the invitation one, as jid can be used publicly (leaking the UUID), and invitation UUID give access to account.
in case of conflict, a suffix number is added to the account until a free one if found (with a failure if SUFFIX_MAX is reached)
password(unicode, None): password to use (will be used for XMPP account and profile)
This will create an XMPP account and a profile, and use a UUID to retrieve them.
The profile is automatically generated in the form guest@@[UUID], this way they
can be retrieved easily
**kwargs: keywords arguments which can have the following keys, unset values are
equivalent to None:
jid_(jid.JID, None): jid to use for invitation, the jid will be created using
XEP-0077
if the jid has no user part, an anonymous account will be used (no XMPP
account created in this case)
if None, automatically generate an account name (in the form
"invitation-[random UUID]@domain.tld") (note that this UUID is not the
same as the invitation one, as jid can be used publicly (leaking the
UUID), and invitation UUID give access to account.
in case of conflict, a suffix number is added to the account until a free
one if found (with a failure if SUFFIX_MAX is reached)
password(unicode, None): password to use (will be used for XMPP account and
profile)
None to automatically generate one
name(unicode, None): name of the invitee
will be set as profile identity if present
host_name(unicode, None): name of the host
email(unicode, None): email to send the invitation to
if None, no invitation email is sent, you can still associate email using extra
if None, no invitation email is sent, you can still associate email using
extra
if email is used, extra can't have "email" key
language(unicode): language of the invitee (used notabily to translate the invitation)
language(unicode): language of the invitee (used notabily to translate the
invitation)
TODO: not used yet
url_template(unicode, None): template to use to construct the invitation URL
use {uuid} as a placeholder for identifier
use None if you don't want to include URL (or if it is already specified in custom message)
use None if you don't want to include URL (or if it is already specified
in custom message)
/!\ you must put full URL, don't forget https://
/!\ the URL will give access to the invitee account, you should warn in message to not publish it publicly
message_subject(unicode, None): customised message body for the invitation email
/!\ the URL will give access to the invitee account, you should warn in
message to not publish it publicly
message_subject(unicode, None): customised message body for the invitation
email
None to use default subject
uses the same substitution as for message_body
message_body(unicode, None): customised message body for the invitation email
@ -169,7 +193,8 @@ class InvitationsPlugin(object):
## initial checks
extra = kwargs.pop('extra', {})
if set(kwargs).intersection(extra):
raise ValueError(_(u"You can't use following key(s) in both args and extra: {}").format(
raise ValueError(
_(u"You can't use following key(s) in both args and extra: {}").format(
u', '.join(set(kwargs).intersection(extra))))
self.checkExtra(extra)
@ -177,10 +202,14 @@ class InvitationsPlugin(object):
email = kwargs.pop(u'email', None)
emails_extra = kwargs.pop(u'emails_extra', [])
if not email and emails_extra:
raise ValueError(_(u'You need to provide a main email address before using emails_extra'))
raise ValueError(
_(u'You need to provide a main email address before using emails_extra'))
if email is not None and not 'url_template' in kwargs and not 'message_body' in kwargs:
raise ValueError(_(u"You need to provide url_template if you use default message body"))
if (email is not None
and not 'url_template' in kwargs
and not 'message_body' in kwargs):
raise ValueError(
_(u"You need to provide url_template if you use default message body"))
## uuid
log.info(_(u"creating an invitation"))
@ -195,8 +224,8 @@ class InvitationsPlugin(object):
# it is needed for invitation as the same password is used for profile
# and SàT need to be able to automatically open the profile with the uuid
# FIXME: we could add an extra encryption key which would be used with the uuid
# when the invitee is connecting (e.g. with URL). This key would not be saved
# and could be used to encrypt profile password.
# when the invitee is connecting (e.g. with URL). This key would not be
# saved and could be used to encrypt profile password.
extra[KEY_PASSWORD] = password
jid_ = kwargs.pop(u'jid_', None)
@ -205,10 +234,12 @@ class InvitationsPlugin(object):
if not domain:
# TODO: fallback to profile's domain
raise ValueError(_(u"You need to specify xmpp_domain in sat.conf"))
jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(), domain=domain)
jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
domain=domain)
jid_ = jid.JID(jid_)
if jid_.user:
# we don't register account if there is no user as anonymous login is then used
# we don't register account if there is no user as anonymous login is then
# used
try:
yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
except error.StanzaError as e:
@ -218,9 +249,11 @@ class InvitationsPlugin(object):
if idx >= SUFFIX_MAX:
raise exceptions.ConflictError(_(u"Can't create XMPP account"))
jid_.user = prefix + '_' + unicode(idx)
log.info(_(u"requested jid already exists, trying with {}".format(jid_.full())))
log.info(_(u"requested jid already exists, trying with {}".format(
jid_.full())))
try:
yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
yield self.host.plugins['XEP-0077'].registerNewAccount(jid_,
password)
except error.StanzaError as e:
idx += 1
else:
@ -236,8 +269,10 @@ class InvitationsPlugin(object):
# profile creation should not fail as we generate unique name ourselves
yield self.host.memory.createProfile(guest_profile, password)
yield self.host.memory.startSession(password, guest_profile)
yield self.host.memory.setParam("JabberID", jid_.full(), "Connection", profile_key=guest_profile)
yield self.host.memory.setParam("Password", password, "Connection", profile_key=guest_profile)
yield self.host.memory.setParam("JabberID", jid_.full(), "Connection",
profile_key=guest_profile)
yield self.host.memory.setParam("Password", password, "Connection",
profile_key=guest_profile)
name = kwargs.pop(u'name', None)
if name is not None:
extra[u'name'] = name
@ -288,7 +323,8 @@ class InvitationsPlugin(object):
yield sat_email.sendEmail(
self.host,
[email] + emails_extra,
(kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(**format_args),
(kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(
**format_args),
(kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args),
)
@ -312,7 +348,8 @@ class InvitationsPlugin(object):
return self.invitations[id_]
def _modify(self, id_, new_extra, replace):
return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()}, replace)
return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()},
replace)
def modify(self, id_, new_extra, replace=False):
"""Modify invitation data
@ -366,6 +403,7 @@ class InvitationsPlugin(object):
"""
invitations = yield self.invitations.items()
if profile != C.PROF_KEY_NONE:
invitations = {id_:data for id_, data in invitations.iteritems() if data.get(u'profile') == profile}
invitations = {id_:data for id_, data in invitations.iteritems()
if data.get(u'profile') == profile}
defer.returnValue(invitations)

View file

@ -59,7 +59,7 @@ KEY_TYPE = u"type"
class ShareNode(object):
"""node containing directory or files to share, virtual or real"""
"""Node containing directory or files to share, virtual or real"""
host = None
@ -112,7 +112,7 @@ class ShareNode(object):
return self.children.itervalues()
def getOrCreate(self, name, type_=TYPE_VIRTUAL, access=None):
"""get a node or create a virtual node and return it"""
"""Get a node or create a virtual node and return it"""
if access is None:
access = {C.ACCESS_PERM_READ: {KEY_TYPE: C.ACCESS_TYPE_PUBLIC}}
try:
@ -157,11 +157,12 @@ class ShareNode(object):
def checkPermissions(
self, client, peer_jid, perms=(C.ACCESS_PERM_READ,), check_parents=True
):
"""check that peer_jid can access this node and all its parents
"""Check that peer_jid can access this node and all its parents
@param peer_jid(jid.JID): entrity trying to access the node
@param perms(unicode): permissions to check, iterable of C.ACCESS_PERM_*
@param check_parents(bool): if True, access of all parents of this node will be checked too
@param check_parents(bool): if True, access of all parents of this node will be
checked too
@return (bool): True if entity can access this node
"""
peer_jid = peer_jid.userhostJID()
@ -191,11 +192,9 @@ class ShareNode(object):
path_elts = filter(None, path.split(u"/"))
if u".." in path_elts:
log.warning(
_(
u'parent dir ("..") found in path, hack attempt? path is {path} [{profile}]'
).format(path=path, profile=client.profile)
)
log.warning(_(
u'parent dir ("..") found in path, hack attempt? path is {path} '
u'[{profile}]').format(path=path, profile=client.profile))
raise exceptions.PermissionError(u"illegal path elements")
if not path_elts:
@ -312,9 +311,10 @@ class XEP_0329(object):
def _fileSendingRequestTrigger(
self, client, session, content_data, content_name, file_data, file_elt
):
"""this trigger check that a requested file is available, and fill suitable data if so
"""This trigger check that a requested file is available, and fill suitable data
path and name are used to retrieve the file. If path is missing, we try our luck with known names
Path and name are used to retrieve the file. If path is missing, we try our luck
with known names
"""
if client.is_component:
return True, None
@ -423,7 +423,8 @@ class XEP_0329(object):
)
query_elt.addChild(file_elt)
# we don't specify hash as it would be too resource intensive to calculate it for all files
# we don't specify hash as it would be too resource intensive to calculate
# it for all files.
# we add file to name_data, so users can request it later
name_data = client._XEP_0329_names_data.setdefault(name, {})
if path not in name_data:
@ -547,7 +548,9 @@ class XEP_0329(object):
"""retrieve files from local files repository according to permissions
result stanza is then built and sent to requestor
@trigger XEP-0329_compGetFilesFromNode(client, iq_elt, owner, node_path, files_data): can be used to add data/elements
@trigger XEP-0329_compGetFilesFromNode(client, iq_elt, owner, node_path,
files_data):
can be used to add data/elements
"""
peer_jid = jid.JID(iq_elt["from"])
try:
@ -660,7 +663,8 @@ class XEP_0329(object):
node = client._XEP_0329_root_node
node_type = TYPE_PATH
if os.path.isfile(path):
#  we have a single file, the workflow is diferrent as we store all single files in the same dir
# we have a single file, the workflow is diferrent as we store all single
# files in the same dir
node = node.getOrCreate(SINGLE_FILES_DIR)
if not name:
@ -675,13 +679,9 @@ class XEP_0329(object):
idx += 1
new_name = name + "_" + unicode(idx)
name = new_name
log.info(
_(
u"A directory with this name is already shared, renamed to {new_name} [{profile}]".format(
new_name=new_name, profile=client.profile
)
)
)
log.info(_(
u"A directory with this name is already shared, renamed to {new_name} "
u"[{profile}]".format( new_name=new_name, profile=client.profile)))
ShareNode(name=name, parent=node, type_=node_type, access=access, path=path)
self.host.bridge.FISSharedPathNew(path, name, client.profile)