From 1abdb0b5d8074d63fc00cadd91daa024262d5c8d Mon Sep 17 00:00:00 2001 From: Vinyl Darkscratch Date: Tue, 26 Feb 2019 07:17:23 -0800 Subject: [PATCH] Add new IS_FILE validator --- gluon/__init__.py | 2 +- gluon/highlight.py | 2 +- gluon/tests/test_validators.py | 59 +++++++++++++++++++++ gluon/validators.py | 93 ++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 2 deletions(-) diff --git a/gluon/__init__.py b/gluon/__init__.py index 882adb68..60c279bb 100644 --- a/gluon/__init__.py +++ b/gluon/__init__.py @@ -10,7 +10,7 @@ Web2Py framework modules ======================== """ -__all__ = ['A', 'B', 'BEAUTIFY', 'BODY', 'BR', 'CAT', 'CENTER', 'CLEANUP', 'CODE', 'CRYPT', 'DAL', 'DIV', 'EM', 'EMBED', 'FIELDSET', 'FORM', 'Field', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEAD', 'HR', 'HTML', 'HTTP', 'I', 'IFRAME', 'IMG', 'INPUT', 'IS_ALPHANUMERIC', 'IS_DATE', 'IS_DATETIME', 'IS_DATETIME_IN_RANGE', 'IS_DATE_IN_RANGE', 'IS_DECIMAL_IN_RANGE', 'IS_EMAIL', 'IS_LIST_OF_EMAILS', 'IS_EMPTY_OR', 'IS_EQUAL_TO', 'IS_EXPR', 'IS_FLOAT_IN_RANGE', 'IS_IMAGE', 'IS_JSON', 'IS_INT_IN_RANGE', 'IS_IN_DB', 'IS_IN_SET', 'IS_IPV4', 'IS_LENGTH', 'IS_LIST_OF', 'IS_LOWER', 'IS_MATCH', 'IS_NOT_EMPTY', 'IS_NOT_IN_DB', 'IS_NULL_OR', 'IS_SLUG', 'IS_STRONG', 'IS_TIME', 'IS_UPLOAD_FILENAME', 'IS_UPPER', 'IS_URL', 'LABEL', 'LEGEND', 'LI', 'LINK', 'LOAD', 'MARKMIN', 'MENU', 'META', 'OBJECT', 'OL', 'ON', 'OPTGROUP', 'OPTION', 'P', 'PRE', 'SCRIPT', 'SELECT', 'SPAN', 'SQLFORM', 'SQLTABLE', 'STRONG', 'STYLE', 'TABLE', 'TAG', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TITLE', 'TR', 'TT', 'UL', 'URL', 'XHTML', 'XML', 'redirect', 'current', 'embed64'] +__all__ = ['A', 'B', 'BEAUTIFY', 'BODY', 'BR', 'CAT', 'CENTER', 'CLEANUP', 'CODE', 'CRYPT', 'DAL', 'DIV', 'EM', 'EMBED', 'FIELDSET', 'FORM', 'Field', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEAD', 'HR', 'HTML', 'HTTP', 'I', 'IFRAME', 'IMG', 'INPUT', 'IS_ALPHANUMERIC', 'IS_DATE', 'IS_DATETIME', 'IS_DATETIME_IN_RANGE', 'IS_DATE_IN_RANGE', 'IS_DECIMAL_IN_RANGE', 'IS_EMAIL', 'IS_LIST_OF_EMAILS', 'IS_EMPTY_OR', 'IS_EQUAL_TO', 'IS_EXPR', 'IS_FILE', 'IS_FLOAT_IN_RANGE', 'IS_IMAGE', 'IS_JSON', 'IS_INT_IN_RANGE', 'IS_IN_DB', 'IS_IN_SET', 'IS_IPV4', 'IS_LENGTH', 'IS_LIST_OF', 'IS_LOWER', 'IS_MATCH', 'IS_NOT_EMPTY', 'IS_NOT_IN_DB', 'IS_NULL_OR', 'IS_SLUG', 'IS_STRONG', 'IS_TIME', 'IS_UPLOAD_FILENAME', 'IS_UPPER', 'IS_URL', 'LABEL', 'LEGEND', 'LI', 'LINK', 'LOAD', 'MARKMIN', 'MENU', 'META', 'OBJECT', 'OL', 'ON', 'OPTGROUP', 'OPTION', 'P', 'PRE', 'SCRIPT', 'SELECT', 'SPAN', 'SQLFORM', 'SQLTABLE', 'STRONG', 'STYLE', 'TABLE', 'TAG', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', 'TITLE', 'TR', 'TT', 'UL', 'URL', 'XHTML', 'XML', 'redirect', 'current', 'embed64'] #: add pydal to sys.modules import os diff --git a/gluon/highlight.py b/gluon/highlight.py index add65b8f..9a47f1cd 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -166,7 +166,7 @@ class Highlighter(object): + r'from|True|False)(?![a-zA-Z0-9_])'), 'color:#185369; font-weight: bold'), ('WEB2PY', - re.compile(r'(request|response|session|cache|redirect|local_import|HTTP|TR|XML|URL|BEAUTIFY|A|BODY|BR|B|CAT|CENTER|CODE|COL|COLGROUP|DIV|EM|EMBED|FIELDSET|LEGEND|FORM|H1|H2|H3|H4|H5|H6|IFRAME|HEAD|HR|HTML|I|IMG|INPUT|LABEL|LI|LINK|MARKMIN|MENU|META|OBJECT|OL|ON|OPTION|P|PRE|SCRIPT|SELECT|SPAN|STYLE|TABLE|THEAD|TBODY|TFOOT|TAG|TD|TEXTAREA|TH|TITLE|TT|T|UL|XHTML|IS_SLUG|IS_STRONG|IS_LOWER|IS_UPPER|IS_ALPHANUMERIC|IS_DATETIME|IS_DATETIME_IN_RANGE|IS_DATE|IS_DATE_IN_RANGE|IS_DECIMAL_IN_RANGE|IS_EMAIL|IS_EXPR|IS_FLOAT_IN_RANGE|IS_IMAGE|IS_INT_IN_RANGE|IS_IN_SET|IS_IPV4|IS_LIST_OF|IS_LENGTH|IS_MATCH|IS_EQUAL_TO|IS_EMPTY_OR|IS_NULL_OR|IS_NOT_EMPTY|IS_TIME|IS_UPLOAD_FILENAME|IS_URL|CLEANUP|CRYPT|IS_IN_DB|IS_NOT_IN_DB|DAL|Field|SQLFORM|SQLTABLE|xmlescape|embed64)(?![a-zA-Z0-9_])' + re.compile(r'(request|response|session|cache|redirect|local_import|HTTP|TR|XML|URL|BEAUTIFY|A|BODY|BR|B|CAT|CENTER|CODE|COL|COLGROUP|DIV|EM|EMBED|FIELDSET|LEGEND|FORM|H1|H2|H3|H4|H5|H6|IFRAME|HEAD|HR|HTML|I|IMG|INPUT|LABEL|LI|LINK|MARKMIN|MENU|META|OBJECT|OL|ON|OPTION|P|PRE|SCRIPT|SELECT|SPAN|STYLE|TABLE|THEAD|TBODY|TFOOT|TAG|TD|TEXTAREA|TH|TITLE|TT|T|UL|XHTML|IS_SLUG|IS_STRONG|IS_LOWER|IS_UPPER|IS_ALPHANUMERIC|IS_DATETIME|IS_DATETIME_IN_RANGE|IS_DATE|IS_DATE_IN_RANGE|IS_DECIMAL_IN_RANGE|IS_EMAIL|IS_EXPR|IS_FILE|IS_FLOAT_IN_RANGE|IS_IMAGE|IS_INT_IN_RANGE|IS_IN_SET|IS_IPV4|IS_LIST_OF|IS_LENGTH|IS_MATCH|IS_EQUAL_TO|IS_EMPTY_OR|IS_NULL_OR|IS_NOT_EMPTY|IS_TIME|IS_UPLOAD_FILENAME|IS_URL|CLEANUP|CRYPT|IS_IN_DB|IS_NOT_IN_DB|DAL|Field|SQLFORM|SQLTABLE|xmlescape|embed64)(?![a-zA-Z0-9_])' ), 'link:%(link)s;text-decoration:None;color:#FF5C1F;'), ('MAGIC', re.compile(r'self|None'), 'color:#185369; font-weight: bold'), diff --git a/gluon/tests/test_validators.py b/gluon/tests/test_validators.py index dde4985d..ad6ec128 100644 --- a/gluon/tests/test_validators.py +++ b/gluon/tests/test_validators.py @@ -1021,6 +1021,65 @@ class TestValidators(unittest.TestCase): rtn = IS_IMAGE(error_message='oops')(img) self.assertEqual(rtn, (img, 'oops')) + def test_IS_FILE(self): + import cgi + from io import BytesIO + + def gen_fake(filename): + formdata_file_data = """ +---123 +Content-Disposition: form-data; name="key2" + +value2y +---123 +Content-Disposition: form-data; name="file_attach"; filename="%s" +Content-Type: text/plain + +this is the content of the fake file + +---123-- +""" % filename + formdata_file_environ = { + 'CONTENT_LENGTH': str(len(formdata_file_data)), + 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', + 'QUERY_STRING': 'key1=value1&key2=value2x', + 'REQUEST_METHOD': 'POST', + } + return cgi.FieldStorage(fp=BytesIO(to_bytes(formdata_file_data)), environ=formdata_file_environ)['file_attach'] + + fake = gen_fake('example.pdf') + rtn = IS_FILE(extension='pdf')(fake) + self.assertEqual(rtn, (fake, None)) + fake = gen_fake('example.gif') + rtn = IS_FILE(extension='pdf')(fake) + self.assertEqual(rtn, (fake, 'Enter valid filename')) + fake = gen_fake('multiple.pdf') + rtn = IS_FILE(extension=['pdf', 'png'])(fake) + self.assertEqual(rtn, (fake, None)) + fake = gen_fake('multiple.png') + rtn = IS_FILE(extension=['pdf', 'png'])(fake) + self.assertEqual(rtn, (fake, None)) + fake = gen_fake('multiple.gif') + rtn = IS_FILE(extension=['pdf', 'png'])(fake) + self.assertEqual(rtn, (fake, 'Enter valid filename')) + fake = gen_fake('backup2014.tar.gz') + rtn = IS_FILE(filename=re.compile('backup.*'), extension='tar.gz', lastdot=False)(fake) + self.assertEqual(rtn, (fake, None)) + fake = gen_fake('README') + rtn = IS_FILE(filename='README', extension='', case=0)(fake) + self.assertEqual(rtn, (fake, None)) + fake = gen_fake('readme') + rtn = IS_FILE(filename='README', extension='', case=0)(fake) + self.assertEqual(rtn, (fake, 'Enter valid filename')) + fake = gen_fake('readme') + rtn = IS_FILE(filename='README', case=2)(fake) + self.assertEqual(rtn, (fake, None)) + fake = gen_fake('README') + rtn = IS_FILE(filename='README', case=2)(fake) + self.assertEqual(rtn, (fake, None)) + rtn = IS_FILE(extension='pdf')('example.pdf') + self.assertEqual(rtn, ('example.pdf', 'Enter valid filename')) + def test_IS_UPLOAD_FILENAME(self): import cgi from io import BytesIO diff --git a/gluon/validators.py b/gluon/validators.py index 0d05e14c..36d3b28e 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -48,6 +48,7 @@ __all__ = [ 'IS_LIST_OF_EMAILS', 'IS_EMPTY_OR', 'IS_EXPR', + 'IS_FILE', 'IS_FLOAT_IN_RANGE', 'IS_IMAGE', 'IS_IN_DB', @@ -3339,8 +3340,100 @@ class IS_IMAGE(Validator): return (-1, -1) +class IS_FILE(Validator): + """ + Checks if name and extension of file uploaded through file input matches + given criteria. + + Does *not* ensure the file type in any way. Returns validation failure + if no data was uploaded. + + Args: + filename: string/compiled regex or a list of strings/regex of valid filenames + extension: string/compiled regex or a list of strings/regex of valid extensions + lastdot: which dot should be used as a filename / extension separator: + True means last dot, eg. file.jpg.png -> file.jpg / png + False means first dot, eg. file.tar.gz -> file / tar.gz + case: 0 - keep the case, 1 - transform the string into lowercase (default), + 2 - transform the string into uppercase + + If there is no dot present, extension checks will be done against empty + string and filename checks against whole value. + + Examples: + Check if file has a pdf extension (case insensitive): + + INPUT(_type='file', _name='name', + requires=IS_FILE(extension='pdf')) + + Check if file is called 'thumbnail' and has a jpg or png extension + (case insensitive): + + INPUT(_type='file', _name='name', + requires=IS_FILE(filename='thumbnail', + extension=['jpg', 'png'])) + + Check if file has a tar.gz extension and name starting with backup: + + INPUT(_type='file', _name='name', + requires=IS_FILE(filename=re.compile('backup.*'), + extension='tar.gz', lastdot=False)) + + Check if file has no extension and name matching README + (case sensitive): + + INPUT(_type='file', _name='name', + requires=IS_FILE(filename='README', + extension='', case=0) + + """ + + def __init__(self, filename=None, extension=None, lastdot=True, case=1, + error_message='Enter valid filename'): + self.filename = filename + self.extension = extension + self.lastdot = lastdot + self.case = case + self.error_message = error_message + + def match(self, value1, value2): + if isinstance(value1, (list, tuple)): + for v in value1: + if self.match(v, value2): + return True + return False + elif isinstance(value1, type(regex_isint)): + return value1.match(value2) + elif isinstance(value1, str): + return value1 == value2 + + def __call__(self, value): + try: + string = value.filename + except: + return (value, translator(self.error_message)) + if self.case == 1: + string = string.lower() + elif self.case == 2: + string = string.upper() + if self.lastdot: + dot = string.rfind('.') + else: + dot = string.find('.') + if dot == -1: + dot = len(string) + if self.filename and not self.match(self.filename, string[:dot]): + return (value, translator(self.error_message)) + elif self.extension and not self.match(self.extension, string[dot + 1:]): + return (value, translator(self.error_message)) + else: + return (value, None) + + class IS_UPLOAD_FILENAME(Validator): """ + For new applications, use IS_FILE(). + Checks if name and extension of file uploaded through file input matches given criteria.