version 2.13.1
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
## master
|
||||
- Change .flash and .hidden to .w2p_flash and .w2p_hidden
|
||||
- copy the static folder from the new welcome app when you update web2py
|
||||
## 2.13.1
|
||||
|
||||
## 2.12.1
|
||||
- added fabfile.py
|
||||
- fixed oauth2 renew token, thanks dokime7
|
||||
- fixed add_membership, del_membership, add_membership IntegrityError (when auth.enable_record_versioning)
|
||||
- allow passing unicode to template render
|
||||
- allow IS_NOT_IN_DB to work with custom primarykey, thanks timmyborg
|
||||
- allow HttpOnly cookies
|
||||
- french pluralizaiton rules, thanks Mathieu Clabaut
|
||||
- fixed bug in redirect to cas service, thanks Fernando González
|
||||
- allow deploying to pythonanywhere from the web2py admin that you're running locally, thanks Leonel
|
||||
- better tests
|
||||
- many more bug fixes
|
||||
|
||||
## 2.12.1-3
|
||||
|
||||
- security fix: Validate for open redirect everywhere, not just in login()
|
||||
- allow to pack invidual apps and selected files as packed exe files
|
||||
|
||||
@@ -32,7 +32,7 @@ update:
|
||||
echo "remember that pymysql was tweaked"
|
||||
src:
|
||||
### Use semantic versioning
|
||||
echo 'Version 2.12.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
|
||||
echo 'Version 2.13.1-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
|
||||
### rm -f all junk files
|
||||
make clean
|
||||
### clean up baisc apps
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ def app_cleanup(app, request):
|
||||
r = False
|
||||
|
||||
# Remove cache files
|
||||
path = apath('%s/cache/' % app, request)
|
||||
path = apath('%s/cache/' % app, request)
|
||||
if os.path.exists(path):
|
||||
CacheOnDisk(folder=path).clear()
|
||||
for f in os.listdir(path):
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ def remove_oldest_entries(storage, percentage=90):
|
||||
# compute current memory usage (%)
|
||||
old_mem = psutil.virtual_memory().percent
|
||||
# if we have data in storage and utilization exceeds 90%
|
||||
while storage and old_mem > percentage:
|
||||
while storage and old_mem > percentage:
|
||||
# removed oldest entry
|
||||
storage.popitem(last=False)
|
||||
# garbage collect
|
||||
@@ -378,7 +378,7 @@ class CacheOnDisk(CacheAbstract):
|
||||
|
||||
|
||||
def safe_apply(self, key, function, default_value=None):
|
||||
"""
|
||||
"""
|
||||
Safely apply a function to the value of a key in storage and set
|
||||
the return value of the function to it.
|
||||
|
||||
|
||||
+30
-30
@@ -470,21 +470,21 @@ def compile_views(folder, skip_failed_views=False):
|
||||
"""
|
||||
|
||||
path = pjoin(folder, 'views')
|
||||
failed_views = []
|
||||
failed_views = []
|
||||
for fname in listdir(path, '^[\w/\-]+(\.\w+)*$'):
|
||||
try:
|
||||
data = parse_template(fname, path)
|
||||
except Exception, e:
|
||||
if skip_failed_views:
|
||||
failed_views.append(file)
|
||||
else:
|
||||
raise Exception("%s in %s" % (e, file))
|
||||
else:
|
||||
filename = ('views/%s.py' % file).replace('/', '_').replace('\\', '_')
|
||||
filename = pjoin(folder, 'compiled', filename)
|
||||
write_file(filename, data)
|
||||
save_pyc(filename)
|
||||
os.unlink(filename)
|
||||
if skip_failed_views:
|
||||
failed_views.append(file)
|
||||
else:
|
||||
raise Exception("%s in %s" % (e, file))
|
||||
else:
|
||||
filename = ('views/%s.py' % file).replace('/', '_').replace('\\', '_')
|
||||
filename = pjoin(folder, 'compiled', filename)
|
||||
write_file(filename, data)
|
||||
save_pyc(filename)
|
||||
os.unlink(filename)
|
||||
return failed_views if failed_views else None
|
||||
|
||||
|
||||
@@ -675,25 +675,25 @@ def run_view_in(environment):
|
||||
restricted(ccode, environment, 'file stream')
|
||||
else:
|
||||
filename = pjoin(folder, 'views', view)
|
||||
if os.path.exists(path): # compiled views
|
||||
x = view.replace('/', '_')
|
||||
files = ['views_%s.pyc' % x]
|
||||
is_compiled = os.path.exists(pjoin(path, files[0]))
|
||||
# Don't use a generic view if the non-compiled view exists.
|
||||
if is_compiled or (not is_compiled and not os.path.exists(filename)):
|
||||
if allow_generic:
|
||||
files.append('views_generic.%s.pyc' % request.extension)
|
||||
# for backward compatibility
|
||||
if request.extension == 'html':
|
||||
files.append('views_%s.pyc' % x[:-5])
|
||||
if allow_generic:
|
||||
files.append('views_generic.pyc')
|
||||
# end backward compatibility code
|
||||
for f in files:
|
||||
compiled = pjoin(path, f)
|
||||
if os.path.exists(compiled):
|
||||
code = read_pyc(compiled)
|
||||
restricted(code, environment, layer=compiled)
|
||||
if os.path.exists(path): # compiled views
|
||||
x = view.replace('/', '_')
|
||||
files = ['views_%s.pyc' % x]
|
||||
is_compiled = os.path.exists(pjoin(path, files[0]))
|
||||
# Don't use a generic view if the non-compiled view exists.
|
||||
if is_compiled or (not is_compiled and not os.path.exists(filename)):
|
||||
if allow_generic:
|
||||
files.append('views_generic.%s.pyc' % request.extension)
|
||||
# for backward compatibility
|
||||
if request.extension == 'html':
|
||||
files.append('views_%s.pyc' % x[:-5])
|
||||
if allow_generic:
|
||||
files.append('views_generic.pyc')
|
||||
# end backward compatibility code
|
||||
for f in files:
|
||||
compiled = pjoin(path, f)
|
||||
if os.path.exists(compiled):
|
||||
code = read_pyc(compiled)
|
||||
restricted(code, environment, layer=compiled)
|
||||
return
|
||||
if not os.path.exists(filename) and allow_generic:
|
||||
view = 'generic.' + request.extension
|
||||
|
||||
@@ -41,7 +41,7 @@ class CustomImportException(ImportError):
|
||||
|
||||
def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
|
||||
"""
|
||||
web2py's custom importer. It behaves like the standard Python importer but
|
||||
web2py's custom importer. It behaves like the standard Python importer but
|
||||
it tries to transform import statements as something like
|
||||
"import applications.app_name.modules.x".
|
||||
If the import fails, it falls back on naive_importer
|
||||
@@ -80,7 +80,7 @@ def custom_importer(name, globals=None, locals=None, fromlist=None, level=-1):
|
||||
if not fromlist:
|
||||
# import like "import x" or "import x.y"
|
||||
result = None
|
||||
for itemname in name.split("."):
|
||||
for itemname in name.split("."):
|
||||
new_mod = base_importer(
|
||||
modules_prefix, globals, locals, [itemname], level)
|
||||
try:
|
||||
|
||||
+1
-1
@@ -900,7 +900,7 @@ def render(content="hello world",
|
||||
|
||||
if isinstance(content, unicode):
|
||||
content = content.encode('utf8')
|
||||
|
||||
|
||||
# save current response class
|
||||
if context and 'response' in context:
|
||||
old_response_body = context['response'].body
|
||||
|
||||
+13
-13
@@ -16,19 +16,19 @@ from gluon.globals import Request, Response, Session
|
||||
from gluon import URL
|
||||
|
||||
def setup_clean_session():
|
||||
request = Request(env={})
|
||||
request.application = 'a'
|
||||
request.controller = 'c'
|
||||
request.function = 'f'
|
||||
request.folder = 'applications/admin'
|
||||
response = Response()
|
||||
session = Session()
|
||||
session.connect(request, response)
|
||||
from gluon.globals import current
|
||||
current.request = request
|
||||
current.response = response
|
||||
current.session = session
|
||||
return current
|
||||
request = Request(env={})
|
||||
request.application = 'a'
|
||||
request.controller = 'c'
|
||||
request.function = 'f'
|
||||
request.folder = 'applications/admin'
|
||||
response = Response()
|
||||
session = Session()
|
||||
session.connect(request, response)
|
||||
from gluon.globals import current
|
||||
current.request = request
|
||||
current.response = response
|
||||
current.session = session
|
||||
return current
|
||||
|
||||
class testResponse(unittest.TestCase):
|
||||
|
||||
|
||||
@@ -309,7 +309,7 @@ class TestBareHelpers(unittest.TestCase):
|
||||
self.assertEqual(XML('<h1>Hello<a data-hello="world">World</a></h1>', sanitize=True),
|
||||
XML('<h1>HelloWorld</h1>'))
|
||||
#bug check for the sanitizer for closing no-close tags
|
||||
self.assertEqual(XML('<p>Test</p><br/><p>Test</p><br/>', sanitize=True),
|
||||
self.assertEqual(XML('<p>Test</p><br/><p>Test</p><br/>', sanitize=True),
|
||||
XML('<p>Test</p><br /><p>Test</p><br />'))
|
||||
|
||||
def testTAG(self):
|
||||
|
||||
@@ -146,7 +146,7 @@ class TestList(unittest.TestCase):
|
||||
'something')
|
||||
# except if default is especified
|
||||
self.assertEqual(b(0, default=0, otherwise=lambda: 'something'), 0)
|
||||
|
||||
|
||||
def test_listgetitem(self):
|
||||
'''Mantains list behaviour.'''
|
||||
a = List((1, 2, 3))
|
||||
|
||||
+11
-11
@@ -24,42 +24,42 @@ class TestUtils(unittest.TestCase):
|
||||
|
||||
data = md5_hash("web2py rocks")
|
||||
self.assertEqual(data, '79509f3246a2824dee64635303e99204')
|
||||
|
||||
|
||||
def test_compare(self):
|
||||
""" Tests the compare funciton """
|
||||
|
||||
|
||||
a, b = 'test123', 'test123'
|
||||
compare_result_true = compare(a, b)
|
||||
self.assertTrue(compare_result_true)
|
||||
|
||||
|
||||
a, b = 'test123', 'test456'
|
||||
compare_result_false = compare(a, b)
|
||||
self.assertFalse(compare_result_false)
|
||||
|
||||
|
||||
def test_simple_hash(self):
|
||||
""" Tests the simple_hash function """
|
||||
|
||||
|
||||
# no key, no salt, md5
|
||||
data_md5 = simple_hash('web2py rocks!', key='', salt='', digest_alg='md5')
|
||||
self.assertEqual(data_md5, '37d95defba6c8834cb8cae86ee888568')
|
||||
|
||||
|
||||
# no key, no salt, sha1
|
||||
data_sha1 = simple_hash('web2py rocks!', key='', salt='', digest_alg='sha1')
|
||||
self.assertEqual(data_sha1, '00489a46753d8db260c71542611cdef80652c4b7')
|
||||
|
||||
|
||||
# no key, no salt, sha224
|
||||
data_sha224 = simple_hash('web2py rocks!', key='', salt='', digest_alg='sha224')
|
||||
self.assertEqual(data_sha224, '84d7054271842c2c17983baa2b1447e0289d101140a8c002d49d60da')
|
||||
|
||||
|
||||
# no key, no salt, sha256
|
||||
data_sha256 = simple_hash('web2py rocks!', key='', salt='', digest_alg='sha256')
|
||||
self.assertEqual(data_sha256, '0849f224d8deb267e4598702aaec1bd749e6caec90832469891012a4be24af08')
|
||||
|
||||
|
||||
# no key, no salt, sha384
|
||||
data_sha384 = simple_hash('web2py rocks!', key='', salt='', digest_alg='sha384')
|
||||
self.assertEqual(data_sha384,
|
||||
self.assertEqual(data_sha384,
|
||||
'3cffaf39371adbe84eb10f588d2718207d8e965e9172a27a278321b86977351376ae79f92e91d8c58cad86c491282d5f')
|
||||
|
||||
|
||||
# no key, no salt, sha512
|
||||
data_sha512 = simple_hash('web2py rocks!', key='', salt='', digest_alg='sha512')
|
||||
self.assertEqual(data_sha512, 'fa3237f594743e1d7b6c800bb134b3255cf4a98ab8b01e2ec23256328c9f8059'
|
||||
|
||||
+20
-20
@@ -1426,7 +1426,7 @@ class Auth(object):
|
||||
csrf_prevention=True, propagate_extension=None,
|
||||
url_index=None):
|
||||
|
||||
## next two lines for backward compatibility
|
||||
## next two lines for backward compatibility
|
||||
if not db and environment and isinstance(environment, DAL):
|
||||
db = environment
|
||||
self.db = db
|
||||
@@ -1544,7 +1544,7 @@ class Auth(object):
|
||||
self.define_signature()
|
||||
else:
|
||||
self.signature = None
|
||||
|
||||
|
||||
def get_vars_next(self):
|
||||
next = current.request.vars._next
|
||||
if isinstance(next, (list, tuple)):
|
||||
@@ -1554,7 +1554,7 @@ class Auth(object):
|
||||
# _next variable in the request.
|
||||
items = next.split('/')
|
||||
if '//' in next and items[2] != current.request.env.http_host:
|
||||
next = None
|
||||
next = None
|
||||
return next
|
||||
|
||||
def _get_user_id(self):
|
||||
@@ -1611,7 +1611,7 @@ class Auth(object):
|
||||
'retrieve_username', 'retrieve_password',
|
||||
'reset_password', 'request_reset_password',
|
||||
'change_password', 'profile', 'groups',
|
||||
'impersonate', 'not_authorized', 'confirm_registration',
|
||||
'impersonate', 'not_authorized', 'confirm_registration',
|
||||
'bulk_register','manage_tokens'):
|
||||
if len(request.args) >= 2 and args[0] == 'impersonate':
|
||||
return getattr(self, args[0])(request.args[1])
|
||||
@@ -2373,7 +2373,7 @@ class Auth(object):
|
||||
and a raw password.
|
||||
"""
|
||||
settings = self._get_login_settings()
|
||||
# users can register_bare even if no password is provided,
|
||||
# users can register_bare even if no password is provided,
|
||||
# in this case they will have to reset their password to login
|
||||
if fields.get(settings.passfield):
|
||||
fields[settings.passfield] = \
|
||||
@@ -2381,7 +2381,7 @@ class Auth(object):
|
||||
if not fields.get(settings.userfield):
|
||||
raise ValueError('register_bare: ' +
|
||||
'userfield not provided or invalid')
|
||||
user = self.get_or_create_user(fields, login=False, get=False,
|
||||
user = self.get_or_create_user(fields, login=False, get=False,
|
||||
update_fields=self.settings.update_fields)
|
||||
if not user:
|
||||
# get or create did not create a user (it ignores duplicate records)
|
||||
@@ -2732,7 +2732,7 @@ class Auth(object):
|
||||
# username and password at the first challenge).
|
||||
# Check if this user is signed up for two-factor authentication
|
||||
# If auth.settings.auth_two_factor_enabled it will enable two factor
|
||||
# for all the app. Another way to anble two factor is that the user
|
||||
# for all the app. Another way to anble two factor is that the user
|
||||
# must be part of a group that is called auth.settings.two_factor_authentication_group
|
||||
if user and self.settings.auth_two_factor_enabled == True:
|
||||
session.auth_two_factor_enabled = True
|
||||
@@ -2763,7 +2763,7 @@ class Auth(object):
|
||||
session.auth_two_factor_tries_left = self.settings.auth_two_factor_tries_left
|
||||
# Set the way we generate the code or we send the code. For example using SMS...
|
||||
two_factor_methods = self.settings.two_factor_methods
|
||||
|
||||
|
||||
if two_factor_methods == []:
|
||||
# TODO: Add some error checking to handle cases where email cannot be sent
|
||||
self.settings.mailer.send(
|
||||
@@ -2780,19 +2780,19 @@ class Auth(object):
|
||||
pass
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
if form.accepts(request, session if self.csrf_prevention else None,
|
||||
formname='login', dbio=False,
|
||||
onvalidation=onvalidation,
|
||||
hideerror=settings.hideerror):
|
||||
accepted_form = True
|
||||
|
||||
|
||||
accepted_form = True
|
||||
|
||||
|
||||
'''
|
||||
The lists is executed after form validation for each of the corresponding action.
|
||||
For example, in your model:
|
||||
|
||||
|
||||
In your models copy and paste:
|
||||
|
||||
#Before define tables, we add some extra field to auth_user
|
||||
@@ -2802,7 +2802,7 @@ class Auth(object):
|
||||
|
||||
OFFSET = 60 #Be sure is the same in your OTP Client
|
||||
|
||||
#Set session.auth_two_factor to None. Because the code is generated by external app.
|
||||
#Set session.auth_two_factor to None. Because the code is generated by external app.
|
||||
# This will avoid to use the default setting and send a code by email.
|
||||
def _set_two_factor(user, auth_two_factor):
|
||||
return None
|
||||
@@ -2823,10 +2823,10 @@ class Auth(object):
|
||||
auth.messages.two_factor_comment = "Verify your OTP Client for the code."
|
||||
auth.settings.two_factor_methods = [lambda user, auth_two_factor: _set_two_factor(user, auth_two_factor)]
|
||||
auth.settings.two_factor_onvalidation = [lambda user, otp: verify_otp(user, otp)]
|
||||
|
||||
|
||||
'''
|
||||
if self.settings.two_factor_onvalidation != []:
|
||||
|
||||
|
||||
for two_factor_onvalidation in self.settings.two_factor_onvalidation:
|
||||
try:
|
||||
session.auth_two_factor = two_factor_onvalidation(session.auth_two_factor_user, form.vars['authentication_code'])
|
||||
@@ -2834,7 +2834,7 @@ class Auth(object):
|
||||
pass
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
if form.vars['authentication_code'] == str(session.auth_two_factor):
|
||||
# Handle the case when the two-factor form has been successfully validated
|
||||
# and the user was previously stored (the current user should be None because
|
||||
@@ -2994,7 +2994,7 @@ class Auth(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.settings.register_verify_password:
|
||||
if self.settings.register_verify_password:
|
||||
if self.settings.register_fields is None:
|
||||
self.settings.register_fields = [f.name for f in table_user if f.writable]
|
||||
k = self.settings.register_fields.index("password")
|
||||
@@ -3005,7 +3005,7 @@ class Auth(object):
|
||||
error_message=self.messages.mismatched_password),
|
||||
label=current.T("Confirm Password"))]
|
||||
else:
|
||||
extra_fields = []
|
||||
extra_fields = []
|
||||
form = SQLFORM(table_user,
|
||||
fields=self.settings.register_fields,
|
||||
hidden=dict(_next=next),
|
||||
@@ -3382,7 +3382,7 @@ class Auth(object):
|
||||
|
||||
if form.process().accepted:
|
||||
emails = re.compile('[^\s\'"@<>,;:]+\@[^\s\'"@<>,;:]+').findall(form.vars.emails)
|
||||
# send the invitations
|
||||
# send the invitations
|
||||
emails_sent = []
|
||||
emails_fail = []
|
||||
emails_exist = []
|
||||
@@ -3403,7 +3403,7 @@ class Auth(object):
|
||||
|
||||
def manage_tokens(self):
|
||||
if not self.user:
|
||||
redirect(self.settings.login_url)
|
||||
redirect(self.settings.login_url)
|
||||
table_token =self.table_token()
|
||||
table_token.user_id.writable = False
|
||||
table_token.user_id.default = self.user.id
|
||||
|
||||
+1
-1
@@ -3367,7 +3367,7 @@ class IS_IPV4(Validator):
|
||||
(number == self.localhost)):
|
||||
ok = False
|
||||
if not (self.is_private is None or self.is_private ==
|
||||
(sum([private_number[0] <= number <= private_number[1]
|
||||
(sum([private_number[0] <= number <= private_number[1]
|
||||
for private_number in self.private]) > 0)):
|
||||
ok = False
|
||||
if not (self.is_automatic is None or self.is_automatic ==
|
||||
|
||||
Reference in New Issue
Block a user