From 928fd364cf00d228d2d5d8b60d28522c6111ec19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 6 Sep 2018 16:16:44 +0100 Subject: [PATCH 01/15] Add SameSite support --- gluon/globals.py | 10 +++++++++- gluon/tests/test_globals.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/gluon/globals.py b/gluon/globals.py index 26fba47c..d557294b 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -1075,6 +1075,8 @@ class Session(Storage): scookies['HttpOnly'] = True if self._secure: scookies['secure'] = True + if self._same_site: + scookies['samesite'] = self._same_site def clear_session_cookies(self): request = current.request @@ -1153,6 +1155,12 @@ class Session(Storage): def secure(self): self._secure = True + def samesite(self, mode='Lax'): + if 'samesite' not in Cookie.Morsel._reserved: + # Python version 3.7 and lower needs this + Cookie.Morsel._reserved['samesite'] = 'SameSite' + self._same_site = mode + def forget(self, response=None): self._close(response) self._forget = True @@ -1180,7 +1188,7 @@ class Session(Storage): def _unchanged(self, response): if response.session_new: - internal = ['_last_timestamp', '_secure', '_start_timestamp'] + internal = ['_last_timestamp', '_secure', '_start_timestamp', '_same_site'] for item in self.keys(): if item not in internal: return False diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py index 666249fa..b8c66dd8 100644 --- a/gluon/tests/test_globals.py +++ b/gluon/tests/test_globals.py @@ -231,6 +231,24 @@ class testResponse(unittest.TestCase): cookie = str(current.response.cookies) self.assertTrue('httponly' not in cookie.lower()) + def test_cookies_samesite(self): + current = setup_clean_session() + current.session._fixup_before_save() + cookie = str(current.response.cookies) + self.assertTrue('samesite' not in cookie.lower()) + + current = setup_clean_session() + current.session.samesite() + current.session._fixup_before_save() + cookie = str(current.response.cookies) + self.assertTrue('samesite=lax' in cookie.lower()) + + current = setup_clean_session() + current.session.samesite('Strict') + current.session._fixup_before_save() + cookie = str(current.response.cookies) + self.assertTrue('samesite=strict' in cookie.lower()) + def test_include_meta(self): response = Response() response.meta[u'web2py'] = 'web2py' From 9375ea737823a4cbc90a9d081b37f435006e5bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=BE=D1=80=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=A0=D0=BE?= =?UTF-8?q?=D0=BC=D0=B0=D0=BD?= Date: Wed, 19 Sep 2018 10:57:57 +0300 Subject: [PATCH 02/15] TypeError when try to disable appliance --- applications/admin/controllers/default.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 28198d2a..211ed36f 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -562,7 +562,11 @@ def enable(): os.unlink(filename) return SPAN(T('Disable'), _style='color:green') else: - safe_open(filename, 'wb').write('disabled: True\ntime-disabled: %s' % request.now) + if PY2: + safe_open(filename, 'wb').write('disabled: True\ntime-disabled: %s' % request.now) + else: + str_ = 'disabled: True\ntime-disabled: %s' % request.now + safe_open(filename, 'wb').write(str_.encode('utf-8')) return SPAN(T('Enable'), _style='color:red') From fba90d31f4925c078b11eadc8c39d5842483ea96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=BE=D1=80=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=A0=D0=BE?= =?UTF-8?q?=D0=BC=D0=B0=D0=BD?= Date: Wed, 19 Sep 2018 11:06:04 +0300 Subject: [PATCH 03/15] Error 'dict_items += list' when try to save code-editor settings --- applications/admin/controllers/default.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 211ed36f..4a142f22 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -646,7 +646,10 @@ def edit(): # show settings tab and save prefernces if 'settings' in request.vars: if request.post_vars: # save new preferences - post_vars = request.post_vars.items() + if PY2: + post_vars = request.post_vars.items() + else: + post_vars = list(request.post_vars.items()) # Since unchecked checkbox are not serialized, we must set them as false by hand to store the correct preference in the settings post_vars += [(opt, 'false') for opt in preferences if opt not in request.post_vars] if config.save(post_vars): From 62f537287621c8ab5fc3d3e5b6e405bc3ebf22dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 20 Sep 2018 12:47:05 +0100 Subject: [PATCH 04/15] Fixes #2007 --- gluon/sqlhtml.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index 68d8014c..dfca9ff1 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2023,7 +2023,7 @@ class SQLFORM(FORM): to hold the fields. """ # this is here to avoid circular references - from gluon.dal import DAL + from gluon.dal import DAL, _default_validators # Define a table name, this way it can be logical to our CSS. # And if you switch from using SQLFORM to SQLFORM.factory # your same css definitions will still apply. @@ -2036,8 +2036,9 @@ class SQLFORM(FORM): # Clone fields, while passing tables straight through fields_with_clones = [f.clone() if isinstance(f, Field) else f for f in fields] - - return SQLFORM(DAL(None).define_table(table_name, *fields_with_clones), **attributes) + dummy_dal = DAL(None) + dummy_dal.validators_method = lambda f: _default_validators(dummy_dal, f) # See https://github.com/web2py/web2py/issues/2007 + return SQLFORM(dummy_dal.define_table(table_name, *fields_with_clones), **attributes) @staticmethod def build_query(fields, keywords): From 11b441b77799958436fb8e8b3678aae9b2834892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 20 Sep 2018 13:02:09 +0100 Subject: [PATCH 05/15] Make SameSite=Lax the default for all web2py apps --- gluon/globals.py | 11 ++++++++--- gluon/tests/test_globals.py | 15 ++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/gluon/globals.py b/gluon/globals.py index d557294b..7390a3f9 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -1075,7 +1075,15 @@ class Session(Storage): scookies['HttpOnly'] = True if self._secure: scookies['secure'] = True + if self._same_site is None: + # Using SameSite Lax Mode is the default + # You actually have to call session.samesite(False) if you really + # dont want the extra protection provided by the SameSite header + self._same_site = 'Lax' if self._same_site: + if 'samesite' not in Cookie.Morsel._reserved: + # Python version 3.7 and lower needs this + Cookie.Morsel._reserved['samesite'] = 'SameSite' scookies['samesite'] = self._same_site def clear_session_cookies(self): @@ -1156,9 +1164,6 @@ class Session(Storage): self._secure = True def samesite(self, mode='Lax'): - if 'samesite' not in Cookie.Morsel._reserved: - # Python version 3.7 and lower needs this - Cookie.Morsel._reserved['samesite'] = 'SameSite' self._same_site = mode def forget(self, response=None): diff --git a/gluon/tests/test_globals.py b/gluon/tests/test_globals.py index b8c66dd8..bd78e4e1 100644 --- a/gluon/tests/test_globals.py +++ b/gluon/tests/test_globals.py @@ -232,17 +232,18 @@ class testResponse(unittest.TestCase): self.assertTrue('httponly' not in cookie.lower()) def test_cookies_samesite(self): + # Test Lax is the default mode current = setup_clean_session() current.session._fixup_before_save() cookie = str(current.response.cookies) - self.assertTrue('samesite' not in cookie.lower()) - - current = setup_clean_session() - current.session.samesite() - current.session._fixup_before_save() - cookie = str(current.response.cookies) self.assertTrue('samesite=lax' in cookie.lower()) - + # Test you can disable samesite + current = setup_clean_session() + current.session.samesite(False) + current.session._fixup_before_save() + cookie = str(current.response.cookies) + self.assertTrue('samesite' not in cookie.lower()) + # Test you can change mode current = setup_clean_session() current.session.samesite('Strict') current.session._fixup_before_save() From 50692a4fd355ebcc8964917dd3352767695a66d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 20 Sep 2018 18:59:41 +0100 Subject: [PATCH 06/15] Fixes #726 --- gluon/tools.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gluon/tools.py b/gluon/tools.py index 0910b6c4..8eb2dc02 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1378,6 +1378,7 @@ class Auth(AuthAPI): login_after_password_change=True, login_after_registration=False, login_captcha=None, + login_specify_error=False, long_expiration=3600 * 30 * 24, # one month mailer=None, manager_actions={}, @@ -2567,6 +2568,8 @@ class Auth(AuthAPI): settings.formstyle, 'captcha__row') accepted_form = False + specific_error = self.messages.invalid_user + if form.accepts(request, session if self.csrf_prevention else None, formname='login', dbio=False, onvalidation=onvalidation, @@ -2582,6 +2585,7 @@ class Auth(AuthAPI): user = table_user(**{username: entered_username}) if user: # user in db, check if registration pending or disabled + specific_error = self.messages.invalid_password temp_user = user if (temp_user.registration_key or '').startswith('pending'): response.flash = self.messages.registration_pending @@ -2631,7 +2635,7 @@ class Auth(AuthAPI): self.log_event(self.messages['login_failed_log'], request.post_vars) # invalid login - session.flash = self.messages.invalid_login + session.flash = specific_error if self.settings.login_specify_error else self.messages.invalid_login callback(onfail, None) redirect( self.url(args=request.args, vars=request.get_vars), From 398fc6de3714c6fde9b4f0dde571a3fb5d425fdb Mon Sep 17 00:00:00 2001 From: Holger Rother Date: Fri, 21 Sep 2018 07:58:12 +0200 Subject: [PATCH 07/15] Testcase for #2007 --- gluon/tests/test_sqlhtml.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/gluon/tests/test_sqlhtml.py b/gluon/tests/test_sqlhtml.py index 92c2916e..63e7805c 100644 --- a/gluon/tests/test_sqlhtml.py +++ b/gluon/tests/test_sqlhtml.py @@ -4,6 +4,7 @@ """ Unit tests for gluon.sqlhtml """ +import datetime import os import sys import unittest @@ -312,12 +313,34 @@ class TestSQLFORM(unittest.TestCase): Field('field_two', 'string')) self.assertEqual(factory_form.xml()[:5], b' Date: Sat, 22 Sep 2018 09:55:26 +0200 Subject: [PATCH 08/15] Simplify testcase --- gluon/tests/test_sqlhtml.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/gluon/tests/test_sqlhtml.py b/gluon/tests/test_sqlhtml.py index 63e7805c..52a69530 100644 --- a/gluon/tests/test_sqlhtml.py +++ b/gluon/tests/test_sqlhtml.py @@ -329,11 +329,8 @@ class TestSQLFORM(unittest.TestCase): # Fake the formkey current.session['_formkey[no_table/create]'] = ['123'] - if factory_form.process().accepted: - self.assertTrue(True) - self.assertIsInstance(factory_form.vars.a_date, datetime.date) - else: - self.assertFalse(True, 'Setup for test failed. Form was not accepted.') + self.assertTrue(factory_form.process().accepted) + self.assertIsInstance(factory_form.vars.a_date, datetime.date) # def test_build_query(self): # pass From d244c342821dd079c53bd42497fbd1d33439936d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Mon, 24 Sep 2018 22:04:04 +0100 Subject: [PATCH 09/15] Fixes #2020 --- gluon/sqlhtml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index dfca9ff1..c928e5af 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2315,7 +2315,7 @@ class SQLFORM(FORM): button='button btn btn-default btn-secondary', buttontext='buttontext button', buttonadd='icon plus icon-plus glyphicon glyphicon-plus', - buttonback='icon leftarrow icon-arrow-left glyphicon glyphicon-arrow-left', + buttonback='icon arrowleft icon-arrow-left glyphicon glyphicon-arrow-left', buttonexport='icon downarrow icon-download glyphicon glyphicon-download', buttondelete='icon trash icon-trash glyphicon glyphicon-trash', buttonedit='icon pen icon-pencil glyphicon glyphicon-pencil', From 904ca403a21dde49c6c971b569816c5981f04cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 27 Sep 2018 01:53:08 +0100 Subject: [PATCH 10/15] Python 3 compatibility Fixes #2024 --- gluon/languages.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gluon/languages.py b/gluon/languages.py index 07dbbac9..72167bcd 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -311,7 +311,7 @@ def write_plural_dict(filename, contents): try: fp = LockedFile(filename, 'w') fp.write('#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n{\n# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...],\n') - for key in sorted(contents, sort_function): + for key in sorted(contents, cmp=sort_function): forms = '[' + ','.join([repr(Utf8(form)) for form in contents[key]]) + ']' fp.write('%s: %s,\n' % (repr(Utf8(key)), forms)) @@ -936,8 +936,8 @@ class translator(object): word = w[1:] fun = cap_fun if i is not None: - return fun(self.plural(word, symbols[int(i)])) - return fun(word) + return to_native(fun(self.plural(word, symbols[int(i)]))) + return to_native(fun(word)) def sub_dict(m): """ word(key or num) From f5638c8f6bdaff2daaa16f5e0b6df56d7d03e79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 27 Sep 2018 16:22:48 +0100 Subject: [PATCH 11/15] Python 3 has no cmp function in sorted --- gluon/languages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/languages.py b/gluon/languages.py index 72167bcd..42a24f2c 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -311,7 +311,7 @@ def write_plural_dict(filename, contents): try: fp = LockedFile(filename, 'w') fp.write('#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n{\n# "singular form (0)": ["first plural form (1)", "second plural form (2)", ...],\n') - for key in sorted(contents, cmp=sort_function): + for key in sorted(contents, key=sort_function): forms = '[' + ','.join([repr(Utf8(form)) for form in contents[key]]) + ']' fp.write('%s: %s,\n' % (repr(Utf8(key)), forms)) @@ -326,7 +326,7 @@ def write_plural_dict(filename, contents): def sort_function(x, y): - return cmp(unicode(x, 'utf-8').lower(), unicode(y, 'utf-8').lower()) + return unicode(x, 'utf-8').lower() def write_dict(filename, contents): From 19c41b308d4f43b8d6c8e6985e4006326eef3d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 27 Sep 2018 17:57:24 +0100 Subject: [PATCH 12/15] Update languages.py --- gluon/languages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/languages.py b/gluon/languages.py index 42a24f2c..af7bc326 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -325,7 +325,7 @@ def write_plural_dict(filename, contents): fp.close() -def sort_function(x, y): +def sort_function(x): return unicode(x, 'utf-8').lower() From 6034368364b5ac42eff1b4b70330c9928e20f2e7 Mon Sep 17 00:00:00 2001 From: Nico Zanferrari Date: Thu, 27 Sep 2018 21:16:05 +0200 Subject: [PATCH 13/15] Python 3 compatibility + fixed link for cookbook --- applications/examples/views/default/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/examples/views/default/index.html b/applications/examples/views/default/index.html index 19708c79..735ee4cc 100644 --- a/applications/examples/views/default/index.html +++ b/applications/examples/views/default/index.html @@ -4,7 +4,7 @@

