passerelle/passerelle/contrib/greco/models.py

280 lines
12 KiB
Python

# Copyright (C) 2016 Entr'ouvert
#
# 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 <http://www.gnu.org/licenses/>.
import base64
import re
from email import encoders
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from suds.client import Client
from suds.transport import Reply
from suds.transport.http import HttpAuthenticated
import suds.sudsobject
from django.db import models
from django.utils import six
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache
from passerelle.base.models import BaseResource
from passerelle.compat import json_loads
from passerelle.utils.api import endpoint, APIError
from passerelle.soap import sudsobject_to_dict
from .formdata import FormData, CREATION_SCHEMA, list_schema_fields
# taken from https://lsimons.wordpress.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/
_illegal_xml_chars_RE = re.compile(u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
def escape_xml_illegal_chars(val, replacement='?'):
return _illegal_xml_chars_RE.sub(replacement, val)
class ParameterTypeError(Exception):
http_status = 400
log_error = False
def fill_sudsobject_with_dict(sudsobject, fields, prefix=None):
for key, value in sudsobject:
if prefix:
attr = '%s_%s' % (prefix, key)
else:
attr = key
if isinstance(value, suds.sudsobject.Object):
fill_sudsobject_with_dict(value, fields, attr)
else:
if attr in fields:
# sudsobject.foo.bar <- fields['foo_bar']
field_value = fields[attr]
# duck-type unicode/str
if hasattr(field_value, 'isnumeric'):
field_value = escape_xml_illegal_chars(field_value)
setattr(sudsobject, key, fields[attr])
class Greco(BaseResource):
application = models.CharField(_('Application identifier'), max_length=200)
token_url = models.URLField(_('Token URL'), max_length=256)
token_authorization = models.CharField(_('Token Authorization'), max_length=128)
wsdl_url = models.CharField(_('WSDL URL'), max_length=256) # not URLField, it can be file://
verify_cert = models.BooleanField(default=True,
verbose_name=_('Check HTTPS Certificate validity'))
category = _('Business Process Connectors')
class Meta:
verbose_name = _('GRECO Webservices')
def get_token(self, renew=False):
cache_key = 'greco-%s-token' % self.id
if not renew:
token = cache.get(cache_key)
if token:
return token
headers = {'Authorization': 'Basic %s' % self.token_authorization}
resp = self.requests.post(self.token_url, headers=headers,
data={'grant_type': 'client_credentials'},
verify=self.verify_cert, timeout=60).json()
token = '%s %s' % (resp.get('token_type'), resp.get('access_token'))
timeout = int(resp.get('expires_in'))
cache.set(cache_key, token, timeout)
self.logger.debug('new token: %s (timeout %ss)', token, timeout)
return token
def get_client(self, attachments=[]):
class Transport(HttpAuthenticated):
def __init__(self, instance, attachments):
self.instance = instance
self.attachments = attachments
HttpAuthenticated.__init__(self)
def send(self, request):
request.message = request.message.replace("contentType", "xm:contentType")
if self.attachments:
# SOAP Attachement format
message = MIMEMultipart('related', type="text/xml",
start="<rootpart@entrouvert.org>")
xml = MIMEText(None, _subtype='xml', _charset='utf-8')
xml.add_header('Content-ID', '<rootpart@entrouvert.org>')
# do not base64-encode the soap message
xml.replace_header('Content-Transfer-Encoding', '8bit')
xml_payload = request.message
# hack payload to include attachment filenames in
# SOAP-ENV:Header.
soap_headers = []
for num, attachment in enumerate(self.attachments):
filename = attachment.get('filename') or 'file%s.bin' % num
if isinstance(filename, six.text_type):
filename = filename.encode('utf-8', 'ignore')
soap_headers.append('<filename%s>%s</filename%s>' % (num, filename, num))
xml_payload = xml_payload.replace('<SOAP-ENV:Header/>',
'<SOAP-ENV:Header>%s</SOAP-ENV:Header>' % ''.join(soap_headers))
xml.set_payload(xml_payload)
message.attach(xml)
for num, attachment in enumerate(self.attachments):
filename = attachment.get('filename') or 'file%s.bin' % num
content = base64.b64decode(attachment.get('content') or '')
content_type = attachment.get('content_type') or 'application/octet-stream'
maintype, subtype = content_type.split('/', 1)
if maintype == 'text':
part = MIMEText(content, _subtype=subtype)
else:
part = MIMEBase(maintype, subtype, name=filename)
part.set_payload(content)
part.add_header('Content-Transfer-Encoding', 'binary')
encoders.encode_noop(part)
part.add_header('Content-Disposition', 'attachment', name=filename, filename=filename)
part.add_header('Content-ID', '<%s>' % filename)
message.attach(part)
message._write_headers = lambda x: None
msg_x = message.as_string(unixfrom=False)
# RFC 2045 defines MIME multipart boundaries:
# * boundary := 0*69<bchars> bcharsnospace
# * dash-boundary := "--" boundary
# * delimiter := CRLF dash-boundary
# but Python doesn't use CRLF, will only use LF (on Unix systems
# at least). This is http://bugs.python.org/issue1349106 and has
# been fixed in Python 3.2.
#
# Manually hack message to put \r\n so that the message is
# correctly read by Apache Axis strict parser.
boundary = message.get_boundary()
request.message = message.as_string(unixfrom=False
).replace(boundary + '\n', boundary + '\r\n'
).replace('\n--' + boundary, '\r\n--' + boundary)
request.headers.update(dict(message._headers))
request.headers['Authorization'] = self.instance.get_token()
resp = self.instance.requests.post(request.url, data=request.message,
headers=request.headers,
verify=self.instance.verify_cert,
timeout=60)
if resp.status_code == 401:
# ask for a new token, and retry
request.headers['Authorization'] = self.instance.get_token(renew=True)
resp = self.instance.requests.post(request.url, data=request.message,
headers=request.headers,
verify=self.instance.verify_cert,
timeout=60)
return Reply(resp.status_code, resp.headers, resp.content)
return Client(url=self.wsdl_url, transport=Transport(self, attachments))
def check_status(self):
if self.get_client().service.communicationTest('ping') is None:
raise Exception('empty answer to communication test')
@endpoint(perm='can_access')
def ping(self, request):
resp = self.get_client().service.communicationTest('ping')
if resp is None:
raise APIError('empty response from communicationTest()')
return {'data': resp}
@endpoint(perm='can_access', methods=['post'])
def create(self, request):
# get creation fields from payload
try:
formdata = FormData(json_loads(request.body), CREATION_SCHEMA)
except ValueError as e:
raise ParameterTypeError(str(e))
# create suds object from formdata
client = self.get_client(formdata.attachments)
creation = client.factory.create('DemandeCreation')
creation.application = self.application
fill_sudsobject_with_dict(creation, formdata.fields)
# send it to "creer"
resp = client.service.creer(creation)
if resp is None:
raise APIError('empty response from creer()')
return {'data': sudsobject_to_dict(resp)}
@classmethod
def creation_fields(cls):
'''used in greco_detail.html template'''
return list_schema_fields(CREATION_SCHEMA)
@endpoint(perm='can_access')
def status(self, request, idgreco, iddemande=None):
resp = self.get_client().service.consulter({
'idgreco': idgreco,
'iddemande': iddemande,
})
if resp is None:
raise APIError('empty response from status()')
return {'data': sudsobject_to_dict(resp)}
@endpoint(perm='can_access')
def answer(self, request, idgreco, iddemande, code=None):
params = {
'idgreco': idgreco,
'iddemande': iddemande,
}
if code:
params['taCode'] = code
resp = self.get_client().service.getMail(params)
if resp is None:
raise APIError('empty response from consulter()')
return {'data': sudsobject_to_dict(resp)}
@endpoint(name='add-information', perm='can_access',
methods=['get', 'post', 'put', 'patch'])
def add_information(self, request, iddemande=None, idgreco=None, information=None):
if request.body:
payload = json_loads(request.body)
if not isinstance(payload, dict):
raise ParameterTypeError('payload must be a dict')
idgreco = payload.get('idgreco') or idgreco
iddemande = payload.get('iddemande') or iddemande
information = payload.get('information') or information
resp = self.get_client().service.ajouterComplementInformation({
'idgreco': idgreco,
'iddemande': iddemande,
'complementInfo': information,
})
if resp is None:
raise APIError('empty response from ajouterComplementInformation()')
return {'data': sudsobject_to_dict(resp)}
@endpoint(perm='can_access',
methods=['get', 'post', 'put', 'patch'])
def update(self, request, iddemande=None, idgreco=None, comment=None):
if request.body:
payload = json_loads(request.body)
if not isinstance(payload, dict):
raise ParameterTypeError('payload must be a dict')
idgreco = payload.get('idgreco') or idgreco
iddemande = payload.get('iddemande') or iddemande
comment = payload.get('comment') or comment
resp = self.get_client().service.relancer({
'idgreco': idgreco,
'iddemande': iddemande,
'commentaire': comment,
})
if resp is None:
raise APIError('empty response from relancer()')
return {'data': sudsobject_to_dict(resp)}