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