1
2
3
4 """
5 This file is part of the web2py Web Framework
6 Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
7 License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
8
9 Contains the classes for the global used variables:
10
11 - Request
12 - Response
13 - Session
14
15 """
16
17 from storage import Storage, List
18 from streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE
19 from xmlrpc import handler
20 from contenttype import contenttype
21 from html import xmlescape, TABLE, TR, PRE, URL
22 from http import HTTP, redirect
23 from fileutils import up
24 from serializers import json, custom_json
25 import settings
26 from utils import web2py_uuid
27 from settings import global_settings
28 import hashlib
29 import portalocker
30 import cPickle
31 import cStringIO
32 import datetime
33 import re
34 import Cookie
35 import os
36 import sys
37 import traceback
38 import threading
39
40 try:
41 from gluon.contrib.minify import minify
42 have_minify = True
43 except ImportError:
44 have_minify = False
45
46 regex_session_id = re.compile('^([\w\-]+/)?[\w\-\.]+$')
47
48 __all__ = ['Request', 'Response', 'Session']
49
50 current = threading.local()
51
52 css_template = '<link href="%s" rel="stylesheet" type="text/css" />'
53 js_template = '<script src="%s" type="text/javascript"></script>'
54 css_inline = '<style type="text/css">\n%s\n</style>'
55 js_inline = '<script type="text/javascript">\n%s\n</script>'
56
58
59 """
60 defines the request object and the default values of its members
61
62 - env: environment variables, by gluon.main.wsgibase()
63 - cookies
64 - get_vars
65 - post_vars
66 - vars
67 - folder
68 - application
69 - function
70 - args
71 - extension
72 - now: datetime.datetime.today()
73 - restful()
74 """
75
94
96 self.uuid = '%s/%s.%s.%s' % (
97 self.application,
98 self.client.replace(':', '_'),
99 self.now.strftime('%Y-%m-%d.%H-%M-%S'),
100 web2py_uuid())
101 return self.uuid
102
112
114 """
115 If request comes in over HTTP, redirect it to HTTPS
116 and secure the session.
117 """
118 if not global_settings.cronjob and not self.is_https:
119 redirect(URL(scheme='https', args=self.args, vars=self.vars))
120
121 current.session.secure()
122
124 def wrapper(action,self=self):
125 def f(_action=action,_self=self,*a,**b):
126 self.is_restful = True
127 method = _self.env.request_method
128 if len(_self.args) and '.' in _self.args[-1]:
129 _self.args[-1],_self.extension = _self.args[-1].rsplit('.',1)
130 current.response.headers['Content-Type'] = \
131 contenttype(_self.extension.lower())
132 if not method in ['GET','POST','DELETE','PUT']:
133 raise HTTP(400,"invalid method")
134 rest_action = _action().get(method,None)
135 if not rest_action:
136 raise HTTP(400,"method not supported")
137 try:
138 return rest_action(*_self.args,**_self.vars)
139 except TypeError, e:
140 exc_type, exc_value, exc_traceback = sys.exc_info()
141 if len(traceback.extract_tb(exc_traceback))==1:
142 raise HTTP(400,"invalid arguments")
143 else:
144 raise e
145 f.__doc__ = action.__doc__
146 f.__name__ = action.__name__
147 return f
148 return wrapper
149
150
152
153 """
154 defines the response object and the default values of its members
155 response.write( ) can be used to write in the output html
156 """
157
159 self.status = 200
160 self.headers = Storage()
161 self.headers['X-Powered-By'] = 'web2py'
162 self.body = cStringIO.StringIO()
163 self.session_id = None
164 self.cookies = Cookie.SimpleCookie()
165 self.postprocessing = []
166 self.flash = ''
167 self.meta = Storage()
168 self.menu = []
169 self.files = []
170 self.generic_patterns = []
171 self.delimiters = ('{{','}}')
172 self._vars = None
173 self._caller = lambda f: f()
174 self._view_environment = None
175 self._custom_commit = None
176 self._custom_rollback = None
177
178 - def write(self, data, escape=True):
183
185 from compileapp import run_view_in
186 if len(a) > 2:
187 raise SyntaxError, 'Response.render can be called with two arguments, at most'
188 elif len(a) == 2:
189 (view, self._vars) = (a[0], a[1])
190 elif len(a) == 1 and isinstance(a[0], str):
191 (view, self._vars) = (a[0], {})
192 elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read):
193 (view, self._vars) = (a[0], {})
194 elif len(a) == 1 and isinstance(a[0], dict):
195 (view, self._vars) = (None, a[0])
196 else:
197 (view, self._vars) = (None, {})
198 self._vars.update(b)
199 self._view_environment.update(self._vars)
200 if view:
201 import cStringIO
202 (obody, oview) = (self.body, self.view)
203 (self.body, self.view) = (cStringIO.StringIO(), view)
204 run_view_in(self._view_environment)
205 page = self.body.getvalue()
206 self.body.close()
207 (self.body, self.view) = (obody, oview)
208 else:
209 run_view_in(self._view_environment)
210 page = self.body.getvalue()
211 return page
212
218
220
221
222 """
223 Caching method for writing out files.
224 By default, caches in ram for 5 minutes. To change,
225 response.cache_includes = (cache_method, time_expire).
226 Example: (cache.disk, 60) # caches to disk for 1 minute.
227 """
228 from gluon import URL
229
230 files = []
231 for item in self.files:
232 if not item in files: files.append(item)
233 if have_minify and (self.optimize_css or self.optimize_js):
234
235 cache = self.cache_includes or (current.cache.ram, 60*5)
236 def call_minify():
237 return minify.minify(files,
238 URL('static','temp'),
239 current.request.folder,
240 self.optimize_css,
241 self.optimize_js)
242 if cache:
243 cache_model, time_expire = cache
244 files = cache_model('response.files.minified',
245 call_minify,
246 time_expire)
247 else:
248 files = call_minify()
249 s = ''
250 for item in files:
251 if isinstance(item,str):
252 f = item.lower()
253 if f.endswith('.css'): s += css_template % item
254 elif f.endswith('.js'): s += js_template % item
255 elif isinstance(item,(list,tuple)):
256 f = item[0]
257 if f=='css:inline': s += css_inline % item[1]
258 elif f=='js:inline': s += js_inline % item[1]
259 self.write(s, escape=False)
260
267 """
268 if a controller function::
269
270 return response.stream(file, 100)
271
272 the file content will be streamed at 100 bytes at the time
273 """
274 if not request:
275 request = current.request
276 if isinstance(stream, (str, unicode)):
277 stream_file_or_304_or_206(stream,
278 chunk_size=chunk_size,
279 request=request,
280 headers=self.headers)
281
282
283
284 if hasattr(stream, 'name'):
285 filename = stream.name
286 else:
287 filename = None
288 keys = [item.lower() for item in self.headers]
289 if filename and not 'content-type' in keys:
290 self.headers['Content-Type'] = contenttype(filename)
291 if filename and not 'content-length' in keys:
292 try:
293 self.headers['Content-Length'] = \
294 os.path.getsize(filename)
295 except OSError:
296 pass
297
298
299 if request.is_https and isinstance(request.env.http_user_agent,str) and \
300 not re.search(r'Opera', request.env.http_user_agent) and \
301 re.search(r'MSIE [5-8][^0-9]', request.env.http_user_agent):
302 self.headers['Pragma'] = 'cache'
303 self.headers['Cache-Control'] = 'private'
304
305 if request and request.env.web2py_use_wsgi_file_wrapper:
306 wrapped = request.env.wsgi_file_wrapper(stream, chunk_size)
307 else:
308 wrapped = streamer(stream, chunk_size=chunk_size)
309 return wrapped
310
312 """
313 example of usage in controller::
314
315 def download():
316 return response.download(request, db)
317
318 downloads from http://..../download/filename
319 """
320
321 import contenttype as c
322 if not request.args:
323 raise HTTP(404)
324 name = request.args[-1]
325 items = re.compile('(?P<table>.*?)\.(?P<field>.*?)\..*')\
326 .match(name)
327 if not items:
328 raise HTTP(404)
329 (t, f) = (items.group('table'), items.group('field'))
330 field = db[t][f]
331 try:
332 (filename, stream) = field.retrieve(name)
333 except IOError:
334 raise HTTP(404)
335 self.headers['Content-Type'] = c.contenttype(name)
336 if attachment:
337 self.headers['Content-Disposition'] = \
338 "attachment; filename=%s" % filename
339 return self.stream(stream, chunk_size = chunk_size, request=request)
340
341 - def json(self, data, default=None):
343
344 - def xmlrpc(self, request, methods):
345 """
346 assuming::
347
348 def add(a, b):
349 return a+b
350
351 if a controller function \"func\"::
352
353 return response.xmlrpc(request, [add])
354
355 the controller will be able to handle xmlrpc requests for
356 the add function. Example::
357
358 import xmlrpclib
359 connection = xmlrpclib.ServerProxy('http://hostname/app/contr/func')
360 print connection.add(3, 4)
361
362 """
363
364 return handler(request, self, methods)
365
391
393
394 """
395 defines the session object and the default values of its members (None)
396 """
397
398 - def connect(
399 self,
400 request,
401 response,
402 db=None,
403 tablename='web2py_session',
404 masterapp=None,
405 migrate=True,
406 separate = None,
407 check_client=False,
408 ):
409 """
410 separate can be separate=lambda(session_name): session_name[-2:]
411 and it is used to determine a session prefix.
412 separate can be True and it is set to session_name[-2:]
413 """
414 if separate == True:
415 separate = lambda session_name: session_name[-2:]
416 self._unlock(response)
417 if not masterapp:
418 masterapp = request.application
419 response.session_id_name = 'session_id_%s' % masterapp.lower()
420
421 if not db:
422 if global_settings.db_sessions is True or masterapp in global_settings.db_sessions:
423 return
424 response.session_new = False
425 client = request.client and request.client.replace(':', '.')
426 if response.session_id_name in request.cookies:
427 response.session_id = \
428 request.cookies[response.session_id_name].value
429 if regex_session_id.match(response.session_id):
430 response.session_filename = \
431 os.path.join(up(request.folder), masterapp,
432 'sessions', response.session_id)
433 else:
434 response.session_id = None
435 if response.session_id:
436 try:
437 response.session_file = \
438 open(response.session_filename, 'rb+')
439 try:
440 portalocker.lock(response.session_file,portalocker.LOCK_EX)
441 response.session_locked = True
442 self.update(cPickle.load(response.session_file))
443 response.session_file.seek(0)
444 oc = response.session_filename.split('/')[-1].split('-')[0]
445 if check_client and client!=oc:
446 raise Exception, "cookie attack"
447 finally:
448 pass
449
450
451 except:
452 response.session_id = None
453 if not response.session_id:
454 uuid = web2py_uuid()
455 response.session_id = '%s-%s' % (client, uuid)
456 if separate:
457 prefix = separate(response.session_id)
458 response.session_id = '%s/%s' % (prefix,response.session_id)
459 response.session_filename = \
460 os.path.join(up(request.folder), masterapp,
461 'sessions', response.session_id)
462 response.session_new = True
463 else:
464 if global_settings.db_sessions is not True:
465 global_settings.db_sessions.add(masterapp)
466 response.session_db = True
467 if response.session_file:
468 self._close(response)
469 if settings.global_settings.web2py_runtime_gae:
470
471 request.tickets_db = db
472 if masterapp == request.application:
473 table_migrate = migrate
474 else:
475 table_migrate = False
476 tname = tablename + '_' + masterapp
477 table = db.get(tname, None)
478 if table is None:
479 table = db.define_table(
480 tname,
481 db.Field('locked', 'boolean', default=False),
482 db.Field('client_ip', length=64),
483 db.Field('created_datetime', 'datetime',
484 default=request.now),
485 db.Field('modified_datetime', 'datetime'),
486 db.Field('unique_key', length=64),
487 db.Field('session_data', 'blob'),
488 migrate=table_migrate,
489 )
490 try:
491 key = request.cookies[response.session_id_name].value
492 (record_id, unique_key) = key.split(':')
493 if record_id == '0':
494 raise Exception, 'record_id == 0'
495 rows = db(table.id == record_id).select()
496 if len(rows) == 0 or rows[0].unique_key != unique_key:
497 raise Exception, 'No record'
498
499
500
501 session_data = cPickle.loads(rows[0].session_data)
502 self.update(session_data)
503 except Exception:
504 record_id = None
505 unique_key = web2py_uuid()
506 session_data = {}
507 response._dbtable_and_field = \
508 (response.session_id_name, table, record_id, unique_key)
509 response.session_id = '%s:%s' % (record_id, unique_key)
510 response.cookies[response.session_id_name] = response.session_id
511 response.cookies[response.session_id_name]['path'] = '/'
512 self.__hash = hashlib.md5(str(self)).digest()
513 if self.flash:
514 (response.flash, self.flash) = (self.flash, None)
515
517 if self._start_timestamp:
518 return False
519 else:
520 self._start_timestamp = datetime.datetime.today()
521 return True
522
524 now = datetime.datetime.today()
525 if not self._last_timestamp or \
526 self._last_timestamp + datetime.timedelta(seconds = seconds) > now:
527 self._last_timestamp = now
528 return False
529 else:
530 return True
531
534
535 - def forget(self, response=None):
538
540
541
542 if not response.session_db or not response.session_id or self._forget:
543 return
544
545
546 __hash = self.__hash
547 if __hash is not None:
548 del self.__hash
549 if __hash == hashlib.md5(str(self)).digest():
550 return
551
552 (record_id_name, table, record_id, unique_key) = \
553 response._dbtable_and_field
554 dd = dict(locked=False, client_ip=request.client.replace(':','.'),
555 modified_datetime=request.now,
556 session_data=cPickle.dumps(dict(self)),
557 unique_key=unique_key)
558 if record_id:
559 table._db(table.id == record_id).update(**dd)
560 else:
561 record_id = table.insert(**dd)
562 response.cookies[response.session_id_name] = '%s:%s'\
563 % (record_id, unique_key)
564 response.cookies[response.session_id_name]['path'] = '/'
565
597
605
614