Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9ee4d4730 | ||
|
|
8fd7a27d5f | ||
|
|
69231bdd7f | ||
|
|
55dfb9e8c4 | ||
|
|
7761219cba | ||
|
|
e31e4e236f | ||
|
|
a43d822412 | ||
|
|
5775d2788d | ||
|
|
048f275076 | ||
|
|
d7caaf04cc | ||
|
|
e95115deb4 | ||
|
|
42c69b6343 | ||
|
|
d2347dec41 | ||
|
|
8420020c21 | ||
|
|
571fc6d919 | ||
|
|
52ec228eeb | ||
|
|
5848d9acaa | ||
|
|
3e8cbd5a0d | ||
|
|
e276cc2fc1 | ||
|
|
39a048db61 | ||
|
|
df4b896334 | ||
|
|
6d58845153 | ||
|
|
ba1f8bf741 | ||
|
|
a378ab3e51 | ||
|
|
2d866647e2 | ||
|
|
81863d69c9 | ||
|
|
ee2879442f | ||
|
|
ad68d2415d | ||
|
|
928de67f8d | ||
|
|
68296f9e65 | ||
|
|
7ac6edae52 | ||
|
|
1fc90fdb6d | ||
|
|
34a9d72cde | ||
|
|
198ce939d0 | ||
|
|
e31a099cb3 | ||
|
|
cc7e10d216 | ||
|
|
d8b68036c2 | ||
|
|
f9cd7e4ef4 | ||
|
|
896b45b838 | ||
|
|
d6146c9c5d | ||
|
|
b3be806244 | ||
|
|
eac12d3a57 | ||
|
|
2fc081bc3c | ||
|
|
032af7c04d | ||
|
|
8e63825def | ||
|
|
5d2e5dded3 | ||
|
|
61e33da844 | ||
|
|
da9dbaa5d6 | ||
|
|
7543c54bdb | ||
|
|
00608e4f04 | ||
|
|
cdbf48f09b | ||
|
|
f39db6331a | ||
|
|
ef433da190 | ||
|
|
d2375b4187 | ||
|
|
26d87967c5 | ||
|
|
044b2331c3 | ||
|
|
c89614ada6 | ||
|
|
f0aba167b4 | ||
|
|
bde9562b78 | ||
|
|
9a1229470a | ||
|
|
f781b9e1f5 | ||
|
|
fa32b7577b | ||
|
|
68526a0c6d | ||
|
|
ad2003c618 | ||
|
|
c1ecf823d8 | ||
|
|
6134f82452 | ||
|
|
fbb5a8b9bb | ||
|
|
df34869d65 | ||
|
|
28e6999e7d | ||
|
|
f4f77b0cb6 | ||
|
|
23ddb6c3c2 | ||
|
|
b636a5d6e9 | ||
|
|
efc392966e | ||
|
|
cffa59a80c | ||
|
|
82a1b9f628 | ||
|
|
94d2f1453d | ||
|
|
a1875ee362 | ||
|
|
5f13dca712 | ||
|
|
f78d423c92 | ||
|
|
f60ae809b6 | ||
|
|
34dd8af101 | ||
|
|
6bf6ebab1b | ||
|
|
29bf50425b | ||
|
|
8a7612c976 | ||
|
|
97489fd277 | ||
|
|
b86184fe58 | ||
|
|
2ce53e9957 | ||
|
|
d61c372c95 | ||
|
|
73e176365f | ||
|
|
33f12d91a5 | ||
|
|
d0f1286f03 | ||
|
|
04d698109e | ||
|
|
0e9c5caf4d | ||
|
|
509b0a6987 | ||
|
|
e0074ebcac | ||
|
|
918fdf2f0c | ||
|
|
8e827f7a09 | ||
|
|
cf2d5b637b | ||
|
|
236fdcfafc | ||
|
|
ce0f83d00c | ||
|
|
156d771ab3 | ||
|
|
a2e7794b92 | ||
|
|
842207ab33 | ||
|
|
c58f29bb9c | ||
|
|
0b0f82b514 | ||
|
|
3ab8a7bfd6 | ||
|
|
c5a9d2c456 |
@@ -58,3 +58,4 @@ HOWTO-web2py-devel
|
|||||||
*.sublime-project
|
*.sublime-project
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
.idea/*
|
.idea/*
|
||||||
|
site-packages/
|
||||||
|
|||||||
+2
-2
@@ -17,14 +17,14 @@ install:
|
|||||||
before_script:
|
before_script:
|
||||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --download-cache $HOME/.pip-cache unittest2; fi
|
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --download-cache $HOME/.pip-cache unittest2; fi
|
||||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache coverage; fi;
|
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache coverage; fi;
|
||||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache python-coveralls; fi
|
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --download-cache $HOME/.pip-cache codecov; fi
|
||||||
|
|
||||||
|
|
||||||
script: export COVERAGE_PROCESS_START=gluon/tests/coverage.ini; ./web2py.py --run_system_tests --with_coverage
|
script: export COVERAGE_PROCESS_START=gluon/tests/coverage.ini; ./web2py.py --run_system_tests --with_coverage
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coverage combine; fi
|
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coverage combine; fi
|
||||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --config_file=gluon/tests/coverage.ini; fi
|
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then codecov; fi
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
email: true
|
email: true
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
## 2.12.1
|
||||||
|
|
||||||
|
- security fix: Validate for open redirect everywhere, not just in login()
|
||||||
|
- allow to pack invidual apps and selected files as packed exe files
|
||||||
|
- allow bulk user registration with default bulk_register_enabled=False
|
||||||
|
- allow unsorted multiword query in grid search
|
||||||
|
- better MongoDB support with newer pyDAL
|
||||||
|
- enable <app>/appadmin/manage/auth by default for user admin
|
||||||
|
- allow mail.settings.server='logging:filename' to log emails to a file
|
||||||
|
- better caching logic
|
||||||
|
- fixed order of confirm-password field
|
||||||
|
- TLS support in ldap
|
||||||
|
- prettydate can do UTC
|
||||||
|
- jquery 1.11.3
|
||||||
|
- bootstrap 3.3.5
|
||||||
|
- moved to codecov and enabled appveyor
|
||||||
|
- many bug fixes
|
||||||
|
|
||||||
## 2.11.1
|
## 2.11.1
|
||||||
|
|
||||||
- Many small but significative improvements and bug fixes
|
- Many small but significative improvements and bug fixes
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ update:
|
|||||||
echo "remember that pymysql was tweaked"
|
echo "remember that pymysql was tweaked"
|
||||||
src:
|
src:
|
||||||
### Use semantic versioning
|
### Use semantic versioning
|
||||||
echo 'Version 2.11.1-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
|
echo 'Version 2.12.2-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
|
||||||
### rm -f all junk files
|
### rm -f all junk files
|
||||||
make clean
|
make clean
|
||||||
### clean up baisc apps
|
### clean up baisc apps
|
||||||
|
|||||||
+3
-2
@@ -38,9 +38,10 @@ PyDAL uses a separate stable release cycle to the rest of web2py. PyDAL releases
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
[](https://travis-ci.org/web2py/web2py)
|
[](https://travis-ci.org/web2py/web2py)
|
||||||
|
[](https://ci.appveyor.com/project/web2py/web2py)
|
||||||
|
[](https://codecov.io/github/web2py/web2py)
|
||||||
|
|
||||||
[](https://coveralls.io/r/web2py/web2py)
|
|
||||||
|
|
||||||
## Installation Instructions
|
## Installation Instructions
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Version 2.11.1-stable+timestamp.2015.05.28.23.17.58
|
Version 2.12.2-stable+timestamp.2015.08.09.09.27.09
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ if request.function == 'manage':
|
|||||||
auth.table_group(),
|
auth.table_group(),
|
||||||
auth.table_permission()])
|
auth.table_permission()])
|
||||||
manager_role = manager_action.get('role', None) if manager_action else None
|
manager_role = manager_action.get('role', None) if manager_action else None
|
||||||
auth.requires_membership(manager_role)(lambda: None)()
|
if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
|
||||||
|
raise HTTP(403, "Not authorized")
|
||||||
menu = False
|
menu = False
|
||||||
elif (request.application == 'admin' and not session.authorized) or \
|
elif (request.application == 'admin' and not session.authorized) or \
|
||||||
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
|
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
|
||||||
@@ -80,7 +81,6 @@ if False and request.tickets_db:
|
|||||||
def get_databases(request):
|
def get_databases(request):
|
||||||
dbs = {}
|
dbs = {}
|
||||||
for (key, value) in global_env.items():
|
for (key, value) in global_env.items():
|
||||||
cond = False
|
|
||||||
try:
|
try:
|
||||||
cond = isinstance(value, GQLDB)
|
cond = isinstance(value, GQLDB)
|
||||||
except:
|
except:
|
||||||
@@ -480,12 +480,12 @@ def ccache():
|
|||||||
disk['oldest'] = value[0]
|
disk['oldest'] = value[0]
|
||||||
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
|
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
|
||||||
|
|
||||||
total['entries'] = ram['entries'] + disk['entries']
|
ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
|
||||||
total['bytes'] = ram['bytes'] + disk['bytes']
|
ram_keys.remove('ratio')
|
||||||
total['objects'] = ram['objects'] + disk['objects']
|
ram_keys.remove('oldest')
|
||||||
total['hits'] = ram['hits'] + disk['hits']
|
for key in ram_keys:
|
||||||
total['misses'] = ram['misses'] + disk['misses']
|
total[key] = ram[key] + disk[key]
|
||||||
total['keys'] = ram['keys'] + disk['keys']
|
|
||||||
try:
|
try:
|
||||||
total['ratio'] = total['hits'] * 100 / (total['hits'] +
|
total['ratio'] = total['hits'] * 100 / (total['hits'] +
|
||||||
total['misses'])
|
total['misses'])
|
||||||
@@ -577,9 +577,7 @@ def bg_graph_model():
|
|||||||
group = meta_graphmodel['group'].replace(' ', '')
|
group = meta_graphmodel['group'].replace(' ', '')
|
||||||
if not subgraphs.has_key(group):
|
if not subgraphs.has_key(group):
|
||||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||||
subgraphs[group]['tables'].append(tablename)
|
subgraphs[group]['tables'].append(tablename)
|
||||||
else:
|
|
||||||
subgraphs[group]['tables'].append(tablename)
|
|
||||||
|
|
||||||
graph.add_node(tablename, name=tablename, shape='plaintext',
|
graph.add_node(tablename, name=tablename, shape='plaintext',
|
||||||
label=table_template(tablename))
|
label=table_template(tablename))
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ def list_breakpoints():
|
|||||||
"Return a list of linenumbers for current breakpoints"
|
"Return a list of linenumbers for current breakpoints"
|
||||||
|
|
||||||
breakpoints = []
|
breakpoints = []
|
||||||
ok = None
|
ok = False
|
||||||
try:
|
try:
|
||||||
filename = os.path.join(request.env['applications_parent'],
|
filename = os.path.join(request.env['applications_parent'],
|
||||||
'applications', request.vars.filename)
|
'applications', request.vars.filename)
|
||||||
@@ -235,5 +235,4 @@ def list_breakpoints():
|
|||||||
ok = True
|
ok = True
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
session.flash = str(e)
|
session.flash = str(e)
|
||||||
ok = False
|
|
||||||
return response.json({'ok': ok, 'breakpoints': breakpoints})
|
return response.json({'ok': ok, 'breakpoints': breakpoints})
|
||||||
|
|||||||
@@ -292,9 +292,6 @@ def site():
|
|||||||
log_progress(appname)
|
log_progress(appname)
|
||||||
session.flash = T(msg, dict(appname=appname,
|
session.flash = T(msg, dict(appname=appname,
|
||||||
digest=md5_hash(installed)))
|
digest=md5_hash(installed)))
|
||||||
elif f and form_update.vars.overwrite:
|
|
||||||
msg = 'unable to install application "%(appname)s"'
|
|
||||||
session.flash = T(msg, dict(appname=form_update.vars.name))
|
|
||||||
else:
|
else:
|
||||||
msg = 'unable to install application "%(appname)s"'
|
msg = 'unable to install application "%(appname)s"'
|
||||||
session.flash = T(msg, dict(appname=form_update.vars.name))
|
session.flash = T(msg, dict(appname=form_update.vars.name))
|
||||||
@@ -370,25 +367,56 @@ def pack_plugin():
|
|||||||
session.flash = T('internal error')
|
session.flash = T('internal error')
|
||||||
redirect(URL('plugin', args=request.args))
|
redirect(URL('plugin', args=request.args))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def pack_exe(app, base, filenames=None):
|
||||||
|
import urllib
|
||||||
|
import zipfile
|
||||||
|
from cStringIO import StringIO
|
||||||
|
# Download latest web2py_win and open it with zipfile
|
||||||
|
download_url = 'http://www.web2py.com/examples/static/web2py_win.zip'
|
||||||
|
out = StringIO()
|
||||||
|
out.write(urllib.urlopen(download_url).read())
|
||||||
|
web2py_win = zipfile.ZipFile(out, mode='a')
|
||||||
|
# Write routes.py with the application as default
|
||||||
|
routes = u'# -*- coding: utf-8 -*-\nrouters = dict(BASE=dict(default_application="%s"))' % app
|
||||||
|
web2py_win.writestr('web2py/routes.py', routes.encode('utf-8'))
|
||||||
|
# Copy the application into the zipfile
|
||||||
|
common_root = os.path.dirname(base)
|
||||||
|
for filename in filenames:
|
||||||
|
fname = os.path.join(base, filename)
|
||||||
|
arcname = os.path.join('web2py/applications', app, filename)
|
||||||
|
web2py_win.write(fname, arcname)
|
||||||
|
web2py_win.close()
|
||||||
|
response.headers['Content-Type'] = 'application/zip'
|
||||||
|
response.headers['Content-Disposition'] = 'attachment; filename=web2py.app.%s.zip' % app
|
||||||
|
out.seek(0)
|
||||||
|
return response.stream(out)
|
||||||
|
|
||||||
|
|
||||||
def pack_custom():
|
def pack_custom():
|
||||||
app = get_app()
|
app = get_app()
|
||||||
base = apath(app, r=request)
|
base = apath(app, r=request)
|
||||||
if request.post_vars.file:
|
if request.post_vars.file:
|
||||||
|
|
||||||
files = request.post_vars.file
|
files = request.post_vars.file
|
||||||
files = [files] if not isinstance(files,list) else files
|
files = [files] if not isinstance(files,list) else files
|
||||||
fname = 'web2py.app.%s.w2p' % app
|
if request.post_vars.doexe is None:
|
||||||
try:
|
fname = 'web2py.app.%s.w2p' % app
|
||||||
filename = app_pack(app, request, raise_ex=True, filenames=files)
|
try:
|
||||||
except Exception, e:
|
filename = app_pack(app, request, raise_ex=True, filenames=files)
|
||||||
filename = None
|
except Exception, e:
|
||||||
if filename:
|
filename = None
|
||||||
response.headers['Content-Type'] = 'application/w2p'
|
if filename:
|
||||||
disposition = 'attachment; filename=%s' % fname
|
response.headers['Content-Type'] = 'application/w2p'
|
||||||
response.headers['Content-Disposition'] = disposition
|
disposition = 'attachment; filename=%s' % fname
|
||||||
return safe_read(filename, 'rb')
|
response.headers['Content-Disposition'] = disposition
|
||||||
|
return safe_read(filename, 'rb')
|
||||||
|
else:
|
||||||
|
session.flash = T('internal error: %s', e)
|
||||||
|
redirect(URL(args=request.args))
|
||||||
else:
|
else:
|
||||||
session.flash = T('internal error: %s', e)
|
return pack_exe(app, base, files)
|
||||||
redirect(URL(args=request.args))
|
|
||||||
def ignore(fs):
|
def ignore(fs):
|
||||||
return [f for f in fs if not (
|
return [f for f in fs if not (
|
||||||
f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))]
|
f[:1] in '#' or f.endswith('~') or f.endswith('.bak'))]
|
||||||
@@ -867,13 +895,9 @@ def resolve():
|
|||||||
|
|
||||||
def getclass(item):
|
def getclass(item):
|
||||||
""" Determine item class """
|
""" Determine item class """
|
||||||
|
operators = {' ':'normal', '+':'plus', '-':'minus'}
|
||||||
|
|
||||||
if item[0] == ' ':
|
return operators[item[0]]
|
||||||
return 'normal'
|
|
||||||
if item[0] == '+':
|
|
||||||
return 'plus'
|
|
||||||
if item[0] == '-':
|
|
||||||
return 'minus'
|
|
||||||
|
|
||||||
if request.vars:
|
if request.vars:
|
||||||
c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0]
|
c = '\n'.join([item[2:].rstrip() for (i, item) in enumerate(d) if item[0]
|
||||||
@@ -1510,7 +1534,7 @@ def upload_file():
|
|||||||
if filename:
|
if filename:
|
||||||
d = dict(filename=filename[len(path):])
|
d = dict(filename=filename[len(path):])
|
||||||
else:
|
else:
|
||||||
d = dict(filename='unkown')
|
d = dict(filename='unknown')
|
||||||
session.flash = T('cannot upload file "%(filename)s"', d)
|
session.flash = T('cannot upload file "%(filename)s"', d)
|
||||||
|
|
||||||
redirect(request.vars.sender)
|
redirect(request.vars.sender)
|
||||||
|
|||||||
+5
-4
File diff suppressed because one or more lines are too long
@@ -1,5 +1,7 @@
|
|||||||
{{extend 'layout.html'}}
|
{{extend 'layout.html'}}
|
||||||
{{
|
{{
|
||||||
|
import re
|
||||||
|
regex_space = re.compile('\s+')
|
||||||
def all(items):
|
def all(items):
|
||||||
return reduce(lambda a,b:a and b,items,True)
|
return reduce(lambda a,b:a and b,items,True)
|
||||||
def peekfile(path,file,vars={},title=None):
|
def peekfile(path,file,vars={},title=None):
|
||||||
@@ -304,7 +306,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
|
|||||||
while path!=file_path:
|
while path!=file_path:
|
||||||
if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]):
|
if len(file_path)>=len(path) and all([v==file_path[k] for k,v in enumerate(path)]):
|
||||||
path.append(file_path[len(path)])
|
path.append(file_path[len(path)])
|
||||||
thispath='static__'+'__'.join(path)
|
thispath = regex_space.sub('-', 'static__'+'__'.join(path))
|
||||||
}}
|
}}
|
||||||
<li class="folder"><i> </i>
|
<li class="folder"><i> </i>
|
||||||
<a href="javascript:collapse('{{=thispath}}');" class="file">{{=path[-1]}}/</a>
|
<a href="javascript:collapse('{{=thispath}}');" class="file">{{=path[-1]}}/</a>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<form action="{{=URL(args=request.args)}}" method="POST">
|
<form action="{{=URL(args=request.args)}}" method="POST">
|
||||||
<h2>{{=T('Select Files to Package')}}</h2>
|
<h2>{{=T('Select Files to Package')}}</h2>
|
||||||
<input type="submit" value="{{=T('Download .w2p')}}" class="btn"/>
|
<input type="submit" value="{{=T('Download .w2p')}}" class="btn"/>
|
||||||
|
<input type="submit" name="doexe" value="{{=T('Download as .exe')}}" class="btn"/>
|
||||||
<div style="margin-top:20px">
|
<div style="margin-top:20px">
|
||||||
{{tree(base)}}
|
{{tree(base)}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ if request.function == 'manage':
|
|||||||
auth.table_group(),
|
auth.table_group(),
|
||||||
auth.table_permission()])
|
auth.table_permission()])
|
||||||
manager_role = manager_action.get('role', None) if manager_action else None
|
manager_role = manager_action.get('role', None) if manager_action else None
|
||||||
auth.requires_membership(manager_role)(lambda: None)()
|
if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
|
||||||
|
raise HTTP(403, "Not authorized")
|
||||||
menu = False
|
menu = False
|
||||||
elif (request.application == 'admin' and not session.authorized) or \
|
elif (request.application == 'admin' and not session.authorized) or \
|
||||||
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
|
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
|
||||||
@@ -80,7 +81,6 @@ if False and request.tickets_db:
|
|||||||
def get_databases(request):
|
def get_databases(request):
|
||||||
dbs = {}
|
dbs = {}
|
||||||
for (key, value) in global_env.items():
|
for (key, value) in global_env.items():
|
||||||
cond = False
|
|
||||||
try:
|
try:
|
||||||
cond = isinstance(value, GQLDB)
|
cond = isinstance(value, GQLDB)
|
||||||
except:
|
except:
|
||||||
@@ -480,12 +480,12 @@ def ccache():
|
|||||||
disk['oldest'] = value[0]
|
disk['oldest'] = value[0]
|
||||||
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
|
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
|
||||||
|
|
||||||
total['entries'] = ram['entries'] + disk['entries']
|
ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
|
||||||
total['bytes'] = ram['bytes'] + disk['bytes']
|
ram_keys.remove('ratio')
|
||||||
total['objects'] = ram['objects'] + disk['objects']
|
ram_keys.remove('oldest')
|
||||||
total['hits'] = ram['hits'] + disk['hits']
|
for key in ram_keys:
|
||||||
total['misses'] = ram['misses'] + disk['misses']
|
total[key] = ram[key] + disk[key]
|
||||||
total['keys'] = ram['keys'] + disk['keys']
|
|
||||||
try:
|
try:
|
||||||
total['ratio'] = total['hits'] * 100 / (total['hits'] +
|
total['ratio'] = total['hits'] * 100 / (total['hits'] +
|
||||||
total['misses'])
|
total['misses'])
|
||||||
@@ -577,9 +577,7 @@ def bg_graph_model():
|
|||||||
group = meta_graphmodel['group'].replace(' ', '')
|
group = meta_graphmodel['group'].replace(' ', '')
|
||||||
if not subgraphs.has_key(group):
|
if not subgraphs.has_key(group):
|
||||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||||
subgraphs[group]['tables'].append(tablename)
|
subgraphs[group]['tables'].append(tablename)
|
||||||
else:
|
|
||||||
subgraphs[group]['tables'].append(tablename)
|
|
||||||
|
|
||||||
graph.add_node(tablename, name=tablename, shape='plaintext',
|
graph.add_node(tablename, name=tablename, shape='plaintext',
|
||||||
label=table_template(tablename))
|
label=table_template(tablename))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#### Learning and Demos
|
#### Learning and Demos
|
||||||
- [[Intro video http://www.youtube.com/watch?v=BXzqmHx6edY]] and [[code examples https://github.com/mjhea0/web2py]]
|
- [[Intro video http://www.youtube.com/watch?v=BXzqmHx6edY]] and [[code examples https://github.com/mjhea0/web2py]]
|
||||||
|
- [[Step by step tutorial https://milesm.pythonanywhere.com/wiki]]
|
||||||
- [[Killer Web Development Tutorial http://killer-web-development.com/]]
|
- [[Killer Web Development Tutorial http://killer-web-development.com/]]
|
||||||
- [[Real Python for the Web http://www.realpython.com]] (web development with web2py and more!)
|
- [[Real Python for the Web http://www.realpython.com]] (web development with web2py and more!)
|
||||||
- [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE)
|
- [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE)
|
||||||
|
|||||||
+5
-4
File diff suppressed because one or more lines are too long
@@ -17,15 +17,19 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a target="_blank" href="http://experts4solutions.com">Experts4Soutions</a> (worldwide)</li>
|
<li><a target="_blank" href="http://experts4solutions.com">Experts4Soutions</a> (worldwide)</li>
|
||||||
<li><a target="_blank" href="http://www.planethost.com">PlanetHost</a> (USA)</li>
|
<li><a target="_blank" href="http://www.planethost.com">PlanetHost</a> (USA)</li>
|
||||||
|
<li><a target="_blank" href="http://www.10biosystems.com">10BioSystems</a> (USA)</li>
|
||||||
<li><a target="_blank" href="http://www.formatics.nl">Formatics</a> (Netherlands)</li>
|
<li><a target="_blank" href="http://www.formatics.nl">Formatics</a> (Netherlands)</li>
|
||||||
|
<li><a target="_blank" href="http://www.corebyte.nl">Corebyte</a> (Netherlands)</li>
|
||||||
|
<li><a target="_blank" href="http://www.dutveul.nl">Dutveul</a> (Netherlands)</li>
|
||||||
<li><a target="_blank" href="http://www.onemewebservices.com">OneMeWebServices</a> (Canada)</li>
|
<li><a target="_blank" href="http://www.onemewebservices.com">OneMeWebServices</a> (Canada)</li>
|
||||||
<li><a target="_blank" href="http://www.budgetbytes.nl">BudgetBytes</a> (The Netherlands)</li>
|
<li><a target="_blank" href="http://www.budgetbytes.nl">BudgetBytes</a> (The Netherlands)</li>
|
||||||
<li><a target="_blank" href="http://www.androsoft.pl">ANDROSoft</a> (Poland)</li>
|
<li><a target="_blank" href="http://www.androsoft.pl">ANDROSoft</a> (Poland)</li>
|
||||||
|
|
||||||
<li><a target="_blank" href="www.sonnetech.com.br">Sonne Tech</a> (Brazil)</li>
|
<li><a target="_blank" href="http://www.sonnetech.com.br">Sonne Tech</a> (Brazil)</li>
|
||||||
<li><a target="_blank" href="http://www.nrg.com.br">NRG Internet Solutions</a> (Brazil)</li>
|
<li><a target="_blank" href="http://www.nrg.com.br">NRG Internet Solutions</a> (Brazil)</li>
|
||||||
<li><a target="_blank" href="http://itjp.net.br/">ITJP</a> (Brazil)</li>
|
<li><a target="_blank" href="http://itjp.net.br/">ITJP</a> (Brazil)</li>
|
||||||
<li><a target="_blank" href="http://i-am.pt">I am Consultoria</a> (Portugal)</li>
|
<li><a target="_blank" href="http://i-am.pt">I am Consultoria</a> (Portugal)</li>
|
||||||
|
<li><a target="_blank" href="http://www.definescope.com/">DefineScope</a> (Portugal)</li>
|
||||||
<li><a target="_blank" href="http://lpfx.com.br">LPFX</a> (Brazil)</li>
|
<li><a target="_blank" href="http://lpfx.com.br">LPFX</a> (Brazil)</li>
|
||||||
<li><a target="_blank" href="http://emotionull.com">Emotionull</a> (Greece and Cyprus)</li>
|
<li><a target="_blank" href="http://emotionull.com">Emotionull</a> (Greece and Cyprus)</li>
|
||||||
<li><a target="_blank" href="http://www.vsa-services.com/">VSA Services</a> (Singapore)</li>
|
<li><a target="_blank" href="http://www.vsa-services.com/">VSA Services</a> (Singapore)</li>
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ if request.function == 'manage':
|
|||||||
auth.table_group(),
|
auth.table_group(),
|
||||||
auth.table_permission()])
|
auth.table_permission()])
|
||||||
manager_role = manager_action.get('role', None) if manager_action else None
|
manager_role = manager_action.get('role', None) if manager_action else None
|
||||||
auth.requires_membership(manager_role)(lambda: None)()
|
if not (gluon.fileutils.check_credentials(request) or auth.has_membership(manager_role)):
|
||||||
|
raise HTTP(403, "Not authorized")
|
||||||
menu = False
|
menu = False
|
||||||
elif (request.application == 'admin' and not session.authorized) or \
|
elif (request.application == 'admin' and not session.authorized) or \
|
||||||
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
|
(request.application != 'admin' and not gluon.fileutils.check_credentials(request)):
|
||||||
@@ -80,7 +81,6 @@ if False and request.tickets_db:
|
|||||||
def get_databases(request):
|
def get_databases(request):
|
||||||
dbs = {}
|
dbs = {}
|
||||||
for (key, value) in global_env.items():
|
for (key, value) in global_env.items():
|
||||||
cond = False
|
|
||||||
try:
|
try:
|
||||||
cond = isinstance(value, GQLDB)
|
cond = isinstance(value, GQLDB)
|
||||||
except:
|
except:
|
||||||
@@ -480,12 +480,12 @@ def ccache():
|
|||||||
disk['oldest'] = value[0]
|
disk['oldest'] = value[0]
|
||||||
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
|
disk['keys'].append((key, GetInHMS(time.time() - value[0])))
|
||||||
|
|
||||||
total['entries'] = ram['entries'] + disk['entries']
|
ram_keys = ram.keys() # ['hits', 'objects', 'ratio', 'entries', 'keys', 'oldest', 'bytes', 'misses']
|
||||||
total['bytes'] = ram['bytes'] + disk['bytes']
|
ram_keys.remove('ratio')
|
||||||
total['objects'] = ram['objects'] + disk['objects']
|
ram_keys.remove('oldest')
|
||||||
total['hits'] = ram['hits'] + disk['hits']
|
for key in ram_keys:
|
||||||
total['misses'] = ram['misses'] + disk['misses']
|
total[key] = ram[key] + disk[key]
|
||||||
total['keys'] = ram['keys'] + disk['keys']
|
|
||||||
try:
|
try:
|
||||||
total['ratio'] = total['hits'] * 100 / (total['hits'] +
|
total['ratio'] = total['hits'] * 100 / (total['hits'] +
|
||||||
total['misses'])
|
total['misses'])
|
||||||
@@ -577,9 +577,7 @@ def bg_graph_model():
|
|||||||
group = meta_graphmodel['group'].replace(' ', '')
|
group = meta_graphmodel['group'].replace(' ', '')
|
||||||
if not subgraphs.has_key(group):
|
if not subgraphs.has_key(group):
|
||||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||||
subgraphs[group]['tables'].append(tablename)
|
subgraphs[group]['tables'].append(tablename)
|
||||||
else:
|
|
||||||
subgraphs[group]['tables'].append(tablename)
|
|
||||||
|
|
||||||
graph.add_node(tablename, name=tablename, shape='plaintext',
|
graph.add_node(tablename, name=tablename, shape='plaintext',
|
||||||
label=table_template(tablename))
|
label=table_template(tablename))
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ def user():
|
|||||||
http://..../[app]/default/user/retrieve_password
|
http://..../[app]/default/user/retrieve_password
|
||||||
http://..../[app]/default/user/change_password
|
http://..../[app]/default/user/change_password
|
||||||
http://..../[app]/default/user/manage_users (requires membership in
|
http://..../[app]/default/user/manage_users (requires membership in
|
||||||
|
http://..../[app]/default/user/bulk_register
|
||||||
use @auth.requires_login()
|
use @auth.requires_login()
|
||||||
@auth.requires_membership('group name')
|
@auth.requires_membership('group name')
|
||||||
@auth.requires_permission('read','table name',record_id)
|
@auth.requires_permission('read','table name',record_id)
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
@@ -24,7 +24,9 @@ div.flash.alert:hover {
|
|||||||
.ie-lte8 div.flash:hover {
|
.ie-lte8 div.flash:hover {
|
||||||
filter: alpha(opacity=25);
|
filter: alpha(opacity=25);
|
||||||
}
|
}
|
||||||
|
.main-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
div.error {
|
div.error {
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -283,6 +285,7 @@ li.w2p_grid_breadcrumb_elem {
|
|||||||
.web2py_console .form-control {
|
.web2py_console .form-control {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
display: inline;
|
display: inline;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
.web2py_console #w2p_keywords {
|
.web2py_console #w2p_keywords {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
|||||||
+4
-4
File diff suppressed because one or more lines are too long
+5
-4
File diff suppressed because one or more lines are too long
@@ -75,7 +75,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
<!-- Main ========================================= -->
|
<!-- Main ========================================= -->
|
||||||
<!-- Begin page content -->
|
<!-- Begin page content -->
|
||||||
<div class="container-fluid">
|
<div class="container-fluid main-container">
|
||||||
{{if left_sidebar_enabled:}}
|
{{if left_sidebar_enabled:}}
|
||||||
<div class="col-md-3 left-sidebar">
|
<div class="col-md-3 left-sidebar">
|
||||||
{{block left_sidebar}}
|
{{block left_sidebar}}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
build: false
|
||||||
|
|
||||||
|
environment:
|
||||||
|
matrix:
|
||||||
|
- PYTHON: "C:/Python27"
|
||||||
|
COVERAGE_PROCESS_START: gluon/tests/coverage.ini
|
||||||
|
|
||||||
|
clone_depth: 50
|
||||||
|
|
||||||
|
init:
|
||||||
|
- "ECHO %PYTHON%"
|
||||||
|
- set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%
|
||||||
|
|
||||||
|
install:
|
||||||
|
- ps: Start-FileDownload https://bootstrap.pypa.io/get-pip.py
|
||||||
|
- python get-pip.py
|
||||||
|
- pip install codecov
|
||||||
|
- git submodule update --init --recursive
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- python web2py.py --run_system_tests --with_coverage
|
||||||
|
|
||||||
|
after_test:
|
||||||
|
- coverage combine
|
||||||
|
- codecov
|
||||||
+3
-2
@@ -59,6 +59,8 @@ def app_pack(app, request, raise_ex=False, filenames=None):
|
|||||||
w2p_pack(filename, apath(app, request), filenames=filenames)
|
w2p_pack(filename, apath(app, request), filenames=filenames)
|
||||||
return filename
|
return filename
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
|
import traceback
|
||||||
|
print traceback.format_exc()
|
||||||
if raise_ex:
|
if raise_ex:
|
||||||
raise
|
raise
|
||||||
return False
|
return False
|
||||||
@@ -119,9 +121,8 @@ def app_cleanup(app, request):
|
|||||||
|
|
||||||
# Remove cache files
|
# Remove cache files
|
||||||
path = apath('%s/cache/' % app, request)
|
path = apath('%s/cache/' % app, request)
|
||||||
CacheOnDisk(folder=path).clear()
|
|
||||||
|
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
|
CacheOnDisk(folder=path).clear()
|
||||||
for f in os.listdir(path):
|
for f in os.listdir(path):
|
||||||
try:
|
try:
|
||||||
if f[:1] != '.': recursive_unlink(os.path.join(path, f))
|
if f[:1] != '.': recursive_unlink(os.path.join(path, f))
|
||||||
|
|||||||
+9
-8
@@ -261,7 +261,7 @@ class LoadFactory(object):
|
|||||||
import globals
|
import globals
|
||||||
target = target or 'c' + str(random.random())[2:]
|
target = target or 'c' + str(random.random())[2:]
|
||||||
attr['_id'] = target
|
attr['_id'] = target
|
||||||
request = self.environment['request']
|
request = current.request
|
||||||
if '.' in f:
|
if '.' in f:
|
||||||
f, extension = f.rsplit('.', 1)
|
f, extension = f.rsplit('.', 1)
|
||||||
if url or ajax:
|
if url or ajax:
|
||||||
@@ -532,10 +532,11 @@ def run_models_in(environment):
|
|||||||
It tries pre-compiled models first before compiling them.
|
It tries pre-compiled models first before compiling them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
folder = environment['request'].folder
|
request = current.request
|
||||||
c = environment['request'].controller
|
folder = request.folder
|
||||||
|
c = request.controller
|
||||||
#f = environment['request'].function
|
#f = environment['request'].function
|
||||||
response = environment['response']
|
response = current.response
|
||||||
|
|
||||||
path = pjoin(folder, 'models')
|
path = pjoin(folder, 'models')
|
||||||
cpath = pjoin(folder, 'compiled')
|
cpath = pjoin(folder, 'compiled')
|
||||||
@@ -577,7 +578,7 @@ def run_controller_in(controller, function, environment):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# if compiled should run compiled!
|
# if compiled should run compiled!
|
||||||
folder = environment['request'].folder
|
folder = current.request.folder
|
||||||
path = pjoin(folder, 'compiled')
|
path = pjoin(folder, 'compiled')
|
||||||
badc = 'invalid controller (%s/%s)' % (controller, function)
|
badc = 'invalid controller (%s/%s)' % (controller, function)
|
||||||
badf = 'invalid function (%s/%s)' % (controller, function)
|
badf = 'invalid function (%s/%s)' % (controller, function)
|
||||||
@@ -631,7 +632,7 @@ def run_controller_in(controller, function, environment):
|
|||||||
layer = filename + ':' + function
|
layer = filename + ':' + function
|
||||||
code = getcfs(layer, filename, lambda: compile2(code, layer))
|
code = getcfs(layer, filename, lambda: compile2(code, layer))
|
||||||
restricted(code, environment, filename)
|
restricted(code, environment, filename)
|
||||||
response = environment['response']
|
response = current.response
|
||||||
vars = response._vars
|
vars = response._vars
|
||||||
if response.postprocessing:
|
if response.postprocessing:
|
||||||
vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars)
|
vars = reduce(lambda vars, p: p(vars), response.postprocessing, vars)
|
||||||
@@ -649,8 +650,8 @@ def run_view_in(environment):
|
|||||||
or `view/generic.extension`
|
or `view/generic.extension`
|
||||||
It tries the pre-compiled views_controller_function.pyc before compiling it.
|
It tries the pre-compiled views_controller_function.pyc before compiling it.
|
||||||
"""
|
"""
|
||||||
request = environment['request']
|
request = current.request
|
||||||
response = environment['response']
|
response = current.response
|
||||||
view = response.view
|
view = response.view
|
||||||
folder = request.folder
|
folder = request.folder
|
||||||
path = pjoin(folder, 'compiled')
|
path = pjoin(folder, 'compiled')
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ def ldap_auth(server='ldap', port=None,
|
|||||||
group_name_attrib='cn',
|
group_name_attrib='cn',
|
||||||
group_member_attrib='memberUid',
|
group_member_attrib='memberUid',
|
||||||
group_filterstr='objectClass=*',
|
group_filterstr='objectClass=*',
|
||||||
|
tls=False,
|
||||||
logging_level='error'):
|
logging_level='error'):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -80,6 +81,13 @@ def ldap_auth(server='ldap', port=None,
|
|||||||
If ldap is using GnuTLS then you need cert_file="..." instead cert_path
|
If ldap is using GnuTLS then you need cert_file="..." instead cert_path
|
||||||
because cert_path isn't implemented in GnuTLS :(
|
because cert_path isn't implemented in GnuTLS :(
|
||||||
|
|
||||||
|
To enable TLS, set tls=True:
|
||||||
|
|
||||||
|
auth.settings.login_methods.append(ldap_auth(
|
||||||
|
server='my.ldap.server',
|
||||||
|
base_dn='ou=Users,dc=domain,dc=com',
|
||||||
|
tls=True))
|
||||||
|
|
||||||
If you need to bind to the directory with an admin account in order to
|
If you need to bind to the directory with an admin account in order to
|
||||||
search it then specify bind_dn & bind_pw to use for this.
|
search it then specify bind_dn & bind_pw to use for this.
|
||||||
- currently only implemented for Active Directory
|
- currently only implemented for Active Directory
|
||||||
@@ -610,6 +618,8 @@ def ldap_auth(server='ldap', port=None,
|
|||||||
ldap_port = 389
|
ldap_port = 389
|
||||||
con = ldap.initialize(
|
con = ldap.initialize(
|
||||||
"ldap://" + ldap_server + ":" + str(ldap_port))
|
"ldap://" + ldap_server + ":" + str(ldap_port))
|
||||||
|
if tls:
|
||||||
|
con.start_tls_s()
|
||||||
return con
|
return con
|
||||||
|
|
||||||
def get_user_groups_from_ldap(username,
|
def get_user_groups_from_ldap(username,
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ class TokenHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class DistributeHandler(tornado.websocket.WebSocketHandler):
|
class DistributeHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
|
||||||
|
def check_origin(self, origin):
|
||||||
|
return True
|
||||||
|
|
||||||
def open(self, params):
|
def open(self, params):
|
||||||
group, token, name = params.split('/') + [None, None]
|
group, token, name = params.split('/') + [None, None]
|
||||||
self.group = group or 'default'
|
self.group = group or 'default'
|
||||||
|
|||||||
+1
-1
Submodule gluon/packages/dal updated: 4d36919c11...62eb7767db
@@ -119,7 +119,7 @@ class LockedFile(object):
|
|||||||
lock(self.file, LOCK_EX)
|
lock(self.file, LOCK_EX)
|
||||||
if not 'a' in mode:
|
if not 'a' in mode:
|
||||||
self.file.seek(0)
|
self.file.seek(0)
|
||||||
self.file.truncate()
|
self.file.truncate(0)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("invalid LockedFile(...,mode)")
|
raise RuntimeError("invalid LockedFile(...,mode)")
|
||||||
|
|
||||||
|
|||||||
+19
-15
@@ -66,14 +66,15 @@ class XssCleaner(HTMLParser):
|
|||||||
|
|
||||||
#to strip or escape disallowed tags?
|
#to strip or escape disallowed tags?
|
||||||
self.strip_disallowed = strip_disallowed
|
self.strip_disallowed = strip_disallowed
|
||||||
self.in_disallowed = False
|
# there might be data after final closing tag, that is to be ignored
|
||||||
|
self.in_disallowed = [False]
|
||||||
|
|
||||||
def handle_data(self, data):
|
def handle_data(self, data):
|
||||||
if data and not self.in_disallowed:
|
if data and not self.in_disallowed[-1]:
|
||||||
self.result += xssescape(data)
|
self.result += xssescape(data)
|
||||||
|
|
||||||
def handle_charref(self, ref):
|
def handle_charref(self, ref):
|
||||||
if self.in_disallowed:
|
if self.in_disallowed[-1]:
|
||||||
return
|
return
|
||||||
elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe
|
elif len(ref) < 7 and (ref.isdigit() or ref == 'x27'): # x27 is a special case for apostrophe
|
||||||
self.result += '&#%s;' % ref
|
self.result += '&#%s;' % ref
|
||||||
@@ -81,7 +82,7 @@ class XssCleaner(HTMLParser):
|
|||||||
self.result += xssescape('&#%s' % ref)
|
self.result += xssescape('&#%s' % ref)
|
||||||
|
|
||||||
def handle_entityref(self, ref):
|
def handle_entityref(self, ref):
|
||||||
if self.in_disallowed:
|
if self.in_disallowed[-1]:
|
||||||
return
|
return
|
||||||
elif ref in entitydefs:
|
elif ref in entitydefs:
|
||||||
self.result += '&%s;' % ref
|
self.result += '&%s;' % ref
|
||||||
@@ -89,7 +90,7 @@ class XssCleaner(HTMLParser):
|
|||||||
self.result += xssescape('&%s' % ref)
|
self.result += xssescape('&%s' % ref)
|
||||||
|
|
||||||
def handle_comment(self, comment):
|
def handle_comment(self, comment):
|
||||||
if self.in_disallowed:
|
if self.in_disallowed[-1]:
|
||||||
return
|
return
|
||||||
elif comment:
|
elif comment:
|
||||||
self.result += xssescape('<!--%s-->' % comment)
|
self.result += xssescape('<!--%s-->' % comment)
|
||||||
@@ -100,11 +101,11 @@ class XssCleaner(HTMLParser):
|
|||||||
attrs
|
attrs
|
||||||
):
|
):
|
||||||
if tag not in self.permitted_tags:
|
if tag not in self.permitted_tags:
|
||||||
if self.strip_disallowed:
|
self.in_disallowed.append(True)
|
||||||
self.in_disallowed = True
|
if (not self.strip_disallowed):
|
||||||
else:
|
|
||||||
self.result += xssescape('<%s>' % tag)
|
self.result += xssescape('<%s>' % tag)
|
||||||
else:
|
else:
|
||||||
|
self.in_disallowed.append(False)
|
||||||
bt = '<' + tag
|
bt = '<' + tag
|
||||||
if tag in self.allowed_attributes:
|
if tag in self.allowed_attributes:
|
||||||
attrs = dict(attrs)
|
attrs = dict(attrs)
|
||||||
@@ -119,6 +120,7 @@ class XssCleaner(HTMLParser):
|
|||||||
else:
|
else:
|
||||||
bt += ' %s=%s' % (xssescape(attribute),
|
bt += ' %s=%s' % (xssescape(attribute),
|
||||||
quoteattr(attrs[attribute]))
|
quoteattr(attrs[attribute]))
|
||||||
|
# deal with <a> without href and <img> without src
|
||||||
if bt == '<a' or bt == '<img':
|
if bt == '<a' or bt == '<img':
|
||||||
return
|
return
|
||||||
if tag in self.requires_no_close:
|
if tag in self.requires_no_close:
|
||||||
@@ -129,10 +131,9 @@ class XssCleaner(HTMLParser):
|
|||||||
|
|
||||||
def handle_endtag(self, tag):
|
def handle_endtag(self, tag):
|
||||||
bracketed = '</%s>' % tag
|
bracketed = '</%s>' % tag
|
||||||
|
self.in_disallowed.pop()
|
||||||
if tag not in self.permitted_tags:
|
if tag not in self.permitted_tags:
|
||||||
if self.strip_disallowed:
|
if (not self.strip_disallowed):
|
||||||
self.in_disallowed = False
|
|
||||||
else:
|
|
||||||
self.result += xssescape(bracketed)
|
self.result += xssescape(bracketed)
|
||||||
elif tag in self.open_tags:
|
elif tag in self.open_tags:
|
||||||
self.result += bracketed
|
self.result += bracketed
|
||||||
@@ -143,10 +144,13 @@ class XssCleaner(HTMLParser):
|
|||||||
Accepts relative, absolute, and mailto urls
|
Accepts relative, absolute, and mailto urls
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parsed = urlparse(url)
|
if url.startswith('#'):
|
||||||
return (parsed[0] in self.allowed_schemes and '.' in parsed[1]) \
|
return True
|
||||||
or (parsed[0] in self.allowed_schemes and '@' in parsed[2]) \
|
else:
|
||||||
or (parsed[0] == '' and parsed[2].startswith('/'))
|
parsed = urlparse(url)
|
||||||
|
return ((parsed[0] in self.allowed_schemes and '.' in parsed[1]) or
|
||||||
|
(parsed[0] in self.allowed_schemes and '@' in parsed[2]) or
|
||||||
|
(parsed[0] == '' and parsed[2].startswith('/')))
|
||||||
|
|
||||||
def strip(self, rawstring, escape=True):
|
def strip(self, rawstring, escape=True):
|
||||||
"""
|
"""
|
||||||
|
|||||||
+54
-38
@@ -29,7 +29,7 @@ from gluon.html import URL, FIELDSET, P, DEFAULT_PASSWORD_DISPLAY
|
|||||||
from pydal.base import DEFAULT
|
from pydal.base import DEFAULT
|
||||||
from pydal.objects import Table, Row, Expression, Field
|
from pydal.objects import Table, Row, Expression, Field
|
||||||
from pydal.adapters.base import CALLABLETYPES
|
from pydal.adapters.base import CALLABLETYPES
|
||||||
from pydal.helpers.methods import smart_query, bar_encode
|
from pydal.helpers.methods import smart_query, bar_encode, _repr_ref
|
||||||
from pydal.helpers.classes import Reference, SQLCustomType
|
from pydal.helpers.classes import Reference, SQLCustomType
|
||||||
from gluon.storage import Storage
|
from gluon.storage import Storage
|
||||||
from gluon.utils import md5_hash
|
from gluon.utils import md5_hash
|
||||||
@@ -71,6 +71,26 @@ def represent(field, value, record):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError("field representation must take 1 or 2 args")
|
raise RuntimeError("field representation must take 1 or 2 args")
|
||||||
|
|
||||||
|
class CacheRepresenter(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.cache = {}
|
||||||
|
def __call__(self, field, value, row):
|
||||||
|
cache = self.cache
|
||||||
|
if field not in cache:
|
||||||
|
cache[field] = {}
|
||||||
|
try:
|
||||||
|
nvalue = cache[field][value]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
nvalue = field.represent(value, row)
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
nvalue = field.represent(value, row[field.tablename])
|
||||||
|
except KeyError:
|
||||||
|
nvalue = None
|
||||||
|
if isinstance(field, _repr_ref):
|
||||||
|
cache[field][value] = nvalue
|
||||||
|
return nvalue
|
||||||
|
|
||||||
def safe_int(x):
|
def safe_int(x):
|
||||||
try:
|
try:
|
||||||
@@ -1126,7 +1146,8 @@ class SQLFORM(FORM):
|
|||||||
extra_fields = extra_fields or []
|
extra_fields = extra_fields or []
|
||||||
self.extra_fields = {}
|
self.extra_fields = {}
|
||||||
for extra_field in extra_fields:
|
for extra_field in extra_fields:
|
||||||
self.fields.append(extra_field.name)
|
if not extra_field.name in self.fields:
|
||||||
|
self.fields.append(extra_field.name)
|
||||||
self.extra_fields[extra_field.name] = extra_field
|
self.extra_fields[extra_field.name] = extra_field
|
||||||
extra_field.db = table._db
|
extra_field.db = table._db
|
||||||
extra_field.table = table
|
extra_field.table = table
|
||||||
@@ -1678,7 +1699,7 @@ class SQLFORM(FORM):
|
|||||||
self.vars.update(pk)
|
self.vars.update(pk)
|
||||||
else:
|
else:
|
||||||
ret = False
|
ret = False
|
||||||
else:
|
elif self.table._db._uri:
|
||||||
if record_id:
|
if record_id:
|
||||||
self.vars.id = self.record[self.id_field_name]
|
self.vars.id = self.record[self.id_field_name]
|
||||||
if fields:
|
if fields:
|
||||||
@@ -1691,6 +1712,7 @@ class SQLFORM(FORM):
|
|||||||
|
|
||||||
AUTOTYPES = {
|
AUTOTYPES = {
|
||||||
type(''): ('string', None),
|
type(''): ('string', None),
|
||||||
|
type(u''): ('string',None),
|
||||||
type(True): ('boolean', None),
|
type(True): ('boolean', None),
|
||||||
type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
|
type(1): ('integer', IS_INT_IN_RANGE(-1e12, +1e12)),
|
||||||
type(1.0): ('double', IS_FLOAT_IN_RANGE()),
|
type(1.0): ('double', IS_FLOAT_IN_RANGE()),
|
||||||
@@ -1755,10 +1777,16 @@ class SQLFORM(FORM):
|
|||||||
keywords = keywords[0]
|
keywords = keywords[0]
|
||||||
request.vars.keywords = keywords
|
request.vars.keywords = keywords
|
||||||
key = keywords.strip()
|
key = keywords.strip()
|
||||||
if key and ' ' not in key and not '"' in key and not "'" in key:
|
if key and not '"' in key:
|
||||||
SEARCHABLE_TYPES = ('string', 'text', 'list:string')
|
SEARCHABLE_TYPES = ('string', 'text', 'list:string')
|
||||||
parts = [field.contains(
|
sfields = [field for field in fields if field.type in SEARCHABLE_TYPES]
|
||||||
key) for field in fields if field.type in SEARCHABLE_TYPES]
|
if settings.global_settings.web2py_runtime_gae:
|
||||||
|
return reduce(lambda a,b: a|b, [field.contains(key) for field in sfields])
|
||||||
|
else:
|
||||||
|
return reduce(lambda a,b:a&b,[
|
||||||
|
reduce(lambda a,b: a|b, [
|
||||||
|
field.contains(k) for field in sfields]
|
||||||
|
) for k in key.split()])
|
||||||
|
|
||||||
# from https://groups.google.com/forum/#!topic/web2py/hKe6lI25Bv4
|
# from https://groups.google.com/forum/#!topic/web2py/hKe6lI25Bv4
|
||||||
# needs testing...
|
# needs testing...
|
||||||
@@ -1772,10 +1800,6 @@ class SQLFORM(FORM):
|
|||||||
# filters.append(reduce(lambda a, b: (a & b), all_words_filters))
|
# filters.append(reduce(lambda a, b: (a & b), all_words_filters))
|
||||||
#parts = filters
|
#parts = filters
|
||||||
|
|
||||||
else:
|
|
||||||
parts = None
|
|
||||||
if parts:
|
|
||||||
return reduce(lambda a, b: a | b, parts)
|
|
||||||
else:
|
else:
|
||||||
return smart_query(fields, key)
|
return smart_query(fields, key)
|
||||||
|
|
||||||
@@ -1838,15 +1862,19 @@ class SQLFORM(FORM):
|
|||||||
operators = SELECT(*[OPTION(T(option), _value=option) for option in options], _class='form-control')
|
operators = SELECT(*[OPTION(T(option), _value=option) for option in options], _class='form-control')
|
||||||
_id = "%s_%s" % (value_id, name)
|
_id = "%s_%s" % (value_id, name)
|
||||||
if field_type in ['boolean', 'double', 'time', 'integer']:
|
if field_type in ['boolean', 'double', 'time', 'integer']:
|
||||||
value_input = SQLFORM.widgets[field_type].widget(field, field.default, _id=_id, _class='form-control')
|
widget_ = SQLFORM.widgets[field_type]
|
||||||
|
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control')
|
||||||
elif field_type == 'date':
|
elif field_type == 'date':
|
||||||
iso_format = {'_data-w2p_date_format' : '%Y-%m-%d'}
|
iso_format = {'_data-w2p_date_format': '%Y-%m-%d'}
|
||||||
value_input = SQLFORM.widgets.date.widget(field, field.default, _id=_id, _class='form-control', **iso_format)
|
widget_ = SQLFORM.widgets.date
|
||||||
|
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format)
|
||||||
elif field_type == 'datetime':
|
elif field_type == 'datetime':
|
||||||
iso_format = {'_data-w2p_datetime_format' : '%Y-%m-%d %H:%M:%S'}
|
iso_format = {'_data-w2p_datetime_format': '%Y-%m-%d %H:%M:%S'}
|
||||||
value_input = SQLFORM.widgets.datetime.widget(field, field.default, _id=_id, _class='form-control', **iso_format)
|
widget_ = SQLFORM.widgets.datetime
|
||||||
|
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control', **iso_format)
|
||||||
elif (field_type.startswith('reference ') or
|
elif (field_type.startswith('reference ') or
|
||||||
field_type.startswith('list:reference ')) and \
|
field_type.startswith('list:reference ')) and \
|
||||||
|
hasattr(field.requires, 'options') or \
|
||||||
hasattr(field.requires, 'options'):
|
hasattr(field.requires, 'options'):
|
||||||
value_input = SELECT(
|
value_input = SELECT(
|
||||||
*[OPTION(v, _value=k)
|
*[OPTION(v, _value=k)
|
||||||
@@ -1856,7 +1884,8 @@ class SQLFORM(FORM):
|
|||||||
elif field_type.startswith('reference ') or \
|
elif field_type.startswith('reference ') or \
|
||||||
field_type.startswith('list:integer') or \
|
field_type.startswith('list:integer') or \
|
||||||
field_type.startswith('list:reference '):
|
field_type.startswith('list:reference '):
|
||||||
value_input = SQLFORM.widgets.integer.widget(field, field.default, _id=_id, _class='form-control')
|
widget_ = SQLFORM.widgets.integer
|
||||||
|
value_input = widget_.widget(field, field.default, _id=_id, _class=widget_._class + ' form-control')
|
||||||
else:
|
else:
|
||||||
value_input = INPUT(
|
value_input = INPUT(
|
||||||
_type='text', _id=_id,
|
_type='text', _id=_id,
|
||||||
@@ -2106,10 +2135,8 @@ class SQLFORM(FORM):
|
|||||||
# - url has valid signature (vars are not signed, only path_info)
|
# - url has valid signature (vars are not signed, only path_info)
|
||||||
# = url does not contain 'create','delete','edit' (readonly)
|
# = url does not contain 'create','delete','edit' (readonly)
|
||||||
if user_signature:
|
if user_signature:
|
||||||
if not (
|
if not ('/'.join(map(str,args)) == '/'.join(map(str,request.args)) or
|
||||||
'/'.join(str(a) for a in args) == '/'.join(request.args) or
|
URL.verify(request, user_signature=user_signature, hash_vars=False) or
|
||||||
URL.verify(request, user_signature=user_signature,
|
|
||||||
hash_vars=False) or
|
|
||||||
(request.args(len(args)) == 'view' and not logged)):
|
(request.args(len(args)) == 'view' and not logged)):
|
||||||
session.flash = T('not authorized')
|
session.flash = T('not authorized')
|
||||||
redirect(referrer)
|
redirect(referrer)
|
||||||
@@ -2662,7 +2689,7 @@ class SQLFORM(FORM):
|
|||||||
htmltable = TABLE(COLGROUP(*cols), THEAD(head))
|
htmltable = TABLE(COLGROUP(*cols), THEAD(head))
|
||||||
tbody = TBODY()
|
tbody = TBODY()
|
||||||
numrec = 0
|
numrec = 0
|
||||||
repr_cache = {}
|
repr_cache = CacheRepresenter()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
trcols = []
|
trcols = []
|
||||||
id = row[field_id]
|
id = row[field_id]
|
||||||
@@ -2675,31 +2702,20 @@ class SQLFORM(FORM):
|
|||||||
continue
|
continue
|
||||||
if field.type == 'blob':
|
if field.type == 'blob':
|
||||||
continue
|
continue
|
||||||
value = row[str(field)]
|
if isinstance(field, Field.Virtual) and field.tablename in row:
|
||||||
|
value = dbset.db[field.tablename][row[field.tablename][field_id]][field.name]
|
||||||
|
else:
|
||||||
|
value = row[str(field)]
|
||||||
maxlength = maxtextlengths.get(str(field), maxtextlength)
|
maxlength = maxtextlengths.get(str(field), maxtextlength)
|
||||||
if field.represent:
|
if field.represent:
|
||||||
if field.type.startswith('reference'):
|
if field.type.startswith('reference'):
|
||||||
if field not in repr_cache:
|
nvalue = repr_cache(field, value, row)
|
||||||
repr_cache[field] = {}
|
|
||||||
try:
|
|
||||||
nvalue = repr_cache[field][value]
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
nvalue = field.represent(value, row)
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
nvalue = field.represent(
|
|
||||||
value, row[field.tablename])
|
|
||||||
except KeyError:
|
|
||||||
nvalue = None
|
|
||||||
repr_cache[field][value] = nvalue
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
nvalue = field.represent(value, row)
|
nvalue = field.represent(value, row)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
try:
|
try:
|
||||||
nvalue = field.represent(
|
nvalue = field.represent(value, row[field.tablename])
|
||||||
value, row[field.tablename])
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
nvalue = None
|
nvalue = None
|
||||||
value = nvalue
|
value = nvalue
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ exclude_lines =
|
|||||||
ignore_errors = True
|
ignore_errors = True
|
||||||
omit = gluon/contrib/*
|
omit = gluon/contrib/*
|
||||||
gluon/tests/*
|
gluon/tests/*
|
||||||
|
gluon/packages/*
|
||||||
|
|
||||||
[html]
|
[html]
|
||||||
directory = coverage_html_report
|
directory = coverage_html_report
|
||||||
|
|||||||
+232
-25
@@ -765,10 +765,13 @@ class Mail(object):
|
|||||||
result = {}
|
result = {}
|
||||||
try:
|
try:
|
||||||
if self.settings.server == 'logging':
|
if self.settings.server == 'logging':
|
||||||
logger.warn('email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' %
|
entry = 'email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \
|
||||||
('-' * 40, sender,
|
('-' * 40, sender, ', '.join(to), subject, text or html, '-' * 40)
|
||||||
', '.join(to), subject,
|
logger.warn(entry)
|
||||||
text or html, '-' * 40))
|
elif self.settings.server.startswith('logging:'):
|
||||||
|
entry = 'email not sent\n%s\nFrom: %s\nTo: %s\nSubject: %s\n\n%s\n%s\n' % \
|
||||||
|
('-' * 40, sender, ', '.join(to), subject, text or html, '-' * 40)
|
||||||
|
open(self.settings.server[8:], 'a').write(entry)
|
||||||
elif self.settings.server == 'gae':
|
elif self.settings.server == 'gae':
|
||||||
xcc = dict()
|
xcc = dict()
|
||||||
if cc:
|
if cc:
|
||||||
@@ -1146,6 +1149,7 @@ class Auth(object):
|
|||||||
reset_password_requires_verification=False,
|
reset_password_requires_verification=False,
|
||||||
registration_requires_verification=False,
|
registration_requires_verification=False,
|
||||||
registration_requires_approval=False,
|
registration_requires_approval=False,
|
||||||
|
bulk_register_enabled=False,
|
||||||
login_after_registration=False,
|
login_after_registration=False,
|
||||||
login_after_password_change=True,
|
login_after_password_change=True,
|
||||||
alternate_requires_registration=False,
|
alternate_requires_registration=False,
|
||||||
@@ -1179,6 +1183,7 @@ class Auth(object):
|
|||||||
table_permission_name='auth_permission',
|
table_permission_name='auth_permission',
|
||||||
table_event_name='auth_event',
|
table_event_name='auth_event',
|
||||||
table_cas_name='auth_cas',
|
table_cas_name='auth_cas',
|
||||||
|
table_token_name='auth_token',
|
||||||
table_user=None,
|
table_user=None,
|
||||||
table_group=None,
|
table_group=None,
|
||||||
table_membership=None,
|
table_membership=None,
|
||||||
@@ -1247,6 +1252,8 @@ class Auth(object):
|
|||||||
retrieve_password_subject='Password retrieve',
|
retrieve_password_subject='Password retrieve',
|
||||||
reset_password='Click on the link %(link)s to reset your password',
|
reset_password='Click on the link %(link)s to reset your password',
|
||||||
reset_password_subject='Password reset',
|
reset_password_subject='Password reset',
|
||||||
|
bulk_invite_subject='Invitation to join%(site)s',
|
||||||
|
bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process',
|
||||||
invalid_reset_password='Invalid reset password',
|
invalid_reset_password='Invalid reset password',
|
||||||
profile_updated='Profile updated',
|
profile_updated='Profile updated',
|
||||||
new_password='New password',
|
new_password='New password',
|
||||||
@@ -1460,6 +1467,7 @@ class Auth(object):
|
|||||||
settings.update(Auth.default_settings)
|
settings.update(Auth.default_settings)
|
||||||
settings.update(
|
settings.update(
|
||||||
cas_domains=[request.env.http_host],
|
cas_domains=[request.env.http_host],
|
||||||
|
enable_tokens=False,
|
||||||
cas_provider=cas_provider,
|
cas_provider=cas_provider,
|
||||||
cas_actions=dict(login='login',
|
cas_actions=dict(login='login',
|
||||||
validate='validate',
|
validate='validate',
|
||||||
@@ -1499,6 +1507,8 @@ class Auth(object):
|
|||||||
change_password_onvalidation = [],
|
change_password_onvalidation = [],
|
||||||
change_password_onaccept = [],
|
change_password_onaccept = [],
|
||||||
retrieve_password_onvalidation = [],
|
retrieve_password_onvalidation = [],
|
||||||
|
request_reset_password_onvalidation = [],
|
||||||
|
request_reset_password_onaccept = [],
|
||||||
reset_password_onvalidation = [],
|
reset_password_onvalidation = [],
|
||||||
reset_password_onaccept = [],
|
reset_password_onaccept = [],
|
||||||
hmac_key = hmac_key,
|
hmac_key = hmac_key,
|
||||||
@@ -1534,6 +1544,12 @@ class Auth(object):
|
|||||||
next = current.request.vars._next
|
next = current.request.vars._next
|
||||||
if isinstance(next, (list, tuple)):
|
if isinstance(next, (list, tuple)):
|
||||||
next = next[0]
|
next = next[0]
|
||||||
|
if next and self.settings.prevent_open_redirect_attacks:
|
||||||
|
# Prevent an attacker from adding an arbitrary url after the
|
||||||
|
# _next variable in the request.
|
||||||
|
items = next.split('/')
|
||||||
|
if '//' in next and items[2] != current.request.env.http_host:
|
||||||
|
next = None
|
||||||
return next
|
return next
|
||||||
|
|
||||||
def _get_user_id(self):
|
def _get_user_id(self):
|
||||||
@@ -1560,6 +1576,9 @@ class Auth(object):
|
|||||||
def table_cas(self):
|
def table_cas(self):
|
||||||
return self.db[self.settings.table_cas_name]
|
return self.db[self.settings.table_cas_name]
|
||||||
|
|
||||||
|
def table_token(self):
|
||||||
|
return self.db[self.settings.table_token_name]
|
||||||
|
|
||||||
def _HTTP(self, *a, **b):
|
def _HTTP(self, *a, **b):
|
||||||
"""
|
"""
|
||||||
only used in lambda: self._HTTP(404)
|
only used in lambda: self._HTTP(404)
|
||||||
@@ -1587,7 +1606,8 @@ class Auth(object):
|
|||||||
'retrieve_username', 'retrieve_password',
|
'retrieve_username', 'retrieve_password',
|
||||||
'reset_password', 'request_reset_password',
|
'reset_password', 'request_reset_password',
|
||||||
'change_password', 'profile', 'groups',
|
'change_password', 'profile', 'groups',
|
||||||
'impersonate', 'not_authorized'):
|
'impersonate', 'not_authorized', 'confirm_registration',
|
||||||
|
'bulk_register','manage_tokens'):
|
||||||
if len(request.args) >= 2 and args[0] == 'impersonate':
|
if len(request.args) >= 2 and args[0] == 'impersonate':
|
||||||
return getattr(self, args[0])(request.args[1])
|
return getattr(self, args[0])(request.args[1])
|
||||||
else:
|
else:
|
||||||
@@ -1914,7 +1934,7 @@ class Auth(object):
|
|||||||
writable=False, readable=False,
|
writable=False, readable=False,
|
||||||
label=T('Modified By'), ondelete=ondelete))
|
label=T('Modified By'), ondelete=ondelete))
|
||||||
|
|
||||||
def define_tables(self, username=None, signature=None,
|
def define_tables(self, username=None, signature=None, enable_tokens=False,
|
||||||
migrate=None, fake_migrate=None):
|
migrate=None, fake_migrate=None):
|
||||||
"""
|
"""
|
||||||
To be called unless tables are defined manually
|
To be called unless tables are defined manually
|
||||||
@@ -1941,6 +1961,7 @@ class Auth(object):
|
|||||||
username = settings.use_username
|
username = settings.use_username
|
||||||
else:
|
else:
|
||||||
settings.use_username = username
|
settings.use_username = username
|
||||||
|
settings.enable_tokens = enable_tokens
|
||||||
if not self.signature:
|
if not self.signature:
|
||||||
self.define_signature()
|
self.define_signature()
|
||||||
if signature == True:
|
if signature == True:
|
||||||
@@ -2124,6 +2145,21 @@ class Auth(object):
|
|||||||
migrate=self.__get_migrate(
|
migrate=self.__get_migrate(
|
||||||
settings.table_cas_name, migrate),
|
settings.table_cas_name, migrate),
|
||||||
fake_migrate=fake_migrate))
|
fake_migrate=fake_migrate))
|
||||||
|
if settings.enable_tokens:
|
||||||
|
extra_fields = settings.extra_fields.get(
|
||||||
|
settings.table_token_name, []) + signature_list
|
||||||
|
if not settings.table_token_name in db.tables:
|
||||||
|
db.define_table(
|
||||||
|
settings.table_token_name,
|
||||||
|
Field('user_id', reference_table_user, default=None,
|
||||||
|
label=self.messages.label_user_id),
|
||||||
|
Field('expires_on', 'datetime', default=datetime.datetime(2999,12,31)),
|
||||||
|
Field('token',writable=False,default=web2py_uuid(),unique=True),
|
||||||
|
*extra_fields,
|
||||||
|
**dict(
|
||||||
|
migrate=self.__get_migrate(
|
||||||
|
settings.table_token_name, migrate),
|
||||||
|
fake_migrate=fake_migrate))
|
||||||
if not db._lazy_tables:
|
if not db._lazy_tables:
|
||||||
settings.table_user = db[settings.table_user_name]
|
settings.table_user = db[settings.table_user_name]
|
||||||
settings.table_group = db[settings.table_group_name]
|
settings.table_group = db[settings.table_group_name]
|
||||||
@@ -2323,8 +2359,8 @@ class Auth(object):
|
|||||||
# user not in database try other login methods
|
# user not in database try other login methods
|
||||||
for login_method in self.settings.login_methods:
|
for login_method in self.settings.login_methods:
|
||||||
if login_method != self and login_method(username, password):
|
if login_method != self and login_method(username, password):
|
||||||
self.user = username
|
self.user = user
|
||||||
return username
|
return user
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def register_bare(self, **fields):
|
def register_bare(self, **fields):
|
||||||
@@ -2333,14 +2369,16 @@ class Auth(object):
|
|||||||
and a raw password.
|
and a raw password.
|
||||||
"""
|
"""
|
||||||
settings = self._get_login_settings()
|
settings = self._get_login_settings()
|
||||||
if not fields.get(settings.passfield):
|
# users can register_bare even if no password is provided,
|
||||||
raise ValueError("register_bare: " +
|
# in this case they will have to reset their password to login
|
||||||
"password not provided or invalid")
|
if fields.get(settings.passfield):
|
||||||
elif not fields.get(settings.userfield):
|
fields[settings.passfield] = \
|
||||||
|
settings.table_user[settings.passfield].validate(fields[settings.passfield])[0]
|
||||||
|
if not fields.get(settings.userfield):
|
||||||
raise ValueError("register_bare: " +
|
raise ValueError("register_bare: " +
|
||||||
"userfield not provided or invalid")
|
"userfield not provided or invalid")
|
||||||
fields[settings.passfield] = settings.table_user[settings.passfield].validate(fields[settings.passfield])[0]
|
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)
|
update_fields=self.settings.update_fields)
|
||||||
if not user:
|
if not user:
|
||||||
# get or create did not create a user (it ignores duplicate records)
|
# get or create did not create a user (it ignores duplicate records)
|
||||||
return False
|
return False
|
||||||
@@ -2484,10 +2522,6 @@ class Auth(object):
|
|||||||
|
|
||||||
### use session for federated login
|
### use session for federated login
|
||||||
snext = self.get_vars_next()
|
snext = self.get_vars_next()
|
||||||
if snext and self.settings.prevent_open_redirect_attacks:
|
|
||||||
items = snext.split('/')
|
|
||||||
if '//' in snext and items[2] != request.env.http_host:
|
|
||||||
snext = None
|
|
||||||
|
|
||||||
if snext:
|
if snext:
|
||||||
session._auth_next = snext
|
session._auth_next = snext
|
||||||
@@ -2861,6 +2895,10 @@ class Auth(object):
|
|||||||
passfield = self.settings.password_field
|
passfield = self.settings.password_field
|
||||||
formstyle = self.settings.formstyle
|
formstyle = self.settings.formstyle
|
||||||
if self.settings.register_verify_password:
|
if self.settings.register_verify_password:
|
||||||
|
if self.settings.register_fields == None:
|
||||||
|
self.settings.register_fields = [f.name for f in table_user if f.writable]
|
||||||
|
k = self.settings.register_fields.index("password")
|
||||||
|
self.settings.register_fields.insert(k+1, "password_two")
|
||||||
extra_fields = [
|
extra_fields = [
|
||||||
Field("password_two", "password", requires=IS_EQUAL_TO(
|
Field("password_two", "password", requires=IS_EQUAL_TO(
|
||||||
request.post_vars.get(passfield, None),
|
request.post_vars.get(passfield, None),
|
||||||
@@ -3136,6 +3174,147 @@ class Auth(object):
|
|||||||
table_user.email.requires = old_requires
|
table_user.email.requires = old_requires
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
def confirm_registration(
|
||||||
|
self,
|
||||||
|
next=DEFAULT,
|
||||||
|
onvalidation=DEFAULT,
|
||||||
|
onaccept=DEFAULT,
|
||||||
|
log=DEFAULT,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Returns a form to confirm user registration
|
||||||
|
"""
|
||||||
|
|
||||||
|
table_user = self.table_user()
|
||||||
|
request = current.request
|
||||||
|
# response = current.response
|
||||||
|
session = current.session
|
||||||
|
|
||||||
|
if next is DEFAULT:
|
||||||
|
next = self.get_vars_next() or self.settings.reset_password_next
|
||||||
|
|
||||||
|
if self.settings.prevent_password_reset_attacks:
|
||||||
|
key = request.vars.key
|
||||||
|
if not key and len(request.args)>1:
|
||||||
|
key = request.args[-1]
|
||||||
|
if key:
|
||||||
|
session._reset_password_key = key
|
||||||
|
redirect(self.url(args='confirm_registration'))
|
||||||
|
else:
|
||||||
|
key = session._reset_password_key
|
||||||
|
else:
|
||||||
|
key = request.vars.key or getarg(-1)
|
||||||
|
try:
|
||||||
|
t0 = int(key.split('-')[0])
|
||||||
|
if time.time() - t0 > 60 * 60 * 24:
|
||||||
|
raise Exception
|
||||||
|
user = table_user(reset_password_key=key)
|
||||||
|
if not user:
|
||||||
|
raise Exception
|
||||||
|
except Exception as e:
|
||||||
|
session.flash = self.messages.invalid_reset_password
|
||||||
|
redirect(self.url('login', vars=dict(test=e)))
|
||||||
|
redirect(next, client_side=self.settings.client_side)
|
||||||
|
passfield = self.settings.password_field
|
||||||
|
form = SQLFORM.factory(
|
||||||
|
Field('first_name',
|
||||||
|
label='First Name',
|
||||||
|
required=True),
|
||||||
|
Field('last_name',
|
||||||
|
label='Last Name',
|
||||||
|
required=True),
|
||||||
|
Field('new_password', 'password',
|
||||||
|
label=self.messages.new_password,
|
||||||
|
requires=self.table_user()[passfield].requires),
|
||||||
|
Field('new_password2', 'password',
|
||||||
|
label=self.messages.verify_password,
|
||||||
|
requires=[IS_EXPR(
|
||||||
|
'value==%s' % repr(request.vars.new_password),
|
||||||
|
self.messages.mismatched_password)]),
|
||||||
|
submit_button='Confirm Registration',
|
||||||
|
hidden=dict(_next=next),
|
||||||
|
formstyle=self.settings.formstyle,
|
||||||
|
separator=self.settings.label_separator
|
||||||
|
)
|
||||||
|
if form.process().accepted:
|
||||||
|
user.update_record(
|
||||||
|
**{passfield: str(form.vars.new_password),
|
||||||
|
'first_name': str(form.vars.first_name),
|
||||||
|
'last_name': str(form.vars.last_name),
|
||||||
|
'registration_key': '',
|
||||||
|
'reset_password_key': ''})
|
||||||
|
session.flash = self.messages.password_changed
|
||||||
|
if self.settings.login_after_password_change:
|
||||||
|
self.login_user(user)
|
||||||
|
redirect(next, client_side=self.settings.client_side)
|
||||||
|
return form
|
||||||
|
|
||||||
|
def email_registration(self, subject, body, user):
|
||||||
|
"""
|
||||||
|
Sends and email invitation to a user informing they have been registered with the application
|
||||||
|
"""
|
||||||
|
reset_password_key = str(int(time.time())) + '-' + web2py_uuid()
|
||||||
|
link = self.url(self.settings.function,
|
||||||
|
args=('confirm_registration',), vars={'key': reset_password_key},
|
||||||
|
scheme=True)
|
||||||
|
d = dict(user)
|
||||||
|
d.update(dict(key=reset_password_key, link=link, site=current.request.env.http_host))
|
||||||
|
if self.settings.mailer and self.settings.mailer.send(
|
||||||
|
to=user.email,
|
||||||
|
subject=subject % d,
|
||||||
|
message=body % d):
|
||||||
|
user.update_record(reset_password_key=reset_password_key)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def bulk_register(self, max_emails=100):
|
||||||
|
"""
|
||||||
|
Creates a form for ther user to send invites to other users to join
|
||||||
|
"""
|
||||||
|
if not self.user:
|
||||||
|
redirect(self.settings.login_url)
|
||||||
|
if not self.setting.bulk_register_enabled:
|
||||||
|
return HTTP(404)
|
||||||
|
|
||||||
|
form = SQLFORM.factory(
|
||||||
|
Field('subject','string',default=self.messages.bulk_invite_subject,requires=IS_NOT_EMPTY()),
|
||||||
|
Field('emails','text',requires=IS_NOT_EMPTY()),
|
||||||
|
Field('message','text',default=self.messages.bulk_invite_body,requires=IS_NOT_EMPTY()),
|
||||||
|
formstyle=self.settings.formstyle)
|
||||||
|
|
||||||
|
if form.process().accepted:
|
||||||
|
emails = re.compile('[^\s\'"@<>,;:]+\@[^\s\'"@<>,;:]+').findall(form.vars.emails)
|
||||||
|
# send the invitations
|
||||||
|
emails_sent = []
|
||||||
|
emails_fail = []
|
||||||
|
emails_exist = []
|
||||||
|
for email in emails[:max_emails]:
|
||||||
|
if self.table_user()(email=email):
|
||||||
|
emails_exist.append(email)
|
||||||
|
else:
|
||||||
|
user = self.register_bare(email=email)
|
||||||
|
if self.email_registration(form.vars.subject, form.vars.message, user):
|
||||||
|
emails_sent.append(email)
|
||||||
|
else:
|
||||||
|
emails_fail.append(email)
|
||||||
|
emails_fail += emails[max_emails:]
|
||||||
|
form = DIV(H4('Emails sent'),UL(*[A(x,_href='mailto:'+x) for x in emails_sent]),
|
||||||
|
H4('Emails failed'),UL(*[A(x,_href='mailto:'+x) for x in emails_fail]),
|
||||||
|
H4('Emails existing'),UL(*[A(x,_href='mailto:'+x) for x in emails_exist]))
|
||||||
|
return form
|
||||||
|
|
||||||
|
def manage_tokens(self):
|
||||||
|
if not self.user:
|
||||||
|
redirect(self.settings.login_url)
|
||||||
|
table_token =self.table_token()
|
||||||
|
table_token.user_id.writable = False
|
||||||
|
table_token.user_id.default = self.user.id
|
||||||
|
table_token.token.writable = False
|
||||||
|
if current.request.args(1) == 'new':
|
||||||
|
table_token.token.readable = False
|
||||||
|
form = SQLFORM.grid(table_token, args=['manage_tokens'])
|
||||||
|
return form
|
||||||
|
|
||||||
def reset_password(self,
|
def reset_password(self,
|
||||||
next=DEFAULT,
|
next=DEFAULT,
|
||||||
onvalidation=DEFAULT,
|
onvalidation=DEFAULT,
|
||||||
@@ -3173,6 +3352,12 @@ class Auth(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
session.flash = self.messages.invalid_reset_password
|
session.flash = self.messages.invalid_reset_password
|
||||||
redirect(next, client_side=self.settings.client_side)
|
redirect(next, client_side=self.settings.client_side)
|
||||||
|
|
||||||
|
if onvalidation is DEFAULT:
|
||||||
|
onvalidation = self.settings.reset_password_onvalidation
|
||||||
|
if onaccept is DEFAULT:
|
||||||
|
onaccept = self.settings.reset_password_onaccept
|
||||||
|
|
||||||
passfield = self.settings.password_field
|
passfield = self.settings.password_field
|
||||||
form = SQLFORM.factory(
|
form = SQLFORM.factory(
|
||||||
Field('new_password', 'password',
|
Field('new_password', 'password',
|
||||||
@@ -3188,7 +3373,7 @@ class Auth(object):
|
|||||||
formstyle=self.settings.formstyle,
|
formstyle=self.settings.formstyle,
|
||||||
separator=self.settings.label_separator
|
separator=self.settings.label_separator
|
||||||
)
|
)
|
||||||
if form.accepts(request, session,
|
if form.accepts(request, session, onvalidation=onvalidation,
|
||||||
hideerror=self.settings.hideerror):
|
hideerror=self.settings.hideerror):
|
||||||
user.update_record(
|
user.update_record(
|
||||||
**{passfield: str(form.vars.new_password),
|
**{passfield: str(form.vars.new_password),
|
||||||
@@ -3197,6 +3382,7 @@ class Auth(object):
|
|||||||
session.flash = self.messages.password_changed
|
session.flash = self.messages.password_changed
|
||||||
if self.settings.login_after_password_change:
|
if self.settings.login_after_password_change:
|
||||||
self.login_user(user)
|
self.login_user(user)
|
||||||
|
callback(onaccept, form)
|
||||||
redirect(next, client_side=self.settings.client_side)
|
redirect(next, client_side=self.settings.client_side)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
@@ -3222,9 +3408,9 @@ class Auth(object):
|
|||||||
response.flash = self.messages.function_disabled
|
response.flash = self.messages.function_disabled
|
||||||
return ''
|
return ''
|
||||||
if onvalidation is DEFAULT:
|
if onvalidation is DEFAULT:
|
||||||
onvalidation = self.settings.reset_password_onvalidation
|
onvalidation = self.settings.request_reset_password_onvalidation
|
||||||
if onaccept is DEFAULT:
|
if onaccept is DEFAULT:
|
||||||
onaccept = self.settings.reset_password_onaccept
|
onaccept = self.settings.request_reset_password_onaccept
|
||||||
if log is DEFAULT:
|
if log is DEFAULT:
|
||||||
log = self.messages['reset_password_log']
|
log = self.messages['reset_password_log']
|
||||||
userfield = self.settings.login_userfield or 'username' \
|
userfield = self.settings.login_userfield or 'username' \
|
||||||
@@ -3594,6 +3780,26 @@ class Auth(object):
|
|||||||
"""
|
"""
|
||||||
return self.requires(True, otherwise=otherwise)
|
return self.requires(True, otherwise=otherwise)
|
||||||
|
|
||||||
|
def requires_login_or_token(self, otherwise=None):
|
||||||
|
if self.settings.enable_tokens == True:
|
||||||
|
user = None
|
||||||
|
request = current.request
|
||||||
|
token = request.env.http_web2py_user_token or request.vars._token
|
||||||
|
table_token = self.table_token()
|
||||||
|
table_user = self.table_user()
|
||||||
|
from gluon.settings import global_settings
|
||||||
|
if global_settings.web2py_runtime_gae:
|
||||||
|
row = table_token(token=token)
|
||||||
|
if row:
|
||||||
|
user = table_user(row.user_id)
|
||||||
|
else:
|
||||||
|
row = self.db(table_token.token==token)(table_user.id==table_token.user_id).select().first()
|
||||||
|
if row:
|
||||||
|
user = row[table_user._tablename]
|
||||||
|
if user:
|
||||||
|
self.login_user(user)
|
||||||
|
return self.requires(True, otherwise=otherwise)
|
||||||
|
|
||||||
def requires_membership(self, role=None, group_id=None, otherwise=None):
|
def requires_membership(self, role=None, group_id=None, otherwise=None):
|
||||||
"""
|
"""
|
||||||
Decorator that prevents access to action if not logged in or
|
Decorator that prevents access to action if not logged in or
|
||||||
@@ -5317,11 +5523,12 @@ def completion(callback):
|
|||||||
return _completion
|
return _completion
|
||||||
|
|
||||||
|
|
||||||
def prettydate(d, T=lambda x: x):
|
def prettydate(d, T=lambda x: x, utc=False):
|
||||||
|
now = datetime.datetime.utcnow() if utc else datetime.datetime.now()
|
||||||
if isinstance(d, datetime.datetime):
|
if isinstance(d, datetime.datetime):
|
||||||
dt = datetime.datetime.now() - d
|
dt = now - d
|
||||||
elif isinstance(d, datetime.date):
|
elif isinstance(d, datetime.date):
|
||||||
dt = datetime.date.today() - d
|
dt = now.date() - d
|
||||||
elif not d:
|
elif not d:
|
||||||
return ''
|
return ''
|
||||||
else:
|
else:
|
||||||
@@ -6192,7 +6399,7 @@ class Wiki(object):
|
|||||||
count = db.wiki_tag.wiki_page.count()
|
count = db.wiki_tag.wiki_page.count()
|
||||||
fields = [db.wiki_page.id, db.wiki_page.slug,
|
fields = [db.wiki_page.id, db.wiki_page.slug,
|
||||||
db.wiki_page.title, db.wiki_page.tags,
|
db.wiki_page.title, db.wiki_page.tags,
|
||||||
db.wiki_page.can_read]
|
db.wiki_page.can_read, db.wiki+page.can_edit]
|
||||||
if preview:
|
if preview:
|
||||||
fields.append(db.wiki_page.body)
|
fields.append(db.wiki_page.body)
|
||||||
if query is None:
|
if query is None:
|
||||||
|
|||||||
+17
-12
@@ -200,8 +200,11 @@ class IS_MATCH(Validator):
|
|||||||
self.is_unicode = is_unicode
|
self.is_unicode = is_unicode
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
if self.is_unicode and not isinstance(value, unicode):
|
if self.is_unicode:
|
||||||
match = self.regex.search(str(value).decode('utf8'))
|
if isinstance(value,unicode):
|
||||||
|
match = self.regex.search(value)
|
||||||
|
else:
|
||||||
|
match = self.regex.search(str(value).decode('utf8'))
|
||||||
else:
|
else:
|
||||||
match = self.regex.search(str(value))
|
match = self.regex.search(str(value))
|
||||||
if match is not None:
|
if match is not None:
|
||||||
@@ -3479,7 +3482,7 @@ class IS_IPV6(Validator):
|
|||||||
from gluon.contrib import ipaddr as ipaddress
|
from gluon.contrib import ipaddr as ipaddress
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ip = ipaddress.IPv6Address(value)
|
ip = ipaddress.IPv6Address(value.decode('utf-8'))
|
||||||
ok = True
|
ok = True
|
||||||
except ipaddress.AddressValueError:
|
except ipaddress.AddressValueError:
|
||||||
return (value, translate(self.error_message))
|
return (value, translate(self.error_message))
|
||||||
@@ -3491,7 +3494,7 @@ class IS_IPV6(Validator):
|
|||||||
self.subnets = [self.subnets]
|
self.subnets = [self.subnets]
|
||||||
for network in self.subnets:
|
for network in self.subnets:
|
||||||
try:
|
try:
|
||||||
ipnet = ipaddress.IPv6Network(network)
|
ipnet = ipaddress.IPv6Network(network.decode('utf-8'))
|
||||||
except (ipaddress.NetmaskValueError, ipaddress.AddressValueError):
|
except (ipaddress.NetmaskValueError, ipaddress.AddressValueError):
|
||||||
return (value, translate('invalid subnet provided'))
|
return (value, translate('invalid subnet provided'))
|
||||||
if ip in ipnet:
|
if ip in ipnet:
|
||||||
@@ -3700,20 +3703,22 @@ class IS_IPADDRESS(Validator):
|
|||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
try:
|
try:
|
||||||
import ipaddress
|
from ipaddress import ip_address as IPAddress
|
||||||
|
from ipaddress import IPv6Address, IPv4Address
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from gluon.contrib import ipaddr as ipaddress
|
from gluon.contrib.ipaddr import (IPAddress, IPv4Address,
|
||||||
|
IPv6Address)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ip = ipaddress.IPAddress(value)
|
ip = IPAddress(value.decode('utf-8'))
|
||||||
except ValueError, e:
|
except ValueError:
|
||||||
return (value, translate(self.error_message))
|
return (value, translate(self.error_message))
|
||||||
|
|
||||||
if self.is_ipv4 and isinstance(ip, ipaddress.IPv6Address):
|
if self.is_ipv4 and isinstance(ip, IPv6Address):
|
||||||
retval = (value, translate(self.error_message))
|
retval = (value, translate(self.error_message))
|
||||||
elif self.is_ipv6 and isinstance(ip, ipaddress.IPv4Address):
|
elif self.is_ipv6 and isinstance(ip, IPv4Address):
|
||||||
retval = (value, translate(self.error_message))
|
retval = (value, translate(self.error_message))
|
||||||
elif self.is_ipv4 or isinstance(ip, ipaddress.IPv4Address):
|
elif self.is_ipv4 or isinstance(ip, IPv4Address):
|
||||||
retval = IS_IPV4(
|
retval = IS_IPV4(
|
||||||
minip=self.minip,
|
minip=self.minip,
|
||||||
maxip=self.maxip,
|
maxip=self.maxip,
|
||||||
@@ -3723,7 +3728,7 @@ class IS_IPADDRESS(Validator):
|
|||||||
is_automatic=self.is_automatic,
|
is_automatic=self.is_automatic,
|
||||||
error_message=self.error_message
|
error_message=self.error_message
|
||||||
)(value)
|
)(value)
|
||||||
elif self.is_ipv6 or isinstance(ip, ipaddress.IPv6Address):
|
elif self.is_ipv6 or isinstance(ip, IPv6Address):
|
||||||
retval = IS_IPV6(
|
retval = IS_IPV6(
|
||||||
is_private=self.is_private,
|
is_private=self.is_private,
|
||||||
is_link_local=self.is_link_local,
|
is_link_local=self.is_link_local,
|
||||||
|
|||||||
@@ -1058,6 +1058,11 @@ def start_schedulers(options):
|
|||||||
print 'starting single-scheduler for "%s"...' % app_
|
print 'starting single-scheduler for "%s"...' % app_
|
||||||
run(app_, True, True, None, False, code)
|
run(app_, True, True, None, False, code)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Work around OS X problem: http://bugs.python.org/issue9405
|
||||||
|
import urllib
|
||||||
|
urllib.getproxies()
|
||||||
|
|
||||||
for app in apps:
|
for app in apps:
|
||||||
app_, code = get_code_for_scheduler(app, options)
|
app_, code = get_code_for_scheduler(app, options)
|
||||||
if not app_:
|
if not app_:
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ NameVirtualHost *:80
|
|||||||
NameVirtualHost *:443
|
NameVirtualHost *:443
|
||||||
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1
|
WSGIDaemonProcess web2py user=apache group=apache
|
||||||
WSGIProcessGroup web2py
|
WSGIProcessGroup web2py
|
||||||
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
|
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
|
||||||
WSGIPassAuthorization On
|
WSGIPassAuthorization On
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ NameVirtualHost *:80
|
|||||||
NameVirtualHost *:443
|
NameVirtualHost *:443
|
||||||
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1
|
WSGIDaemonProcess web2py user=apache group=apache
|
||||||
WSGIProcessGroup web2py
|
WSGIProcessGroup web2py
|
||||||
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
|
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ NameVirtualHost *:80
|
|||||||
NameVirtualHost *:443
|
NameVirtualHost *:443
|
||||||
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
WSGIDaemonProcess web2py user=apache group=apache processes=1 threads=1
|
WSGIDaemonProcess web2py user=apache group=apache
|
||||||
WSGIProcessGroup web2py
|
WSGIProcessGroup web2py
|
||||||
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
|
WSGIScriptAlias / /opt/web-apps/web2py/wsgihandler.py
|
||||||
WSGIPassAuthorization On
|
WSGIPassAuthorization On
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script will install web2py with nginx+uwsgi on centos 7
|
||||||
|
# This script is based on excellent tutorial by Justin Ellingwood on
|
||||||
|
# https://www.digitalocean.com/community/tutorials/how-to-deploy-web2py-python-applications-with-uwsgi-and-nginx-on-centos-7
|
||||||
|
|
||||||
|
#
|
||||||
|
# Phase 1: First, let's ask a few things
|
||||||
|
#
|
||||||
|
|
||||||
|
read -p "Enter username under which web2py will be installed [web2py]: " USERNAME
|
||||||
|
USERNAME=${USERNAME:-web2py}
|
||||||
|
|
||||||
|
read -p "Enter path where web2py will be installed [/opt/web2py_apps]: " WEB2PY_PATH
|
||||||
|
WEB2PY_PATH=${WEB2PY_PATH:-/opt/web2py_apps}
|
||||||
|
|
||||||
|
read -p "Web2py subdirectory will be called: [web2py]: " WEB2PY_APP
|
||||||
|
WEB2PY_APP=${WEB2PY_APP:-web2py}
|
||||||
|
|
||||||
|
read -p "Enter your web2py admin password: " WEB2PY_PASS
|
||||||
|
|
||||||
|
read -p "Enter your domain name: " YOUR_SERVER_DOMAIN
|
||||||
|
|
||||||
|
# open new user
|
||||||
|
useradd -d $WEB2PY_PATH $USERNAME
|
||||||
|
|
||||||
|
# if it's not already open, let's create a directory for web2py
|
||||||
|
mkdir -p $WEB2PY_PATH
|
||||||
|
|
||||||
|
# now let's create a self signed certificate
|
||||||
|
cd $WEB2PY_PATH
|
||||||
|
|
||||||
|
openssl req -x509 -new -newkey rsa:4096 -days 3652 -nodes -keyout $WEB2PY_APP.key -out $WEB2PY_APP.crt
|
||||||
|
|
||||||
|
#
|
||||||
|
# phase 2: That was all the input that we needed so let's install the components
|
||||||
|
#
|
||||||
|
|
||||||
|
echo "Installing necessary components"
|
||||||
|
|
||||||
|
# Verify packages are up to date
|
||||||
|
yum -y upgrade
|
||||||
|
|
||||||
|
# Install required packages
|
||||||
|
yum install -y epel-release
|
||||||
|
yum install -y python-devel python-pip gcc nginx wget unzip python-psycopg2 MySQL-python
|
||||||
|
|
||||||
|
# download and unzip web2py
|
||||||
|
|
||||||
|
echo "Downloading web2py"
|
||||||
|
|
||||||
|
cd $WEB2PY_PATH
|
||||||
|
wget http://web2py.com/examples/static/web2py_src.zip
|
||||||
|
unzip web2py_src.zip
|
||||||
|
rm web2py_src.zip
|
||||||
|
|
||||||
|
# preparing wsgihandler
|
||||||
|
chown -R $USERNAME.$USERNAME $WEB2PY_PATH/$WEB2PY_APP
|
||||||
|
mv $WEB2PY_PATH/$WEB2PY_APP/handlers/wsgihandler.py $WEB2PY_PATH/$WEB2PY_APP
|
||||||
|
|
||||||
|
# now let's install uwsgi
|
||||||
|
|
||||||
|
pip install uwsgi
|
||||||
|
|
||||||
|
# preparing directories
|
||||||
|
mkdir -p /etc/uwsgi/sites
|
||||||
|
mkdir -p /var/log/uwsgi
|
||||||
|
mkdir -p /etc/nginx/ssl/
|
||||||
|
|
||||||
|
#
|
||||||
|
# Phase 3: Ok, everything is installed now so we'll configure things
|
||||||
|
#
|
||||||
|
|
||||||
|
# Create configuration file for uwsgi in /etc/uwsgi/$WEB2PY_APP.ini
|
||||||
|
echo '[uwsgi]
|
||||||
|
chdir = WEB2PY_PATH_PLACEHOLDER/WEB2PY_APP_PLACEHOLDER
|
||||||
|
module = wsgihandler:application
|
||||||
|
|
||||||
|
master = true
|
||||||
|
processes = 5
|
||||||
|
|
||||||
|
uid = USERNAME_PLACEHOLDER
|
||||||
|
socket = /run/uwsgi/WEB2PY_APP_PLACEHOLDER.sock
|
||||||
|
chown-socket = USERNAME_PLACEHOLDER:nginx
|
||||||
|
chmod-socket = 660
|
||||||
|
vacuum = true
|
||||||
|
' >/etc/uwsgi/sites/$WEB2PY_APP.ini
|
||||||
|
|
||||||
|
sed -i "s@WEB2PY_PATH_PLACEHOLDER@$WEB2PY_PATH@" /etc/uwsgi/sites/$WEB2PY_APP.ini
|
||||||
|
sed -i "s@WEB2PY_APP_PLACEHOLDER@$WEB2PY_APP@" /etc/uwsgi/sites/$WEB2PY_APP.ini
|
||||||
|
sed -i "s@USERNAME_PLACEHOLDER@$USERNAME@" /etc/uwsgi/sites/$WEB2PY_APP.ini
|
||||||
|
|
||||||
|
# Create a daemon configuration file for uwsgi
|
||||||
|
cat > /etc/systemd/system/uwsgi.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=uWSGI Emperor service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStartPre=/usr/bin/bash -c 'mkdir -p /run/uwsgi; chown USERNAME_PLACEHOLDER:nginx /run/uwsgi'
|
||||||
|
ExecStart=/usr/bin/uwsgi --emperor /etc/uwsgi/sites
|
||||||
|
Restart=always
|
||||||
|
KillSignal=SIGQUIT
|
||||||
|
Type=notify
|
||||||
|
NotifyAccess=all
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sed -i "s@USERNAME_PLACEHOLDER@$USERNAME@" /etc/systemd/system/uwsgi.service
|
||||||
|
|
||||||
|
#chmod 777 /etc/systemd/system/uwsgi.service
|
||||||
|
|
||||||
|
# create a nginx configuration file
|
||||||
|
cat > /etc/nginx/nginx.conf <<EOF
|
||||||
|
# For more information on configuration, see:
|
||||||
|
# * Official English Documentation: http://nginx.org/en/docs/
|
||||||
|
# * Official Russian Documentation: http://nginx.org/ru/docs/
|
||||||
|
|
||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
pid /run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
|
||||||
|
'\$status \$body_bytes_sent "\$http_referer" '
|
||||||
|
'"\$http_user_agent" "\$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Load modular configuration files from the /etc/nginx/conf.d directory.
|
||||||
|
# See http://nginx.org/en/docs/ngx_core_module.html#include
|
||||||
|
# for more information.
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name YOUR_SERVER_DOMAIN_PLACEHOLDER;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
# Load configuration files for the default server block.
|
||||||
|
include /etc/nginx/default.d/*.conf;
|
||||||
|
|
||||||
|
location ~* /(\w+)/static/ {
|
||||||
|
root WEB2PY_PATH_PLACEHOLDER/WEB2PY_APP_PLACEHOLDER/applications/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass unix:/run/uwsgi/WEB2PY_APP_PLACEHOLDER.sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 404 /404.html;
|
||||||
|
location = /40x.html {
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443;
|
||||||
|
server_name YOUR_SERVER_DOMAIN_PLACEHOLDER;
|
||||||
|
|
||||||
|
ssl on;
|
||||||
|
ssl_certificate /etc/nginx/ssl/WEB2PY_APP_PLACEHOLDER.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/WEB2PY_APP_PLACEHOLDER.key;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||||
|
ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass unix:/run/uwsgi/WEB2PY_APP_PLACEHOLDER.sock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sed -i "s@YOUR_SERVER_DOMAIN_PLACEHOLDER@$YOUR_SERVER_DOMAIN@" /etc/nginx/nginx.conf
|
||||||
|
sed -i "s@WEB2PY_PATH_PLACEHOLDER@$WEB2PY_PATH@" /etc/nginx/nginx.conf
|
||||||
|
sed -i "s@WEB2PY_APP_PLACEHOLDER@$WEB2PY_APP@" /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
#
|
||||||
|
# Phase 4: everything is configured now, just a few final touches
|
||||||
|
#
|
||||||
|
|
||||||
|
# copying certificates to nginx directory
|
||||||
|
mv $WEB2PY_PATH/$WEB2PY_APP.crt* /etc/nginx/ssl
|
||||||
|
mv $WEB2PY_PATH/$WEB2PY_APP.key* /etc/nginx/ssl
|
||||||
|
|
||||||
|
# creating web2py admin password
|
||||||
|
cd $WEB2PY_PATH/$WEB2PY_APP
|
||||||
|
python -c "from gluon.main import save_password; save_password('$WEB2PY_PASS',443)"
|
||||||
|
chown -R $USERNAME.$USERNAME $WEB2PY_PATH/$WEB2PY_APP
|
||||||
|
|
||||||
|
# taking care of permissions
|
||||||
|
chmod 700 /etc/nginx/ssl
|
||||||
|
usermod -a -G $USERNAME nginx
|
||||||
|
chmod 710 $WEB2PY_PATH
|
||||||
|
|
||||||
|
# enabling daemons
|
||||||
|
systemctl start nginx
|
||||||
|
systemctl start uwsgi
|
||||||
|
systemctl enable nginx
|
||||||
|
systemctl enable uwsgi
|
||||||
|
|
||||||
|
# If firewall is active make sure these ports are open
|
||||||
|
|
||||||
|
firewall-cmd --zone=public --add-port=80/tcp --permanent
|
||||||
|
firewall-cmd --zone=public --add-port=443/tcp --permanent
|
||||||
|
firewall-cmd --zone=public --add-port=22/tcp --permanent
|
||||||
|
firewall-cmd --reload
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo 'Web2py is now installed on this server!'
|
||||||
|
echo
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
echo "This script will:
|
echo "This script will:
|
||||||
1) install all modules need to run web2py on Ubuntu 14.04
|
1) install all modules need to run web2py on Ubuntu 14.04
|
||||||
2) install web2py in /home/www-data/
|
2) install web2py in /home/www-data/
|
||||||
3) create a self signed sll certificate
|
3) create a self signed ssl certificate
|
||||||
4) setup web2py with mod_wsgi
|
4) setup web2py with mod_wsgi
|
||||||
5) overwrite /etc/apache2/sites-available/default
|
5) overwrite /etc/apache2/sites-available/default
|
||||||
6) restart apache.
|
6) restart apache.
|
||||||
@@ -84,7 +84,7 @@ openssl x509 -noout -fingerprint -text < /etc/apache2/ssl/self_signed.cert > /et
|
|||||||
echo "rewriting your apache config file to use mod_wsgi"
|
echo "rewriting your apache config file to use mod_wsgi"
|
||||||
echo "================================================="
|
echo "================================================="
|
||||||
echo '
|
echo '
|
||||||
WSGIDaemonProcess web2py user=www-data group=www-data processes=1 threads=1
|
WSGIDaemonProcess web2py user=www-data group=www-data
|
||||||
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
|
|
||||||
|
|||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
description "web2py task scheduler"
|
||||||
|
|
||||||
|
# INSTRUCTIONS:
|
||||||
|
# COPY THIS FILE IN:
|
||||||
|
# /etc/init/web2py-scheduler.con
|
||||||
|
#
|
||||||
|
# To start/stop the scheduler, use
|
||||||
|
# "sudo start web2py-scheduler"
|
||||||
|
# "sudo stop web2py-scheduler"
|
||||||
|
# "sudo status web2py-scheduler"
|
||||||
|
#
|
||||||
|
# YOU MAY HAVE TO EDIT PATH TO WEB2PY BELOW
|
||||||
|
|
||||||
|
start on (local-filesystems and net-device-up IFACE=eth0)
|
||||||
|
stop on shutdown
|
||||||
|
|
||||||
|
# Give up if restart occurs 8 times in 60 seconds.
|
||||||
|
respawn limit 8 60
|
||||||
|
|
||||||
|
exec sudo -u www-data python /home/www-data/web2py/web2py.py -K parking > /tmp/scheduler.out
|
||||||
|
respawn
|
||||||
Reference in New Issue
Block a user