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
|
|
|
|