1 | # -*- coding: utf-8 -*- |
---|
2 | |
---|
3 | """ |
---|
4 | | This file is part of the web2py Web Framework |
---|
5 | | Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> |
---|
6 | | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) |
---|
7 | |
---|
8 | Contains the classes for the global used variables: |
---|
9 | |
---|
10 | - Request |
---|
11 | - Response |
---|
12 | - Session |
---|
13 | |
---|
14 | """ |
---|
15 | from gluon._compat import pickle, StringIO, copyreg, Cookie, urlparse, PY2, iteritems, to_unicode, to_native, \ |
---|
16 | to_bytes, unicodeT, long, hashlib_md5, urllib_quote, to_native |
---|
17 | from gluon.storage import Storage, List |
---|
18 | from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE |
---|
19 | from gluon.contenttype import contenttype |
---|
20 | from gluon.html import xmlescape, TABLE, TR, PRE, URL |
---|
21 | from gluon.http import HTTP, redirect |
---|
22 | from gluon.fileutils import up |
---|
23 | from gluon.serializers import json, custom_json |
---|
24 | import gluon.settings as settings |
---|
25 | from gluon.utils import web2py_uuid, secure_dumps, secure_loads |
---|
26 | from gluon.settings import global_settings |
---|
27 | from gluon import recfile |
---|
28 | from gluon.cache import CacheInRam |
---|
29 | import hashlib |
---|
30 | from pydal.contrib import portalocker |
---|
31 | from pickle import Pickler, MARK, DICT, EMPTY_DICT |
---|
32 | # from types import DictionaryType |
---|
33 | import datetime |
---|
34 | import re |
---|
35 | import os |
---|
36 | import sys |
---|
37 | import traceback |
---|
38 | import threading |
---|
39 | import cgi |
---|
40 | import copy |
---|
41 | import tempfile |
---|
42 | import json as json_parser |
---|
43 | |
---|
44 | |
---|
45 | FMT = '%a, %d-%b-%Y %H:%M:%S PST' |
---|
46 | PAST = 'Sat, 1-Jan-1971 00:00:00' |
---|
47 | FUTURE = 'Tue, 1-Dec-2999 23:59:59' |
---|
48 | |
---|
49 | try: |
---|
50 | # FIXME PY3 |
---|
51 | from gluon.contrib.minify import minify |
---|
52 | have_minify = True |
---|
53 | except ImportError: |
---|
54 | have_minify = False |
---|
55 | |
---|
56 | |
---|
57 | __all__ = ['Request', 'Response', 'Session'] |
---|
58 | |
---|
59 | current = threading.local() # thread-local storage for request-scope globals |
---|
60 | |
---|
61 | css_template = '<link href="%s" rel="stylesheet" type="text/css" />' |
---|
62 | js_template = '<script src="%s" type="text/javascript"></script>' |
---|
63 | coffee_template = '<script src="%s" type="text/coffee"></script>' |
---|
64 | typescript_template = '<script src="%s" type="text/typescript"></script>' |
---|
65 | less_template = '<link href="%s" rel="stylesheet/less" type="text/css" />' |
---|
66 | css_inline = '<style type="text/css">\n%s\n</style>' |
---|
67 | js_inline = '<script type="text/javascript">\n%s\n</script>' |
---|
68 | |
---|
69 | template_mapping = { |
---|
70 | 'css': css_template, |
---|
71 | 'js': js_template, |
---|
72 | 'coffee': coffee_template, |
---|
73 | 'ts': typescript_template, |
---|
74 | 'less': less_template, |
---|
75 | 'css:inline': css_inline, |
---|
76 | 'js:inline': js_inline |
---|
77 | } |
---|
78 | |
---|
79 | |
---|
80 | # IMPORTANT: |
---|
81 | # this is required so that pickled dict(s) and class.__dict__ |
---|
82 | # are sorted and web2py can detect without ambiguity when a session changes |
---|
83 | class SortingPickler(Pickler): |
---|
84 | def save_dict(self, obj): |
---|
85 | self.write(EMPTY_DICT if self.bin else MARK + DICT) |
---|
86 | self.memoize(obj) |
---|
87 | self._batch_setitems([(key, obj[key]) for key in sorted(obj)]) |
---|
88 | |
---|
89 | if PY2: |
---|
90 | SortingPickler.dispatch = copy.copy(Pickler.dispatch) |
---|
91 | SortingPickler.dispatch[dict] = SortingPickler.save_dict |
---|
92 | else: |
---|
93 | SortingPickler.dispatch_table = copyreg.dispatch_table.copy() |
---|
94 | SortingPickler.dispatch_table[dict] = SortingPickler.save_dict |
---|
95 | |
---|
96 | |
---|
97 | def sorting_dumps(obj, protocol=None): |
---|
98 | file = StringIO() |
---|
99 | SortingPickler(file, protocol).dump(obj) |
---|
100 | return file.getvalue() |
---|
101 | # END ##################################################################### |
---|
102 | |
---|
103 | |
---|
104 | def copystream(src, dest, size, chunk_size, cache_inc=None): |
---|
105 | while size > 0: |
---|
106 | if size < chunk_size: |
---|
107 | data = src.read(size) |
---|
108 | callable(cache_inc) and cache_inc(size) |
---|
109 | else: |
---|
110 | data = src.read(chunk_size) |
---|
111 | callable(cache_inc) and cache_inc(chunk_size) |
---|
112 | length = len(data) |
---|
113 | if length > size: |
---|
114 | (data, length) = (data[:size], size) |
---|
115 | size -= length |
---|
116 | if length == 0: |
---|
117 | break |
---|
118 | dest.write(data) |
---|
119 | if length < chunk_size: |
---|
120 | break |
---|
121 | dest.seek(0) |
---|
122 | return |
---|
123 | |
---|
124 | def copystream_progress(request, chunk_size=10 ** 5): |
---|
125 | """ |
---|
126 | Copies request.env.wsgi_input into request.body |
---|
127 | and stores progress upload status in cache_ram |
---|
128 | X-Progress-ID:length and X-Progress-ID:uploaded |
---|
129 | """ |
---|
130 | env = request.env |
---|
131 | if not env.get('CONTENT_LENGTH', None): |
---|
132 | return StringIO() |
---|
133 | source = env['wsgi.input'] |
---|
134 | try: |
---|
135 | size = int(env['CONTENT_LENGTH']) |
---|
136 | except ValueError: |
---|
137 | raise HTTP(400, "Invalid Content-Length header") |
---|
138 | try: # Android requires this |
---|
139 | dest = tempfile.NamedTemporaryFile() |
---|
140 | except NotImplementedError: # and GAE this |
---|
141 | dest = tempfile.TemporaryFile() |
---|
142 | if 'X-Progress-ID' not in request.get_vars: |
---|
143 | copystream(source, dest, size, chunk_size) |
---|
144 | return dest |
---|
145 | cache_key = 'X-Progress-ID:' + request.get_vars['X-Progress-ID'] |
---|
146 | cache_ram = CacheInRam(request) # same as cache.ram because meta_storage |
---|
147 | cache_ram(cache_key + ':length', lambda: size, 0) |
---|
148 | cache_ram(cache_key + ':uploaded', lambda: 0, 0) |
---|
149 | copystream(source, dest, size, chunk_size, |
---|
150 | lambda v : cache_ram.increment(cache_key + ':uploaded', v)) |
---|
151 | cache_ram(cache_key + ':length', None) |
---|
152 | cache_ram(cache_key + ':uploaded', None) |
---|
153 | return dest |
---|
154 | |
---|
155 | |
---|
156 | class Request(Storage): |
---|
157 | |
---|
158 | """ |
---|
159 | Defines the request object and the default values of its members |
---|
160 | |
---|
161 | - env: environment variables, by gluon.main.wsgibase() |
---|
162 | - cookies |
---|
163 | - get_vars |
---|
164 | - post_vars |
---|
165 | - vars |
---|
166 | - folder |
---|
167 | - application |
---|
168 | - function |
---|
169 | - method |
---|
170 | - args |
---|
171 | - extension |
---|
172 | - now: datetime.datetime.now() |
---|
173 | - utcnow : datetime.datetime.utcnow() |
---|
174 | - is_local |
---|
175 | - is_https |
---|
176 | - restful() |
---|
177 | """ |
---|
178 | |
---|
179 | def __init__(self, env): |
---|
180 | Storage.__init__(self) |
---|
181 | self.env = Storage(env) |
---|
182 | self.env.web2py_path = global_settings.applications_parent |
---|
183 | self.env.update(global_settings) |
---|
184 | self.cookies = Cookie.SimpleCookie() |
---|
185 | self.method = self.env.get('REQUEST_METHOD') |
---|
186 | self._get_vars = None |
---|
187 | self._post_vars = None |
---|
188 | self._vars = None |
---|
189 | self._body = None |
---|
190 | self.folder = None |
---|
191 | self.application = None |
---|
192 | self.function = None |
---|
193 | self.args = List() |
---|
194 | self.extension = 'html' |
---|
195 | self.now = datetime.datetime.now() |
---|
196 | self.utcnow = datetime.datetime.utcnow() |
---|
197 | self.is_restful = False |
---|
198 | self.is_https = False |
---|
199 | self.is_local = False |
---|
200 | self.global_settings = settings.global_settings |
---|
201 | self._uuid = None |
---|
202 | |
---|
203 | def parse_get_vars(self): |
---|
204 | """Takes the QUERY_STRING and unpacks it to get_vars |
---|
205 | """ |
---|
206 | query_string = self.env.get('query_string', '') |
---|
207 | dget = urlparse.parse_qs(query_string, keep_blank_values=1) |
---|
208 | # Ref: https://docs.python.org/2/library/cgi.html#cgi.parse_qs |
---|
209 | get_vars = self._get_vars = Storage(dget) |
---|
210 | for (key, value) in iteritems(get_vars): |
---|
211 | if isinstance(value, list) and len(value) == 1: |
---|
212 | get_vars[key] = value[0] |
---|
213 | |
---|
214 | def parse_post_vars(self): |
---|
215 | """Takes the body of the request and unpacks it into |
---|
216 | post_vars. application/json is also automatically parsed |
---|
217 | """ |
---|
218 | env = self.env |
---|
219 | post_vars = self._post_vars = Storage() |
---|
220 | body = self.body |
---|
221 | # if content-type is application/json, we must read the body |
---|
222 | is_json = env.get('content_type', '')[:16] == 'application/json' |
---|
223 | |
---|
224 | if is_json: |
---|
225 | try: |
---|
226 | # In Python 3 versions prior to 3.6 load doesn't accept bytes and |
---|
227 | # bytearray, so we read the body convert to native and use loads |
---|
228 | # instead of load. |
---|
229 | # This line can be simplified to json_vars = json_parser.load(body) |
---|
230 | # if and when we drop support for python versions under 3.6 |
---|
231 | json_vars = json_parser.loads(to_native(body.read())) |
---|
232 | except: |
---|
233 | # incoherent request bodies can still be parsed "ad-hoc" |
---|
234 | json_vars = {} |
---|
235 | pass |
---|
236 | # update vars and get_vars with what was posted as json |
---|
237 | if isinstance(json_vars, dict): |
---|
238 | post_vars.update(json_vars) |
---|
239 | |
---|
240 | body.seek(0) |
---|
241 | |
---|
242 | # parse POST variables on POST, PUT, BOTH only in post_vars |
---|
243 | if body and not is_json and env.request_method in ('POST', 'PUT', 'DELETE', 'BOTH'): |
---|
244 | query_string = env.pop('QUERY_STRING', None) |
---|
245 | dpost = cgi.FieldStorage(fp=body, environ=env, keep_blank_values=1) |
---|
246 | try: |
---|
247 | post_vars.update(dpost) |
---|
248 | except: |
---|
249 | pass |
---|
250 | if query_string is not None: |
---|
251 | env['QUERY_STRING'] = query_string |
---|
252 | # The same detection used by FieldStorage to detect multipart POSTs |
---|
253 | body.seek(0) |
---|
254 | |
---|
255 | def listify(a): |
---|
256 | return (not isinstance(a, list) and [a]) or a |
---|
257 | try: |
---|
258 | keys = sorted(dpost) |
---|
259 | except TypeError: |
---|
260 | keys = [] |
---|
261 | for key in keys: |
---|
262 | if key is None: |
---|
263 | continue # not sure why cgi.FieldStorage returns None key |
---|
264 | dpk = dpost[key] |
---|
265 | # if an element is not a file replace it with |
---|
266 | # its value else leave it alone |
---|
267 | |
---|
268 | pvalue = listify([(_dpk if _dpk.filename else _dpk.value) |
---|
269 | for _dpk in dpk] |
---|
270 | if isinstance(dpk, list) else |
---|
271 | (dpk if dpk.filename else dpk.value)) |
---|
272 | if len(pvalue): |
---|
273 | post_vars[key] = (len(pvalue) > 1 and pvalue) or pvalue[0] |
---|
274 | |
---|
275 | @property |
---|
276 | def body(self): |
---|
277 | if self._body is None: |
---|
278 | try: |
---|
279 | self._body = copystream_progress(self) |
---|
280 | except IOError: |
---|
281 | raise HTTP(400, "Bad Request - HTTP body is incomplete") |
---|
282 | return self._body |
---|
283 | |
---|
284 | def parse_all_vars(self): |
---|
285 | """Merges get_vars and post_vars to vars |
---|
286 | """ |
---|
287 | self._vars = copy.copy(self.get_vars) |
---|
288 | for key, value in iteritems(self.post_vars): |
---|
289 | if key not in self._vars: |
---|
290 | self._vars[key] = value |
---|
291 | else: |
---|
292 | if not isinstance(self._vars[key], list): |
---|
293 | self._vars[key] = [self._vars[key]] |
---|
294 | self._vars[key] += value if isinstance(value, list) else [value] |
---|
295 | |
---|
296 | @property |
---|
297 | def get_vars(self): |
---|
298 | """Lazily parses the query string into get_vars |
---|
299 | """ |
---|
300 | if self._get_vars is None: |
---|
301 | self.parse_get_vars() |
---|
302 | return self._get_vars |
---|
303 | |
---|
304 | @property |
---|
305 | def post_vars(self): |
---|
306 | """Lazily parse the body into post_vars |
---|
307 | """ |
---|
308 | if self._post_vars is None: |
---|
309 | self.parse_post_vars() |
---|
310 | return self._post_vars |
---|
311 | |
---|
312 | @property |
---|
313 | def vars(self): |
---|
314 | """Lazily parses all get_vars and post_vars to fill vars |
---|
315 | """ |
---|
316 | if self._vars is None: |
---|
317 | self.parse_all_vars() |
---|
318 | return self._vars |
---|
319 | |
---|
320 | @property |
---|
321 | def uuid(self): |
---|
322 | """Lazily uuid |
---|
323 | """ |
---|
324 | if self._uuid is None: |
---|
325 | self.compute_uuid() |
---|
326 | return self._uuid |
---|
327 | |
---|
328 | def compute_uuid(self): |
---|
329 | self._uuid = '%s/%s.%s.%s' % ( |
---|
330 | self.application, |
---|
331 | self.client.replace(':', '_'), |
---|
332 | self.now.strftime('%Y-%m-%d.%H-%M-%S'), |
---|
333 | web2py_uuid()) |
---|
334 | return self._uuid |
---|
335 | |
---|
336 | def user_agent(self): |
---|
337 | from gluon.contrib import user_agent_parser |
---|
338 | session = current.session |
---|
339 | user_agent = session._user_agent |
---|
340 | if user_agent: |
---|
341 | return user_agent |
---|
342 | http_user_agent = self.env.http_user_agent or '' |
---|
343 | user_agent = user_agent_parser.detect(http_user_agent) |
---|
344 | for key, value in user_agent.items(): |
---|
345 | if isinstance(value, dict): |
---|
346 | user_agent[key] = Storage(value) |
---|
347 | user_agent = Storage(user_agent) |
---|
348 | user_agent.is_mobile = 'Mobile' in http_user_agent |
---|
349 | user_agent.is_tablet = 'Tablet' in http_user_agent |
---|
350 | session._user_agent = user_agent |
---|
351 | |
---|
352 | return user_agent |
---|
353 | |
---|
354 | def requires_https(self): |
---|
355 | """ |
---|
356 | If request comes in over HTTP, redirects it to HTTPS |
---|
357 | and secures the session. |
---|
358 | """ |
---|
359 | cmd_opts = global_settings.cmd_options |
---|
360 | # checking if this is called within the scheduler or within the shell |
---|
361 | # in addition to checking if it's a cron job |
---|
362 | if (self.is_https or self.is_scheduler or cmd_opts and ( |
---|
363 | cmd_opts.shell or cmd_opts.cron_job)): |
---|
364 | current.session.secure() |
---|
365 | else: |
---|
366 | current.session.forget() |
---|
367 | redirect(URL(scheme='https', args=self.args, vars=self.vars)) |
---|
368 | |
---|
369 | def restful(self, ignore_extension=False): |
---|
370 | def wrapper(action, request=self): |
---|
371 | def f(_action=action, *a, **b): |
---|
372 | request.is_restful = True |
---|
373 | env = request.env |
---|
374 | is_json = env.content_type == 'application/json' |
---|
375 | method = env.request_method |
---|
376 | if not ignore_extension and len(request.args) and '.' in request.args[-1]: |
---|
377 | request.args[-1], _, request.extension = request.args[-1].rpartition('.') |
---|
378 | current.response.headers['Content-Type'] = \ |
---|
379 | contenttype('.' + request.extension.lower()) |
---|
380 | rest_action = _action().get(method, None) |
---|
381 | if not (rest_action and method == method.upper() |
---|
382 | and callable(rest_action)): |
---|
383 | raise HTTP(405, "method not allowed") |
---|
384 | try: |
---|
385 | res = rest_action(*request.args, **request.vars) |
---|
386 | if is_json and not isinstance(res, str): |
---|
387 | res = json(res) |
---|
388 | return res |
---|
389 | except TypeError as e: |
---|
390 | exc_type, exc_value, exc_traceback = sys.exc_info() |
---|
391 | if len(traceback.extract_tb(exc_traceback)) == 1: |
---|
392 | raise HTTP(400, "invalid arguments") |
---|
393 | else: |
---|
394 | raise |
---|
395 | f.__doc__ = action.__doc__ |
---|
396 | f.__name__ = action.__name__ |
---|
397 | return f |
---|
398 | return wrapper |
---|
399 | |
---|
400 | |
---|
401 | class Response(Storage): |
---|
402 | |
---|
403 | """ |
---|
404 | Defines the response object and the default values of its members |
---|
405 | response.write( ) can be used to write in the output html |
---|
406 | """ |
---|
407 | |
---|
408 | def __init__(self): |
---|
409 | Storage.__init__(self) |
---|
410 | self.status = 200 |
---|
411 | self.headers = dict() |
---|
412 | self.headers['X-Powered-By'] = 'web2py' |
---|
413 | self.body = StringIO() |
---|
414 | self.session_id = None |
---|
415 | self.cookies = Cookie.SimpleCookie() |
---|
416 | self.postprocessing = [] |
---|
417 | self.flash = '' # used by the default view layout |
---|
418 | self.meta = Storage() # used by web2py_ajax.html |
---|
419 | self.menu = [] # used by the default view layout |
---|
420 | self.files = [] # used by web2py_ajax.html |
---|
421 | self._vars = None |
---|
422 | self._caller = lambda f: f() |
---|
423 | self._view_environment = None |
---|
424 | self._custom_commit = None |
---|
425 | self._custom_rollback = None |
---|
426 | self.generic_patterns = ['*'] |
---|
427 | self.delimiters = ('{{', '}}') |
---|
428 | self.formstyle = 'table3cols' |
---|
429 | self.form_label_separator = ': ' |
---|
430 | |
---|
431 | def write(self, data, escape=True): |
---|
432 | if not escape: |
---|
433 | self.body.write(str(data)) |
---|
434 | else: |
---|
435 | self.body.write(to_native(xmlescape(data))) |
---|
436 | |
---|
437 | def render(self, *a, **b): |
---|
438 | from gluon.compileapp import run_view_in |
---|
439 | if len(a) > 2: |
---|
440 | raise SyntaxError( |
---|
441 | 'Response.render can be called with two arguments, at most') |
---|
442 | elif len(a) == 2: |
---|
443 | (view, self._vars) = (a[0], a[1]) |
---|
444 | elif len(a) == 1 and isinstance(a[0], str): |
---|
445 | (view, self._vars) = (a[0], {}) |
---|
446 | elif len(a) == 1 and hasattr(a[0], 'read') and callable(a[0].read): |
---|
447 | (view, self._vars) = (a[0], {}) |
---|
448 | elif len(a) == 1 and isinstance(a[0], dict): |
---|
449 | (view, self._vars) = (None, a[0]) |
---|
450 | else: |
---|
451 | (view, self._vars) = (None, {}) |
---|
452 | self._vars.update(b) |
---|
453 | self._view_environment.update(self._vars) |
---|
454 | if view: |
---|
455 | from gluon._compat import StringIO |
---|
456 | (obody, oview) = (self.body, self.view) |
---|
457 | (self.body, self.view) = (StringIO(), view) |
---|
458 | page = run_view_in(self._view_environment) |
---|
459 | self.body.close() |
---|
460 | (self.body, self.view) = (obody, oview) |
---|
461 | else: |
---|
462 | page = run_view_in(self._view_environment) |
---|
463 | return page |
---|
464 | |
---|
465 | def include_meta(self): |
---|
466 | s = "\n" |
---|
467 | for meta in iteritems((self.meta or {})): |
---|
468 | k, v = meta |
---|
469 | if isinstance(v, dict): |
---|
470 | s += '<meta' + ''.join(' %s="%s"' % (to_native(xmlescape(key)), |
---|
471 | to_native(xmlescape(v[key]))) for key in v) + ' />\n' |
---|
472 | else: |
---|
473 | s += '<meta name="%s" content="%s" />\n' % (k, to_native(xmlescape(v))) |
---|
474 | self.write(s, escape=False) |
---|
475 | |
---|
476 | def include_files(self, extensions=None): |
---|
477 | """ |
---|
478 | Includes files (usually in the head). |
---|
479 | Can minify and cache local files |
---|
480 | By default, caches in ram for 5 minutes. To change, |
---|
481 | response.cache_includes = (cache_method, time_expire). |
---|
482 | Example: (cache.disk, 60) # caches to disk for 1 minute. |
---|
483 | """ |
---|
484 | app = current.request.application |
---|
485 | |
---|
486 | # We start by building a files list in which adjacent files internal to |
---|
487 | # the application are placed in a list inside the files list. |
---|
488 | # |
---|
489 | # We will only minify and concat adjacent internal files as there's |
---|
490 | # no way to know if changing the order with which the files are apppended |
---|
491 | # will break things since the order matters in both CSS and JS and |
---|
492 | # internal files may be interleaved with external ones. |
---|
493 | files = [] |
---|
494 | # For the adjacent list we're going to use storage List to both distinguish |
---|
495 | # from the regular list and so we can add attributes |
---|
496 | internal = List() |
---|
497 | internal.has_js = False |
---|
498 | internal.has_css = False |
---|
499 | done = set() # to remove duplicates |
---|
500 | for item in self.files: |
---|
501 | if not isinstance(item, list): |
---|
502 | if item in done: |
---|
503 | continue |
---|
504 | done.add(item) |
---|
505 | if isinstance(item, (list, tuple)) or not item.startswith('/' + app): # also consider items in other web2py applications to be external |
---|
506 | if internal: |
---|
507 | files.append(internal) |
---|
508 | internal = List() |
---|
509 | internal.has_js = False |
---|
510 | internal.has_css = False |
---|
511 | files.append(item) |
---|
512 | continue |
---|
513 | if extensions and not item.rpartition('.')[2] in extensions: |
---|
514 | continue |
---|
515 | internal.append(item) |
---|
516 | if item.endswith('.js'): |
---|
517 | internal.has_js = True |
---|
518 | if item.endswith('.css'): |
---|
519 | internal.has_css = True |
---|
520 | if internal: |
---|
521 | files.append(internal) |
---|
522 | |
---|
523 | # We're done we can now minify |
---|
524 | if have_minify: |
---|
525 | for i, f in enumerate(files): |
---|
526 | if isinstance(f, List) and ((self.optimize_css and f.has_css) or (self.optimize_js and f.has_js)): |
---|
527 | # cache for 5 minutes by default |
---|
528 | key = hashlib_md5(repr(f)).hexdigest() |
---|
529 | cache = self.cache_includes or (current.cache.ram, 60 * 5) |
---|
530 | def call_minify(files=f): |
---|
531 | return List(minify.minify(files, |
---|
532 | URL('static', 'temp'), |
---|
533 | current.request.folder, |
---|
534 | self.optimize_css, |
---|
535 | self.optimize_js)) |
---|
536 | if cache: |
---|
537 | cache_model, time_expire = cache |
---|
538 | files[i] = cache_model('response.files.minified/' + key, |
---|
539 | call_minify, |
---|
540 | time_expire) |
---|
541 | else: |
---|
542 | files[i] = call_minify() |
---|
543 | |
---|
544 | def static_map(s, item): |
---|
545 | if isinstance(item, str): |
---|
546 | f = item.lower().split('?')[0] |
---|
547 | ext = f.rpartition('.')[2] |
---|
548 | # if static_version we need also to check for |
---|
549 | # static_version_urls. In that case, the _.x.x.x |
---|
550 | # bit would have already been added by the URL() |
---|
551 | # function |
---|
552 | if self.static_version and not self.static_version_urls: |
---|
553 | item = item.replace( |
---|
554 | '/static/', '/static/_%s/' % self.static_version, 1) |
---|
555 | tmpl = template_mapping.get(ext) |
---|
556 | if tmpl: |
---|
557 | s.append(tmpl % item) |
---|
558 | elif isinstance(item, (list, tuple)): |
---|
559 | f = item[0] |
---|
560 | tmpl = template_mapping.get(f) |
---|
561 | if tmpl: |
---|
562 | s.append(tmpl % item[1]) |
---|
563 | |
---|
564 | s = [] |
---|
565 | for item in files: |
---|
566 | if isinstance(item, List): |
---|
567 | for f in item: |
---|
568 | static_map(s, f) |
---|
569 | else: |
---|
570 | static_map(s, item) |
---|
571 | self.write(''.join(s), escape=False) |
---|
572 | |
---|
573 | def stream(self, |
---|
574 | stream, |
---|
575 | chunk_size=DEFAULT_CHUNK_SIZE, |
---|
576 | request=None, |
---|
577 | attachment=False, |
---|
578 | filename=None |
---|
579 | ): |
---|
580 | """ |
---|
581 | If in a controller function:: |
---|
582 | |
---|
583 | return response.stream(file, 100) |
---|
584 | |
---|
585 | the file content will be streamed at 100 bytes at the time |
---|
586 | |
---|
587 | Args: |
---|
588 | stream: filename or read()able content |
---|
589 | chunk_size(int): Buffer size |
---|
590 | request: the request object |
---|
591 | attachment(bool): prepares the correct headers to download the file |
---|
592 | as an attachment. Usually creates a pop-up download window |
---|
593 | on browsers |
---|
594 | filename(str): the name for the attachment |
---|
595 | |
---|
596 | Note: |
---|
597 | for using the stream name (filename) with attachments |
---|
598 | the option must be explicitly set as function parameter (will |
---|
599 | default to the last request argument otherwise) |
---|
600 | """ |
---|
601 | |
---|
602 | headers = self.headers |
---|
603 | # for attachment settings and backward compatibility |
---|
604 | keys = [item.lower() for item in headers] |
---|
605 | if attachment: |
---|
606 | # FIXME: should be done like in next download method |
---|
607 | if filename is None: |
---|
608 | attname = "" |
---|
609 | else: |
---|
610 | attname = filename |
---|
611 | headers["Content-Disposition"] = \ |
---|
612 | 'attachment; filename="%s"' % attname |
---|
613 | |
---|
614 | if not request: |
---|
615 | request = current.request |
---|
616 | if isinstance(stream, (str, unicodeT)): |
---|
617 | stream_file_or_304_or_206(stream, |
---|
618 | chunk_size=chunk_size, |
---|
619 | request=request, |
---|
620 | headers=headers, |
---|
621 | status=self.status) |
---|
622 | |
---|
623 | # ## the following is for backward compatibility |
---|
624 | if hasattr(stream, 'name'): |
---|
625 | filename = stream.name |
---|
626 | |
---|
627 | if filename and 'content-type' not in keys: |
---|
628 | headers['Content-Type'] = contenttype(filename) |
---|
629 | if filename and 'content-length' not in keys: |
---|
630 | try: |
---|
631 | headers['Content-Length'] = \ |
---|
632 | os.path.getsize(filename) |
---|
633 | except OSError: |
---|
634 | pass |
---|
635 | |
---|
636 | env = request.env |
---|
637 | # Internet Explorer < 9.0 will not allow downloads over SSL unless caching is enabled |
---|
638 | if request.is_https and isinstance(env.http_user_agent, str) and \ |
---|
639 | not re.search(r'Opera', env.http_user_agent) and \ |
---|
640 | re.search(r'MSIE [5-8][^0-9]', env.http_user_agent): |
---|
641 | headers['Pragma'] = 'cache' |
---|
642 | headers['Cache-Control'] = 'private' |
---|
643 | |
---|
644 | if request and env.web2py_use_wsgi_file_wrapper: |
---|
645 | wrapped = env.wsgi_file_wrapper(stream, chunk_size) |
---|
646 | else: |
---|
647 | wrapped = streamer(stream, chunk_size=chunk_size) |
---|
648 | return wrapped |
---|
649 | |
---|
650 | def download(self, request, db, chunk_size=DEFAULT_CHUNK_SIZE, attachment=True, download_filename=None): |
---|
651 | """ |
---|
652 | Example of usage in controller:: |
---|
653 | |
---|
654 | def download(): |
---|
655 | return response.download(request, db) |
---|
656 | |
---|
657 | Downloads from http://..../download/filename |
---|
658 | """ |
---|
659 | from pydal.helpers.regex import REGEX_UPLOAD_PATTERN |
---|
660 | from pydal.exceptions import NotAuthorizedException, NotFoundException |
---|
661 | |
---|
662 | current.session.forget(current.response) |
---|
663 | |
---|
664 | if not request.args: |
---|
665 | raise HTTP(404) |
---|
666 | name = request.args[-1] |
---|
667 | items = re.match(REGEX_UPLOAD_PATTERN, name) |
---|
668 | if not items: |
---|
669 | raise HTTP(404) |
---|
670 | t = items.group('table'); f = items.group('field') |
---|
671 | try: |
---|
672 | field = db[t][f] |
---|
673 | except (AttributeError, KeyError): |
---|
674 | raise HTTP(404) |
---|
675 | try: |
---|
676 | (filename, stream) = field.retrieve(name, nameonly=True) |
---|
677 | except NotAuthorizedException: |
---|
678 | raise HTTP(403) |
---|
679 | except NotFoundException: |
---|
680 | raise HTTP(404) |
---|
681 | except IOError: |
---|
682 | raise HTTP(404) |
---|
683 | headers = self.headers |
---|
684 | headers['Content-Type'] = contenttype(name) |
---|
685 | if download_filename is None: |
---|
686 | download_filename = filename |
---|
687 | if attachment: |
---|
688 | # Browsers still don't have a simple uniform way to have non ascii |
---|
689 | # characters in the filename so for now we are percent encoding it |
---|
690 | if isinstance(download_filename, unicodeT): |
---|
691 | download_filename = download_filename.encode('utf-8') |
---|
692 | download_filename = urllib_quote(download_filename) |
---|
693 | headers['Content-Disposition'] = \ |
---|
694 | 'attachment; filename="%s"' % download_filename.replace('"', '\\"') |
---|
695 | return self.stream(stream, chunk_size=chunk_size, request=request) |
---|
696 | |
---|
697 | def json(self, data, default=None, indent=None): |
---|
698 | if 'Content-Type' not in self.headers: |
---|
699 | self.headers['Content-Type'] = 'application/json' |
---|
700 | return json(data, default=default or custom_json, indent=indent) |
---|
701 | |
---|
702 | def xmlrpc(self, request, methods): |
---|
703 | from gluon.xmlrpc import handler |
---|
704 | """ |
---|
705 | assuming:: |
---|
706 | |
---|
707 | def add(a, b): |
---|
708 | return a+b |
---|
709 | |
---|
710 | if a controller function \"func\":: |
---|
711 | |
---|
712 | return response.xmlrpc(request, [add]) |
---|
713 | |
---|
714 | the controller will be able to handle xmlrpc requests for |
---|
715 | the add function. Example:: |
---|
716 | |
---|
717 | import xmlrpclib |
---|
718 | connection = xmlrpclib.ServerProxy( |
---|
719 | 'http://hostname/app/contr/func') |
---|
720 | print(connection.add(3, 4)) |
---|
721 | |
---|
722 | """ |
---|
723 | |
---|
724 | return handler(request, self, methods) |
---|
725 | |
---|
726 | def toolbar(self): |
---|
727 | from gluon.html import DIV, SCRIPT, BEAUTIFY, TAG, A |
---|
728 | BUTTON = TAG.button |
---|
729 | admin = URL("admin", "default", "design", extension='html', |
---|
730 | args=current.request.application) |
---|
731 | from gluon.dal import DAL |
---|
732 | dbstats = [] |
---|
733 | dbtables = {} |
---|
734 | infos = DAL.get_instances() |
---|
735 | for k, v in iteritems(infos): |
---|
736 | dbstats.append(TABLE(*[TR(PRE(row[0]), '%.2fms' % (row[1]*1000)) |
---|
737 | for row in v['dbstats']])) |
---|
738 | dbtables[k] = dict(defined=v['dbtables']['defined'] or '[no defined tables]', |
---|
739 | lazy=v['dbtables']['lazy'] or '[no lazy tables]') |
---|
740 | u = web2py_uuid() |
---|
741 | backtotop = A('Back to top', _href="#totop-%s" % u) |
---|
742 | # Convert lazy request.vars from property to Storage so they |
---|
743 | # will be displayed in the toolbar. |
---|
744 | request = copy.copy(current.request) |
---|
745 | request.update(vars=current.request.vars, |
---|
746 | get_vars=current.request.get_vars, |
---|
747 | post_vars=current.request.post_vars) |
---|
748 | return DIV( |
---|
749 | BUTTON('design', _onclick="document.location='%s'" % admin), |
---|
750 | BUTTON('request', |
---|
751 | _onclick="jQuery('#request-%s').slideToggle()" % u), |
---|
752 | BUTTON('response', |
---|
753 | _onclick="jQuery('#response-%s').slideToggle()" % u), |
---|
754 | BUTTON('session', |
---|
755 | _onclick="jQuery('#session-%s').slideToggle()" % u), |
---|
756 | BUTTON('db tables', |
---|
757 | _onclick="jQuery('#db-tables-%s').slideToggle()" % u), |
---|
758 | BUTTON('db stats', |
---|
759 | _onclick="jQuery('#db-stats-%s').slideToggle()" % u), |
---|
760 | DIV(BEAUTIFY(request), backtotop, |
---|
761 | _class="w2p-toolbar-hidden", _id="request-%s" % u), |
---|
762 | DIV(BEAUTIFY(current.session), backtotop, |
---|
763 | _class="w2p-toolbar-hidden", _id="session-%s" % u), |
---|
764 | DIV(BEAUTIFY(current.response), backtotop, |
---|
765 | _class="w2p-toolbar-hidden", _id="response-%s" % u), |
---|
766 | DIV(BEAUTIFY(dbtables), backtotop, |
---|
767 | _class="w2p-toolbar-hidden", _id="db-tables-%s" % u), |
---|
768 | DIV(BEAUTIFY(dbstats), backtotop, |
---|
769 | _class="w2p-toolbar-hidden", _id="db-stats-%s" % u), |
---|
770 | SCRIPT("jQuery('.w2p-toolbar-hidden').hide()"), |
---|
771 | _id="totop-%s" % u |
---|
772 | ) |
---|
773 | |
---|
774 | |
---|
775 | class Session(Storage): |
---|
776 | """ |
---|
777 | Defines the session object and the default values of its members (None) |
---|
778 | |
---|
779 | - session_storage_type : 'file', 'db', or 'cookie' |
---|
780 | - session_cookie_compression_level : |
---|
781 | - session_cookie_expires : cookie expiration |
---|
782 | - session_cookie_key : for encrypted sessions in cookies |
---|
783 | - session_id : a number or None if no session |
---|
784 | - session_id_name : |
---|
785 | - session_locked : |
---|
786 | - session_masterapp : |
---|
787 | - session_new : a new session obj is being created |
---|
788 | - session_hash : hash of the pickled loaded session |
---|
789 | - session_pickled : picked session |
---|
790 | |
---|
791 | if session in cookie: |
---|
792 | |
---|
793 | - session_data_name : name of the cookie for session data |
---|
794 | |
---|
795 | if session in db: |
---|
796 | |
---|
797 | - session_db_record_id |
---|
798 | - session_db_table |
---|
799 | - session_db_unique_key |
---|
800 | |
---|
801 | if session in file: |
---|
802 | |
---|
803 | - session_file |
---|
804 | - session_filename |
---|
805 | """ |
---|
806 | |
---|
807 | REGEX_SESSION_FILE = r'^(?:[\w-]+/)?[\w.-]+$' |
---|
808 | |
---|
809 | def connect(self, |
---|
810 | request=None, |
---|
811 | response=None, |
---|
812 | db=None, |
---|
813 | tablename='web2py_session', |
---|
814 | masterapp=None, |
---|
815 | migrate=True, |
---|
816 | separate=None, |
---|
817 | check_client=False, |
---|
818 | cookie_key=None, |
---|
819 | cookie_expires=None, |
---|
820 | compression_level=None |
---|
821 | ): |
---|
822 | """ |
---|
823 | Used in models, allows to customize Session handling |
---|
824 | |
---|
825 | Args: |
---|
826 | request: the request object |
---|
827 | response: the response object |
---|
828 | db: to store/retrieve sessions in db (a table is created) |
---|
829 | tablename(str): table name |
---|
830 | masterapp(str): points to another's app sessions. This enables a |
---|
831 | "SSO" environment among apps |
---|
832 | migrate: passed to the underlying db |
---|
833 | separate: with True, creates a folder with the 2 initials of the |
---|
834 | session id. Can also be a function, e.g. :: |
---|
835 | |
---|
836 | separate=lambda(session_name): session_name[-2:] |
---|
837 | |
---|
838 | check_client: if True, sessions can only come from the same ip |
---|
839 | cookie_key(str): secret for cookie encryption |
---|
840 | cookie_expires: sets the expiration of the cookie |
---|
841 | compression_level(int): 0-9, sets zlib compression on the data |
---|
842 | before the encryption |
---|
843 | """ |
---|
844 | request = request or current.request |
---|
845 | response = response or current.response |
---|
846 | masterapp = masterapp or request.application |
---|
847 | cookies = request.cookies |
---|
848 | |
---|
849 | self._unlock(response) |
---|
850 | |
---|
851 | response.session_masterapp = masterapp |
---|
852 | response.session_id_name = 'session_id_%s' % masterapp.lower() |
---|
853 | response.session_data_name = 'session_data_%s' % masterapp.lower() |
---|
854 | response.session_cookie_expires = cookie_expires |
---|
855 | response.session_client = str(request.client).replace(':', '.') |
---|
856 | current._session_cookie_key = cookie_key |
---|
857 | response.session_cookie_compression_level = compression_level |
---|
858 | |
---|
859 | # check if there is a session_id in cookies |
---|
860 | try: |
---|
861 | old_session_id = cookies[response.session_id_name].value |
---|
862 | except KeyError: |
---|
863 | old_session_id = None |
---|
864 | response.session_id = old_session_id |
---|
865 | |
---|
866 | # if we are supposed to use cookie based session data |
---|
867 | if cookie_key: |
---|
868 | response.session_storage_type = 'cookie' |
---|
869 | elif db: |
---|
870 | response.session_storage_type = 'db' |
---|
871 | else: |
---|
872 | response.session_storage_type = 'file' |
---|
873 | # why do we do this? |
---|
874 | # because connect may be called twice, by web2py and in models. |
---|
875 | # the first time there is no db yet so it should do nothing |
---|
876 | if (global_settings.db_sessions is True |
---|
877 | or masterapp in global_settings.db_sessions): |
---|
878 | return |
---|
879 | |
---|
880 | if response.session_storage_type == 'cookie': |
---|
881 | # check if there is session data in cookies |
---|
882 | if response.session_data_name in cookies: |
---|
883 | session_cookie_data = cookies[response.session_data_name].value |
---|
884 | else: |
---|
885 | session_cookie_data = None |
---|
886 | if session_cookie_data: |
---|
887 | data = secure_loads(session_cookie_data, cookie_key, |
---|
888 | compression_level=compression_level) |
---|
889 | if data: |
---|
890 | self.update(data) |
---|
891 | response.session_id = True |
---|
892 | |
---|
893 | # else if we are supposed to use file based sessions |
---|
894 | elif response.session_storage_type == 'file': |
---|
895 | response.session_new = False |
---|
896 | response.session_file = None |
---|
897 | # check if the session_id points to a valid sesion filename |
---|
898 | if response.session_id: |
---|
899 | if not re.match(self.REGEX_SESSION_FILE, response.session_id): |
---|
900 | response.session_id = None |
---|
901 | else: |
---|
902 | response.session_filename = \ |
---|
903 | os.path.join(up(request.folder), masterapp, |
---|
904 | 'sessions', response.session_id) |
---|
905 | try: |
---|
906 | response.session_file = \ |
---|
907 | recfile.open(response.session_filename, 'rb+') |
---|
908 | portalocker.lock(response.session_file, |
---|
909 | portalocker.LOCK_EX) |
---|
910 | response.session_locked = True |
---|
911 | self.update(pickle.load(response.session_file)) |
---|
912 | response.session_file.seek(0) |
---|
913 | oc = response.session_filename.split('/')[-1].split('-')[0] |
---|
914 | if check_client and response.session_client != oc: |
---|
915 | raise Exception("cookie attack") |
---|
916 | except: |
---|
917 | response.session_id = None |
---|
918 | if not response.session_id: |
---|
919 | uuid = web2py_uuid() |
---|
920 | response.session_id = '%s-%s' % (response.session_client, uuid) |
---|
921 | separate = separate and (lambda session_name: session_name[-2:]) |
---|
922 | if separate: |
---|
923 | prefix = separate(response.session_id) |
---|
924 | response.session_id = '%s/%s' % (prefix, response.session_id) |
---|
925 | response.session_filename = \ |
---|
926 | os.path.join(up(request.folder), masterapp, |
---|
927 | 'sessions', response.session_id) |
---|
928 | response.session_new = True |
---|
929 | |
---|
930 | # else the session goes in db |
---|
931 | elif response.session_storage_type == 'db': |
---|
932 | if global_settings.db_sessions is not True: |
---|
933 | global_settings.db_sessions.add(masterapp) |
---|
934 | # if had a session on file already, close it (yes, can happen) |
---|
935 | if response.session_file: |
---|
936 | self._close(response) |
---|
937 | # if on GAE tickets go also in DB |
---|
938 | if settings.global_settings.web2py_runtime_gae: |
---|
939 | request.tickets_db = db |
---|
940 | if masterapp == request.application: |
---|
941 | table_migrate = migrate |
---|
942 | else: |
---|
943 | table_migrate = False |
---|
944 | tname = tablename + '_' + masterapp |
---|
945 | table = db.get(tname, None) |
---|
946 | Field = db.Field |
---|
947 | if table is None: |
---|
948 | db.define_table( |
---|
949 | tname, |
---|
950 | Field('locked', 'boolean', default=False), |
---|
951 | Field('client_ip', length=64), |
---|
952 | Field('created_datetime', 'datetime', |
---|
953 | default=request.now), |
---|
954 | Field('modified_datetime', 'datetime'), |
---|
955 | Field('unique_key', length=64), |
---|
956 | Field('session_data', 'blob'), |
---|
957 | migrate=table_migrate, |
---|
958 | ) |
---|
959 | table = db[tname] # to allow for lazy table |
---|
960 | response.session_db_table = table |
---|
961 | if response.session_id: |
---|
962 | # Get session data out of the database |
---|
963 | try: |
---|
964 | (record_id, unique_key) = response.session_id.split(':') |
---|
965 | record_id = long(record_id) |
---|
966 | except (TypeError, ValueError): |
---|
967 | record_id = None |
---|
968 | |
---|
969 | # Select from database |
---|
970 | if record_id: |
---|
971 | row = table(record_id, unique_key=unique_key) |
---|
972 | # Make sure the session data exists in the database |
---|
973 | if row: |
---|
974 | # rows[0].update_record(locked=True) |
---|
975 | # Unpickle the data |
---|
976 | try: |
---|
977 | session_data = pickle.loads(row['session_data']) |
---|
978 | self.update(session_data) |
---|
979 | response.session_new = False |
---|
980 | except: |
---|
981 | record_id = None |
---|
982 | else: |
---|
983 | record_id = None |
---|
984 | if record_id: |
---|
985 | response.session_id = '%s:%s' % (record_id, unique_key) |
---|
986 | response.session_db_unique_key = unique_key |
---|
987 | response.session_db_record_id = record_id |
---|
988 | else: |
---|
989 | response.session_id = None |
---|
990 | response.session_new = True |
---|
991 | # if there is no session id yet, we'll need to create a |
---|
992 | # new session |
---|
993 | else: |
---|
994 | response.session_new = True |
---|
995 | |
---|
996 | # set the cookie now if you know the session_id so user can set |
---|
997 | # cookie attributes in controllers/models |
---|
998 | # cookie will be reset later |
---|
999 | # yet cookie may be reset later |
---|
1000 | # Removed comparison between old and new session ids - should send |
---|
1001 | # the cookie all the time |
---|
1002 | if isinstance(response.session_id, str): |
---|
1003 | response.cookies[response.session_id_name] = response.session_id |
---|
1004 | response.cookies[response.session_id_name]['path'] = '/' |
---|
1005 | if cookie_expires: |
---|
1006 | response.cookies[response.session_id_name]['expires'] = \ |
---|
1007 | cookie_expires.strftime(FMT) |
---|
1008 | |
---|
1009 | session_pickled = pickle.dumps(self, pickle.HIGHEST_PROTOCOL) |
---|
1010 | response.session_hash = hashlib.md5(session_pickled).hexdigest() |
---|
1011 | |
---|
1012 | if self.flash: |
---|
1013 | (response.flash, self.flash) = (self.flash, None) |
---|
1014 | |
---|
1015 | def renew(self, clear_session=False): |
---|
1016 | |
---|
1017 | if clear_session: |
---|
1018 | self.clear() |
---|
1019 | |
---|
1020 | request = current.request |
---|
1021 | response = current.response |
---|
1022 | session = response.session |
---|
1023 | masterapp = response.session_masterapp |
---|
1024 | cookies = request.cookies |
---|
1025 | |
---|
1026 | if response.session_storage_type == 'cookie': |
---|
1027 | return |
---|
1028 | |
---|
1029 | # if the session goes in file |
---|
1030 | if response.session_storage_type == 'file': |
---|
1031 | self._close(response) |
---|
1032 | uuid = web2py_uuid() |
---|
1033 | response.session_id = '%s-%s' % (response.session_client, uuid) |
---|
1034 | separate = (lambda s: s[-2:]) if session and response.session_id[2:3] == "/" else None |
---|
1035 | if separate: |
---|
1036 | prefix = separate(response.session_id) |
---|
1037 | response.session_id = '%s/%s' % \ |
---|
1038 | (prefix, response.session_id) |
---|
1039 | response.session_filename = \ |
---|
1040 | os.path.join(up(request.folder), masterapp, |
---|
1041 | 'sessions', response.session_id) |
---|
1042 | response.session_new = True |
---|
1043 | |
---|
1044 | # else the session goes in db |
---|
1045 | elif response.session_storage_type == 'db': |
---|
1046 | table = response.session_db_table |
---|
1047 | |
---|
1048 | # verify that session_id exists |
---|
1049 | if response.session_file: |
---|
1050 | self._close(response) |
---|
1051 | if response.session_new: |
---|
1052 | return |
---|
1053 | # Get session data out of the database |
---|
1054 | if response.session_id is None: |
---|
1055 | return |
---|
1056 | (record_id, sep, unique_key) = response.session_id.partition(':') |
---|
1057 | |
---|
1058 | if record_id.isdigit() and long(record_id) > 0: |
---|
1059 | new_unique_key = web2py_uuid() |
---|
1060 | row = table(record_id) |
---|
1061 | if row and to_native(row['unique_key']) == to_native(unique_key): |
---|
1062 | table._db(table.id == record_id).update(unique_key=new_unique_key) |
---|
1063 | else: |
---|
1064 | record_id = None |
---|
1065 | if record_id: |
---|
1066 | response.session_id = '%s:%s' % (record_id, new_unique_key) |
---|
1067 | response.session_db_record_id = record_id |
---|
1068 | response.session_db_unique_key = new_unique_key |
---|
1069 | else: |
---|
1070 | response.session_new = True |
---|
1071 | |
---|
1072 | def _fixup_before_save(self): |
---|
1073 | response = current.response |
---|
1074 | rcookies = response.cookies |
---|
1075 | scookies = rcookies.get(response.session_id_name) |
---|
1076 | if not scookies: |
---|
1077 | return |
---|
1078 | if self._forget: |
---|
1079 | del rcookies[response.session_id_name] |
---|
1080 | return |
---|
1081 | if self.get('httponly_cookies', True): |
---|
1082 | scookies['HttpOnly'] = True |
---|
1083 | if self._secure: |
---|
1084 | scookies['secure'] = True |
---|
1085 | if self._same_site is None: |
---|
1086 | # Using SameSite Lax Mode is the default |
---|
1087 | # You actually have to call session.samesite(False) if you really |
---|
1088 | # dont want the extra protection provided by the SameSite header |
---|
1089 | self._same_site = 'Lax' |
---|
1090 | if self._same_site: |
---|
1091 | if 'samesite' not in Cookie.Morsel._reserved: |
---|
1092 | # Python version 3.7 and lower needs this |
---|
1093 | Cookie.Morsel._reserved['samesite'] = 'SameSite' |
---|
1094 | scookies['samesite'] = self._same_site |
---|
1095 | |
---|
1096 | def clear_session_cookies(self): |
---|
1097 | request = current.request |
---|
1098 | response = current.response |
---|
1099 | session = response.session |
---|
1100 | masterapp = response.session_masterapp |
---|
1101 | cookies = request.cookies |
---|
1102 | rcookies = response.cookies |
---|
1103 | # if not cookie_key, but session_data_name in cookies |
---|
1104 | # expire session_data_name from cookies |
---|
1105 | if response.session_data_name in cookies: |
---|
1106 | rcookies[response.session_data_name] = 'expired' |
---|
1107 | rcookies[response.session_data_name]['path'] = '/' |
---|
1108 | rcookies[response.session_data_name]['expires'] = PAST |
---|
1109 | if response.session_id_name in rcookies: |
---|
1110 | del rcookies[response.session_id_name] |
---|
1111 | |
---|
1112 | def save_session_id_cookie(self): |
---|
1113 | request = current.request |
---|
1114 | response = current.response |
---|
1115 | session = response.session |
---|
1116 | masterapp = response.session_masterapp |
---|
1117 | cookies = request.cookies |
---|
1118 | rcookies = response.cookies |
---|
1119 | |
---|
1120 | # if not cookie_key, but session_data_name in cookies |
---|
1121 | # expire session_data_name from cookies |
---|
1122 | if not current._session_cookie_key: |
---|
1123 | if response.session_data_name in cookies: |
---|
1124 | rcookies[response.session_data_name] = 'expired' |
---|
1125 | rcookies[response.session_data_name]['path'] = '/' |
---|
1126 | rcookies[response.session_data_name]['expires'] = PAST |
---|
1127 | if response.session_id: |
---|
1128 | rcookies[response.session_id_name] = response.session_id |
---|
1129 | rcookies[response.session_id_name]['path'] = '/' |
---|
1130 | expires = response.session_cookie_expires |
---|
1131 | if isinstance(expires, datetime.datetime): |
---|
1132 | expires = expires.strftime(FMT) |
---|
1133 | if expires: |
---|
1134 | rcookies[response.session_id_name]['expires'] = expires |
---|
1135 | |
---|
1136 | def clear(self): |
---|
1137 | # see https://github.com/web2py/web2py/issues/735 |
---|
1138 | response = current.response |
---|
1139 | if response.session_storage_type == 'file': |
---|
1140 | target = recfile.generate(response.session_filename) |
---|
1141 | try: |
---|
1142 | self._close(response) |
---|
1143 | os.unlink(target) |
---|
1144 | except: |
---|
1145 | pass |
---|
1146 | elif response.session_storage_type == 'db': |
---|
1147 | table = response.session_db_table |
---|
1148 | if response.session_id: |
---|
1149 | (record_id, sep, unique_key) = response.session_id.partition(':') |
---|
1150 | if record_id.isdigit() and long(record_id) > 0: |
---|
1151 | table._db(table.id == record_id).delete() |
---|
1152 | Storage.clear(self) |
---|
1153 | |
---|
1154 | def is_new(self): |
---|
1155 | if self._start_timestamp: |
---|
1156 | return False |
---|
1157 | else: |
---|
1158 | self._start_timestamp = datetime.datetime.today() |
---|
1159 | return True |
---|
1160 | |
---|
1161 | def is_expired(self, seconds=3600): |
---|
1162 | now = datetime.datetime.today() |
---|
1163 | if not self._last_timestamp or \ |
---|
1164 | self._last_timestamp + datetime.timedelta(seconds=seconds) > now: |
---|
1165 | self._last_timestamp = now |
---|
1166 | return False |
---|
1167 | else: |
---|
1168 | return True |
---|
1169 | |
---|
1170 | def secure(self): |
---|
1171 | self._secure = True |
---|
1172 | |
---|
1173 | def samesite(self, mode='Lax'): |
---|
1174 | self._same_site = mode |
---|
1175 | |
---|
1176 | def forget(self, response=None): |
---|
1177 | self._close(response) |
---|
1178 | self._forget = True |
---|
1179 | |
---|
1180 | def _try_store_in_cookie(self, request, response): |
---|
1181 | if self._forget or self._unchanged(response): |
---|
1182 | # self.clear_session_cookies() |
---|
1183 | self.save_session_id_cookie() |
---|
1184 | return False |
---|
1185 | name = response.session_data_name |
---|
1186 | compression_level = response.session_cookie_compression_level |
---|
1187 | value = secure_dumps(dict(self), |
---|
1188 | current._session_cookie_key, |
---|
1189 | compression_level=compression_level) |
---|
1190 | rcookies = response.cookies |
---|
1191 | rcookies.pop(name, None) |
---|
1192 | rcookies[name] = to_native(value) |
---|
1193 | rcookies[name]['path'] = '/' |
---|
1194 | expires = response.session_cookie_expires |
---|
1195 | if isinstance(expires, datetime.datetime): |
---|
1196 | expires = expires.strftime(FMT) |
---|
1197 | if expires: |
---|
1198 | rcookies[name]['expires'] = expires |
---|
1199 | return True |
---|
1200 | |
---|
1201 | def _unchanged(self, response): |
---|
1202 | if response.session_new: |
---|
1203 | internal = ['_last_timestamp', '_secure', '_start_timestamp', '_same_site'] |
---|
1204 | for item in self.keys(): |
---|
1205 | if item not in internal: |
---|
1206 | return False |
---|
1207 | return True |
---|
1208 | session_pickled = pickle.dumps(self, pickle.HIGHEST_PROTOCOL) |
---|
1209 | response.session_pickled = session_pickled |
---|
1210 | session_hash = hashlib.md5(session_pickled).hexdigest() |
---|
1211 | return response.session_hash == session_hash |
---|
1212 | |
---|
1213 | def _try_store_in_db(self, request, response): |
---|
1214 | # don't save if file-based sessions, |
---|
1215 | # no session id, or session being forgotten |
---|
1216 | # or no changes to session (Unless the session is new) |
---|
1217 | if (not response.session_db_table |
---|
1218 | or self._forget |
---|
1219 | or (self._unchanged(response) and not response.session_new)): |
---|
1220 | if (not response.session_db_table |
---|
1221 | and global_settings.db_sessions is not True |
---|
1222 | and response.session_masterapp in global_settings.db_sessions): |
---|
1223 | global_settings.db_sessions.remove(response.session_masterapp) |
---|
1224 | # self.clear_session_cookies() |
---|
1225 | self.save_session_id_cookie() |
---|
1226 | return False |
---|
1227 | |
---|
1228 | table = response.session_db_table |
---|
1229 | record_id = response.session_db_record_id |
---|
1230 | if response.session_new: |
---|
1231 | unique_key = web2py_uuid() |
---|
1232 | else: |
---|
1233 | unique_key = response.session_db_unique_key |
---|
1234 | |
---|
1235 | session_pickled = response.session_pickled or pickle.dumps(self, pickle.HIGHEST_PROTOCOL) |
---|
1236 | |
---|
1237 | dd = dict(locked=0, |
---|
1238 | client_ip=response.session_client, |
---|
1239 | modified_datetime=request.now.isoformat(), |
---|
1240 | session_data=session_pickled, |
---|
1241 | unique_key=unique_key) |
---|
1242 | if record_id: |
---|
1243 | if not table._db(table.id == record_id).update(**dd): |
---|
1244 | record_id = None |
---|
1245 | if not record_id: |
---|
1246 | record_id = table.insert(**dd) |
---|
1247 | response.session_id = '%s:%s' % (record_id, unique_key) |
---|
1248 | response.session_db_unique_key = unique_key |
---|
1249 | response.session_db_record_id = record_id |
---|
1250 | |
---|
1251 | self.save_session_id_cookie() |
---|
1252 | return True |
---|
1253 | |
---|
1254 | def _try_store_in_cookie_or_file(self, request, response): |
---|
1255 | if response.session_storage_type == 'file': |
---|
1256 | return self._try_store_in_file(request, response) |
---|
1257 | if response.session_storage_type == 'cookie': |
---|
1258 | return self._try_store_in_cookie(request, response) |
---|
1259 | |
---|
1260 | def _try_store_in_file(self, request, response): |
---|
1261 | try: |
---|
1262 | if (not response.session_id or |
---|
1263 | not response.session_filename or |
---|
1264 | self._forget |
---|
1265 | or self._unchanged(response)): |
---|
1266 | # self.clear_session_cookies() |
---|
1267 | return False |
---|
1268 | else: |
---|
1269 | if response.session_new or not response.session_file: |
---|
1270 | # Tests if the session sub-folder exists, if not, create it |
---|
1271 | session_folder = os.path.dirname(response.session_filename) |
---|
1272 | if not os.path.exists(session_folder): |
---|
1273 | os.mkdir(session_folder) |
---|
1274 | response.session_file = recfile.open(response.session_filename, 'wb') |
---|
1275 | portalocker.lock(response.session_file, portalocker.LOCK_EX) |
---|
1276 | response.session_locked = True |
---|
1277 | if response.session_file: |
---|
1278 | session_pickled = response.session_pickled or pickle.dumps(self, pickle.HIGHEST_PROTOCOL) |
---|
1279 | response.session_file.write(session_pickled) |
---|
1280 | response.session_file.truncate() |
---|
1281 | return True |
---|
1282 | finally: |
---|
1283 | self._close(response) |
---|
1284 | self.save_session_id_cookie() |
---|
1285 | |
---|
1286 | def _unlock(self, response): |
---|
1287 | if response and response.session_file and response.session_locked: |
---|
1288 | try: |
---|
1289 | portalocker.unlock(response.session_file) |
---|
1290 | response.session_locked = False |
---|
1291 | except: # this should never happen but happens in Windows |
---|
1292 | pass |
---|
1293 | |
---|
1294 | def _close(self, response): |
---|
1295 | if response and response.session_file: |
---|
1296 | self._unlock(response) |
---|
1297 | try: |
---|
1298 | response.session_file.close() |
---|
1299 | del response.session_file |
---|
1300 | except: |
---|
1301 | pass |
---|
1302 | |
---|
1303 | |
---|
1304 | def pickle_session(s): |
---|
1305 | return Session, (dict(s),) |
---|
1306 | |
---|
1307 | copyreg.pickle(Session, pickle_session) |
---|