source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/globals.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 50.2 KB
Line 
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
8Contains the classes for the global used variables:
9
10- Request
11- Response
12- Session
13
14"""
15from 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
17from gluon.storage import Storage, List
18from gluon.streamer import streamer, stream_file_or_304_or_206, DEFAULT_CHUNK_SIZE
19from gluon.contenttype import contenttype
20from gluon.html import xmlescape, TABLE, TR, PRE, URL
21from gluon.http import HTTP, redirect
22from gluon.fileutils import up
23from gluon.serializers import json, custom_json
24import gluon.settings as settings
25from gluon.utils import web2py_uuid, secure_dumps, secure_loads
26from gluon.settings import global_settings
27from gluon import recfile
28from gluon.cache import CacheInRam
29import hashlib
30from pydal.contrib import portalocker
31from pickle import Pickler, MARK, DICT, EMPTY_DICT
32# from types import DictionaryType
33import datetime
34import re
35import os
36import sys
37import traceback
38import threading
39import cgi
40import copy
41import tempfile
42import json as json_parser
43
44
45FMT = '%a, %d-%b-%Y %H:%M:%S PST'
46PAST = 'Sat, 1-Jan-1971 00:00:00'
47FUTURE = 'Tue, 1-Dec-2999 23:59:59'
48
49try:
50    # FIXME PY3
51    from gluon.contrib.minify import minify
52    have_minify = True
53except ImportError:
54    have_minify = False
55
56
57__all__ = ['Request', 'Response', 'Session']
58
59current = threading.local()  # thread-local storage for request-scope globals
60
61css_template = '<link href="%s" rel="stylesheet" type="text/css" />'
62js_template = '<script src="%s" type="text/javascript"></script>'
63coffee_template = '<script src="%s" type="text/coffee"></script>'
64typescript_template = '<script src="%s" type="text/typescript"></script>'
65less_template = '<link href="%s" rel="stylesheet/less" type="text/css" />'
66css_inline = '<style type="text/css">\n%s\n</style>'
67js_inline = '<script type="text/javascript">\n%s\n</script>'
68
69template_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
83class 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
89if PY2:
90    SortingPickler.dispatch = copy.copy(Pickler.dispatch)
91    SortingPickler.dispatch[dict] = SortingPickler.save_dict
92else:
93    SortingPickler.dispatch_table = copyreg.dispatch_table.copy()
94    SortingPickler.dispatch_table[dict] = SortingPickler.save_dict
95
96
97def sorting_dumps(obj, protocol=None):
98    file = StringIO()
99    SortingPickler(file, protocol).dump(obj)
100    return file.getvalue()
101# END #####################################################################
102
103
104def 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
124def 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
156class 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
401class 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
775class 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
1304def pickle_session(s):
1305    return Session, (dict(s),)
1306
1307copyreg.pickle(Session, pickle_session)
Note: See TracBrowser for help on using the repository browser.