##// END OF EJS Templates
Tripcode should be delimited by 2 hashes
neko259 -
r1923:916b2d74 default
parent child Browse files
Show More
@@ -1,559 +1,559 b''
1 import hashlib
1 import hashlib
2 import logging
2 import logging
3 import re
3 import re
4 import time
4 import time
5 import traceback
5 import traceback
6
6
7 import pytz
7 import pytz
8
8
9 from PIL import Image
9 from PIL import Image
10
10
11 from django import forms
11 from django import forms
12 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
12 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
13 from django.forms.utils import ErrorList
13 from django.forms.utils import ErrorList
14 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
14 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
15 from django.core.files.images import get_image_dimensions
15 from django.core.files.images import get_image_dimensions
16 from django.core.cache import cache
16 from django.core.cache import cache
17
17
18 import boards.settings as board_settings
18 import boards.settings as board_settings
19 import neboard
19 import neboard
20 from boards import utils
20 from boards import utils
21 from boards.abstracts.attachment_alias import get_image_by_alias
21 from boards.abstracts.attachment_alias import get_image_by_alias
22 from boards.abstracts.settingsmanager import get_settings_manager
22 from boards.abstracts.settingsmanager import get_settings_manager
23 from boards.forms.fields import UrlFileField
23 from boards.forms.fields import UrlFileField
24 from boards.mdx_neboard import formatters
24 from boards.mdx_neboard import formatters
25 from boards.models import Attachment
25 from boards.models import Attachment
26 from boards.models import Tag
26 from boards.models import Tag
27 from boards.models.attachment.downloaders import download, REGEX_MAGNET
27 from boards.models.attachment.downloaders import download, REGEX_MAGNET
28 from boards.models.post import TITLE_MAX_LENGTH
28 from boards.models.post import TITLE_MAX_LENGTH
29 from boards.utils import validate_file_size, get_file_mimetype, \
29 from boards.utils import validate_file_size, get_file_mimetype, \
30 FILE_EXTENSION_DELIMITER
30 FILE_EXTENSION_DELIMITER
31 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
31 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
32 from neboard import settings
32 from neboard import settings
33
33
34 SECTION_FORMS = 'Forms'
34 SECTION_FORMS = 'Forms'
35
35
36 POW_HASH_LENGTH = 16
36 POW_HASH_LENGTH = 16
37 POW_LIFE_MINUTES = 5
37 POW_LIFE_MINUTES = 5
38
38
39 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
39 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
40 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
40 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
41 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
41 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
42
42
43 VETERAN_POSTING_DELAY = 5
43 VETERAN_POSTING_DELAY = 5
44
44
45 ATTRIBUTE_PLACEHOLDER = 'placeholder'
45 ATTRIBUTE_PLACEHOLDER = 'placeholder'
46 ATTRIBUTE_ROWS = 'rows'
46 ATTRIBUTE_ROWS = 'rows'
47
47
48 LAST_POST_TIME = 'last_post_time'
48 LAST_POST_TIME = 'last_post_time'
49 LAST_LOGIN_TIME = 'last_login_time'
49 LAST_LOGIN_TIME = 'last_login_time'
50 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
50 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
51 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
51 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
52
52
53 LABEL_TITLE = _('Title')
53 LABEL_TITLE = _('Title')
54 LABEL_TEXT = _('Text')
54 LABEL_TEXT = _('Text')
55 LABEL_TAG = _('Tag')
55 LABEL_TAG = _('Tag')
56 LABEL_SEARCH = _('Search')
56 LABEL_SEARCH = _('Search')
57 LABEL_FILE = _('File')
57 LABEL_FILE = _('File')
58 LABEL_DUPLICATES = _('Check for duplicates')
58 LABEL_DUPLICATES = _('Check for duplicates')
59 LABEL_URL = _('Do not download URLs')
59 LABEL_URL = _('Do not download URLs')
60
60
61 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
61 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
62 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
62 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
63 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
63 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
64 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
64 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
65 ERROR_DUPLICATES = 'Some files are already present on the board.'
65 ERROR_DUPLICATES = 'Some files are already present on the board.'
66
66
67 TAG_MAX_LENGTH = 20
67 TAG_MAX_LENGTH = 20
68
68
69 TEXTAREA_ROWS = 4
69 TEXTAREA_ROWS = 4
70
70
71 TRIPCODE_DELIM = '#'
71 TRIPCODE_DELIM = '##'
72
72
73 # TODO Maybe this may be converted into the database table?
73 # TODO Maybe this may be converted into the database table?
74 MIMETYPE_EXTENSIONS = {
74 MIMETYPE_EXTENSIONS = {
75 'image/jpeg': 'jpeg',
75 'image/jpeg': 'jpeg',
76 'image/png': 'png',
76 'image/png': 'png',
77 'image/gif': 'gif',
77 'image/gif': 'gif',
78 'video/webm': 'webm',
78 'video/webm': 'webm',
79 'application/pdf': 'pdf',
79 'application/pdf': 'pdf',
80 'x-diff': 'diff',
80 'x-diff': 'diff',
81 'image/svg+xml': 'svg',
81 'image/svg+xml': 'svg',
82 'application/x-shockwave-flash': 'swf',
82 'application/x-shockwave-flash': 'swf',
83 'image/x-ms-bmp': 'bmp',
83 'image/x-ms-bmp': 'bmp',
84 'image/bmp': 'bmp',
84 'image/bmp': 'bmp',
85 }
85 }
86
86
87 DOWN_MODE_DOWNLOAD = 'DOWNLOAD'
87 DOWN_MODE_DOWNLOAD = 'DOWNLOAD'
88 DOWN_MODE_URL = 'URL'
88 DOWN_MODE_URL = 'URL'
89 DOWN_MODE_TRY = 'TRY'
89 DOWN_MODE_TRY = 'TRY'
90
90
91
91
92 logger = logging.getLogger('boards.forms')
92 logger = logging.getLogger('boards.forms')
93
93
94
94
95 def get_timezones():
95 def get_timezones():
96 timezones = []
96 timezones = []
97 for tz in pytz.common_timezones:
97 for tz in pytz.common_timezones:
98 timezones.append((tz, tz),)
98 timezones.append((tz, tz),)
99 return timezones
99 return timezones
100
100
101
101
102 class FormatPanel(forms.Textarea):
102 class FormatPanel(forms.Textarea):
103 """
103 """
104 Panel for text formatting. Consists of buttons to add different tags to the
104 Panel for text formatting. Consists of buttons to add different tags to the
105 form text area.
105 form text area.
106 """
106 """
107
107
108 def render(self, name, value, attrs=None):
108 def render(self, name, value, attrs=None):
109 output = '<div id="mark-panel">'
109 output = '<div id="mark-panel">'
110 for formatter in formatters:
110 for formatter in formatters:
111 output += '<span class="mark_btn"' + \
111 output += '<span class="mark_btn"' + \
112 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
112 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
113 '\', \'' + formatter.format_right + '\')">' + \
113 '\', \'' + formatter.format_right + '\')">' + \
114 formatter.preview_left + formatter.name + \
114 formatter.preview_left + formatter.name + \
115 formatter.preview_right + '</span>'
115 formatter.preview_right + '</span>'
116
116
117 output += '</div>'
117 output += '</div>'
118 output += super(FormatPanel, self).render(name, value, attrs=attrs)
118 output += super(FormatPanel, self).render(name, value, attrs=attrs)
119
119
120 return output
120 return output
121
121
122
122
123 class PlainErrorList(ErrorList):
123 class PlainErrorList(ErrorList):
124 def __unicode__(self):
124 def __unicode__(self):
125 return self.as_text()
125 return self.as_text()
126
126
127 def as_text(self):
127 def as_text(self):
128 return ''.join(['(!) %s ' % e for e in self])
128 return ''.join(['(!) %s ' % e for e in self])
129
129
130
130
131 class NeboardForm(forms.Form):
131 class NeboardForm(forms.Form):
132 """
132 """
133 Form with neboard-specific formatting.
133 Form with neboard-specific formatting.
134 """
134 """
135 required_css_class = 'required-field'
135 required_css_class = 'required-field'
136
136
137 def as_div(self):
137 def as_div(self):
138 """
138 """
139 Returns this form rendered as HTML <as_div>s.
139 Returns this form rendered as HTML <as_div>s.
140 """
140 """
141
141
142 return self._html_output(
142 return self._html_output(
143 # TODO Do not show hidden rows in the list here
143 # TODO Do not show hidden rows in the list here
144 normal_row='<div class="form-row">'
144 normal_row='<div class="form-row">'
145 '<div class="form-label">'
145 '<div class="form-label">'
146 '%(label)s'
146 '%(label)s'
147 '</div>'
147 '</div>'
148 '<div class="form-input">'
148 '<div class="form-input">'
149 '%(field)s'
149 '%(field)s'
150 '</div>'
150 '</div>'
151 '</div>'
151 '</div>'
152 '<div class="form-row">'
152 '<div class="form-row">'
153 '%(help_text)s'
153 '%(help_text)s'
154 '</div>',
154 '</div>',
155 error_row='<div class="form-row">'
155 error_row='<div class="form-row">'
156 '<div class="form-label"></div>'
156 '<div class="form-label"></div>'
157 '<div class="form-errors">%s</div>'
157 '<div class="form-errors">%s</div>'
158 '</div>',
158 '</div>',
159 row_ender='</div>',
159 row_ender='</div>',
160 help_text_html='%s',
160 help_text_html='%s',
161 errors_on_separate_row=True)
161 errors_on_separate_row=True)
162
162
163 def as_json_errors(self):
163 def as_json_errors(self):
164 errors = []
164 errors = []
165
165
166 for name, field in list(self.fields.items()):
166 for name, field in list(self.fields.items()):
167 if self[name].errors:
167 if self[name].errors:
168 errors.append({
168 errors.append({
169 'field': name,
169 'field': name,
170 'errors': self[name].errors.as_text(),
170 'errors': self[name].errors.as_text(),
171 })
171 })
172
172
173 return errors
173 return errors
174
174
175
175
176 class PostForm(NeboardForm):
176 class PostForm(NeboardForm):
177
177
178 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
178 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
179 label=LABEL_TITLE,
179 label=LABEL_TITLE,
180 widget=forms.TextInput(
180 widget=forms.TextInput(
181 attrs={ATTRIBUTE_PLACEHOLDER: 'title#tripcode'}))
181 attrs={ATTRIBUTE_PLACEHOLDER: 'Title{}tripcode'.format(TRIPCODE_DELIM)}))
182 text = forms.CharField(
182 text = forms.CharField(
183 widget=FormatPanel(attrs={
183 widget=FormatPanel(attrs={
184 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
184 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
185 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
185 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
186 }),
186 }),
187 required=False, label=LABEL_TEXT)
187 required=False, label=LABEL_TEXT)
188 download_mode = forms.ChoiceField(
188 download_mode = forms.ChoiceField(
189 choices=(
189 choices=(
190 (DOWN_MODE_TRY, _('Download or add URL')),
190 (DOWN_MODE_TRY, _('Download or add URL')),
191 (DOWN_MODE_DOWNLOAD, _('Download or fail')),
191 (DOWN_MODE_DOWNLOAD, _('Download or fail')),
192 (DOWN_MODE_URL, _('Insert as URLs')),
192 (DOWN_MODE_URL, _('Insert as URLs')),
193 ),
193 ),
194 initial=DOWN_MODE_TRY,
194 initial=DOWN_MODE_TRY,
195 label=_('URL download mode'))
195 label=_('URL download mode'))
196 file = UrlFileField(required=False, label=LABEL_FILE)
196 file = UrlFileField(required=False, label=LABEL_FILE)
197
197
198 # This field is for spam prevention only
198 # This field is for spam prevention only
199 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
199 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
200 widget=forms.TextInput(attrs={
200 widget=forms.TextInput(attrs={
201 'class': 'form-email'}))
201 'class': 'form-email'}))
202 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
202 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
203 check_duplicates = forms.BooleanField(required=False, label=LABEL_DUPLICATES)
203 check_duplicates = forms.BooleanField(required=False, label=LABEL_DUPLICATES)
204
204
205 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
205 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
206 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
206 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
207 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
207 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
208
208
209 session = None
209 session = None
210 need_to_ban = False
210 need_to_ban = False
211
211
212 def clean_title(self):
212 def clean_title(self):
213 title = self.cleaned_data['title']
213 title = self.cleaned_data['title']
214 if title:
214 if title:
215 if len(title) > TITLE_MAX_LENGTH:
215 if len(title) > TITLE_MAX_LENGTH:
216 raise forms.ValidationError(_('Title must have less than %s '
216 raise forms.ValidationError(_('Title must have less than %s '
217 'characters') %
217 'characters') %
218 str(TITLE_MAX_LENGTH))
218 str(TITLE_MAX_LENGTH))
219 return title
219 return title
220
220
221 def clean_text(self):
221 def clean_text(self):
222 text = self.cleaned_data['text'].strip()
222 text = self.cleaned_data['text'].strip()
223 if text:
223 if text:
224 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
224 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
225 if len(text) > max_length:
225 if len(text) > max_length:
226 raise forms.ValidationError(_('Text must have less than %s '
226 raise forms.ValidationError(_('Text must have less than %s '
227 'characters') % str(max_length))
227 'characters') % str(max_length))
228 return text
228 return text
229
229
230 def clean_file(self):
230 def clean_file(self):
231 return self._clean_files(self.cleaned_data['file'])
231 return self._clean_files(self.cleaned_data['file'])
232
232
233 def clean(self):
233 def clean(self):
234 cleaned_data = super(PostForm, self).clean()
234 cleaned_data = super(PostForm, self).clean()
235
235
236 if cleaned_data['email']:
236 if cleaned_data['email']:
237 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
237 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
238 self.need_to_ban = True
238 self.need_to_ban = True
239 raise forms.ValidationError('A human cannot enter a hidden field')
239 raise forms.ValidationError('A human cannot enter a hidden field')
240
240
241 if not self.errors:
241 if not self.errors:
242 self._clean_text_file()
242 self._clean_text_file()
243
243
244 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
244 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
245 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
245 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
246
246
247 settings_manager = get_settings_manager(self)
247 settings_manager = get_settings_manager(self)
248 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
248 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
249 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
249 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
250 if pow_difficulty > 0:
250 if pow_difficulty > 0:
251 # PoW-based
251 # PoW-based
252 if cleaned_data['timestamp'] \
252 if cleaned_data['timestamp'] \
253 and cleaned_data['iteration'] and cleaned_data['guess'] \
253 and cleaned_data['iteration'] and cleaned_data['guess'] \
254 and not settings_manager.get_setting('confirmed_user'):
254 and not settings_manager.get_setting('confirmed_user'):
255 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
255 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
256 else:
256 else:
257 # Time-based
257 # Time-based
258 self._validate_posting_speed()
258 self._validate_posting_speed()
259 settings_manager.set_setting('confirmed_user', True)
259 settings_manager.set_setting('confirmed_user', True)
260 if self.cleaned_data['check_duplicates']:
260 if self.cleaned_data['check_duplicates']:
261 self._check_file_duplicates(self.get_files())
261 self._check_file_duplicates(self.get_files())
262
262
263 return cleaned_data
263 return cleaned_data
264
264
265 def get_files(self):
265 def get_files(self):
266 """
266 """
267 Gets file from form or URL.
267 Gets file from form or URL.
268 """
268 """
269
269
270 files = []
270 files = []
271 for file in self.cleaned_data['file']:
271 for file in self.cleaned_data['file']:
272 if isinstance(file, UploadedFile):
272 if isinstance(file, UploadedFile):
273 files.append(file)
273 files.append(file)
274
274
275 return files
275 return files
276
276
277 def get_file_urls(self):
277 def get_file_urls(self):
278 files = []
278 files = []
279 for file in self.cleaned_data['file']:
279 for file in self.cleaned_data['file']:
280 if type(file) == str:
280 if type(file) == str:
281 files.append(file)
281 files.append(file)
282
282
283 return files
283 return files
284
284
285 def get_tripcode(self):
285 def get_tripcode(self):
286 title = self.cleaned_data['title']
286 title = self.cleaned_data['title']
287 if title is not None and TRIPCODE_DELIM in title:
287 if title is not None and TRIPCODE_DELIM in title:
288 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
288 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
289 tripcode = hashlib.md5(code.encode()).hexdigest()
289 tripcode = hashlib.md5(code.encode()).hexdigest()
290 else:
290 else:
291 tripcode = ''
291 tripcode = ''
292 return tripcode
292 return tripcode
293
293
294 def get_title(self):
294 def get_title(self):
295 title = self.cleaned_data['title']
295 title = self.cleaned_data['title']
296 if title is not None and TRIPCODE_DELIM in title:
296 if title is not None and TRIPCODE_DELIM in title:
297 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
297 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
298 else:
298 else:
299 return title
299 return title
300
300
301 def get_images(self):
301 def get_images(self):
302 return self.cleaned_data.get('stickers', [])
302 return self.cleaned_data.get('stickers', [])
303
303
304 def is_subscribe(self):
304 def is_subscribe(self):
305 return self.cleaned_data['subscribe']
305 return self.cleaned_data['subscribe']
306
306
307 def _update_file_extension(self, file):
307 def _update_file_extension(self, file):
308 if file:
308 if file:
309 mimetype = get_file_mimetype(file)
309 mimetype = get_file_mimetype(file)
310 extension = MIMETYPE_EXTENSIONS.get(mimetype)
310 extension = MIMETYPE_EXTENSIONS.get(mimetype)
311 if extension:
311 if extension:
312 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
312 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
313 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
313 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
314
314
315 file.name = new_filename
315 file.name = new_filename
316 else:
316 else:
317 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
317 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
318
318
319 def _clean_files(self, inputs):
319 def _clean_files(self, inputs):
320 files = []
320 files = []
321
321
322 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
322 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
323 if len(inputs) > max_file_count:
323 if len(inputs) > max_file_count:
324 raise forms.ValidationError(
324 raise forms.ValidationError(
325 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
325 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
326 max_file_count) % {'files': max_file_count})
326 max_file_count) % {'files': max_file_count})
327 for file_input in inputs:
327 for file_input in inputs:
328 if isinstance(file_input, UploadedFile):
328 if isinstance(file_input, UploadedFile):
329 files.append(self._clean_file_file(file_input))
329 files.append(self._clean_file_file(file_input))
330 else:
330 else:
331 files.append(self._clean_file_url(file_input))
331 files.append(self._clean_file_url(file_input))
332
332
333 for file in files:
333 for file in files:
334 self._validate_image_dimensions(file)
334 self._validate_image_dimensions(file)
335
335
336 return files
336 return files
337
337
338 def _validate_image_dimensions(self, file):
338 def _validate_image_dimensions(self, file):
339 if isinstance(file, UploadedFile):
339 if isinstance(file, UploadedFile):
340 mimetype = get_file_mimetype(file)
340 mimetype = get_file_mimetype(file)
341 if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
341 if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
342 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
342 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
343 try:
343 try:
344 print(get_image_dimensions(file))
344 print(get_image_dimensions(file))
345 except Exception:
345 except Exception:
346 raise forms.ValidationError('Possible decompression bomb or large image.')
346 raise forms.ValidationError('Possible decompression bomb or large image.')
347
347
348 def _clean_file_file(self, file):
348 def _clean_file_file(self, file):
349 validate_file_size(file.size)
349 validate_file_size(file.size)
350 self._update_file_extension(file)
350 self._update_file_extension(file)
351
351
352 return file
352 return file
353
353
354 def _clean_file_url(self, url):
354 def _clean_file_url(self, url):
355 file = None
355 file = None
356
356
357 if url:
357 if url:
358 mode = self.cleaned_data['download_mode']
358 mode = self.cleaned_data['download_mode']
359 if mode == DOWN_MODE_URL:
359 if mode == DOWN_MODE_URL:
360 return url
360 return url
361
361
362 try:
362 try:
363 image = get_image_by_alias(url, self.session)
363 image = get_image_by_alias(url, self.session)
364 if image is not None:
364 if image is not None:
365 if 'stickers' not in self.cleaned_data:
365 if 'stickers' not in self.cleaned_data:
366 self.cleaned_data['stickers'] = []
366 self.cleaned_data['stickers'] = []
367 self.cleaned_data['stickers'].append(image)
367 self.cleaned_data['stickers'].append(image)
368 return
368 return
369
369
370 if file is None:
370 if file is None:
371 file = self._get_file_from_url(url)
371 file = self._get_file_from_url(url)
372 if not file:
372 if not file:
373 raise forms.ValidationError(_('Invalid URL'))
373 raise forms.ValidationError(_('Invalid URL'))
374 else:
374 else:
375 validate_file_size(file.size)
375 validate_file_size(file.size)
376 self._update_file_extension(file)
376 self._update_file_extension(file)
377 except forms.ValidationError as e:
377 except forms.ValidationError as e:
378 # Assume we will get the plain URL instead of a file and save it
378 # Assume we will get the plain URL instead of a file and save it
379 if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)):
379 if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)):
380 logger.info('Error in forms: {}'.format(e))
380 logger.info('Error in forms: {}'.format(e))
381 return url
381 return url
382 else:
382 else:
383 raise e
383 raise e
384
384
385 return file
385 return file
386
386
387 def _clean_text_file(self):
387 def _clean_text_file(self):
388 text = self.cleaned_data.get('text')
388 text = self.cleaned_data.get('text')
389 file = self.get_files()
389 file = self.get_files()
390 file_url = self.get_file_urls()
390 file_url = self.get_file_urls()
391 images = self.get_images()
391 images = self.get_images()
392
392
393 if (not text) and (not file) and (not file_url) and len(images) == 0:
393 if (not text) and (not file) and (not file_url) and len(images) == 0:
394 error_message = _('Either text or file must be entered.')
394 error_message = _('Either text or file must be entered.')
395 self._add_general_error(error_message)
395 self._add_general_error(error_message)
396
396
397 def _get_cache_key(self, key):
397 def _get_cache_key(self, key):
398 return '{}_{}'.format(self.session.session_key, key)
398 return '{}_{}'.format(self.session.session_key, key)
399
399
400 def _set_session_cache(self, key, value):
400 def _set_session_cache(self, key, value):
401 cache.set(self._get_cache_key(key), value)
401 cache.set(self._get_cache_key(key), value)
402
402
403 def _get_session_cache(self, key):
403 def _get_session_cache(self, key):
404 return cache.get(self._get_cache_key(key))
404 return cache.get(self._get_cache_key(key))
405
405
406 def _get_last_post_time(self):
406 def _get_last_post_time(self):
407 last = self._get_session_cache(LAST_POST_TIME)
407 last = self._get_session_cache(LAST_POST_TIME)
408 if last is None:
408 if last is None:
409 last = self.session.get(LAST_POST_TIME)
409 last = self.session.get(LAST_POST_TIME)
410 return last
410 return last
411
411
412 def _validate_posting_speed(self):
412 def _validate_posting_speed(self):
413 can_post = True
413 can_post = True
414
414
415 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
415 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
416
416
417 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
417 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
418 now = time.time()
418 now = time.time()
419
419
420 current_delay = 0
420 current_delay = 0
421
421
422 if LAST_POST_TIME not in self.session:
422 if LAST_POST_TIME not in self.session:
423 self.session[LAST_POST_TIME] = now
423 self.session[LAST_POST_TIME] = now
424
424
425 need_delay = True
425 need_delay = True
426 else:
426 else:
427 last_post_time = self._get_last_post_time()
427 last_post_time = self._get_last_post_time()
428 current_delay = int(now - last_post_time)
428 current_delay = int(now - last_post_time)
429
429
430 need_delay = current_delay < posting_delay
430 need_delay = current_delay < posting_delay
431
431
432 self._set_session_cache(LAST_POST_TIME, now)
432 self._set_session_cache(LAST_POST_TIME, now)
433
433
434 if need_delay:
434 if need_delay:
435 delay = posting_delay - current_delay
435 delay = posting_delay - current_delay
436 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
436 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
437 delay) % {'delay': delay}
437 delay) % {'delay': delay}
438 self._add_general_error(error_message)
438 self._add_general_error(error_message)
439
439
440 can_post = False
440 can_post = False
441
441
442 if can_post:
442 if can_post:
443 self.session[LAST_POST_TIME] = now
443 self.session[LAST_POST_TIME] = now
444 else:
444 else:
445 # Reset the time since posting failed
445 # Reset the time since posting failed
446 self._set_session_cache(LAST_POST_TIME, self.session[LAST_POST_TIME])
446 self._set_session_cache(LAST_POST_TIME, self.session[LAST_POST_TIME])
447
447
448 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
448 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
449 """
449 """
450 Gets an file file from URL.
450 Gets an file file from URL.
451 """
451 """
452
452
453 try:
453 try:
454 return download(url)
454 return download(url)
455 except forms.ValidationError as e:
455 except forms.ValidationError as e:
456 raise e
456 raise e
457 except Exception as e:
457 except Exception as e:
458 raise forms.ValidationError(e)
458 raise forms.ValidationError(e)
459
459
460 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
460 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
461 payload = timestamp + message.replace('\r\n', '\n')
461 payload = timestamp + message.replace('\r\n', '\n')
462 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
462 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
463 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
463 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
464 if len(target) < POW_HASH_LENGTH:
464 if len(target) < POW_HASH_LENGTH:
465 target = '0' * (POW_HASH_LENGTH - len(target)) + target
465 target = '0' * (POW_HASH_LENGTH - len(target)) + target
466
466
467 computed_guess = hashlib.sha256((payload + iteration).encode())\
467 computed_guess = hashlib.sha256((payload + iteration).encode())\
468 .hexdigest()[0:POW_HASH_LENGTH]
468 .hexdigest()[0:POW_HASH_LENGTH]
469 if guess != computed_guess or guess > target:
469 if guess != computed_guess or guess > target:
470 self._add_general_error(_('Invalid PoW.'))
470 self._add_general_error(_('Invalid PoW.'))
471
471
472 def _check_file_duplicates(self, files):
472 def _check_file_duplicates(self, files):
473 for file in files:
473 for file in files:
474 file_hash = utils.get_file_hash(file)
474 file_hash = utils.get_file_hash(file)
475 if Attachment.objects.get_existing_duplicate(file_hash, file):
475 if Attachment.objects.get_existing_duplicate(file_hash, file):
476 self._add_general_error(_(ERROR_DUPLICATES))
476 self._add_general_error(_(ERROR_DUPLICATES))
477
477
478 def _add_general_error(self, message):
478 def _add_general_error(self, message):
479 self.add_error('text', forms.ValidationError(message))
479 self.add_error('text', forms.ValidationError(message))
480
480
481
481
482 class ThreadForm(PostForm):
482 class ThreadForm(PostForm):
483
483
484 tags = forms.CharField(
484 tags = forms.CharField(
485 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
485 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
486 max_length=100, label=_('Tags'), required=True)
486 max_length=100, label=_('Tags'), required=True)
487 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
487 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
488
488
489 def clean_tags(self):
489 def clean_tags(self):
490 tags = self.cleaned_data['tags'].strip()
490 tags = self.cleaned_data['tags'].strip()
491
491
492 if not tags or not REGEX_TAGS.match(tags):
492 if not tags or not REGEX_TAGS.match(tags):
493 raise forms.ValidationError(
493 raise forms.ValidationError(
494 _('Inappropriate characters in tags.'))
494 _('Inappropriate characters in tags.'))
495
495
496 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
496 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
497 .strip().lower()
497 .strip().lower()
498
498
499 required_tag_exists = False
499 required_tag_exists = False
500 tag_set = set()
500 tag_set = set()
501 for tag_string in tags.split():
501 for tag_string in tags.split():
502 tag_name = tag_string.strip().lower()
502 tag_name = tag_string.strip().lower()
503 if tag_name == default_tag_name:
503 if tag_name == default_tag_name:
504 required_tag_exists = True
504 required_tag_exists = True
505 tag, created = Tag.objects.get_or_create_with_alias(
505 tag, created = Tag.objects.get_or_create_with_alias(
506 name=tag_name, required=True)
506 name=tag_name, required=True)
507 else:
507 else:
508 tag, created = Tag.objects.get_or_create_with_alias(name=tag_name)
508 tag, created = Tag.objects.get_or_create_with_alias(name=tag_name)
509 tag_set.add(tag)
509 tag_set.add(tag)
510
510
511 # If this is a new tag, don't check for its parents because nobody
511 # If this is a new tag, don't check for its parents because nobody
512 # added them yet
512 # added them yet
513 if not created:
513 if not created:
514 tag_set |= set(tag.get_all_parents())
514 tag_set |= set(tag.get_all_parents())
515
515
516 for tag in tag_set:
516 for tag in tag_set:
517 if tag.required:
517 if tag.required:
518 required_tag_exists = True
518 required_tag_exists = True
519 break
519 break
520
520
521 # Use default tag if no section exists
521 # Use default tag if no section exists
522 if not required_tag_exists:
522 if not required_tag_exists:
523 default_tag, created = Tag.objects.get_or_create_with_alias(
523 default_tag, created = Tag.objects.get_or_create_with_alias(
524 name=default_tag_name, required=True)
524 name=default_tag_name, required=True)
525 tag_set.add(default_tag)
525 tag_set.add(default_tag)
526
526
527 return tag_set
527 return tag_set
528
528
529 def clean(self):
529 def clean(self):
530 cleaned_data = super(ThreadForm, self).clean()
530 cleaned_data = super(ThreadForm, self).clean()
531
531
532 return cleaned_data
532 return cleaned_data
533
533
534 def is_monochrome(self):
534 def is_monochrome(self):
535 return self.cleaned_data['monochrome']
535 return self.cleaned_data['monochrome']
536
536
537
537
538 class SettingsForm(NeboardForm):
538 class SettingsForm(NeboardForm):
539
539
540 theme = forms.ChoiceField(
540 theme = forms.ChoiceField(
541 choices=board_settings.get_list_dict('View', 'Themes'),
541 choices=board_settings.get_list_dict('View', 'Themes'),
542 label=_('Theme'))
542 label=_('Theme'))
543 image_viewer = forms.ChoiceField(
543 image_viewer = forms.ChoiceField(
544 choices=board_settings.get_list_dict('View', 'ImageViewers'),
544 choices=board_settings.get_list_dict('View', 'ImageViewers'),
545 label=_('Image view mode'))
545 label=_('Image view mode'))
546 username = forms.CharField(label=_('User name'), required=False)
546 username = forms.CharField(label=_('User name'), required=False)
547 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
547 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
548
548
549 def clean_username(self):
549 def clean_username(self):
550 username = self.cleaned_data['username']
550 username = self.cleaned_data['username']
551
551
552 if username and not REGEX_USERNAMES.match(username):
552 if username and not REGEX_USERNAMES.match(username):
553 raise forms.ValidationError(_('Inappropriate characters.'))
553 raise forms.ValidationError(_('Inappropriate characters.'))
554
554
555 return username
555 return username
556
556
557
557
558 class SearchForm(NeboardForm):
558 class SearchForm(NeboardForm):
559 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
559 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
General Comments 0
You need to be logged in to leave comments. Login now