Completed
Push — feature-datacookie ( 457ec2 )
by Felipe A.
42s
created

Clipboard._read_paginated_cookie()   A

Complexity

Conditions 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
c 2
b 0
f 0
dl 0
loc 12
rs 9.2
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
import os
5
import json
6
import base64
7
import logging
8
import hashlib
9
10
from flask import request
11
from browsepy.http import DataCookie
12
from browsepy.exceptions import InvalidCookieSizeError
13
14
from .exceptions import InvalidClipboardSizeError
15
16
logger = logging.getLogger(__name__)
17
18
19
class Clipboard(set):
20
    '''
21
    Clipboard (set) with convenience methods to pick its state from request
22
    cookies and save it to response cookies.
23
    '''
24
    data_cookie = DataCookie('clipboard', max_pages=20)
25
    secret = os.urandom(256)
26
    request_cache_field = '_browsepy_file_actions_clipboard_cache'
27
28
    @classmethod
29
    def count(cls, request=request):
30
        '''
31
        Get how many clipboard items are stores on request cookies.
32
33
        :param request: optional request, defaults to current flask request
34
        :type request: werkzeug.Request
35
        :return: number of clipboard items on request's cookies
36
        :rtype: int
37
        '''
38
        return len(cls.from_request(request))
39
40
    @classmethod
41
    def from_request(cls, request=request):
42
        '''
43
        Create clipboard object from request cookies.
44
        Uses request itself for cache.
45
46
        :param request: optional request, defaults to current flask request
47
        :type request: werkzeug.Request
48
        :returns: clipboard instance
49
        :rtype: Clipboard
50
        '''
51
        cached = getattr(request, cls.request_cache_field, None)
52
        if cached is not None:
53
            return cached
54
        self = cls()
55
        setattr(request, cls.request_cache_field, self)
56
        try:
57
            self.__setstate__(cls.data_cookie.load_headers(request.headers))
58
        except BaseException:
59
            pass
60
        return self
61
62
    @classmethod
63
    def _signature(cls, items, method):
64
        serialized = json.dumps(items).encode('utf-8')
65
        data = cls.secret + method.encode('utf-8') + serialized
66
        return base64.b64encode(hashlib.sha512(data).digest()).decode('ascii')
67
68
    def __init__(self, iterable=(), mode='copy'):
69
        self.mode = mode
70
        super(Clipboard, self).__init__(iterable)
71
72
    def __getstate__(self):
73
        items = list(self)
74
        return {
75
            'mode': self.mode,
76
            'items': items,
77
            'signature': self._signature(items, self.mode),
78
            }
79
80
    def __setstate__(self, data):
81
        if data['signature'] == self._signature(data['items'], data['mode']):
82
            self.update(data['items'])
83
            self.mode = data['mode']
84
85
    def to_response(self, response, request=request):
86
        '''
87
        Save clipboard state to response taking care of disposing old clipboard
88
        cookies from request.
89
90
        :param response: response object to write cookies on
91
        :type response: werkzeug.Response
92
        :param request: optional request, defaults to current flask request
93
        :type request: werkzeug.Request
94
        '''
95
        if self:
96
            data = self.__getstate__()
97
            try:
98
                headers = self.data_cookie.dump_headers(data, request.headers)
99
            except InvalidCookieSizeError as e:
100
                raise InvalidClipboardSizeError(
101
                    clipboard=self,
102
                    max_cookies=e.max_cookies
103
                    )
104
        else:
105
            headers = self.data_cookie.truncate_headers(request.headers)
106
        response.headers.extend(headers)
107