##// END OF EJS Templates
Autodetect only a white list of mimetypes
neko259 -
r1372:cab9c599 default
parent child Browse files
Show More
@@ -1,387 +1,404 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3 import time
3 import time
4 import logging
5 import pytz
4
6
5 import pytz
6 from django import forms
7 from django import forms
7 from django.core.files.uploadedfile import SimpleUploadedFile
8 from django.core.files.uploadedfile import SimpleUploadedFile
8 from django.core.exceptions import ObjectDoesNotExist
9 from django.core.exceptions import ObjectDoesNotExist
9 from django.forms.util import ErrorList
10 from django.forms.util import ErrorList
10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11
12
12 from boards.mdx_neboard import formatters
13 from boards.mdx_neboard import formatters
13 from boards.models.attachment.downloaders import Downloader
14 from boards.models.attachment.downloaders import Downloader
14 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models import Tag, Post
16 from boards.models import Tag, Post
16 from boards.utils import validate_file_size, get_file_mimetype, \
17 from boards.utils import validate_file_size, get_file_mimetype, \
17 FILE_EXTENSION_DELIMITER
18 FILE_EXTENSION_DELIMITER
18 from neboard import settings
19 from neboard import settings
19 import boards.settings as board_settings
20 import boards.settings as board_settings
20 import neboard
21 import neboard
21
22
22 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
23
24
24 VETERAN_POSTING_DELAY = 5
25 VETERAN_POSTING_DELAY = 5
25
26
26 ATTRIBUTE_PLACEHOLDER = 'placeholder'
27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
27 ATTRIBUTE_ROWS = 'rows'
28 ATTRIBUTE_ROWS = 'rows'
28
29
29 LAST_POST_TIME = 'last_post_time'
30 LAST_POST_TIME = 'last_post_time'
30 LAST_LOGIN_TIME = 'last_login_time'
31 LAST_LOGIN_TIME = 'last_login_time'
31 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
32 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
32 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
33
34
34 LABEL_TITLE = _('Title')
35 LABEL_TITLE = _('Title')
35 LABEL_TEXT = _('Text')
36 LABEL_TEXT = _('Text')
36 LABEL_TAG = _('Tag')
37 LABEL_TAG = _('Tag')
37 LABEL_SEARCH = _('Search')
38 LABEL_SEARCH = _('Search')
38
39
39 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
40 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
40 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
41 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
41
42
42 TAG_MAX_LENGTH = 20
43 TAG_MAX_LENGTH = 20
43
44
44 TEXTAREA_ROWS = 4
45 TEXTAREA_ROWS = 4
45
46
46 TRIPCODE_DELIM = '#'
47 TRIPCODE_DELIM = '#'
47
48
49 # TODO Maybe this may be converted into the database table?
50 MIMETYPE_EXTENSIONS = {
51 'image/jpeg': 'jpeg',
52 'image/png': 'png',
53 'image/gif': 'gif',
54 'video/webm': 'webm',
55 'application/pdf': 'pdf',
56 'x-diff': 'diff',
57 }
58
48
59
49 def get_timezones():
60 def get_timezones():
50 timezones = []
61 timezones = []
51 for tz in pytz.common_timezones:
62 for tz in pytz.common_timezones:
52 timezones.append((tz, tz),)
63 timezones.append((tz, tz),)
53 return timezones
64 return timezones
54
65
55
66
56 class FormatPanel(forms.Textarea):
67 class FormatPanel(forms.Textarea):
57 """
68 """
58 Panel for text formatting. Consists of buttons to add different tags to the
69 Panel for text formatting. Consists of buttons to add different tags to the
59 form text area.
70 form text area.
60 """
71 """
61
72
62 def render(self, name, value, attrs=None):
73 def render(self, name, value, attrs=None):
63 output = '<div id="mark-panel">'
74 output = '<div id="mark-panel">'
64 for formatter in formatters:
75 for formatter in formatters:
65 output += '<span class="mark_btn"' + \
76 output += '<span class="mark_btn"' + \
66 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
77 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
67 '\', \'' + formatter.format_right + '\')">' + \
78 '\', \'' + formatter.format_right + '\')">' + \
68 formatter.preview_left + formatter.name + \
79 formatter.preview_left + formatter.name + \
69 formatter.preview_right + '</span>'
80 formatter.preview_right + '</span>'
70
81
71 output += '</div>'
82 output += '</div>'
72 output += super(FormatPanel, self).render(name, value, attrs=None)
83 output += super(FormatPanel, self).render(name, value, attrs=None)
73
84
74 return output
85 return output
75
86
76
87
77 class PlainErrorList(ErrorList):
88 class PlainErrorList(ErrorList):
78 def __unicode__(self):
89 def __unicode__(self):
79 return self.as_text()
90 return self.as_text()
80
91
81 def as_text(self):
92 def as_text(self):
82 return ''.join(['(!) %s ' % e for e in self])
93 return ''.join(['(!) %s ' % e for e in self])
83
94
84
95
85 class NeboardForm(forms.Form):
96 class NeboardForm(forms.Form):
86 """
97 """
87 Form with neboard-specific formatting.
98 Form with neboard-specific formatting.
88 """
99 """
89
100
90 def as_div(self):
101 def as_div(self):
91 """
102 """
92 Returns this form rendered as HTML <as_div>s.
103 Returns this form rendered as HTML <as_div>s.
93 """
104 """
94
105
95 return self._html_output(
106 return self._html_output(
96 # TODO Do not show hidden rows in the list here
107 # TODO Do not show hidden rows in the list here
97 normal_row='<div class="form-row">'
108 normal_row='<div class="form-row">'
98 '<div class="form-label">'
109 '<div class="form-label">'
99 '%(label)s'
110 '%(label)s'
100 '</div>'
111 '</div>'
101 '<div class="form-input">'
112 '<div class="form-input">'
102 '%(field)s'
113 '%(field)s'
103 '</div>'
114 '</div>'
104 '</div>'
115 '</div>'
105 '<div class="form-row">'
116 '<div class="form-row">'
106 '%(help_text)s'
117 '%(help_text)s'
107 '</div>',
118 '</div>',
108 error_row='<div class="form-row">'
119 error_row='<div class="form-row">'
109 '<div class="form-label"></div>'
120 '<div class="form-label"></div>'
110 '<div class="form-errors">%s</div>'
121 '<div class="form-errors">%s</div>'
111 '</div>',
122 '</div>',
112 row_ender='</div>',
123 row_ender='</div>',
113 help_text_html='%s',
124 help_text_html='%s',
114 errors_on_separate_row=True)
125 errors_on_separate_row=True)
115
126
116 def as_json_errors(self):
127 def as_json_errors(self):
117 errors = []
128 errors = []
118
129
119 for name, field in list(self.fields.items()):
130 for name, field in list(self.fields.items()):
120 if self[name].errors:
131 if self[name].errors:
121 errors.append({
132 errors.append({
122 'field': name,
133 'field': name,
123 'errors': self[name].errors.as_text(),
134 'errors': self[name].errors.as_text(),
124 })
135 })
125
136
126 return errors
137 return errors
127
138
128
139
129 class PostForm(NeboardForm):
140 class PostForm(NeboardForm):
130
141
131 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
142 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
132 label=LABEL_TITLE,
143 label=LABEL_TITLE,
133 widget=forms.TextInput(
144 widget=forms.TextInput(
134 attrs={ATTRIBUTE_PLACEHOLDER:
145 attrs={ATTRIBUTE_PLACEHOLDER:
135 'test#tripcode'}))
146 'test#tripcode'}))
136 text = forms.CharField(
147 text = forms.CharField(
137 widget=FormatPanel(attrs={
148 widget=FormatPanel(attrs={
138 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
149 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
139 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
150 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
140 }),
151 }),
141 required=False, label=LABEL_TEXT)
152 required=False, label=LABEL_TEXT)
142 file = forms.FileField(required=False, label=_('File'),
153 file = forms.FileField(required=False, label=_('File'),
143 widget=forms.ClearableFileInput(
154 widget=forms.ClearableFileInput(
144 attrs={'accept': 'file/*'}))
155 attrs={'accept': 'file/*'}))
145 file_url = forms.CharField(required=False, label=_('File URL'),
156 file_url = forms.CharField(required=False, label=_('File URL'),
146 widget=forms.TextInput(
157 widget=forms.TextInput(
147 attrs={ATTRIBUTE_PLACEHOLDER:
158 attrs={ATTRIBUTE_PLACEHOLDER:
148 'http://example.com/image.png'}))
159 'http://example.com/image.png'}))
149
160
150 # This field is for spam prevention only
161 # This field is for spam prevention only
151 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
162 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
152 widget=forms.TextInput(attrs={
163 widget=forms.TextInput(attrs={
153 'class': 'form-email'}))
164 'class': 'form-email'}))
154 threads = forms.CharField(required=False, label=_('Additional threads'),
165 threads = forms.CharField(required=False, label=_('Additional threads'),
155 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
166 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
156 '123 456 789'}))
167 '123 456 789'}))
157
168
158 session = None
169 session = None
159 need_to_ban = False
170 need_to_ban = False
160
171
161 def _update_file_extension(self, file):
172 def _update_file_extension(self, file):
162 if file:
173 if file:
163 extension = get_file_mimetype(file)
174 mimetype = get_file_mimetype(file)
164 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
175 extension = MIMETYPE_EXTENSIONS.get(mimetype)
165 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
176 if extension:
177 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
178 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
166
179
167 file.name = new_filename
180 file.name = new_filename
181 else:
182 logger = logging.getLogger('boards.forms.extension')
183
184 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
168
185
169 def clean_title(self):
186 def clean_title(self):
170 title = self.cleaned_data['title']
187 title = self.cleaned_data['title']
171 if title:
188 if title:
172 if len(title) > TITLE_MAX_LENGTH:
189 if len(title) > TITLE_MAX_LENGTH:
173 raise forms.ValidationError(_('Title must have less than %s '
190 raise forms.ValidationError(_('Title must have less than %s '
174 'characters') %
191 'characters') %
175 str(TITLE_MAX_LENGTH))
192 str(TITLE_MAX_LENGTH))
176 return title
193 return title
177
194
178 def clean_text(self):
195 def clean_text(self):
179 text = self.cleaned_data['text'].strip()
196 text = self.cleaned_data['text'].strip()
180 if text:
197 if text:
181 max_length = board_settings.get_int('Forms', 'MaxTextLength')
198 max_length = board_settings.get_int('Forms', 'MaxTextLength')
182 if len(text) > max_length:
199 if len(text) > max_length:
183 raise forms.ValidationError(_('Text must have less than %s '
200 raise forms.ValidationError(_('Text must have less than %s '
184 'characters') % str(max_length))
201 'characters') % str(max_length))
185 return text
202 return text
186
203
187 def clean_file(self):
204 def clean_file(self):
188 file = self.cleaned_data['file']
205 file = self.cleaned_data['file']
189
206
190 if file:
207 if file:
191 validate_file_size(file.size)
208 validate_file_size(file.size)
192 self._update_file_extension(file)
209 self._update_file_extension(file)
193
210
194 return file
211 return file
195
212
196 def clean_file_url(self):
213 def clean_file_url(self):
197 url = self.cleaned_data['file_url']
214 url = self.cleaned_data['file_url']
198
215
199 file = None
216 file = None
200 if url:
217 if url:
201 file = self._get_file_from_url(url)
218 file = self._get_file_from_url(url)
202
219
203 if not file:
220 if not file:
204 raise forms.ValidationError(_('Invalid URL'))
221 raise forms.ValidationError(_('Invalid URL'))
205 else:
222 else:
206 validate_file_size(file.size)
223 validate_file_size(file.size)
207 self._update_file_extension(file)
224 self._update_file_extension(file)
208
225
209 return file
226 return file
210
227
211 def clean_threads(self):
228 def clean_threads(self):
212 threads_str = self.cleaned_data['threads']
229 threads_str = self.cleaned_data['threads']
213
230
214 if len(threads_str) > 0:
231 if len(threads_str) > 0:
215 threads_id_list = threads_str.split(' ')
232 threads_id_list = threads_str.split(' ')
216
233
217 threads = list()
234 threads = list()
218
235
219 for thread_id in threads_id_list:
236 for thread_id in threads_id_list:
220 try:
237 try:
221 thread = Post.objects.get(id=int(thread_id))
238 thread = Post.objects.get(id=int(thread_id))
222 if not thread.is_opening() or thread.get_thread().archived:
239 if not thread.is_opening() or thread.get_thread().archived:
223 raise ObjectDoesNotExist()
240 raise ObjectDoesNotExist()
224 threads.append(thread)
241 threads.append(thread)
225 except (ObjectDoesNotExist, ValueError):
242 except (ObjectDoesNotExist, ValueError):
226 raise forms.ValidationError(_('Invalid additional thread list'))
243 raise forms.ValidationError(_('Invalid additional thread list'))
227
244
228 return threads
245 return threads
229
246
230 def clean(self):
247 def clean(self):
231 cleaned_data = super(PostForm, self).clean()
248 cleaned_data = super(PostForm, self).clean()
232
249
233 if cleaned_data['email']:
250 if cleaned_data['email']:
234 self.need_to_ban = True
251 self.need_to_ban = True
235 raise forms.ValidationError('A human cannot enter a hidden field')
252 raise forms.ValidationError('A human cannot enter a hidden field')
236
253
237 if not self.errors:
254 if not self.errors:
238 self._clean_text_file()
255 self._clean_text_file()
239
256
240 if not self.errors and self.session:
257 if not self.errors and self.session:
241 self._validate_posting_speed()
258 self._validate_posting_speed()
242
259
243 return cleaned_data
260 return cleaned_data
244
261
245 def get_file(self):
262 def get_file(self):
246 """
263 """
247 Gets file from form or URL.
264 Gets file from form or URL.
248 """
265 """
249
266
250 file = self.cleaned_data['file']
267 file = self.cleaned_data['file']
251 return file or self.cleaned_data['file_url']
268 return file or self.cleaned_data['file_url']
252
269
253 def get_tripcode(self):
270 def get_tripcode(self):
254 title = self.cleaned_data['title']
271 title = self.cleaned_data['title']
255 if title is not None and TRIPCODE_DELIM in title:
272 if title is not None and TRIPCODE_DELIM in title:
256 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
273 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
257 tripcode = hashlib.md5(code.encode()).hexdigest()
274 tripcode = hashlib.md5(code.encode()).hexdigest()
258 else:
275 else:
259 tripcode = ''
276 tripcode = ''
260 return tripcode
277 return tripcode
261
278
262 def get_title(self):
279 def get_title(self):
263 title = self.cleaned_data['title']
280 title = self.cleaned_data['title']
264 if title is not None and TRIPCODE_DELIM in title:
281 if title is not None and TRIPCODE_DELIM in title:
265 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
282 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
266 else:
283 else:
267 return title
284 return title
268
285
269 def _clean_text_file(self):
286 def _clean_text_file(self):
270 text = self.cleaned_data.get('text')
287 text = self.cleaned_data.get('text')
271 file = self.get_file()
288 file = self.get_file()
272
289
273 if (not text) and (not file):
290 if (not text) and (not file):
274 error_message = _('Either text or file must be entered.')
291 error_message = _('Either text or file must be entered.')
275 self._errors['text'] = self.error_class([error_message])
292 self._errors['text'] = self.error_class([error_message])
276
293
277 def _validate_posting_speed(self):
294 def _validate_posting_speed(self):
278 can_post = True
295 can_post = True
279
296
280 posting_delay = settings.POSTING_DELAY
297 posting_delay = settings.POSTING_DELAY
281
298
282 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
299 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
283 now = time.time()
300 now = time.time()
284
301
285 current_delay = 0
302 current_delay = 0
286
303
287 if LAST_POST_TIME not in self.session:
304 if LAST_POST_TIME not in self.session:
288 self.session[LAST_POST_TIME] = now
305 self.session[LAST_POST_TIME] = now
289
306
290 need_delay = True
307 need_delay = True
291 else:
308 else:
292 last_post_time = self.session.get(LAST_POST_TIME)
309 last_post_time = self.session.get(LAST_POST_TIME)
293 current_delay = int(now - last_post_time)
310 current_delay = int(now - last_post_time)
294
311
295 need_delay = current_delay < posting_delay
312 need_delay = current_delay < posting_delay
296
313
297 if need_delay:
314 if need_delay:
298 delay = posting_delay - current_delay
315 delay = posting_delay - current_delay
299 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
316 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
300 delay) % {'delay': delay}
317 delay) % {'delay': delay}
301 self._errors['text'] = self.error_class([error_message])
318 self._errors['text'] = self.error_class([error_message])
302
319
303 can_post = False
320 can_post = False
304
321
305 if can_post:
322 if can_post:
306 self.session[LAST_POST_TIME] = now
323 self.session[LAST_POST_TIME] = now
307
324
308 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
325 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
309 """
326 """
310 Gets an file file from URL.
327 Gets an file file from URL.
311 """
328 """
312
329
313 img_temp = None
330 img_temp = None
314
331
315 try:
332 try:
316 for downloader in Downloader.__subclasses__():
333 for downloader in Downloader.__subclasses__():
317 if downloader.handles(url):
334 if downloader.handles(url):
318 return downloader.download(url)
335 return downloader.download(url)
319 # If nobody of the specific downloaders handles this, use generic
336 # If nobody of the specific downloaders handles this, use generic
320 # one
337 # one
321 return Downloader.download(url)
338 return Downloader.download(url)
322 except forms.ValidationError as e:
339 except forms.ValidationError as e:
323 raise e
340 raise e
324 except Exception as e:
341 except Exception as e:
325 # Just return no file
342 # Just return no file
326 pass
343 pass
327
344
328
345
329 class ThreadForm(PostForm):
346 class ThreadForm(PostForm):
330
347
331 tags = forms.CharField(
348 tags = forms.CharField(
332 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
349 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
333 max_length=100, label=_('Tags'), required=True)
350 max_length=100, label=_('Tags'), required=True)
334
351
335 def clean_tags(self):
352 def clean_tags(self):
336 tags = self.cleaned_data['tags'].strip()
353 tags = self.cleaned_data['tags'].strip()
337
354
338 if not tags or not REGEX_TAGS.match(tags):
355 if not tags or not REGEX_TAGS.match(tags):
339 raise forms.ValidationError(
356 raise forms.ValidationError(
340 _('Inappropriate characters in tags.'))
357 _('Inappropriate characters in tags.'))
341
358
342 required_tag_exists = False
359 required_tag_exists = False
343 tag_set = set()
360 tag_set = set()
344 for tag_string in tags.split():
361 for tag_string in tags.split():
345 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
362 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
346 tag_set.add(tag)
363 tag_set.add(tag)
347
364
348 # If this is a new tag, don't check for its parents because nobody
365 # If this is a new tag, don't check for its parents because nobody
349 # added them yet
366 # added them yet
350 if not created:
367 if not created:
351 tag_set |= set(tag.get_all_parents())
368 tag_set |= set(tag.get_all_parents())
352
369
353 for tag in tag_set:
370 for tag in tag_set:
354 if tag.required:
371 if tag.required:
355 required_tag_exists = True
372 required_tag_exists = True
356 break
373 break
357
374
358 if not required_tag_exists:
375 if not required_tag_exists:
359 raise forms.ValidationError(
376 raise forms.ValidationError(
360 _('Need at least one section.'))
377 _('Need at least one section.'))
361
378
362 return tag_set
379 return tag_set
363
380
364 def clean(self):
381 def clean(self):
365 cleaned_data = super(ThreadForm, self).clean()
382 cleaned_data = super(ThreadForm, self).clean()
366
383
367 return cleaned_data
384 return cleaned_data
368
385
369
386
370 class SettingsForm(NeboardForm):
387 class SettingsForm(NeboardForm):
371
388
372 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
389 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
373 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
390 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
374 username = forms.CharField(label=_('User name'), required=False)
391 username = forms.CharField(label=_('User name'), required=False)
375 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
392 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
376
393
377 def clean_username(self):
394 def clean_username(self):
378 username = self.cleaned_data['username']
395 username = self.cleaned_data['username']
379
396
380 if username and not REGEX_TAGS.match(username):
397 if username and not REGEX_TAGS.match(username):
381 raise forms.ValidationError(_('Inappropriate characters.'))
398 raise forms.ValidationError(_('Inappropriate characters.'))
382
399
383 return username
400 return username
384
401
385
402
386 class SearchForm(NeboardForm):
403 class SearchForm(NeboardForm):
387 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
404 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,40 +1,41 b''
1 from django.db import models
1 from django.db import models
2
2
3 from boards import utils
3 from boards import utils
4 from boards.models.attachment.viewers import get_viewers, AbstractViewer
4 from boards.models.attachment.viewers import get_viewers, AbstractViewer
5 from boards.utils import get_upload_filename, get_file_mimetype
5 from boards.utils import get_upload_filename, get_file_mimetype
6
6
7
7
8 class AttachmentManager(models.Manager):
8 class AttachmentManager(models.Manager):
9 def create_with_hash(self, file):
9 def create_with_hash(self, file):
10 file_hash = utils.get_file_hash(file)
10 file_hash = utils.get_file_hash(file)
11 existing = self.filter(hash=file_hash)
11 existing = self.filter(hash=file_hash)
12 if len(existing) > 0:
12 if len(existing) > 0:
13 attachment = existing[0]
13 attachment = existing[0]
14 else:
14 else:
15 file_type = get_file_mimetype(file)
15 # FIXME Use full mimetype here, need to modify viewers too
16 file_type = get_file_mimetype(file).split('/')[-1]
16 attachment = self.create(file=file, mimetype=file_type,
17 attachment = self.create(file=file, mimetype=file_type,
17 hash=file_hash)
18 hash=file_hash)
18
19
19 return attachment
20 return attachment
20
21
21
22
22 class Attachment(models.Model):
23 class Attachment(models.Model):
23 objects = AttachmentManager()
24 objects = AttachmentManager()
24
25
25 file = models.FileField(upload_to=get_upload_filename)
26 file = models.FileField(upload_to=get_upload_filename)
26 mimetype = models.CharField(max_length=50)
27 mimetype = models.CharField(max_length=50)
27 hash = models.CharField(max_length=36)
28 hash = models.CharField(max_length=36)
28
29
29 def get_view(self):
30 def get_view(self):
30 file_viewer = None
31 file_viewer = None
31 for viewer in get_viewers():
32 for viewer in get_viewers():
32 if viewer.supports(self.mimetype):
33 if viewer.supports(self.mimetype):
33 file_viewer = viewer
34 file_viewer = viewer
34 break
35 break
35 if file_viewer is None:
36 if file_viewer is None:
36 file_viewer = AbstractViewer
37 file_viewer = AbstractViewer
37
38
38 return file_viewer(self.file, self.mimetype).get_view()
39 return file_viewer(self.file, self.mimetype).get_view()
39
40
40
41
@@ -1,181 +1,181 b''
1 /*
1 /*
2 @licstart The following is the entire license notice for the
2 @licstart The following is the entire license notice for the
3 JavaScript code in this page.
3 JavaScript code in this page.
4
4
5
5
6 Copyright (C) 2013 neko259
6 Copyright (C) 2013 neko259
7
7
8 The JavaScript code in this page is free software: you can
8 The JavaScript code in this page is free software: you can
9 redistribute it and/or modify it under the terms of the GNU
9 redistribute it and/or modify it under the terms of the GNU
10 General Public License (GNU GPL) as published by the Free Software
10 General Public License (GNU GPL) as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option)
11 Foundation, either version 3 of the License, or (at your option)
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
12 any later version. The code is distributed WITHOUT ANY WARRANTY;
13 without even the implied warranty of MERCHANTABILITY or FITNESS
13 without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
14 FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
15
15
16 As additional permission under GNU GPL version 3 section 7, you
16 As additional permission under GNU GPL version 3 section 7, you
17 may distribute non-source (e.g., minimized or compacted) forms of
17 may distribute non-source (e.g., minimized or compacted) forms of
18 that code without the copy of the GNU GPL normally required by
18 that code without the copy of the GNU GPL normally required by
19 section 4, provided you include this license notice and a URL
19 section 4, provided you include this license notice and a URL
20 through which recipients can access the Corresponding Source.
20 through which recipients can access the Corresponding Source.
21
21
22 @licend The above is the entire license notice
22 @licend The above is the entire license notice
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var IMAGE_POPUP_MARGIN = 10;
26 var IMAGE_POPUP_MARGIN = 10;
27
27
28
28
29 var IMAGE_VIEWERS = [
29 var IMAGE_VIEWERS = [
30 ['simple', new SimpleImageViewer()],
30 ['simple', new SimpleImageViewer()],
31 ['popup', new PopupImageViewer()]
31 ['popup', new PopupImageViewer()]
32 ];
32 ];
33
33
34 var FULL_IMG_CLASS = 'post-image-full';
34 var FULL_IMG_CLASS = 'post-image-full';
35
35
36 var ATTR_SCALE = 'scale';
36 var ATTR_SCALE = 'scale';
37
37
38
38
39 function ImageViewer() {}
39 function ImageViewer() {}
40 ImageViewer.prototype.view = function (post) {};
40 ImageViewer.prototype.view = function (post) {};
41
41
42 function SimpleImageViewer() {}
42 function SimpleImageViewer() {}
43 SimpleImageViewer.prototype.view = function (post) {
43 SimpleImageViewer.prototype.view = function (post) {
44 var images = post.find('img');
44 var images = post.find('img');
45 images.toggle();
45 images.toggle();
46
46
47 // When we first enlarge an image, a full image needs to be created
47 // When we first enlarge an image, a full image needs to be created
48 if (images.length == 1) {
48 if (images.length == 1) {
49 var thumb = images.first();
49 var thumb = images.first();
50
50
51 var width = thumb.attr('data-width');
51 var width = thumb.attr('data-width');
52 var height = thumb.attr('data-height');
52 var height = thumb.attr('data-height');
53
53
54 if (width == null || height == null) {
54 if (width == null || height == null) {
55 width = '100%';
55 width = '100%';
56 height = '100%';
56 height = '100%';
57 }
57 }
58
58
59 var parent = images.first().parent();
59 var parent = images.first().parent();
60 var link = parent.attr('href');
60 var link = parent.attr('href');
61
61
62 var fullImg = $('<img />')
62 var fullImg = $('<img />')
63 .addClass(FULL_IMG_CLASS)
63 .addClass(FULL_IMG_CLASS)
64 .attr('src', link)
64 .attr('src', link)
65 .attr('width', width)
65 .attr('width', width)
66 .attr('height', height);
66 .attr('height', height);
67
67
68 parent.append(fullImg);
68 parent.append(fullImg);
69 }
69 }
70 };
70 };
71
71
72 function PopupImageViewer() {}
72 function PopupImageViewer() {}
73 PopupImageViewer.prototype.view = function (post) {
73 PopupImageViewer.prototype.view = function (post) {
74 var el = post;
74 var el = post;
75 var thumb_id = 'full' + el.find('img').attr('alt');
75 var thumb_id = 'full' + el.find('img').attr('alt');
76
76
77 var existingPopups = $('#' + thumb_id);
77 var existingPopups = $('#' + thumb_id);
78 if (!existingPopups.length) {
78 if (!existingPopups.length) {
79 var imgElement= el.find('img');
79 var imgElement= el.find('img');
80
80
81 var full_img_w = imgElement.attr('data-width');
81 var full_img_w = imgElement.attr('data-width');
82 var full_img_h = imgElement.attr('data-height');
82 var full_img_h = imgElement.attr('data-height');
83
83
84 var win = $(window);
84 var win = $(window);
85
85
86 var win_w = win.width();
86 var win_w = win.width();
87 var win_h = win.height();
87 var win_h = win.height();
88
88
89 // New image size
89 // New image size
90 var w_scale = 1;
90 var w_scale = 1;
91 var h_scale = 1;
91 var h_scale = 1;
92
92
93 var freeWidth = win_w - 2 * IMAGE_POPUP_MARGIN;
93 var freeWidth = win_w - 2 * IMAGE_POPUP_MARGIN;
94 var freeHeight = win_h - 2 * IMAGE_POPUP_MARGIN;
94 var freeHeight = win_h - 2 * IMAGE_POPUP_MARGIN;
95
95
96 if (full_img_w > freeWidth) {
96 if (full_img_w > freeWidth) {
97 w_scale = full_img_w / freeWidth;
97 w_scale = full_img_w / freeWidth;
98 }
98 }
99 if (full_img_h > freeHeight) {
99 if (full_img_h > freeHeight) {
100 h_scale = full_img_h / freeHeight;
100 h_scale = full_img_h / freeHeight;
101 }
101 }
102
102
103 var scale = Math.max(w_scale, h_scale)
103 var scale = Math.max(w_scale, h_scale)
104 var img_w = full_img_w / scale;
104 var img_w = full_img_w / scale;
105 var img_h = full_img_h / scale;
105 var img_h = full_img_h / scale;
106
106
107 var postNode = $(el);
107 var postNode = $(el);
108
108
109 var img_pv = new Image();
109 var img_pv = new Image();
110 var newImage = $(img_pv);
110 var newImage = $(img_pv);
111 newImage.addClass('img-full')
111 newImage.addClass('img-full')
112 .attr('id', thumb_id)
112 .attr('id', thumb_id)
113 .attr('src', postNode.attr('href'))
113 .attr('src', postNode.attr('href'))
114 .attr(ATTR_SCALE, scale)
114 .attr(ATTR_SCALE, scale)
115 .appendTo(postNode)
116 .css({
115 .css({
117 'width': img_w,
116 'width': img_w,
118 'height': img_h,
117 'height': img_h,
119 'left': (win_w - img_w) / 2,
118 'left': (win_w - img_w) / 2,
120 'top': ((win_h - img_h) / 2)
119 'top': ((win_h - img_h) / 2)
121 })
120 })
121 .appendTo(postNode)
122 //scaling preview
122 //scaling preview
123 .mousewheel(function(event, delta) {
123 .mousewheel(function(event, delta) {
124 var cx = event.originalEvent.clientX;
124 var cx = event.originalEvent.clientX;
125 var cy = event.originalEvent.clientY;
125 var cy = event.originalEvent.clientY;
126
126
127 var scale = newImage.attr(ATTR_SCALE) / (delta > 0 ? 1.25 : 0.8);
127 var scale = newImage.attr(ATTR_SCALE) / (delta > 0 ? 1.25 : 0.8);
128
128
129 var oldWidth = newImage.width();
129 var oldWidth = newImage.width();
130 var oldHeight = newImage.height();
130 var oldHeight = newImage.height();
131
131
132 var newIW = full_img_w / scale;
132 var newIW = full_img_w / scale;
133 var newIH = full_img_h / scale;
133 var newIH = full_img_h / scale;
134
134
135 newImage.width(newIW);
135 newImage.width(newIW);
136 newImage.height(newIH);
136 newImage.height(newIH);
137 newImage.attr(ATTR_SCALE, scale);
137 newImage.attr(ATTR_SCALE, scale);
138
138
139 // Set position
139 // Set position
140 var oldPosition = newImage.position();
140 var oldPosition = newImage.position();
141 newImage.css({
141 newImage.css({
142 left: parseInt(cx - (newIW/oldWidth) * (cx - parseInt(oldPosition.left, 10)), 10),
142 left: parseInt(cx - (newIW/oldWidth) * (cx - parseInt(oldPosition.left, 10)), 10),
143 top: parseInt(cy - (newIH/oldHeight) * (cy - parseInt(oldPosition.top, 10)), 10)
143 top: parseInt(cy - (newIH/oldHeight) * (cy - parseInt(oldPosition.top, 10)), 10)
144 });
144 });
145
145
146 return false;
146 return false;
147 }
147 }
148 )
148 )
149 .draggable({
149 .draggable({
150 addClasses: false,
150 addClasses: false,
151 stack: '.img-full'
151 stack: '.img-full'
152 });
152 });
153 } else {
153 } else {
154 existingPopups.remove();
154 existingPopups.remove();
155 }
155 }
156 };
156 };
157
157
158 function addImgPreview() {
158 function addImgPreview() {
159 var viewerName = $('body').attr('data-image-viewer');
159 var viewerName = $('body').attr('data-image-viewer');
160 var viewer = ImageViewer();
160 var viewer = ImageViewer();
161 for (var i = 0; i < IMAGE_VIEWERS.length; i++) {
161 for (var i = 0; i < IMAGE_VIEWERS.length; i++) {
162 var item = IMAGE_VIEWERS[i];
162 var item = IMAGE_VIEWERS[i];
163 if (item[0] === viewerName) {
163 if (item[0] === viewerName) {
164 viewer = item[1];
164 viewer = item[1];
165 break;
165 break;
166 }
166 }
167 }
167 }
168
168
169 //keybind
169 //keybind
170 $(document).on('keyup.removepic', function(e) {
170 $(document).on('keyup.removepic', function(e) {
171 if(e.which === 27) {
171 if(e.which === 27) {
172 $('.img-full').remove();
172 $('.img-full').remove();
173 }
173 }
174 });
174 });
175
175
176 $('body').on('click', '.thumb', function() {
176 $('body').on('click', '.thumb', function() {
177 viewer.view($(this));
177 viewer.view($(this));
178
178
179 return false;
179 return false;
180 });
180 });
181 }
181 }
@@ -1,148 +1,144 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
4 import hashlib
5 from random import random
5 from random import random
6 import time
6 import time
7 import hmac
7 import hmac
8
8
9 from django.core.cache import cache
9 from django.core.cache import cache
10 from django.db.models import Model
10 from django.db.models import Model
11 from django import forms
11 from django import forms
12 from django.utils import timezone
12 from django.utils import timezone
13 from django.utils.translation import ugettext_lazy as _
13 from django.utils.translation import ugettext_lazy as _
14 import magic
14 import magic
15 from portage import os
15 from portage import os
16
16
17 import boards
17 import boards
18 from boards.settings import get_bool
18 from boards.settings import get_bool
19 from neboard import settings
19 from neboard import settings
20
20
21 CACHE_KEY_DELIMITER = '_'
21 CACHE_KEY_DELIMITER = '_'
22 PERMISSION_MODERATE = 'moderation'
22 PERMISSION_MODERATE = 'moderation'
23
23
24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
25 META_REMOTE_ADDR = 'REMOTE_ADDR'
25 META_REMOTE_ADDR = 'REMOTE_ADDR'
26
26
27 SETTING_MESSAGES = 'Messages'
27 SETTING_MESSAGES = 'Messages'
28 SETTING_ANON_MODE = 'AnonymousMode'
28 SETTING_ANON_MODE = 'AnonymousMode'
29
29
30 ANON_IP = '127.0.0.1'
30 ANON_IP = '127.0.0.1'
31
31
32 UPLOAD_DIRS ={
32 UPLOAD_DIRS ={
33 'PostImage': 'images/',
33 'PostImage': 'images/',
34 'Attachment': 'files/',
34 'Attachment': 'files/',
35 }
35 }
36 FILE_EXTENSION_DELIMITER = '.'
36 FILE_EXTENSION_DELIMITER = '.'
37
37
38
38
39 def is_anonymous_mode():
39 def is_anonymous_mode():
40 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
40 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
41
41
42
42
43 def get_client_ip(request):
43 def get_client_ip(request):
44 if is_anonymous_mode():
44 if is_anonymous_mode():
45 ip = ANON_IP
45 ip = ANON_IP
46 else:
46 else:
47 x_forwarded_for = request.META.get(HTTP_FORWARDED)
47 x_forwarded_for = request.META.get(HTTP_FORWARDED)
48 if x_forwarded_for:
48 if x_forwarded_for:
49 ip = x_forwarded_for.split(',')[-1].strip()
49 ip = x_forwarded_for.split(',')[-1].strip()
50 else:
50 else:
51 ip = request.META.get(META_REMOTE_ADDR)
51 ip = request.META.get(META_REMOTE_ADDR)
52 return ip
52 return ip
53
53
54
54
55 # TODO The output format is not epoch because it includes microseconds
55 # TODO The output format is not epoch because it includes microseconds
56 def datetime_to_epoch(datetime):
56 def datetime_to_epoch(datetime):
57 return int(time.mktime(timezone.localtime(
57 return int(time.mktime(timezone.localtime(
58 datetime,timezone.get_current_timezone()).timetuple())
58 datetime,timezone.get_current_timezone()).timetuple())
59 * 1000000 + datetime.microsecond)
59 * 1000000 + datetime.microsecond)
60
60
61
61
62 def get_websocket_token(user_id='', timestamp=''):
62 def get_websocket_token(user_id='', timestamp=''):
63 """
63 """
64 Create token to validate information provided by new connection.
64 Create token to validate information provided by new connection.
65 """
65 """
66
66
67 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
67 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
68 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
68 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
69 sign.update(user_id.encode())
69 sign.update(user_id.encode())
70 sign.update(timestamp.encode())
70 sign.update(timestamp.encode())
71 token = sign.hexdigest()
71 token = sign.hexdigest()
72
72
73 return token
73 return token
74
74
75
75
76 def cached_result(key_method=None):
76 def cached_result(key_method=None):
77 """
77 """
78 Caches method result in the Django's cache system, persisted by object name,
78 Caches method result in the Django's cache system, persisted by object name,
79 object name and model id if object is a Django model.
79 object name and model id if object is a Django model.
80 """
80 """
81 def _cached_result(function):
81 def _cached_result(function):
82 def inner_func(obj, *args, **kwargs):
82 def inner_func(obj, *args, **kwargs):
83 # TODO Include method arguments to the cache key
83 # TODO Include method arguments to the cache key
84 cache_key_params = [obj.__class__.__name__, function.__name__]
84 cache_key_params = [obj.__class__.__name__, function.__name__]
85 if isinstance(obj, Model):
85 if isinstance(obj, Model):
86 cache_key_params.append(str(obj.id))
86 cache_key_params.append(str(obj.id))
87
87
88 if key_method is not None:
88 if key_method is not None:
89 cache_key_params += [str(arg) for arg in key_method(obj)]
89 cache_key_params += [str(arg) for arg in key_method(obj)]
90
90
91 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
91 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
92
92
93 persisted_result = cache.get(cache_key)
93 persisted_result = cache.get(cache_key)
94 if persisted_result is not None:
94 if persisted_result is not None:
95 result = persisted_result
95 result = persisted_result
96 else:
96 else:
97 result = function(obj, *args, **kwargs)
97 result = function(obj, *args, **kwargs)
98 cache.set(cache_key, result)
98 cache.set(cache_key, result)
99
99
100 return result
100 return result
101
101
102 return inner_func
102 return inner_func
103 return _cached_result
103 return _cached_result
104
104
105
105
106 def is_moderator(request):
106 def is_moderator(request):
107 try:
107 try:
108 moderate = request.user.has_perm(PERMISSION_MODERATE)
108 moderate = request.user.has_perm(PERMISSION_MODERATE)
109 except AttributeError:
109 except AttributeError:
110 moderate = False
110 moderate = False
111
111
112 return moderate
112 return moderate
113
113
114
114
115 def get_file_hash(file) -> str:
115 def get_file_hash(file) -> str:
116 md5 = hashlib.md5()
116 md5 = hashlib.md5()
117 for chunk in file.chunks():
117 for chunk in file.chunks():
118 md5.update(chunk)
118 md5.update(chunk)
119 return md5.hexdigest()
119 return md5.hexdigest()
120
120
121
121
122 def validate_file_size(size: int):
122 def validate_file_size(size: int):
123 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
123 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
124 if size > max_size:
124 if size > max_size:
125 raise forms.ValidationError(
125 raise forms.ValidationError(
126 _('File must be less than %s bytes')
126 _('File must be less than %s bytes')
127 % str(max_size))
127 % str(max_size))
128
128
129
129
130 def get_upload_filename(model_instance, old_filename):
130 def get_upload_filename(model_instance, old_filename):
131 # TODO Use something other than random number in file name
131 # TODO Use something other than random number in file name
132 if hasattr(model_instance, 'mimetype'):
132 extension = old_filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
133 extension = model_instance.mimetype
134 else:
135 extension = old_filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
136 new_name = '{}{}.{}'.format(
133 new_name = '{}{}.{}'.format(
137 str(int(time.mktime(time.gmtime()))),
134 str(int(time.mktime(time.gmtime()))),
138 str(int(random() * 1000)),
135 str(int(random() * 1000)),
139 extension)
136 extension)
140
137
141 directory = UPLOAD_DIRS[type(model_instance).__name__]
138 directory = UPLOAD_DIRS[type(model_instance).__name__]
142
139
143 return os.path.join(directory, new_name)
140 return os.path.join(directory, new_name)
144
141
145
142
146 def get_file_mimetype(file) -> str:
143 def get_file_mimetype(file) -> str:
147 return magic.from_buffer(file.chunks().__next__(), mime=True) \
144 return magic.from_buffer(file.chunks().__next__(), mime=True).decode()
148 .decode().split('/')[-1]
General Comments 0
You need to be logged in to leave comments. Login now