Web Framework

-

Free open source full-stack framework for rapid development of fast, scalable, secure and portable database-driven web-based applications. Written and programmable in Python.

+

Free open source full-stack framework for rapid development of fast, scalable, secure and portable database-driven web-based applications. Written and programmable in Python (version 3 and 2.7).

From 4c87932f069a5e60de04cf24a653cc6f52290ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Wed, 3 Oct 2018 12:53:58 +0100 Subject: [PATCH 14/15] Show readable fields with a default in create form Fixes #2006 This is a backwards compatibility fix. --- gluon/sqlhtml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index c928e5af..ac45c040 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -1351,7 +1351,7 @@ class SQLFORM(FORM): if not readonly: if not record: # create form should only show writable fields - fields = [f.name for f in table if (ignore_rw or f.writable) and not f.compute] + fields = [f.name for f in table if (ignore_rw or f.writable or (f.readable and f.default)) and not f.compute] else: # update form should also show readable fields and computed fields (but in reaodnly mode) fields = [f.name for f in table if (ignore_rw or f.writable or f.readable)] From 800bd538708bff02db51ddfc4ef979c5e19cff66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonel=20C=C3=A2mara?= Date: Thu, 4 Oct 2018 12:43:49 +0100 Subject: [PATCH 15/15] Use export columns as is supposed instead of selectable ones Fixes #2014 --- gluon/sqlhtml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index c928e5af..4cb8a39d 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -2693,13 +2693,13 @@ class SQLFORM(FORM): dbset = dbset(SQLFORM.build_query( sfields, keywords)) rows = dbset.select(left=left, orderby=orderby, - cacheable=True, *selectable_columns) + cacheable=True, *expcolumns) except Exception as e: response.flash = T('Internal Error') rows = [] else: rows = dbset.select(left=left, orderby=orderby, - cacheable=True, *selectable_columns) + cacheable=True, *expcolumns) value = exportManager[export_type] clazz = value[0] if hasattr(value, '__getitem__') else value
@@ -18,7 +18,7 @@ - +