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