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

Clipboard.detect_selection()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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