diff --git a/Makefile b/Makefile
index 97967812..6a646213 100644
--- a/Makefile
+++ b/Makefile
@@ -63,6 +63,7 @@ app:
python2.5 -c 'import compileall; compileall.compile_dir("gluon/")'
#python web2py.py -S welcome -R __exit__.py
find gluon -path '*.pyc' -exec cp {} ../web2py_osx/site-packages/{} \;
+ cd ../web2py_osx/site-packages/; unzip ../site-packages.zip
cd ../web2py_osx/site-packages/; zip -r ../site-packages.zip *
mv ../web2py_osx/site-packages.zip ../web2py_osx/web2py/web2py.app/Contents/Resources/lib/python2.5
cp README.markdown ../web2py_osx/web2py/web2py.app/Contents/Resources
@@ -86,6 +87,7 @@ win:
echo 'did you uncomment import_all in gluon/main.py?'
python2.5 -c 'import compileall; compileall.compile_dir("gluon/")'
find gluon -path '*.pyc' -exec cp {} ../web2py_win/library/{} \;
+ cd ../web2py_win/library/; unzip ../library.zip
cd ../web2py_win/library/; zip -r ../library.zip *
mv ../web2py_win/library.zip ../web2py_win/web2py
cp README.markdown ../web2py_win/web2py/
diff --git a/VERSION b/VERSION
index 7bc11a3d..f702230c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-Version 2.00.0 (2012-06-05 20:02:32) dev
+Version 2.00.0 (2012-06-12 08:25:19) dev
diff --git a/applications/admin/controllers/appadmin.py b/applications/admin/controllers/appadmin.py
index 7a2ce1f7..70f66878 100644
--- a/applications/admin/controllers/appadmin.py
+++ b/applications/admin/controllers/appadmin.py
@@ -25,8 +25,7 @@ try:
except:
hosts = (http_host, )
-if request.env.http_x_forwarded_for or request.env.wsgi_url_scheme\
- in ['https', 'HTTPS']:
+if request.env.http_x_forwarded_for or request.is_https:
session.secure()
elif (remote_addr not in hosts) and (remote_addr != "127.0.0.1"):
raise HTTP(200, T('appadmin is disabled because insecure channel'))
diff --git a/applications/admin/static/js/web2py.js b/applications/admin/static/js/web2py.js
index b2f7e8dd..71dbc8e8 100644
--- a/applications/admin/static/js/web2py.js
+++ b/applications/admin/static/js/web2py.js
@@ -104,8 +104,8 @@ function web2py_ajax_page(method, action, data, target) {
function web2py_component(action, target, timeout, times){
jQuery(function(){
- var element = $("#" + target).get(0);
- var statement = "$('#" + target + "').get(0).reload();";
+ var element = jQuery("#" + target).get(0);
+ var statement = "jQuery('#" + target + "').get(0).reload();";
element.reload = function (){
// Continue if times is Infinity or
// the times limit is not reached
diff --git a/applications/examples/controllers/appadmin.py b/applications/examples/controllers/appadmin.py
index 7a2ce1f7..70f66878 100644
--- a/applications/examples/controllers/appadmin.py
+++ b/applications/examples/controllers/appadmin.py
@@ -25,8 +25,7 @@ try:
except:
hosts = (http_host, )
-if request.env.http_x_forwarded_for or request.env.wsgi_url_scheme\
- in ['https', 'HTTPS']:
+if request.env.http_x_forwarded_for or request.is_https:
session.secure()
elif (remote_addr not in hosts) and (remote_addr != "127.0.0.1"):
raise HTTP(200, T('appadmin is disabled because insecure channel'))
diff --git a/applications/examples/static/js/web2py.js b/applications/examples/static/js/web2py.js
index b2f7e8dd..71dbc8e8 100644
--- a/applications/examples/static/js/web2py.js
+++ b/applications/examples/static/js/web2py.js
@@ -104,8 +104,8 @@ function web2py_ajax_page(method, action, data, target) {
function web2py_component(action, target, timeout, times){
jQuery(function(){
- var element = $("#" + target).get(0);
- var statement = "$('#" + target + "').get(0).reload();";
+ var element = jQuery("#" + target).get(0);
+ var statement = "jQuery('#" + target + "').get(0).reload();";
element.reload = function (){
// Continue if times is Infinity or
// the times limit is not reached
diff --git a/applications/welcome/static/js/web2py.js b/applications/welcome/static/js/web2py.js
index b2f7e8dd..71dbc8e8 100644
--- a/applications/welcome/static/js/web2py.js
+++ b/applications/welcome/static/js/web2py.js
@@ -104,8 +104,8 @@ function web2py_ajax_page(method, action, data, target) {
function web2py_component(action, target, timeout, times){
jQuery(function(){
- var element = $("#" + target).get(0);
- var statement = "$('#" + target + "').get(0).reload();";
+ var element = jQuery("#" + target).get(0);
+ var statement = "jQuery('#" + target + "').get(0).reload();";
element.reload = function (){
// Continue if times is Infinity or
// the times limit is not reached
diff --git a/applications/welcome/views/layout.html b/applications/welcome/views/layout.html
index 7454e378..1e793206 100644
--- a/applications/welcome/views/layout.html
+++ b/applications/welcome/views/layout.html
@@ -93,7 +93,7 @@
web2py™
-
{{='auth' in globals() and auth.navbar(separators=(' ',' ',''))}}
+ {{='auth' in globals() and auth.navbar(separators=(' ',' ','')) or ''}}
{{is_mobile=request.user_agent().is_mobile}}
{{if response.menu:}}
diff --git a/gluon/contrib/aes.py b/gluon/contrib/aes.py
new file mode 100644
index 00000000..24e40878
--- /dev/null
+++ b/gluon/contrib/aes.py
@@ -0,0 +1,502 @@
+"""Simple AES cipher implementation in pure Python following PEP-272 API
+
+Homepage: https://bitbucket.org/intgr/pyaes/
+
+The goal of this module is to be as fast as reasonable in Python while still
+being Pythonic and readable/understandable. It is licensed under the permissive
+MIT license.
+
+Hopefully the code is readable and commented enough that it can serve as an
+introduction to the AES cipher for Python coders. In fact, it should go along
+well with the Stick Figure Guide to AES:
+http://www.moserware.com/2009/09/stick-figure-guide-to-advanced.html
+
+Contrary to intuition, this implementation numbers the 4x4 matrices from top to
+bottom for efficiency reasons::
+
+ 0 4 8 12
+ 1 5 9 13
+ 2 6 10 14
+ 3 7 11 15
+
+Effectively it's the transposition of what you'd expect. This actually makes
+the code simpler -- except the ShiftRows step, but hopefully the explanation
+there clears it up.
+
+"""
+
+####
+# Copyright (c) 2010 Marti Raudsepp
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+####
+
+
+from array import array
+
+# Globals mandated by PEP 272:
+# http://www.python.org/dev/peps/pep-0272/
+MODE_ECB = 1
+MODE_CBC = 2
+#MODE_CTR = 6
+
+block_size = 16
+key_size = None
+
+def new(key, mode=MODE_CBC, IV=None):
+ if mode == MODE_ECB:
+ return ECBMode(AES(key))
+ elif mode == MODE_CBC:
+ if IV is None:
+ raise ValueError, "CBC mode needs an IV value!"
+
+ return CBCMode(AES(key), IV)
+ else:
+ raise NotImplementedError
+
+#### AES cipher implementation
+
+class AES(object):
+ block_size = 16
+
+ def __init__(self, key):
+ self.setkey(key)
+
+ def setkey(self, key):
+ """Sets the key and performs key expansion."""
+
+ self.key = key
+ self.key_size = len(key)
+
+ if self.key_size == 16:
+ self.rounds = 10
+ elif self.key_size == 24:
+ self.rounds = 12
+ elif self.key_size == 32:
+ self.rounds = 14
+ else:
+ raise ValueError, "Key length must be 16, 24 or 32 bytes"
+
+ self.expand_key()
+
+ def expand_key(self):
+ """Performs AES key expansion on self.key and stores in self.exkey"""
+
+ # The key schedule specifies how parts of the key are fed into the
+ # cipher's round functions. "Key expansion" means performing this
+ # schedule in advance. Almost all implementations do this.
+ #
+ # Here's a description of AES key schedule:
+ # http://en.wikipedia.org/wiki/Rijndael_key_schedule
+
+ # The expanded key starts with the actual key itself
+ exkey = array('B', self.key)
+
+ # extra key expansion steps
+ if self.key_size == 16:
+ extra_cnt = 0
+ elif self.key_size == 24:
+ extra_cnt = 2
+ else:
+ extra_cnt = 3
+
+ # 4-byte temporary variable for key expansion
+ word = exkey[-4:]
+ # Each expansion cycle uses 'i' once for Rcon table lookup
+ for i in xrange(1, 11):
+
+ #### key schedule core:
+ # left-rotate by 1 byte
+ word = word[1:4] + word[0:1]
+
+ # apply S-box to all bytes
+ for j in xrange(4):
+ word[j] = aes_sbox[word[j]]
+
+ # apply the Rcon table to the leftmost byte
+ word[0] = word[0] ^ aes_Rcon[i]
+ #### end key schedule core
+
+ for z in xrange(4):
+ for j in xrange(4):
+ # mix in bytes from the last subkey
+ word[j] ^= exkey[-self.key_size + j]
+ exkey.extend(word)
+
+ # Last key expansion cycle always finishes here
+ if len(exkey) >= (self.rounds+1) * self.block_size:
+ break
+
+ # Special substitution step for 256-bit key
+ if self.key_size == 32:
+ for j in xrange(4):
+ # mix in bytes from the last subkey XORed with S-box of
+ # current word bytes
+ word[j] = aes_sbox[word[j]] ^ exkey[-self.key_size + j]
+ exkey.extend(word)
+
+ # Twice for 192-bit key, thrice for 256-bit key
+ for z in xrange(extra_cnt):
+ for j in xrange(4):
+ # mix in bytes from the last subkey
+ word[j] ^= exkey[-self.key_size + j]
+ exkey.extend(word)
+
+ self.exkey = exkey
+
+ def add_round_key(self, block, round):
+ """AddRoundKey step in AES. This is where the key is mixed into plaintext"""
+
+ offset = round * 16
+ exkey = self.exkey
+
+ for i in xrange(16):
+ block[i] ^= exkey[offset + i]
+
+ #print 'AddRoundKey:', block
+
+ def sub_bytes(self, block, sbox):
+ """SubBytes step, apply S-box to all bytes
+
+ Depending on whether encrypting or decrypting, a different sbox array
+ is passed in.
+ """
+
+ for i in xrange(16):
+ block[i] = sbox[block[i]]
+
+ #print 'SubBytes :', block
+
+ def shift_rows(self, b):
+ """ShiftRows step. Shifts 2nd row to left by 1, 3rd row by 2, 4th row by 3
+
+ Since we're performing this on a transposed matrix, cells are numbered
+ from top to bottom::
+
+ 0 4 8 12 -> 0 4 8 12 -- 1st row doesn't change
+ 1 5 9 13 -> 5 9 13 1 -- row shifted to left by 1 (wraps around)
+ 2 6 10 14 -> 10 14 2 6 -- shifted by 2
+ 3 7 11 15 -> 15 3 7 11 -- shifted by 3
+ """
+
+ b[1], b[5], b[ 9], b[13] = b[ 5], b[ 9], b[13], b[ 1]
+ b[2], b[6], b[10], b[14] = b[10], b[14], b[ 2], b[ 6]
+ b[3], b[7], b[11], b[15] = b[15], b[ 3], b[ 7], b[11]
+
+ #print 'ShiftRows :', b
+
+ def shift_rows_inv(self, b):
+ """Similar to shift_rows above, but performed in inverse for decryption."""
+
+ b[ 5], b[ 9], b[13], b[ 1] = b[1], b[5], b[ 9], b[13]
+ b[10], b[14], b[ 2], b[ 6] = b[2], b[6], b[10], b[14]
+ b[15], b[ 3], b[ 7], b[11] = b[3], b[7], b[11], b[15]
+
+ #print 'ShiftRows :', b
+
+ def mix_columns(self, block):
+ """MixColumns step. Mixes the values in each column"""
+
+ # Cache global multiplication tables (see below)
+ mul_by_2 = gf_mul_by_2
+ mul_by_3 = gf_mul_by_3
+
+ # Since we're dealing with a transposed matrix, columns are already
+ # sequential
+ for i in xrange(4):
+ col = i * 4
+
+ #v0, v1, v2, v3 = block[col : col+4]
+ v0, v1, v2, v3 = (block[col], block[col + 1], block[col + 2],
+ block[col + 3])
+
+ block[col ] = mul_by_2[v0] ^ v3 ^ v2 ^ mul_by_3[v1]
+ block[col+1] = mul_by_2[v1] ^ v0 ^ v3 ^ mul_by_3[v2]
+ block[col+2] = mul_by_2[v2] ^ v1 ^ v0 ^ mul_by_3[v3]
+ block[col+3] = mul_by_2[v3] ^ v2 ^ v1 ^ mul_by_3[v0]
+
+ #print 'MixColumns :', block
+
+ def mix_columns_inv(self, block):
+ """Similar to mix_columns above, but performed in inverse for decryption."""
+
+ # Cache global multiplication tables (see below)
+ mul_9 = gf_mul_by_9
+ mul_11 = gf_mul_by_11
+ mul_13 = gf_mul_by_13
+ mul_14 = gf_mul_by_14
+
+ # Since we're dealing with a transposed matrix, columns are already
+ # sequential
+ for i in xrange(4):
+ col = i * 4
+
+ v0, v1, v2, v3 = (block[col], block[col + 1], block[col + 2],
+ block[col + 3])
+ #v0, v1, v2, v3 = block[col:col+4]
+
+ block[col ] = mul_14[v0] ^ mul_9[v3] ^ mul_13[v2] ^ mul_11[v1]
+ block[col+1] = mul_14[v1] ^ mul_9[v0] ^ mul_13[v3] ^ mul_11[v2]
+ block[col+2] = mul_14[v2] ^ mul_9[v1] ^ mul_13[v0] ^ mul_11[v3]
+ block[col+3] = mul_14[v3] ^ mul_9[v2] ^ mul_13[v1] ^ mul_11[v0]
+
+ #print 'MixColumns :', block
+
+ def encrypt_block(self, block):
+ """Encrypts a single block. This is the main AES function"""
+
+ # For efficiency reasons, the state between steps is transmitted via a
+ # mutable array, not returned.
+ self.add_round_key(block, 0)
+
+ for round in xrange(1, self.rounds):
+ self.sub_bytes(block, aes_sbox)
+ self.shift_rows(block)
+ self.mix_columns(block)
+ self.add_round_key(block, round)
+
+ self.sub_bytes(block, aes_sbox)
+ self.shift_rows(block)
+ # no mix_columns step in the last round
+ self.add_round_key(block, self.rounds)
+
+ def decrypt_block(self, block):
+ """Decrypts a single block. This is the main AES decryption function"""
+
+ # For efficiency reasons, the state between steps is transmitted via a
+ # mutable array, not returned.
+ self.add_round_key(block, self.rounds)
+
+ # count rounds down from 15 ... 1
+ for round in xrange(self.rounds-1, 0, -1):
+ self.shift_rows_inv(block)
+ self.sub_bytes(block, aes_inv_sbox)
+ self.add_round_key(block, round)
+ self.mix_columns_inv(block)
+
+ self.shift_rows_inv(block)
+ self.sub_bytes(block, aes_inv_sbox)
+ self.add_round_key(block, 0)
+ # no mix_columns step in the last round
+
+
+#### ECB mode implementation
+
+class ECBMode(object):
+ """Electronic CodeBook (ECB) mode encryption.
+
+ Basically this mode applies the cipher function to each block individually;
+ no feedback is done. NB! This is insecure for almost all purposes
+ """
+
+ def __init__(self, cipher):
+ self.cipher = cipher
+ self.block_size = cipher.block_size
+
+ def ecb(self, data, block_func):
+ """Perform ECB mode with the given function"""
+
+ if len(data) % self.block_size != 0:
+ raise ValueError, "Plaintext length must be multiple of 16"
+
+ block_size = self.block_size
+ data = array('B', data)
+
+ for offset in xrange(0, len(data), block_size):
+ block = data[offset : offset+block_size]
+ block_func(block)
+ data[offset : offset+block_size] = block
+
+ return data.tostring()
+
+ def encrypt(self, data):
+ """Encrypt data in ECB mode"""
+
+ return self.ecb(data, self.cipher.encrypt_block)
+
+ def decrypt(self, data):
+ """Decrypt data in ECB mode"""
+
+ return self.ecb(data, self.cipher.decrypt_block)
+
+#### CBC mode
+
+class CBCMode(object):
+ """Cipher Block Chaining (CBC) mode encryption. This mode avoids content leaks.
+
+ In CBC encryption, each plaintext block is XORed with the ciphertext block
+ preceding it; decryption is simply the inverse.
+ """
+
+ # A better explanation of CBC can be found here:
+ # http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29
+
+ def __init__(self, cipher, IV):
+ self.cipher = cipher
+ self.block_size = cipher.block_size
+ self.IV = array('B', IV)
+
+ def encrypt(self, data):
+ """Encrypt data in CBC mode"""
+
+ block_size = self.block_size
+ if len(data) % block_size != 0:
+ raise ValueError, "Plaintext length must be multiple of 16"
+
+ data = array('B', data)
+ IV = self.IV
+
+ for offset in xrange(0, len(data), block_size):
+ block = data[offset : offset+block_size]
+
+ # Perform CBC chaining
+ for i in xrange(block_size):
+ block[i] ^= IV[i]
+
+ self.cipher.encrypt_block(block)
+ data[offset : offset+block_size] = block
+ IV = block
+
+ self.IV = IV
+ return data.tostring()
+
+ def decrypt(self, data):
+ """Decrypt data in CBC mode"""
+
+ block_size = self.block_size
+ if len(data) % block_size != 0:
+ raise ValueError, "Ciphertext length must be multiple of 16"
+
+ data = array('B', data)
+ IV = self.IV
+
+ for offset in xrange(0, len(data), block_size):
+ ctext = data[offset : offset+block_size]
+ block = ctext[:]
+ self.cipher.decrypt_block(block)
+
+ # Perform CBC chaining
+ #for i in xrange(block_size):
+ # data[offset + i] ^= IV[i]
+ for i in xrange(block_size):
+ block[i] ^= IV[i]
+ data[offset : offset+block_size] = block
+
+ IV = ctext
+ #data[offset : offset+block_size] = block
+
+ self.IV = IV
+ return data.tostring()
+
+####
+
+def galois_multiply(a, b):
+ """Galois Field multiplicaiton for AES"""
+ p = 0
+ while b:
+ if b & 1:
+ p ^= a
+ a <<= 1
+ if a & 0x100:
+ a ^= 0x1b
+ b >>= 1
+
+ return p & 0xff
+
+# Precompute the multiplication tables for encryption
+gf_mul_by_2 = array('B', [galois_multiply(x, 2) for x in range(256)])
+gf_mul_by_3 = array('B', [galois_multiply(x, 3) for x in range(256)])
+# ... for decryption
+gf_mul_by_9 = array('B', [galois_multiply(x, 9) for x in range(256)])
+gf_mul_by_11 = array('B', [galois_multiply(x, 11) for x in range(256)])
+gf_mul_by_13 = array('B', [galois_multiply(x, 13) for x in range(256)])
+gf_mul_by_14 = array('B', [galois_multiply(x, 14) for x in range(256)])
+
+####
+
+# The S-box is a 256-element array, that maps a single byte value to another
+# byte value. Since it's designed to be reversible, each value occurs only once
+# in the S-box
+#
+# More information: http://en.wikipedia.org/wiki/Rijndael_S-box
+
+aes_sbox = array('B',
+ '637c777bf26b6fc53001672bfed7ab76'
+ 'ca82c97dfa5947f0add4a2af9ca472c0'
+ 'b7fd9326363ff7cc34a5e5f171d83115'
+ '04c723c31896059a071280e2eb27b275'
+ '09832c1a1b6e5aa0523bd6b329e32f84'
+ '53d100ed20fcb15b6acbbe394a4c58cf'
+ 'd0efaafb434d338545f9027f503c9fa8'
+ '51a3408f929d38f5bcb6da2110fff3d2'
+ 'cd0c13ec5f974417c4a77e3d645d1973'
+ '60814fdc222a908846eeb814de5e0bdb'
+ 'e0323a0a4906245cc2d3ac629195e479'
+ 'e7c8376d8dd54ea96c56f4ea657aae08'
+ 'ba78252e1ca6b4c6e8dd741f4bbd8b8a'
+ '703eb5664803f60e613557b986c11d9e'
+ 'e1f8981169d98e949b1e87e9ce5528df'
+ '8ca1890dbfe6426841992d0fb054bb16'.decode('hex')
+)
+
+# This is the inverse of the above. In other words:
+# aes_inv_sbox[aes_sbox[val]] == val
+
+aes_inv_sbox = array('B',
+ '52096ad53036a538bf40a39e81f3d7fb'
+ '7ce339829b2fff87348e4344c4dee9cb'
+ '547b9432a6c2233dee4c950b42fac34e'
+ '082ea16628d924b2765ba2496d8bd125'
+ '72f8f66486689816d4a45ccc5d65b692'
+ '6c704850fdedb9da5e154657a78d9d84'
+ '90d8ab008cbcd30af7e45805b8b34506'
+ 'd02c1e8fca3f0f02c1afbd0301138a6b'
+ '3a9111414f67dcea97f2cfcef0b4e673'
+ '96ac7422e7ad3585e2f937e81c75df6e'
+ '47f11a711d29c5896fb7620eaa18be1b'
+ 'fc563e4bc6d279209adbc0fe78cd5af4'
+ '1fdda8338807c731b11210592780ec5f'
+ '60517fa919b54a0d2de57a9f93c99cef'
+ 'a0e03b4dae2af5b0c8ebbb3c83539961'
+ '172b047eba77d626e169146355210c7d'.decode('hex')
+)
+
+# The Rcon table is used in AES's key schedule (key expansion)
+# It's a pre-computed table of exponentation of 2 in AES's finite field
+#
+# More information: http://en.wikipedia.org/wiki/Rijndael_key_schedule
+
+aes_Rcon = array('B',
+ '8d01020408102040801b366cd8ab4d9a'
+ '2f5ebc63c697356ad4b37dfaefc59139'
+ '72e4d3bd61c29f254a943366cc831d3a'
+ '74e8cb8d01020408102040801b366cd8'
+ 'ab4d9a2f5ebc63c697356ad4b37dfaef'
+ 'c5913972e4d3bd61c29f254a943366cc'
+ '831d3a74e8cb8d01020408102040801b'
+ '366cd8ab4d9a2f5ebc63c697356ad4b3'
+ '7dfaefc5913972e4d3bd61c29f254a94'
+ '3366cc831d3a74e8cb8d010204081020'
+ '40801b366cd8ab4d9a2f5ebc63c69735'
+ '6ad4b37dfaefc5913972e4d3bd61c29f'
+ '254a943366cc831d3a74e8cb8d010204'
+ '08102040801b366cd8ab4d9a2f5ebc63'
+ 'c697356ad4b37dfaefc5913972e4d3bd'
+ '61c29f254a943366cc831d3a74e8cb'.decode('hex')
+)
diff --git a/gluon/custom_import.py b/gluon/custom_import.py
index ebf683dc..e37708ef 100644
--- a/gluon/custom_import.py
+++ b/gluon/custom_import.py
@@ -313,7 +313,7 @@ class _Web2pyImporter(_BaseImporter):
try:
result = result or new_mod.__dict__[name]
except KeyError, e:
- raise ImportError, ('Cannot import module %s' % str(e),)
+ raise ImportError, 'Cannot import module %s' % str(e)
prefix += "." + name
return result
diff --git a/gluon/dal.py b/gluon/dal.py
index 05aa72df..016d6786 100644
--- a/gluon/dal.py
+++ b/gluon/dal.py
@@ -4080,9 +4080,21 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
if use_common_filters(query):
query = self.common_filter(query,[tablename])
+ if len(self.db[tablename].fields) == len(fields):
+ #getting all fields, not a projection query
+ projection = None
+ else:
+ projection = [f.name for f in fields]
+
+ #tableobj is a GAE Model class (or subclass)
tableobj = self.db[tablename]._tableobj
- items = tableobj.all()
filters = self.expand(query)
+
+ #projection's can't include 'id'. it will be added to the result later
+ query_projection = [p for p in projection if p != 'id'] if projection \
+ else None
+ items = gae.Query(tableobj, projection=query_projection)
+
for filter in filters:
if filter.name=='__key__' and filter.op=='>' and filter.value==0:
continue
@@ -4090,17 +4102,23 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
if filter.value==0:
items = []
elif isinstance(filter.value, Key):
+ #key qeuries return a class instance, can't use projection
+ # extra values will be ignored in post-processing later
item = tableobj.get(filter.value)
items = (item and [item]) or []
else:
+ #key qeuries return a class instance, can't use projection
+ # extra values will be ignored in post-processing later
item = tableobj.get_by_id(filter.value)
items = (item and [item]) or []
elif isinstance(items,list): # i.e. there is a single record!
- items = [i for i in items if filter.apply(getattr(item,filter.name),
- filter.value)]
+ items = [i for i in items if filter.apply(
+ getattr(item,filter.name),filter.value)]
else:
- if filter.name=='__key__': items.order('__key__')
- items = items.filter('%s %s' % (filter.name,filter.op),filter.value)
+ if filter.name=='__key__':
+ items.order('__key__')
+ items = items.filter('%s %s' % (filter.name,filter.op),
+ filter.value)
if not isinstance(items,list):
if attributes.get('left', None):
raise SyntaxError, 'Set: no left join in appengine'
@@ -4120,9 +4138,8 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
if attributes.get('limitby', None):
(lmin, lmax) = attributes['limitby']
(limit, offset) = (lmax - lmin, lmin)
- items = items.fetch(limit, offset=offset)
- fields = self.db[tablename].fields
- return (items, tablename, fields)
+ items = items.fetch(limit,offset=offset)
+ return (items, tablename, projection or self.db[tablename].fields)
def select(self,query,fields,attributes):
(items, tablename, fields) = self.select_raw(query,fields,attributes)
@@ -4134,7 +4151,6 @@ class GoogleDatastoreAdapter(NoSQLAdapter):
processor = attributes.get('processor',self.parse)
return processor(rows,fields,colnames,False)
-
def count(self,query,distinct=None):
if distinct:
raise RuntimeError, "COUNT DISTINCT not supported"
@@ -4569,7 +4585,7 @@ class MongoDBAdapter(NoSQLAdapter):
return expression
def _select(self,query,fields,attributes):
- from pymongo import son
+ from pymongo.son import SON
for key in set(attributes.keys())-set(('limitby','orderby')):
raise SyntaxError, 'invalid select attribute: %s' % key
@@ -4598,7 +4614,7 @@ class MongoDBAdapter(NoSQLAdapter):
else:
limitby_skip = limitby_limit = 0
- mongofields_dict = son.SON()
+ mongofields_dict = SON()
mongoqry_dict = {}
for item in fields:
if isinstance(item,SQLALL):
diff --git a/gluon/globals.py b/gluon/globals.py
index f265c1da..63e22e0d 100644
--- a/gluon/globals.py
+++ b/gluon/globals.py
@@ -36,6 +36,13 @@ import os
import sys
import traceback
import threading
+import hmac
+import base64
+
+try:
+ from Crypto.Cipher import AES
+except ImportError:
+ from contrib import aes as AES
try:
from gluon.contrib.minify import minify
@@ -409,6 +416,7 @@ class Session(Storage):
migrate=True,
separate = None,
check_client=False,
+ cookie_key=None,
):
"""
separate can be separate=lambda(session_name): session_name[-2:]
@@ -421,9 +429,30 @@ class Session(Storage):
if not masterapp:
masterapp = request.application
response.session_id_name = 'session_id_%s' % masterapp.lower()
+
+ # Load session data from cookie
+
+ if cookie_key:
+ response.session_cookie_key = cookie_key
+ response.session_cookie_key2 = hashlib.md5(cookie_key).digest()
+ cookie_name = request.application.lower()+'_session_data'
+ response.session_cookie_name = cookie_name
+ if cookie_data in request.cookies:
+ cookie_value = request.cookies[cookie_name].value
+ cookie_parts = cookie_value.split(":")
+ enc = cookie_parts[2]
+ cipher = AES.new(cookie_key)
+ decrypted = cipher.decrypt(base64.b64decode(enc)).rstrip('{')
+ check = hmac.new(response.session_cookie_key2,enc).hexdigest()
+ if cookie_parts[0] == check:
+ session_data = cPickle.loads(decrypted)
+ self.update(session_data)
+ else:
+ return
if not db:
- if global_settings.db_sessions is True or masterapp in global_settings.db_sessions:
+ if global_settings.db_sessions is True \
+ or masterapp in global_settings.db_sessions:
return
response.session_new = False
client = request.client and request.client.replace(':', '.')
@@ -441,11 +470,13 @@ class Session(Storage):
response.session_file = \
open(response.session_filename, 'rb+')
try:
- portalocker.lock(response.session_file,portalocker.LOCK_EX)
+ portalocker.lock(response.session_file,
+ portalocker.LOCK_EX)
response.session_locked = True
self.update(cPickle.load(response.session_file))
response.session_file.seek(0)
- oc = response.session_filename.split('/')[-1].split('-')[0]
+ oc = response.session_filename.split('/')[-1]\
+ .split('-')[0]
if check_client and client!=oc:
raise Exception, "cookie attack"
finally:
@@ -459,7 +490,8 @@ class Session(Storage):
response.session_id = '%s-%s' % (client, uuid)
if separate:
prefix = separate(response.session_id)
- response.session_id = '%s/%s' % (prefix,response.session_id)
+ response.session_id = '%s/%s' % \
+ (prefix,response.session_id)
response.session_filename = \
os.path.join(up(request.folder), masterapp,
'sessions', response.session_id)
@@ -492,16 +524,25 @@ class Session(Storage):
migrate=table_migrate,
)
try:
+
+ # Get session data out of the database
+
+ # Key comes from the cookie
key = request.cookies[response.session_id_name].value
(record_id, unique_key) = key.split(':')
if record_id == '0':
raise Exception, 'record_id == 0'
+ # Select from database.
rows = db(table.id == record_id).select()
+
+ # Make sure the session data exists in the database
if len(rows) == 0 or rows[0].unique_key != unique_key:
raise Exception, 'No record'
# rows[0].update_record(locked=True)
+
+ # Unpickle the data
session_data = cPickle.loads(rows[0].session_data)
self.update(session_data)
except Exception:
@@ -540,8 +581,18 @@ class Session(Storage):
self._close(response)
self._forget = True
- def _try_store_in_db(self, request, response):
+ def _try_store_in_cookie(self, request, response):
+ pad = lambda s: s + (32 - len(s) % 32) * '{'
+ data = cPickle.dumps(dict(self))
+ cipher = AES.new(response.session_cookie_key)
+ encrypted_data = base64.b64encode(cipher.encrypt(pad(data)))
+ signature = hmac.new(response.session_cookie_key2,encrypted_data)\
+ .hexdigest()
+ value = signature+':'+encrypted_data
+ response.cookies[response.session_cookie_name] = value
+ response.cookies[response.session_cookie_name]['path'] = '/'
+ def _try_store_in_db(self, request, response):
# don't save if file-based sessions, no session id, or session being forgotten
if not response.session_db or not response.session_id or self._forget:
return
@@ -618,4 +669,3 @@ class Session(Storage):
-
diff --git a/gluon/newcron.py b/gluon/newcron.py
index 85392427..cb2f3c8f 100644
--- a/gluon/newcron.py
+++ b/gluon/newcron.py
@@ -224,7 +224,11 @@ class cronlauncher(threading.Thread):
def run(self):
import subprocess
- proc = subprocess.Popen(self.cmd,
+ if isinstance(self.cmd, (list,tuple)):
+ cmd = self.cmd
+ else:
+ cmd = self.cmd.split()
+ proc = subprocess.Popen(cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
diff --git a/gluon/scheduler.py b/gluon/scheduler.py
index 49b94121..205d39c5 100644
--- a/gluon/scheduler.py
+++ b/gluon/scheduler.py
@@ -361,7 +361,7 @@ class Scheduler(MetaScheduler):
Field('enabled','boolean',default=True),
Field('start_time','datetime',default=now),
Field('next_run_time','datetime',default=now),
- Field('stop_time','datetime',default=now+datetime.timedelta(days=1)),
+ Field('stop_time','datetime',default=None),
Field('repeats','integer',default=1,comment="0=unlimted"),
Field('period','integer',default=60,comment='seconds'),
Field('timeout','integer',default=60,comment='seconds'),
@@ -542,7 +542,7 @@ class Scheduler(MetaScheduler):
all_available = db(ts.status.belongs((QUEUED,ASSIGNED)))\
((ts.times_runnow)\
+ ((ts.stop_time==None) | (ts.stop_time>now))\
(ts.next_run_time<=now)\
(ts.enabled==True)\
(ts.group_name.belongs(self.group_names)) #\
diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py
index 47cfd9f0..8268ddb4 100644
--- a/gluon/sqlhtml.py
+++ b/gluon/sqlhtml.py
@@ -1130,23 +1130,6 @@ class SQLFORM(FORM):
hideerror=hideerror,
)
- if not ret and self.record and self.errors:
- ### if there are errors in update mode
- # and some errors refers to an already uploaded file
- # delete error if
- # - user not trying to upload a new file
- # - there is existing file and user is not trying to delete it
- # this is because removing the file may not pass validation
- for key in self.errors.keys():
- if key in self.table \
- and self.table[key].type == 'upload' \
- and request_vars.get(key, None) in (None, '') \
- and self.record[key] \
- and not key + UploadWidget.ID_DELETE_SUFFIX in request_vars:
- del self.errors[key]
- if not self.errors:
- ret = True
-
self.deleted = \
request_vars.get(self.FIELDNAME_REQUEST_DELETE, False)
diff --git a/gluon/tools.py b/gluon/tools.py
index 61923d55..bed6b623 100644
--- a/gluon/tools.py
+++ b/gluon/tools.py
@@ -1513,7 +1513,7 @@ class Auth(object):
*settings.extra_fields.get(settings.table_cas_name,[]),
**dict(
migrate=self.__get_migrate(
- settings.table_event_name, migrate),
+ settings.table_cas_name, migrate),
fake_migrate=fake_migrate))
table.user_id.requires = IS_IN_DB(db, '%s.id' % \
settings.table_user_name,
diff --git a/setup_exe.py b/setup_exe.py
index 5c3df1d1..965efe03 100755
--- a/setup_exe.py
+++ b/setup_exe.py
@@ -110,7 +110,7 @@ if remove_msft_dlls:
os.unlink(os.path.join('dist',f))
except:
print "unable to delete dist/"+f
- sys.exit(1)
+ #sys.exit(1)
#Should we include applications?
diff --git a/web2py.py b/web2py.py
index 69829978..d6d41ec2 100755
--- a/web2py.py
+++ b/web2py.py
@@ -17,6 +17,11 @@ import gluon.widget
# Start Web2py and Web2py cron service!
if __name__ == '__main__':
+ try:
+ from multiprocessing import freeze_support
+ freeze_support()
+ except:
+ sys.stderr.write('Sorry, -K only supported for python 2.6-2.7\n')
gluon.widget.start(cron=True)