Completed
Push — file-actions ( 5064d9 )
by Felipe A.
26s
created

Clipboard._paginated_cookie_length()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
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.compat import range
12
13
14
logger = logging.getLogger(__name__)
15
16
17
class Clipboard(set):
18
    cookie_secret = os.urandom(256)
19
    cookie_sign_name = 'clipboard-signature'
20
    cookie_mode_name = 'clipboard-mode'
21
    cookie_list_name = 'clipboard-{:x}'
22
    request_cache_field = '_browsepy_file_actions_clipboard_cache'
23
    max_pages = 0xffffffff
24
25
    @classmethod
26
    def count(cls, request=request):
27
        return len(cls.from_request(request))
28
29
    @classmethod
30
    def detect(cls, node):
31
        return bool(cls.count())
32
33
    @classmethod
34
    def detect_target(cls, node):
35
        return node.can_upload and cls.detect(node)
36
37
    @classmethod
38
    def from_request(cls, request=request):
39
        cached = getattr(request, cls.request_cache_field, None)
40
        if cached is not None:
41
            return cached
42
        self = cls()
43
        signature = cls._cookiebytes(cls.cookie_sign_name, request)
44
        data = cls._read_paginated_cookie(request)
45
        method = cls._cookietext(cls.cookie_mode_name, request)
46
        if cls._signature(data, method) == signature:
47
            try:
48
                self.method = method
49
                self.update(json.loads(base64.b64decode(data).decode('utf-8')))
50
            except BaseException as e:
51
                logger.warn('Bad cookie')
52
        return self
53
54
    @classmethod
55
    def _cookiebytes(cls, name, request=request):
56
        return request.cookies.get(name, '').encode('ascii')
57
58
    @classmethod
59
    def _cookietext(cls, name, request=request):
60
        return request.cookies.get(name, '')
61
62
    @classmethod
63
    def _paginated_cookie_length(cls, page=0, path='/'):
64
        name_fnc = cls.cookie_list_name.format
65
        return 3990 - len(name_fnc(page) + path)  # 4000 - len('=""; Path=')
66
67
    @classmethod
68
    def _read_paginated_cookie(cls, request=request):
69
        chunks = []
70
        if request:
71
            name_fnc = cls.cookie_list_name.format
72
            for i in range(cls.max_pages):  # 2 ** 32 - 1
73
                cookie = request.cookies.get(name_fnc(i), '').encode('ascii')
74
                chunks.append(cookie)
75
                if len(cookie) < cls._paginated_cookie_length(i):
76
                    break
77
        return b''.join(chunks)
78
79
    @classmethod
80
    def _write_paginated_cookie(cls, data, response):
81
        name_fnc = cls.cookie_list_name.format
82
        start = 0
83
        size = len(data)
84
        for i in range(cls.max_pages):
85
            end = cls._paginated_cookie_length(i)
86
            response.set_cookie(name_fnc(i), data[start:end].decode('ascii'))
87
            start = end
88
            if start > size:  # we need an empty page after start == size
89
                return i
90
        return 0
91
92
    @classmethod
93
    def _delete_paginated_cookie(cls, response, start=0, request=request):
94
        name_fnc = cls.cookie_list_name.format
95
        for i in range(start, cls.max_pages):
96
            name = name_fnc(i)
97
            if name not in request.cookies:
98
                break
99
            response.set_cookie(name, '', expires=0)
100
101
    @classmethod
102
    def _signature(cls, data, method):
103
        data = cls.cookie_secret + method.encode('utf-8') + data
104
        return base64.b64encode(hashlib.sha512(data).digest())
105
106
    def __init__(self, iterable=(), mode='copy'):
107
        self.mode = mode
108
        super(Clipboard, self).__init__(iterable)
109
110
    def to_response(self, response, request=request):
111
        if self:
112
            data = base64.b64encode(json.dumps(list(self)).encode('utf-8'))
113
            signature = self._signature(data, self.mode)
114
            start = self._write_paginated_cookie(data, response)
115
        else:
116
            signature = b''
117
            start = 0
118
        self._delete_paginated_cookie(response, start, request)
119
        response.set_cookie(self.cookie_mode_name, self.mode)
120
        response.set_cookie(self.cookie_sign_name, signature)
121