##// END OF EJS Templates
Recognize flash mimetype properly. Store extension as mimetype in attachment...
neko259 -
r1382:8669bd71 default
parent child Browse files
Show More
@@ -1,405 +1,406 b''
1 import hashlib
1 import hashlib
2 import re
2 import re
3 import time
3 import time
4 import logging
4 import logging
5 import pytz
5 import pytz
6
6
7 from django import forms
7 from django import forms
8 from django.core.files.uploadedfile import SimpleUploadedFile
8 from django.core.files.uploadedfile import SimpleUploadedFile
9 from django.core.exceptions import ObjectDoesNotExist
9 from django.core.exceptions import ObjectDoesNotExist
10 from django.forms.util import ErrorList
10 from django.forms.util import ErrorList
11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
12
12
13 from boards.mdx_neboard import formatters
13 from boards.mdx_neboard import formatters
14 from boards.models.attachment.downloaders import Downloader
14 from boards.models.attachment.downloaders import Downloader
15 from boards.models.post import TITLE_MAX_LENGTH
15 from boards.models.post import TITLE_MAX_LENGTH
16 from boards.models import Tag, Post
16 from boards.models import Tag, Post
17 from boards.utils import validate_file_size, get_file_mimetype, \
17 from boards.utils import validate_file_size, get_file_mimetype, \
18 FILE_EXTENSION_DELIMITER
18 FILE_EXTENSION_DELIMITER
19 from neboard import settings
19 from neboard import settings
20 import boards.settings as board_settings
20 import boards.settings as board_settings
21 import neboard
21 import neboard
22
22
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
23 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
24
24
25 VETERAN_POSTING_DELAY = 5
25 VETERAN_POSTING_DELAY = 5
26
26
27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
27 ATTRIBUTE_PLACEHOLDER = 'placeholder'
28 ATTRIBUTE_ROWS = 'rows'
28 ATTRIBUTE_ROWS = 'rows'
29
29
30 LAST_POST_TIME = 'last_post_time'
30 LAST_POST_TIME = 'last_post_time'
31 LAST_LOGIN_TIME = 'last_login_time'
31 LAST_LOGIN_TIME = 'last_login_time'
32 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.')
33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
33 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
34
34
35 LABEL_TITLE = _('Title')
35 LABEL_TITLE = _('Title')
36 LABEL_TEXT = _('Text')
36 LABEL_TEXT = _('Text')
37 LABEL_TAG = _('Tag')
37 LABEL_TAG = _('Tag')
38 LABEL_SEARCH = _('Search')
38 LABEL_SEARCH = _('Search')
39
39
40 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
40 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
41 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
41 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
42
42
43 TAG_MAX_LENGTH = 20
43 TAG_MAX_LENGTH = 20
44
44
45 TEXTAREA_ROWS = 4
45 TEXTAREA_ROWS = 4
46
46
47 TRIPCODE_DELIM = '#'
47 TRIPCODE_DELIM = '#'
48
48
49 # TODO Maybe this may be converted into the database table?
49 # TODO Maybe this may be converted into the database table?
50 MIMETYPE_EXTENSIONS = {
50 MIMETYPE_EXTENSIONS = {
51 'image/jpeg': 'jpeg',
51 'image/jpeg': 'jpeg',
52 'image/png': 'png',
52 'image/png': 'png',
53 'image/gif': 'gif',
53 'image/gif': 'gif',
54 'video/webm': 'webm',
54 'video/webm': 'webm',
55 'application/pdf': 'pdf',
55 'application/pdf': 'pdf',
56 'x-diff': 'diff',
56 'x-diff': 'diff',
57 'image/svg+xml': 'svg',
57 'image/svg+xml': 'svg',
58 'application/x-shockwave-flash': 'swf',
58 }
59 }
59
60
60
61
61 def get_timezones():
62 def get_timezones():
62 timezones = []
63 timezones = []
63 for tz in pytz.common_timezones:
64 for tz in pytz.common_timezones:
64 timezones.append((tz, tz),)
65 timezones.append((tz, tz),)
65 return timezones
66 return timezones
66
67
67
68
68 class FormatPanel(forms.Textarea):
69 class FormatPanel(forms.Textarea):
69 """
70 """
70 Panel for text formatting. Consists of buttons to add different tags to the
71 Panel for text formatting. Consists of buttons to add different tags to the
71 form text area.
72 form text area.
72 """
73 """
73
74
74 def render(self, name, value, attrs=None):
75 def render(self, name, value, attrs=None):
75 output = '<div id="mark-panel">'
76 output = '<div id="mark-panel">'
76 for formatter in formatters:
77 for formatter in formatters:
77 output += '<span class="mark_btn"' + \
78 output += '<span class="mark_btn"' + \
78 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
79 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
79 '\', \'' + formatter.format_right + '\')">' + \
80 '\', \'' + formatter.format_right + '\')">' + \
80 formatter.preview_left + formatter.name + \
81 formatter.preview_left + formatter.name + \
81 formatter.preview_right + '</span>'
82 formatter.preview_right + '</span>'
82
83
83 output += '</div>'
84 output += '</div>'
84 output += super(FormatPanel, self).render(name, value, attrs=None)
85 output += super(FormatPanel, self).render(name, value, attrs=None)
85
86
86 return output
87 return output
87
88
88
89
89 class PlainErrorList(ErrorList):
90 class PlainErrorList(ErrorList):
90 def __unicode__(self):
91 def __unicode__(self):
91 return self.as_text()
92 return self.as_text()
92
93
93 def as_text(self):
94 def as_text(self):
94 return ''.join(['(!) %s ' % e for e in self])
95 return ''.join(['(!) %s ' % e for e in self])
95
96
96
97
97 class NeboardForm(forms.Form):
98 class NeboardForm(forms.Form):
98 """
99 """
99 Form with neboard-specific formatting.
100 Form with neboard-specific formatting.
100 """
101 """
101
102
102 def as_div(self):
103 def as_div(self):
103 """
104 """
104 Returns this form rendered as HTML <as_div>s.
105 Returns this form rendered as HTML <as_div>s.
105 """
106 """
106
107
107 return self._html_output(
108 return self._html_output(
108 # TODO Do not show hidden rows in the list here
109 # TODO Do not show hidden rows in the list here
109 normal_row='<div class="form-row">'
110 normal_row='<div class="form-row">'
110 '<div class="form-label">'
111 '<div class="form-label">'
111 '%(label)s'
112 '%(label)s'
112 '</div>'
113 '</div>'
113 '<div class="form-input">'
114 '<div class="form-input">'
114 '%(field)s'
115 '%(field)s'
115 '</div>'
116 '</div>'
116 '</div>'
117 '</div>'
117 '<div class="form-row">'
118 '<div class="form-row">'
118 '%(help_text)s'
119 '%(help_text)s'
119 '</div>',
120 '</div>',
120 error_row='<div class="form-row">'
121 error_row='<div class="form-row">'
121 '<div class="form-label"></div>'
122 '<div class="form-label"></div>'
122 '<div class="form-errors">%s</div>'
123 '<div class="form-errors">%s</div>'
123 '</div>',
124 '</div>',
124 row_ender='</div>',
125 row_ender='</div>',
125 help_text_html='%s',
126 help_text_html='%s',
126 errors_on_separate_row=True)
127 errors_on_separate_row=True)
127
128
128 def as_json_errors(self):
129 def as_json_errors(self):
129 errors = []
130 errors = []
130
131
131 for name, field in list(self.fields.items()):
132 for name, field in list(self.fields.items()):
132 if self[name].errors:
133 if self[name].errors:
133 errors.append({
134 errors.append({
134 'field': name,
135 'field': name,
135 'errors': self[name].errors.as_text(),
136 'errors': self[name].errors.as_text(),
136 })
137 })
137
138
138 return errors
139 return errors
139
140
140
141
141 class PostForm(NeboardForm):
142 class PostForm(NeboardForm):
142
143
143 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
144 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
144 label=LABEL_TITLE,
145 label=LABEL_TITLE,
145 widget=forms.TextInput(
146 widget=forms.TextInput(
146 attrs={ATTRIBUTE_PLACEHOLDER:
147 attrs={ATTRIBUTE_PLACEHOLDER:
147 'test#tripcode'}))
148 'test#tripcode'}))
148 text = forms.CharField(
149 text = forms.CharField(
149 widget=FormatPanel(attrs={
150 widget=FormatPanel(attrs={
150 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
151 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
151 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
152 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
152 }),
153 }),
153 required=False, label=LABEL_TEXT)
154 required=False, label=LABEL_TEXT)
154 file = forms.FileField(required=False, label=_('File'),
155 file = forms.FileField(required=False, label=_('File'),
155 widget=forms.ClearableFileInput(
156 widget=forms.ClearableFileInput(
156 attrs={'accept': 'file/*'}))
157 attrs={'accept': 'file/*'}))
157 file_url = forms.CharField(required=False, label=_('File URL'),
158 file_url = forms.CharField(required=False, label=_('File URL'),
158 widget=forms.TextInput(
159 widget=forms.TextInput(
159 attrs={ATTRIBUTE_PLACEHOLDER:
160 attrs={ATTRIBUTE_PLACEHOLDER:
160 'http://example.com/image.png'}))
161 'http://example.com/image.png'}))
161
162
162 # This field is for spam prevention only
163 # This field is for spam prevention only
163 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
164 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
164 widget=forms.TextInput(attrs={
165 widget=forms.TextInput(attrs={
165 'class': 'form-email'}))
166 'class': 'form-email'}))
166 threads = forms.CharField(required=False, label=_('Additional threads'),
167 threads = forms.CharField(required=False, label=_('Additional threads'),
167 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
168 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
168 '123 456 789'}))
169 '123 456 789'}))
169
170
170 session = None
171 session = None
171 need_to_ban = False
172 need_to_ban = False
172
173
173 def _update_file_extension(self, file):
174 def _update_file_extension(self, file):
174 if file:
175 if file:
175 mimetype = get_file_mimetype(file)
176 mimetype = get_file_mimetype(file)
176 extension = MIMETYPE_EXTENSIONS.get(mimetype)
177 extension = MIMETYPE_EXTENSIONS.get(mimetype)
177 if extension:
178 if extension:
178 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
179 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
179 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
180 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
180
181
181 file.name = new_filename
182 file.name = new_filename
182 else:
183 else:
183 logger = logging.getLogger('boards.forms.extension')
184 logger = logging.getLogger('boards.forms.extension')
184
185
185 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
186 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
186
187
187 def clean_title(self):
188 def clean_title(self):
188 title = self.cleaned_data['title']
189 title = self.cleaned_data['title']
189 if title:
190 if title:
190 if len(title) > TITLE_MAX_LENGTH:
191 if len(title) > TITLE_MAX_LENGTH:
191 raise forms.ValidationError(_('Title must have less than %s '
192 raise forms.ValidationError(_('Title must have less than %s '
192 'characters') %
193 'characters') %
193 str(TITLE_MAX_LENGTH))
194 str(TITLE_MAX_LENGTH))
194 return title
195 return title
195
196
196 def clean_text(self):
197 def clean_text(self):
197 text = self.cleaned_data['text'].strip()
198 text = self.cleaned_data['text'].strip()
198 if text:
199 if text:
199 max_length = board_settings.get_int('Forms', 'MaxTextLength')
200 max_length = board_settings.get_int('Forms', 'MaxTextLength')
200 if len(text) > max_length:
201 if len(text) > max_length:
201 raise forms.ValidationError(_('Text must have less than %s '
202 raise forms.ValidationError(_('Text must have less than %s '
202 'characters') % str(max_length))
203 'characters') % str(max_length))
203 return text
204 return text
204
205
205 def clean_file(self):
206 def clean_file(self):
206 file = self.cleaned_data['file']
207 file = self.cleaned_data['file']
207
208
208 if file:
209 if file:
209 validate_file_size(file.size)
210 validate_file_size(file.size)
210 self._update_file_extension(file)
211 self._update_file_extension(file)
211
212
212 return file
213 return file
213
214
214 def clean_file_url(self):
215 def clean_file_url(self):
215 url = self.cleaned_data['file_url']
216 url = self.cleaned_data['file_url']
216
217
217 file = None
218 file = None
218 if url:
219 if url:
219 file = self._get_file_from_url(url)
220 file = self._get_file_from_url(url)
220
221
221 if not file:
222 if not file:
222 raise forms.ValidationError(_('Invalid URL'))
223 raise forms.ValidationError(_('Invalid URL'))
223 else:
224 else:
224 validate_file_size(file.size)
225 validate_file_size(file.size)
225 self._update_file_extension(file)
226 self._update_file_extension(file)
226
227
227 return file
228 return file
228
229
229 def clean_threads(self):
230 def clean_threads(self):
230 threads_str = self.cleaned_data['threads']
231 threads_str = self.cleaned_data['threads']
231
232
232 if len(threads_str) > 0:
233 if len(threads_str) > 0:
233 threads_id_list = threads_str.split(' ')
234 threads_id_list = threads_str.split(' ')
234
235
235 threads = list()
236 threads = list()
236
237
237 for thread_id in threads_id_list:
238 for thread_id in threads_id_list:
238 try:
239 try:
239 thread = Post.objects.get(id=int(thread_id))
240 thread = Post.objects.get(id=int(thread_id))
240 if not thread.is_opening() or thread.get_thread().archived:
241 if not thread.is_opening() or thread.get_thread().archived:
241 raise ObjectDoesNotExist()
242 raise ObjectDoesNotExist()
242 threads.append(thread)
243 threads.append(thread)
243 except (ObjectDoesNotExist, ValueError):
244 except (ObjectDoesNotExist, ValueError):
244 raise forms.ValidationError(_('Invalid additional thread list'))
245 raise forms.ValidationError(_('Invalid additional thread list'))
245
246
246 return threads
247 return threads
247
248
248 def clean(self):
249 def clean(self):
249 cleaned_data = super(PostForm, self).clean()
250 cleaned_data = super(PostForm, self).clean()
250
251
251 if cleaned_data['email']:
252 if cleaned_data['email']:
252 self.need_to_ban = True
253 self.need_to_ban = True
253 raise forms.ValidationError('A human cannot enter a hidden field')
254 raise forms.ValidationError('A human cannot enter a hidden field')
254
255
255 if not self.errors:
256 if not self.errors:
256 self._clean_text_file()
257 self._clean_text_file()
257
258
258 if not self.errors and self.session:
259 if not self.errors and self.session:
259 self._validate_posting_speed()
260 self._validate_posting_speed()
260
261
261 return cleaned_data
262 return cleaned_data
262
263
263 def get_file(self):
264 def get_file(self):
264 """
265 """
265 Gets file from form or URL.
266 Gets file from form or URL.
266 """
267 """
267
268
268 file = self.cleaned_data['file']
269 file = self.cleaned_data['file']
269 return file or self.cleaned_data['file_url']
270 return file or self.cleaned_data['file_url']
270
271
271 def get_tripcode(self):
272 def get_tripcode(self):
272 title = self.cleaned_data['title']
273 title = self.cleaned_data['title']
273 if title is not None and TRIPCODE_DELIM in title:
274 if title is not None and TRIPCODE_DELIM in title:
274 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
275 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
275 tripcode = hashlib.md5(code.encode()).hexdigest()
276 tripcode = hashlib.md5(code.encode()).hexdigest()
276 else:
277 else:
277 tripcode = ''
278 tripcode = ''
278 return tripcode
279 return tripcode
279
280
280 def get_title(self):
281 def get_title(self):
281 title = self.cleaned_data['title']
282 title = self.cleaned_data['title']
282 if title is not None and TRIPCODE_DELIM in title:
283 if title is not None and TRIPCODE_DELIM in title:
283 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
284 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
284 else:
285 else:
285 return title
286 return title
286
287
287 def _clean_text_file(self):
288 def _clean_text_file(self):
288 text = self.cleaned_data.get('text')
289 text = self.cleaned_data.get('text')
289 file = self.get_file()
290 file = self.get_file()
290
291
291 if (not text) and (not file):
292 if (not text) and (not file):
292 error_message = _('Either text or file must be entered.')
293 error_message = _('Either text or file must be entered.')
293 self._errors['text'] = self.error_class([error_message])
294 self._errors['text'] = self.error_class([error_message])
294
295
295 def _validate_posting_speed(self):
296 def _validate_posting_speed(self):
296 can_post = True
297 can_post = True
297
298
298 posting_delay = settings.POSTING_DELAY
299 posting_delay = settings.POSTING_DELAY
299
300
300 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
301 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
301 now = time.time()
302 now = time.time()
302
303
303 current_delay = 0
304 current_delay = 0
304
305
305 if LAST_POST_TIME not in self.session:
306 if LAST_POST_TIME not in self.session:
306 self.session[LAST_POST_TIME] = now
307 self.session[LAST_POST_TIME] = now
307
308
308 need_delay = True
309 need_delay = True
309 else:
310 else:
310 last_post_time = self.session.get(LAST_POST_TIME)
311 last_post_time = self.session.get(LAST_POST_TIME)
311 current_delay = int(now - last_post_time)
312 current_delay = int(now - last_post_time)
312
313
313 need_delay = current_delay < posting_delay
314 need_delay = current_delay < posting_delay
314
315
315 if need_delay:
316 if need_delay:
316 delay = posting_delay - current_delay
317 delay = posting_delay - current_delay
317 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
318 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
318 delay) % {'delay': delay}
319 delay) % {'delay': delay}
319 self._errors['text'] = self.error_class([error_message])
320 self._errors['text'] = self.error_class([error_message])
320
321
321 can_post = False
322 can_post = False
322
323
323 if can_post:
324 if can_post:
324 self.session[LAST_POST_TIME] = now
325 self.session[LAST_POST_TIME] = now
325
326
326 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
327 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
327 """
328 """
328 Gets an file file from URL.
329 Gets an file file from URL.
329 """
330 """
330
331
331 img_temp = None
332 img_temp = None
332
333
333 try:
334 try:
334 for downloader in Downloader.__subclasses__():
335 for downloader in Downloader.__subclasses__():
335 if downloader.handles(url):
336 if downloader.handles(url):
336 return downloader.download(url)
337 return downloader.download(url)
337 # If nobody of the specific downloaders handles this, use generic
338 # If nobody of the specific downloaders handles this, use generic
338 # one
339 # one
339 return Downloader.download(url)
340 return Downloader.download(url)
340 except forms.ValidationError as e:
341 except forms.ValidationError as e:
341 raise e
342 raise e
342 except Exception as e:
343 except Exception as e:
343 # Just return no file
344 # Just return no file
344 pass
345 pass
345
346
346
347
347 class ThreadForm(PostForm):
348 class ThreadForm(PostForm):
348
349
349 tags = forms.CharField(
350 tags = forms.CharField(
350 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
351 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
351 max_length=100, label=_('Tags'), required=True)
352 max_length=100, label=_('Tags'), required=True)
352
353
353 def clean_tags(self):
354 def clean_tags(self):
354 tags = self.cleaned_data['tags'].strip()
355 tags = self.cleaned_data['tags'].strip()
355
356
356 if not tags or not REGEX_TAGS.match(tags):
357 if not tags or not REGEX_TAGS.match(tags):
357 raise forms.ValidationError(
358 raise forms.ValidationError(
358 _('Inappropriate characters in tags.'))
359 _('Inappropriate characters in tags.'))
359
360
360 required_tag_exists = False
361 required_tag_exists = False
361 tag_set = set()
362 tag_set = set()
362 for tag_string in tags.split():
363 for tag_string in tags.split():
363 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
364 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
364 tag_set.add(tag)
365 tag_set.add(tag)
365
366
366 # If this is a new tag, don't check for its parents because nobody
367 # If this is a new tag, don't check for its parents because nobody
367 # added them yet
368 # added them yet
368 if not created:
369 if not created:
369 tag_set |= set(tag.get_all_parents())
370 tag_set |= set(tag.get_all_parents())
370
371
371 for tag in tag_set:
372 for tag in tag_set:
372 if tag.required:
373 if tag.required:
373 required_tag_exists = True
374 required_tag_exists = True
374 break
375 break
375
376
376 if not required_tag_exists:
377 if not required_tag_exists:
377 raise forms.ValidationError(
378 raise forms.ValidationError(
378 _('Need at least one section.'))
379 _('Need at least one section.'))
379
380
380 return tag_set
381 return tag_set
381
382
382 def clean(self):
383 def clean(self):
383 cleaned_data = super(ThreadForm, self).clean()
384 cleaned_data = super(ThreadForm, self).clean()
384
385
385 return cleaned_data
386 return cleaned_data
386
387
387
388
388 class SettingsForm(NeboardForm):
389 class SettingsForm(NeboardForm):
389
390
390 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
391 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
391 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
392 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
392 username = forms.CharField(label=_('User name'), required=False)
393 username = forms.CharField(label=_('User name'), required=False)
393 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
394 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
394
395
395 def clean_username(self):
396 def clean_username(self):
396 username = self.cleaned_data['username']
397 username = self.cleaned_data['username']
397
398
398 if username and not REGEX_TAGS.match(username):
399 if username and not REGEX_TAGS.match(username):
399 raise forms.ValidationError(_('Inappropriate characters.'))
400 raise forms.ValidationError(_('Inappropriate characters.'))
400
401
401 return username
402 return username
402
403
403
404
404 class SearchForm(NeboardForm):
405 class SearchForm(NeboardForm):
405 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
406 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,41 +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, get_extension
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 # FIXME Use full mimetype here, need to modify viewers too
15 # FIXME Use full mimetype here, need to modify viewers too
16 file_type = get_file_mimetype(file).split('/')[-1]
16 file_type = get_extension(file.name)
17 attachment = self.create(file=file, mimetype=file_type,
17 attachment = self.create(file=file, mimetype=file_type,
18 hash=file_hash)
18 hash=file_hash)
19
19
20 return attachment
20 return attachment
21
21
22
22
23 class Attachment(models.Model):
23 class Attachment(models.Model):
24 objects = AttachmentManager()
24 objects = AttachmentManager()
25
25
26 file = models.FileField(upload_to=get_upload_filename)
26 file = models.FileField(upload_to=get_upload_filename)
27 mimetype = models.CharField(max_length=50)
27 mimetype = models.CharField(max_length=50)
28 hash = models.CharField(max_length=36)
28 hash = models.CharField(max_length=36)
29
29
30 def get_view(self):
30 def get_view(self):
31 file_viewer = None
31 file_viewer = None
32 for viewer in get_viewers():
32 for viewer in get_viewers():
33 if viewer.supports(self.mimetype):
33 if viewer.supports(self.mimetype):
34 file_viewer = viewer
34 file_viewer = viewer
35 break
35 break
36 if file_viewer is None:
36 if file_viewer is None:
37 file_viewer = AbstractViewer
37 file_viewer = AbstractViewer
38
38
39 return file_viewer(self.file, self.mimetype).get_view()
39 return file_viewer(self.file, self.mimetype).get_view()
40
40
41
41
@@ -1,144 +1,148 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_extension(filename):
131 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
132
133
130 def get_upload_filename(model_instance, old_filename):
134 def get_upload_filename(model_instance, old_filename):
131 # TODO Use something other than random number in file name
135 # TODO Use something other than random number in file name
132 extension = old_filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
136 extension = get_extension(old_filename)
133 new_name = '{}{}.{}'.format(
137 new_name = '{}{}.{}'.format(
134 str(int(time.mktime(time.gmtime()))),
138 str(int(time.mktime(time.gmtime()))),
135 str(int(random() * 1000)),
139 str(int(random() * 1000)),
136 extension)
140 extension)
137
141
138 directory = UPLOAD_DIRS[type(model_instance).__name__]
142 directory = UPLOAD_DIRS[type(model_instance).__name__]
139
143
140 return os.path.join(directory, new_name)
144 return os.path.join(directory, new_name)
141
145
142
146
143 def get_file_mimetype(file) -> str:
147 def get_file_mimetype(file) -> str:
144 return magic.from_buffer(file.chunks().__next__(), mime=True).decode()
148 return magic.from_buffer(file.chunks().__next__(), mime=True).decode()
General Comments 0
You need to be logged in to leave comments. Login now