##// END OF EJS Templates
Decompression bomb protection in image viewing
neko259 -
r1820:668d6c7d default
parent child Browse files
Show More
@@ -1,220 +1,230 b''
1 import re
1 import re
2
2
3 from PIL import Image
4
3 from django.contrib.staticfiles import finders
5 from django.contrib.staticfiles import finders
4 from django.contrib.staticfiles.templatetags.staticfiles import static
6 from django.contrib.staticfiles.templatetags.staticfiles import static
5 from django.core.files.images import get_image_dimensions
7 from django.core.files.images import get_image_dimensions
6 from django.template.defaultfilters import filesizeformat
8 from django.template.defaultfilters import filesizeformat
7 from django.core.urlresolvers import reverse
9 from django.core.urlresolvers import reverse
8 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
9
11
10 from boards.utils import get_domain, cached_result
12 from boards.utils import get_domain, cached_result
11 from boards import settings
13 from boards import settings
12
14
13
15
14 FILE_STUB_IMAGE = 'images/file.png'
16 FILE_STUB_IMAGE = 'images/file.png'
15 FILE_STUB_URL = 'url'
17 FILE_STUB_URL = 'url'
16 FILE_FILEFORMAT = 'images/fileformats/{}.png'
18 FILE_FILEFORMAT = 'images/fileformats/{}.png'
17
19
18
20
19 FILE_TYPES_VIDEO = (
21 FILE_TYPES_VIDEO = (
20 'webm',
22 'webm',
21 'mp4',
23 'mp4',
22 'mpeg',
24 'mpeg',
23 'ogv',
25 'ogv',
24 )
26 )
25 FILE_TYPE_SVG = 'svg'
27 FILE_TYPE_SVG = 'svg'
26 FILE_TYPES_AUDIO = (
28 FILE_TYPES_AUDIO = (
27 'ogg',
29 'ogg',
28 'mp3',
30 'mp3',
29 'opus',
31 'opus',
30 )
32 )
31 FILE_TYPES_IMAGE = (
33 FILE_TYPES_IMAGE = (
32 'jpeg',
34 'jpeg',
33 'jpg',
35 'jpg',
34 'png',
36 'png',
35 'bmp',
37 'bmp',
36 'gif',
38 'gif',
37 )
39 )
38
40
39 PLAIN_FILE_FORMATS = {
41 PLAIN_FILE_FORMATS = {
40 'zip': 'archive',
42 'zip': 'archive',
41 'tar': 'archive',
43 'tar': 'archive',
42 'gz': 'archive',
44 'gz': 'archive',
43 'mid' : 'midi',
45 'mid' : 'midi',
44 }
46 }
45
47
46 URL_PROTOCOLS = {
48 URL_PROTOCOLS = {
47 'magnet': 'magnet',
49 'magnet': 'magnet',
48 }
50 }
49
51
50 CSS_CLASS_IMAGE = 'image'
52 CSS_CLASS_IMAGE = 'image'
51 CSS_CLASS_THUMB = 'thumb'
53 CSS_CLASS_THUMB = 'thumb'
52
54
53
55
54 def get_viewers():
56 def get_viewers():
55 return AbstractViewer.__subclasses__()
57 return AbstractViewer.__subclasses__()
56
58
57
59
58 def get_static_dimensions(filename):
60 def get_static_dimensions(filename):
59 file_path = finders.find(filename)
61 file_path = finders.find(filename)
60 return get_image_dimensions(file_path)
62 return get_image_dimensions(file_path)
61
63
62
64
63 # TODO Move this to utils
65 # TODO Move this to utils
64 def file_exists(filename):
66 def file_exists(filename):
65 return finders.find(filename) is not None
67 return finders.find(filename) is not None
66
68
67
69
68 class AbstractViewer:
70 class AbstractViewer:
69 def __init__(self, file, file_type, hash, url):
71 def __init__(self, file, file_type, hash, url):
70 self.file = file
72 self.file = file
71 self.file_type = file_type
73 self.file_type = file_type
72 self.hash = hash
74 self.hash = hash
73 self.url = url
75 self.url = url
74
76
75 @staticmethod
77 @staticmethod
76 def supports(file_type):
78 def supports(file_type):
77 return True
79 return True
78
80
79 def get_view(self):
81 def get_view(self):
80 search_host = settings.get('External', 'ImageSearchHost')
82 search_host = settings.get('External', 'ImageSearchHost')
81 if search_host:
83 if search_host:
82 search_url = search_host + self.file.url
84 search_url = search_host + self.file.url
83 else:
85 else:
84 search_url = ''
86 search_url = ''
85
87
86 return '<div class="image">'\
88 return '<div class="image">'\
87 '{}'\
89 '{}'\
88 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
90 '<div class="image-metadata"><a href="{}" download >{}, {}</a>'\
89 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-hash="{}">πŸ” </a></div>'\
91 ' <a class="file-menu" href="#" data-type="{}" data-search-url="{}" data-hash="{}">πŸ” </a></div>'\
90 '</div>'.format(self.get_format_view(), self.file.url,
92 '</div>'.format(self.get_format_view(), self.file.url,
91 self.file_type, filesizeformat(self.file.size),
93 self.file_type, filesizeformat(self.file.size),
92 self.file_type, search_url, self.hash)
94 self.file_type, search_url, self.hash)
93
95
94 def get_format_view(self):
96 def get_format_view(self):
95 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
97 image_name = PLAIN_FILE_FORMATS.get(self.file_type, self.file_type)
96 file_name = FILE_FILEFORMAT.format(image_name)
98 file_name = FILE_FILEFORMAT.format(image_name)
97
99
98 if file_exists(file_name):
100 if file_exists(file_name):
99 image = file_name
101 image = file_name
100 else:
102 else:
101 image = FILE_STUB_IMAGE
103 image = FILE_STUB_IMAGE
102
104
103 w, h = get_static_dimensions(image)
105 w, h = get_static_dimensions(image)
104
106
105 return '<a href="{}">'\
107 return '<a href="{}">'\
106 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
108 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
107 '</a>'.format(self.file.url, static(image), w, h)
109 '</a>'.format(self.file.url, static(image), w, h)
108
110
109
111
110 class VideoViewer(AbstractViewer):
112 class VideoViewer(AbstractViewer):
111 @staticmethod
113 @staticmethod
112 def supports(file_type):
114 def supports(file_type):
113 return file_type in FILE_TYPES_VIDEO
115 return file_type in FILE_TYPES_VIDEO
114
116
115 def get_format_view(self):
117 def get_format_view(self):
116 return '<video width="200" height="150" controls src="{}"></video>'\
118 return '<video width="200" height="150" controls src="{}"></video>'\
117 .format(self.file.url)
119 .format(self.file.url)
118
120
119
121
120 class AudioViewer(AbstractViewer):
122 class AudioViewer(AbstractViewer):
121 @staticmethod
123 @staticmethod
122 def supports(file_type):
124 def supports(file_type):
123 return file_type in FILE_TYPES_AUDIO
125 return file_type in FILE_TYPES_AUDIO
124
126
125 def get_format_view(self):
127 def get_format_view(self):
126 return '<audio controls src="{}"></audio>'.format(self.file.url)
128 return '<audio controls src="{}"></audio>'.format(self.file.url)
127
129
128
130
129 class SvgViewer(AbstractViewer):
131 class SvgViewer(AbstractViewer):
130 @staticmethod
132 @staticmethod
131 def supports(file_type):
133 def supports(file_type):
132 return file_type == FILE_TYPE_SVG
134 return file_type == FILE_TYPE_SVG
133
135
134 def get_format_view(self):
136 def get_format_view(self):
135 return '<a class="thumb" href="{}">'\
137 return '<a class="thumb" href="{}">'\
136 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
138 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
137 '</a>'.format(self.file.url, self.file.url)
139 '</a>'.format(self.file.url, self.file.url)
138
140
139
141
140 class ImageViewer(AbstractViewer):
142 class ImageViewer(AbstractViewer):
141 @staticmethod
143 @staticmethod
142 def supports(file_type):
144 def supports(file_type):
143 return file_type in FILE_TYPES_IMAGE
145 return file_type in FILE_TYPES_IMAGE
144
146
145 def get_format_view(self):
147 def get_format_view(self):
146 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
148 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
147 filesizeformat(self.file.size))
149 filesizeformat(self.file.size))
148 width, height = get_image_dimensions(self.file.path)
150
151 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
152 try:
153 width, height = get_image_dimensions(self.file.path)
154 except Exception:
155 # If the image is a decompression bomb, treat it as just a regular
156 # file
157 return super().get_format_view()
158
149 preview_path = self.file.path.replace('.', '.200x150.')
159 preview_path = self.file.path.replace('.', '.200x150.')
150 pre_width, pre_height = get_image_dimensions(preview_path)
160 pre_width, pre_height = get_image_dimensions(preview_path)
151
161
152 split = self.file.url.rsplit('.', 1)
162 split = self.file.url.rsplit('.', 1)
153 w, h = 200, 150
163 w, h = 200, 150
154 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
164 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
155
165
156 return '<a class="{}" href="{full}">' \
166 return '<a class="{}" href="{full}">' \
157 '<img class="post-image-preview"' \
167 '<img class="post-image-preview"' \
158 ' src="{}"' \
168 ' src="{}"' \
159 ' alt="{}"' \
169 ' alt="{}"' \
160 ' width="{}"' \
170 ' width="{}"' \
161 ' height="{}"' \
171 ' height="{}"' \
162 ' data-width="{}"' \
172 ' data-width="{}"' \
163 ' data-height="{}" />' \
173 ' data-height="{}" />' \
164 '</a>' \
174 '</a>' \
165 .format(CSS_CLASS_THUMB,
175 .format(CSS_CLASS_THUMB,
166 thumb_url,
176 thumb_url,
167 self.hash,
177 self.hash,
168 str(pre_width),
178 str(pre_width),
169 str(pre_height), str(width), str(height),
179 str(pre_height), str(width), str(height),
170 full=self.file.url, image_meta=metadata)
180 full=self.file.url, image_meta=metadata)
171
181
172
182
173 class UrlViewer(AbstractViewer):
183 class UrlViewer(AbstractViewer):
174 @staticmethod
184 @staticmethod
175 def supports(file_type):
185 def supports(file_type):
176 return file_type is None
186 return file_type is None
177
187
178 def get_view(self):
188 def get_view(self):
179 return '<div class="image">' \
189 return '<div class="image">' \
180 '{}' \
190 '{}' \
181 '<div class="image-metadata">{}</div>' \
191 '<div class="image-metadata">{}</div>' \
182 '</div>'.format(self.get_format_view(), get_domain(self.url))
192 '</div>'.format(self.get_format_view(), get_domain(self.url))
183
193
184 def get_format_view(self):
194 def get_format_view(self):
185 protocol = self.url.split(':')[0]
195 protocol = self.url.split(':')[0]
186
196
187 domain = get_domain(self.url)
197 domain = get_domain(self.url)
188
198
189 if protocol in URL_PROTOCOLS:
199 if protocol in URL_PROTOCOLS:
190 url_image_name = URL_PROTOCOLS.get(protocol)
200 url_image_name = URL_PROTOCOLS.get(protocol)
191 elif domain:
201 elif domain:
192 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
202 url_image_name = self._find_image_for_domains(domain) or FILE_STUB_URL
193 else:
203 else:
194 url_image_name = FILE_STUB_URL
204 url_image_name = FILE_STUB_URL
195
205
196 image_path = 'images/{}.png'.format(url_image_name)
206 image_path = 'images/{}.png'.format(url_image_name)
197 image = static(image_path)
207 image = static(image_path)
198 w, h = get_static_dimensions(image_path)
208 w, h = get_static_dimensions(image_path)
199
209
200 return '<a href="{}">' \
210 return '<a href="{}">' \
201 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
211 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
202 '</a>'.format(self.url, image, w, h)
212 '</a>'.format(self.url, image, w, h)
203
213
204 @cached_result()
214 @cached_result()
205 def _find_image_for_domains(self, domain):
215 def _find_image_for_domains(self, domain):
206 """
216 """
207 Searches for the domain image for every domain level except top.
217 Searches for the domain image for every domain level except top.
208 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
218 E.g. for l3.example.co.uk it will search for l3.example.co.uk, then
209 example.co.uk, then co.uk
219 example.co.uk, then co.uk
210 """
220 """
211 levels = domain.split('.')
221 levels = domain.split('.')
212 while len(levels) > 1:
222 while len(levels) > 1:
213 domain = '.'.join(levels)
223 domain = '.'.join(levels)
214
224
215 filename = 'images/domains/{}.png'.format(domain)
225 filename = 'images/domains/{}.png'.format(domain)
216 if file_exists(filename):
226 if file_exists(filename):
217 return 'domains/' + domain
227 return 'domains/' + domain
218 else:
228 else:
219 del levels[0]
229 del levels[0]
220
230
General Comments 0
You need to be logged in to leave comments. Login now