From fdcd3dff3abe4a05892f38ff1c7668687faa0a01 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Mon, 28 Sep 2015 17:16:21 +0200 Subject: [PATCH 2/2] add support for file validation (#8402) --- wcs/fields.py | 78 +++++++++++++++++++++-------- wcs/file_validation.py | 111 ++++++++++++++++++++++++++++++++++++++++++ wcs/forms/common.py | 30 +++++++++++- wcs/qommon/static/js/fargo.js | 56 +++++++++++++++++++++ wcs/root.py | 7 ++- 5 files changed, 261 insertions(+), 21 deletions(-) create mode 100644 wcs/file_validation.py create mode 100644 wcs/qommon/static/js/fargo.js diff --git a/wcs/fields.py b/wcs/fields.py index de83f3f..348e793 100644 --- a/wcs/fields.py +++ b/wcs/fields.py @@ -30,6 +30,7 @@ from qommon import get_cfg, get_logger from qommon.strftime import strftime import data_sources +import file_validation class PrefillSelectionWidget(CompositeWidget): @@ -668,38 +669,32 @@ register_field_class(BoolField) class FileField(WidgetField): key = 'file' description = N_('File Upload') - file_type = [] + document_type = None max_file_size = None widget_class = FileWithPreviewWidget extra_attributes = ['file_type', 'max_file_size'] + @property + def file_type(self): + if self.document_type: + return self.document_type.get('mimetypes') + def fill_admin_form(self, form): WidgetField.fill_admin_form(self, form) - file_types = [ - ('audio/*', _('Sound files')), - ('video/*', _('Video files')), - ('image/*', _('Image files'))] - filetypes_cfg = get_cfg('filetypes', {}) - if filetypes_cfg: - for file_type in filetypes_cfg.values(): - file_types.append(( - ','.join(file_type['mimetypes']), file_type['label'])) - if self.file_type: - known_file_types = [x[0] for x in file_types] - for file_type in self.file_type: - if not file_type in known_file_types: - file_types.append((file_type, file_type)) - form.add(CheckboxesWidget, 'file_type', title=_('File type suggestion'), - value=self.file_type, elements=file_types, inline=True, - advanced=not(self.file_type)) + document_types, value = self.get_document_types_and_value() + options = [(None, '---', '')] + options += [(doc_type, doc_type['label'], key) for key, doc_type in document_types.iteritems()] + form.add(SingleSelectWidget, 'document_type', title=_('File type suggestion'), + value=value, options=options, advanced=not(self.document_type)) form.add(FileSizeWidget, 'max_file_size', title=('Max file size'), value=self.max_file_size, advanced=not(self.max_file_size)) def get_admin_attributes(self): - return WidgetField.get_admin_attributes(self) + ['file_type', + l = WidgetField.get_admin_attributes(self) + ['document_type', 'max_file_size'] + return l def get_view_value(self, value): return htmltext('%s') % ( @@ -726,6 +721,51 @@ class FileField(WidgetField): if value and hasattr(value, 'token'): get_request().form[self.field_key + '$token'] = value.token + def get_default_document_types(self): + return { + '_audio': { + 'label': _('Sound files'), + 'mimetypes': 'audio/*', + }, + '_video': { + 'label': _('Video files'), + 'mimetypes': 'video/*', + }, + '_image': { + 'label': _('Image files'), + 'mimetypes': 'image/*', + } + } + + def get_document_types_and_value(self): + document_types = self.get_default_document_types() + # Local document types + document_types.update(get_cfg('filetypes', {})) + # Remote documents types + document_types.update(file_validation.get_document_types()) + value = self.document_type + if isinstance(value, str): + value = None + for key, document_type in document_types.iteritems(): + document_type['id'] = key + # normalize current value against known file types + if value and value['id'] == key: + value = document_type + # add current file type if it does not exist anymore in the settings + if value and value['id'] not in document_types: + document_types[value['id']] = value + return document_types, value + + def migrate(self): + if 'file_type' in self.__dict__: + for key, value in self.get_default_document_types().iteritems(): + if self.file_type == value['mimetypes']: + self.document_type = value.copy() + self.document_type['id'] = key + del self.__dict__['file_type'] + return True + return False + register_field_class(FileField) diff --git a/wcs/file_validation.py b/wcs/file_validation.py new file mode 100644 index 0000000..7e96f5a --- /dev/null +++ b/wcs/file_validation.py @@ -0,0 +1,111 @@ +import json +import urlparse +import hashlib +import urllib + +from qommon.misc import http_get_page +from quixote import get_publisher, get_response +from quixote.html import htmltext + + +def json_encode_helper(d, charset): + '''Encode a JSON structure into local charset''' + if isinstance(d, unicode): + return d.encode(charset) + elif isinstance(d, list): + return [json_encode_helper(e, charset) for e in d] + elif isinstance(d, dict): + new_d = {} + for k, v in d.iteritems(): + new_d[json_encode_helper(k, charset)] = json_encode_helper(v, charset) + return new_d + else: + return d + + +def json_encode(d, charset=None): + return json_encode_helper(d, charset or get_publisher().site_charset) + + +def has_file_validation(): + return get_publisher().get_site_option('fargo_url') is not None + + +def fargo_get(path): + fargo_url = get_publisher().get_site_option('fargo_url') + url = urlparse.urljoin(fargo_url, path) + response, status, data, auth_header = http_get_page(url) + if status == 200: + return json_encode(json.loads(data)) + + +def sha256_of_upload(upload): + return hashlib.sha256(upload.get_content()).hexdigest() + + +def get_document_types(): + if not has_file_validation(): + return {} + response = fargo_get('/document-types/') + publisher = get_publisher() + if response.get('err') == 0: + result = {} + for schema in response['data']: + d = { + 'id': schema['name'], + 'label': schema['label'], + 'fargo': True, + } + if 'mimetypes' in schema: + d['mimetypes'] = shema['mimetypes'] + result[d['id']] = d + + return result + return {} + +def validation_path(filled, field, upload): + user = filled.get_user() + if not user: + return + if not user.name_identifiers: + return + if not field.document_type or not field.document_type.get('fargo'): + return + name_id = user.name_identifiers[0] + sha_256 = sha256_of_upload(upload) + document_type = field.document_type + path = '%s/%s/%s/' % ( + urllib.quote(name_id), + urllib.quote(sha_256), + urllib.quote(document_type), + ) + return path + +def validate_upload(filled, field, upload): + path = validation_path(filled, field, upload) + if not path: + return + response = fargo_get('metadata/' + path) + if response is None: + return + if response['err'] == 1: + return False + return response['data'] + +def get_document_type_label(document_type): + return get_document_types().get(document_type, {}).get('label') + +def validation_link(filled, field, upload): + path = validation_path(filled, field, upload) + if not path: + return + get_response().add_css_include('../js/smoothness/jquery-ui-1.10.0.custom.min.css') + get_response().add_javascript(['jquery-ui.js', 'jquery.js', 'fargo.js']) + label = get_document_type_label(field.document_type) + fargo_url = get_publisher().get_site_option('fargo_url') + url = urlparse.urljoin(fargo_url, 'validation/' + path) + url += '?next=%s' % urllib.quote(get_publisher().get_frontoffice_url() + '/reload-top') + return htmltext(_('Validate as a %(document_type_label)s')) % { + 'url': url, + 'document_type_label': label, + } diff --git a/wcs/forms/common.py b/wcs/forms/common.py index 06f0b7e..d883c66 100644 --- a/wcs/forms/common.py +++ b/wcs/forms/common.py @@ -15,12 +15,15 @@ # along with this program; if not, see . import sys +import hashlib +import urlparse from quixote import get_publisher, get_request, get_response, get_session, redirect from quixote.directory import Directory from quixote.html import TemplateIO, htmltext -from wcs.fields import WidgetField +from wcs.fields import WidgetField, FileField +from wcs import file_validation from qommon import template from qommon import get_logger @@ -399,6 +402,10 @@ class FormStatusPage(Directory): s = f.get_view_value(value) s = s.replace(str('[download]'), str('%sdownload' % form_url)) r += s + if isinstance(f, FileField): + s = self.file_validation_status(f, value) + if s: + r += htmltext(str(s)) r += htmltext('') @@ -529,6 +536,27 @@ class FormStatusPage(Directory): else: return redirect('files/%s/' % fn) + def file_validation_status(self, field, value): + status = file_validation.validate_upload(self.filled, field, value) + if status is None: + return + r = TemplateIO(html=True) + r += htmltext('
') + if status is False: + r += file_validation.validation_link(self.filled, field, value) + else: + r += htmltext(_('

%s validated by %s on %s

')) % ( + status['label'], status['creator'], status['created']) + r += htmltext('
    ') + for meta in status['metadata']: + r += htmltext(_('
  • %(label)s: %(value)s
  • ')) % { + 'label': meta['label'], + 'value': meta['value'] + } + r += htmltext('
') + r += htmltext('
') + return str(r) + def _q_lookup(self, component): if component == 'files': self.check_receiver() diff --git a/wcs/qommon/static/js/fargo.js b/wcs/qommon/static/js/fargo.js new file mode 100644 index 0000000..af79ead --- /dev/null +++ b/wcs/qommon/static/js/fargo.js @@ -0,0 +1,56 @@ + +$(function() { + var iframe = $(''); + var dialog = $("
").append(iframe).appendTo("body").dialog({ + autoOpen: false, + modal: true, + resizable: false, + width: "auto", + height: "auto", + close: function () { + iframe.attr("src", ""); + } + }); + $('.file-validation a').click(function (e) { + e.preventDefault(); + var src = $(e.target).attr('href'); + var title = $(e.target).data("title"); + var width = $(e.target).data("width"); + var height = $(e.target).data("height"); + iframe.attr({ + width: parseInt(width), + height: parseInt(height), + src: src + }); + dialog.dialog("option", "title", title); + dialog.dialog("open"); + }); + $('p.use-file-from-fargo span').click(function(e) { + e.preventDefault(); + var base_widget = $(this).parents('.file-upload-widget'); + document.fargo_set_token = function (token, title) { + if (token) { + $(base_widget).find('.filename').text(title); + $(base_widget).find('.fileinfo').show(); + $(base_widget).find('input[type=hidden]').val(token); + $(base_widget).find('input[type=file]').hide(); + } + document.fargo_close_dialog(); + } + document.fargo_close_dialog = function () { + document.fargo_set_token = undefined; + dialog.dialog('close'); + } + var src = $(this).data('src'); + var title = $(this).data("title"); + var width = $(this).data("width"); + var height = $(this).data("height"); + iframe.attr({ + width: parseInt(width), + height: parseInt(height), + src: src + }); + dialog.dialog("option", "title", title); + dialog.dialog("open"); + }); +}); diff --git a/wcs/root.py b/wcs/root.py index 854f172..8eb6ee3 100644 --- a/wcs/root.py +++ b/wcs/root.py @@ -192,7 +192,7 @@ class RootDirectory(Directory): _q_exports = ['admin', 'backoffice', 'forms', 'login', 'logout', 'saml', 'ident', 'register', 'afterjobs', 'themes', 'myspace', 'user', 'roles', 'pages', ('tmp-upload', 'tmp_upload'), 'api', '__version__', - 'tryauth', 'auth', 'preview'] + 'tryauth', 'auth', 'preview', ('reload-top', 'reload_top')] api = ApiDirectory() themes = template.ThemesDirectory() @@ -308,6 +308,11 @@ class RootDirectory(Directory): # or a form ? return forms.root.RootDirectory()._q_lookup(component) + def reload_top(self): + r = TemplateIO(html=True) + r += htmltext('') + return r.getvalue() + admin = None backoffice = None -- 2.1.4