##// END OF EJS Templates
Add setting for form autoban
neko259 -
r1632:40b96286 default
parent child Browse files
Show More
@@ -1,42 +1,43
1 [Version]
1 [Version]
2 Version = 3.1.0 Mitsumune
2 Version = 3.1.0 Mitsumune
3 SiteName = Neboard DEV
3 SiteName = Neboard DEV
4
4
5 [Cache]
5 [Cache]
6 # Timeout for caching, if cache is used
6 # Timeout for caching, if cache is used
7 CacheTimeout = 600
7 CacheTimeout = 600
8
8
9 [Forms]
9 [Forms]
10 # Max post length in characters
10 # Max post length in characters
11 MaxTextLength = 30000
11 MaxTextLength = 30000
12 MaxFileSize = 8000000
12 MaxFileSize = 8000000
13 LimitFirstPosting = true
13 LimitFirstPosting = true
14 LimitPostingSpeed = false
14 LimitPostingSpeed = false
15 PowDifficulty = 0
15 PowDifficulty = 0
16 # Delay in seconds
16 # Delay in seconds
17 PostingDelay = 30
17 PostingDelay = 30
18 Autoban = false
18
19
19 [Messages]
20 [Messages]
20 # Thread bumplimit
21 # Thread bumplimit
21 MaxPostsPerThread = 10
22 MaxPostsPerThread = 10
22 ThreadArchiveDays = 300
23 ThreadArchiveDays = 300
23 AnonymousMode = false
24 AnonymousMode = false
24
25
25 [View]
26 [View]
26 DefaultTheme = md
27 DefaultTheme = md
27 DefaultImageViewer = simple
28 DefaultImageViewer = simple
28 LastRepliesCount = 3
29 LastRepliesCount = 3
29 ThreadsPerPage = 3
30 ThreadsPerPage = 3
30 ImagesPerPageGallery = 20
31 ImagesPerPageGallery = 20
31 MaxFavoriteThreads = 20
32 MaxFavoriteThreads = 20
32
33
33 [Storage]
34 [Storage]
34 # Enable archiving threads instead of deletion when the thread limit is reached
35 # Enable archiving threads instead of deletion when the thread limit is reached
35 ArchiveThreads = true
36 ArchiveThreads = true
36
37
37 [External]
38 [External]
38 # Thread update
39 # Thread update
39 WebsocketsEnabled = false
40 WebsocketsEnabled = false
40
41
41 [RSS]
42 [RSS]
42 MaxItems = 20
43 MaxItems = 20
@@ -1,467 +1,468
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
5
6 import pytz
6 import pytz
7
7
8 from django import forms
8 from django import forms
9 from django.core.files.uploadedfile import SimpleUploadedFile
9 from django.core.files.uploadedfile import SimpleUploadedFile
10 from django.core.exceptions import ObjectDoesNotExist
10 from django.core.exceptions import ObjectDoesNotExist
11 from django.forms.utils import ErrorList
11 from django.forms.utils import ErrorList
12 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
12 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
13 from django.utils import timezone
13 from django.utils import timezone
14
14
15 from boards.abstracts.settingsmanager import get_settings_manager
15 from boards.abstracts.settingsmanager import get_settings_manager
16 from boards.abstracts.attachment_alias import get_image_by_alias
16 from boards.abstracts.attachment_alias import get_image_by_alias
17 from boards.mdx_neboard import formatters
17 from boards.mdx_neboard import formatters
18 from boards.models.attachment.downloaders import download
18 from boards.models.attachment.downloaders import download
19 from boards.models.post import TITLE_MAX_LENGTH
19 from boards.models.post import TITLE_MAX_LENGTH
20 from boards.models import Tag, Post
20 from boards.models import Tag, Post
21 from boards.utils import validate_file_size, get_file_mimetype, \
21 from boards.utils import validate_file_size, get_file_mimetype, \
22 FILE_EXTENSION_DELIMITER
22 FILE_EXTENSION_DELIMITER
23 from neboard import settings
23 from neboard import settings
24 import boards.settings as board_settings
24 import boards.settings as board_settings
25 import neboard
25 import neboard
26
26
27 POW_HASH_LENGTH = 16
27 POW_HASH_LENGTH = 16
28 POW_LIFE_MINUTES = 5
28 POW_LIFE_MINUTES = 5
29
29
30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
32
32
33 VETERAN_POSTING_DELAY = 5
33 VETERAN_POSTING_DELAY = 5
34
34
35 ATTRIBUTE_PLACEHOLDER = 'placeholder'
35 ATTRIBUTE_PLACEHOLDER = 'placeholder'
36 ATTRIBUTE_ROWS = 'rows'
36 ATTRIBUTE_ROWS = 'rows'
37
37
38 LAST_POST_TIME = 'last_post_time'
38 LAST_POST_TIME = 'last_post_time'
39 LAST_LOGIN_TIME = 'last_login_time'
39 LAST_LOGIN_TIME = 'last_login_time'
40 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
40 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
41 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
41 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
42
42
43 LABEL_TITLE = _('Title')
43 LABEL_TITLE = _('Title')
44 LABEL_TEXT = _('Text')
44 LABEL_TEXT = _('Text')
45 LABEL_TAG = _('Tag')
45 LABEL_TAG = _('Tag')
46 LABEL_SEARCH = _('Search')
46 LABEL_SEARCH = _('Search')
47
47
48 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
48 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
49 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
49 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
50
50
51 TAG_MAX_LENGTH = 20
51 TAG_MAX_LENGTH = 20
52
52
53 TEXTAREA_ROWS = 4
53 TEXTAREA_ROWS = 4
54
54
55 TRIPCODE_DELIM = '#'
55 TRIPCODE_DELIM = '#'
56
56
57 # TODO Maybe this may be converted into the database table?
57 # TODO Maybe this may be converted into the database table?
58 MIMETYPE_EXTENSIONS = {
58 MIMETYPE_EXTENSIONS = {
59 'image/jpeg': 'jpeg',
59 'image/jpeg': 'jpeg',
60 'image/png': 'png',
60 'image/png': 'png',
61 'image/gif': 'gif',
61 'image/gif': 'gif',
62 'video/webm': 'webm',
62 'video/webm': 'webm',
63 'application/pdf': 'pdf',
63 'application/pdf': 'pdf',
64 'x-diff': 'diff',
64 'x-diff': 'diff',
65 'image/svg+xml': 'svg',
65 'image/svg+xml': 'svg',
66 'application/x-shockwave-flash': 'swf',
66 'application/x-shockwave-flash': 'swf',
67 'image/x-ms-bmp': 'bmp',
67 'image/x-ms-bmp': 'bmp',
68 'image/bmp': 'bmp',
68 'image/bmp': 'bmp',
69 }
69 }
70
70
71
71
72 def get_timezones():
72 def get_timezones():
73 timezones = []
73 timezones = []
74 for tz in pytz.common_timezones:
74 for tz in pytz.common_timezones:
75 timezones.append((tz, tz),)
75 timezones.append((tz, tz),)
76 return timezones
76 return timezones
77
77
78
78
79 class FormatPanel(forms.Textarea):
79 class FormatPanel(forms.Textarea):
80 """
80 """
81 Panel for text formatting. Consists of buttons to add different tags to the
81 Panel for text formatting. Consists of buttons to add different tags to the
82 form text area.
82 form text area.
83 """
83 """
84
84
85 def render(self, name, value, attrs=None):
85 def render(self, name, value, attrs=None):
86 output = '<div id="mark-panel">'
86 output = '<div id="mark-panel">'
87 for formatter in formatters:
87 for formatter in formatters:
88 output += '<span class="mark_btn"' + \
88 output += '<span class="mark_btn"' + \
89 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
89 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
90 '\', \'' + formatter.format_right + '\')">' + \
90 '\', \'' + formatter.format_right + '\')">' + \
91 formatter.preview_left + formatter.name + \
91 formatter.preview_left + formatter.name + \
92 formatter.preview_right + '</span>'
92 formatter.preview_right + '</span>'
93
93
94 output += '</div>'
94 output += '</div>'
95 output += super(FormatPanel, self).render(name, value, attrs=attrs)
95 output += super(FormatPanel, self).render(name, value, attrs=attrs)
96
96
97 return output
97 return output
98
98
99
99
100 class PlainErrorList(ErrorList):
100 class PlainErrorList(ErrorList):
101 def __unicode__(self):
101 def __unicode__(self):
102 return self.as_text()
102 return self.as_text()
103
103
104 def as_text(self):
104 def as_text(self):
105 return ''.join(['(!) %s ' % e for e in self])
105 return ''.join(['(!) %s ' % e for e in self])
106
106
107
107
108 class NeboardForm(forms.Form):
108 class NeboardForm(forms.Form):
109 """
109 """
110 Form with neboard-specific formatting.
110 Form with neboard-specific formatting.
111 """
111 """
112
112
113 def as_div(self):
113 def as_div(self):
114 """
114 """
115 Returns this form rendered as HTML <as_div>s.
115 Returns this form rendered as HTML <as_div>s.
116 """
116 """
117
117
118 return self._html_output(
118 return self._html_output(
119 # TODO Do not show hidden rows in the list here
119 # TODO Do not show hidden rows in the list here
120 normal_row='<div class="form-row">'
120 normal_row='<div class="form-row">'
121 '<div class="form-label">'
121 '<div class="form-label">'
122 '%(label)s'
122 '%(label)s'
123 '</div>'
123 '</div>'
124 '<div class="form-input">'
124 '<div class="form-input">'
125 '%(field)s'
125 '%(field)s'
126 '</div>'
126 '</div>'
127 '</div>'
127 '</div>'
128 '<div class="form-row">'
128 '<div class="form-row">'
129 '%(help_text)s'
129 '%(help_text)s'
130 '</div>',
130 '</div>',
131 error_row='<div class="form-row">'
131 error_row='<div class="form-row">'
132 '<div class="form-label"></div>'
132 '<div class="form-label"></div>'
133 '<div class="form-errors">%s</div>'
133 '<div class="form-errors">%s</div>'
134 '</div>',
134 '</div>',
135 row_ender='</div>',
135 row_ender='</div>',
136 help_text_html='%s',
136 help_text_html='%s',
137 errors_on_separate_row=True)
137 errors_on_separate_row=True)
138
138
139 def as_json_errors(self):
139 def as_json_errors(self):
140 errors = []
140 errors = []
141
141
142 for name, field in list(self.fields.items()):
142 for name, field in list(self.fields.items()):
143 if self[name].errors:
143 if self[name].errors:
144 errors.append({
144 errors.append({
145 'field': name,
145 'field': name,
146 'errors': self[name].errors.as_text(),
146 'errors': self[name].errors.as_text(),
147 })
147 })
148
148
149 return errors
149 return errors
150
150
151
151
152 class PostForm(NeboardForm):
152 class PostForm(NeboardForm):
153
153
154 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
154 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
155 label=LABEL_TITLE,
155 label=LABEL_TITLE,
156 widget=forms.TextInput(
156 widget=forms.TextInput(
157 attrs={ATTRIBUTE_PLACEHOLDER:
157 attrs={ATTRIBUTE_PLACEHOLDER:
158 'test#tripcode'}))
158 'test#tripcode'}))
159 text = forms.CharField(
159 text = forms.CharField(
160 widget=FormatPanel(attrs={
160 widget=FormatPanel(attrs={
161 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
161 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
162 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
162 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
163 }),
163 }),
164 required=False, label=LABEL_TEXT)
164 required=False, label=LABEL_TEXT)
165 file = forms.FileField(required=False, label=_('File'),
165 file = forms.FileField(required=False, label=_('File'),
166 widget=forms.ClearableFileInput(
166 widget=forms.ClearableFileInput(
167 attrs={'accept': 'file/*'}))
167 attrs={'accept': 'file/*'}))
168 file_url = forms.CharField(required=False, label=_('File URL'),
168 file_url = forms.CharField(required=False, label=_('File URL'),
169 widget=forms.TextInput(
169 widget=forms.TextInput(
170 attrs={ATTRIBUTE_PLACEHOLDER:
170 attrs={ATTRIBUTE_PLACEHOLDER:
171 'http://example.com/image.png'}))
171 'http://example.com/image.png'}))
172
172
173 # This field is for spam prevention only
173 # This field is for spam prevention only
174 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
174 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
175 widget=forms.TextInput(attrs={
175 widget=forms.TextInput(attrs={
176 'class': 'form-email'}))
176 'class': 'form-email'}))
177 threads = forms.CharField(required=False, label=_('Additional threads'),
177 threads = forms.CharField(required=False, label=_('Additional threads'),
178 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
178 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
179 '123 456 789'}))
179 '123 456 789'}))
180 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
180 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
181
181
182 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
182 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
183 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
183 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
184 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
184 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
185
185
186 session = None
186 session = None
187 need_to_ban = False
187 need_to_ban = False
188 image = None
188 image = None
189
189
190 def _update_file_extension(self, file):
190 def _update_file_extension(self, file):
191 if file:
191 if file:
192 mimetype = get_file_mimetype(file)
192 mimetype = get_file_mimetype(file)
193 extension = MIMETYPE_EXTENSIONS.get(mimetype)
193 extension = MIMETYPE_EXTENSIONS.get(mimetype)
194 if extension:
194 if extension:
195 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
195 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
196 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
196 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
197
197
198 file.name = new_filename
198 file.name = new_filename
199 else:
199 else:
200 logger = logging.getLogger('boards.forms.extension')
200 logger = logging.getLogger('boards.forms.extension')
201
201
202 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
202 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
203
203
204 def clean_title(self):
204 def clean_title(self):
205 title = self.cleaned_data['title']
205 title = self.cleaned_data['title']
206 if title:
206 if title:
207 if len(title) > TITLE_MAX_LENGTH:
207 if len(title) > TITLE_MAX_LENGTH:
208 raise forms.ValidationError(_('Title must have less than %s '
208 raise forms.ValidationError(_('Title must have less than %s '
209 'characters') %
209 'characters') %
210 str(TITLE_MAX_LENGTH))
210 str(TITLE_MAX_LENGTH))
211 return title
211 return title
212
212
213 def clean_text(self):
213 def clean_text(self):
214 text = self.cleaned_data['text'].strip()
214 text = self.cleaned_data['text'].strip()
215 if text:
215 if text:
216 max_length = board_settings.get_int('Forms', 'MaxTextLength')
216 max_length = board_settings.get_int('Forms', 'MaxTextLength')
217 if len(text) > max_length:
217 if len(text) > max_length:
218 raise forms.ValidationError(_('Text must have less than %s '
218 raise forms.ValidationError(_('Text must have less than %s '
219 'characters') % str(max_length))
219 'characters') % str(max_length))
220 return text
220 return text
221
221
222 def clean_file(self):
222 def clean_file(self):
223 file = self.cleaned_data['file']
223 file = self.cleaned_data['file']
224
224
225 if file:
225 if file:
226 validate_file_size(file.size)
226 validate_file_size(file.size)
227 self._update_file_extension(file)
227 self._update_file_extension(file)
228
228
229 return file
229 return file
230
230
231 def clean_file_url(self):
231 def clean_file_url(self):
232 url = self.cleaned_data['file_url']
232 url = self.cleaned_data['file_url']
233
233
234 file = None
234 file = None
235
235
236 if url:
236 if url:
237 file = get_image_by_alias(url, self.session)
237 file = get_image_by_alias(url, self.session)
238 self.image = file
238 self.image = file
239
239
240 if file is not None:
240 if file is not None:
241 return
241 return
242
242
243 if file is None:
243 if file is None:
244 file = self._get_file_from_url(url)
244 file = self._get_file_from_url(url)
245 if not file:
245 if not file:
246 raise forms.ValidationError(_('Invalid URL'))
246 raise forms.ValidationError(_('Invalid URL'))
247 else:
247 else:
248 validate_file_size(file.size)
248 validate_file_size(file.size)
249 self._update_file_extension(file)
249 self._update_file_extension(file)
250
250
251 return file
251 return file
252
252
253 def clean_threads(self):
253 def clean_threads(self):
254 threads_str = self.cleaned_data['threads']
254 threads_str = self.cleaned_data['threads']
255
255
256 if len(threads_str) > 0:
256 if len(threads_str) > 0:
257 threads_id_list = threads_str.split(' ')
257 threads_id_list = threads_str.split(' ')
258
258
259 threads = list()
259 threads = list()
260
260
261 for thread_id in threads_id_list:
261 for thread_id in threads_id_list:
262 try:
262 try:
263 thread = Post.objects.get(id=int(thread_id))
263 thread = Post.objects.get(id=int(thread_id))
264 if not thread.is_opening() or thread.get_thread().is_archived():
264 if not thread.is_opening() or thread.get_thread().is_archived():
265 raise ObjectDoesNotExist()
265 raise ObjectDoesNotExist()
266 threads.append(thread)
266 threads.append(thread)
267 except (ObjectDoesNotExist, ValueError):
267 except (ObjectDoesNotExist, ValueError):
268 raise forms.ValidationError(_('Invalid additional thread list'))
268 raise forms.ValidationError(_('Invalid additional thread list'))
269
269
270 return threads
270 return threads
271
271
272 def clean(self):
272 def clean(self):
273 cleaned_data = super(PostForm, self).clean()
273 cleaned_data = super(PostForm, self).clean()
274
274
275 if cleaned_data['email']:
275 if cleaned_data['email']:
276 self.need_to_ban = True
276 if board_settings.get_bool('Forms', 'Autoban'):
277 self.need_to_ban = True
277 raise forms.ValidationError('A human cannot enter a hidden field')
278 raise forms.ValidationError('A human cannot enter a hidden field')
278
279
279 if not self.errors:
280 if not self.errors:
280 self._clean_text_file()
281 self._clean_text_file()
281
282
282 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
283 limit_speed = board_settings.get_bool('Forms', 'LimitPostingSpeed')
283 limit_first = board_settings.get_bool('Forms', 'LimitFirstPosting')
284 limit_first = board_settings.get_bool('Forms', 'LimitFirstPosting')
284
285
285 settings_manager = get_settings_manager(self)
286 settings_manager = get_settings_manager(self)
286 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
287 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
287 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
288 pow_difficulty = board_settings.get_int('Forms', 'PowDifficulty')
288 if pow_difficulty > 0:
289 if pow_difficulty > 0:
289 # PoW-based
290 # PoW-based
290 if cleaned_data['timestamp'] \
291 if cleaned_data['timestamp'] \
291 and cleaned_data['iteration'] and cleaned_data['guess'] \
292 and cleaned_data['iteration'] and cleaned_data['guess'] \
292 and not settings_manager.get_setting('confirmed_user'):
293 and not settings_manager.get_setting('confirmed_user'):
293 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
294 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
294 else:
295 else:
295 # Time-based
296 # Time-based
296 self._validate_posting_speed()
297 self._validate_posting_speed()
297 settings_manager.set_setting('confirmed_user', True)
298 settings_manager.set_setting('confirmed_user', True)
298
299
299
300
300 return cleaned_data
301 return cleaned_data
301
302
302 def get_file(self):
303 def get_file(self):
303 """
304 """
304 Gets file from form or URL.
305 Gets file from form or URL.
305 """
306 """
306
307
307 file = self.cleaned_data['file']
308 file = self.cleaned_data['file']
308 return file or self.cleaned_data['file_url']
309 return file or self.cleaned_data['file_url']
309
310
310 def get_tripcode(self):
311 def get_tripcode(self):
311 title = self.cleaned_data['title']
312 title = self.cleaned_data['title']
312 if title is not None and TRIPCODE_DELIM in title:
313 if title is not None and TRIPCODE_DELIM in title:
313 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
314 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
314 tripcode = hashlib.md5(code.encode()).hexdigest()
315 tripcode = hashlib.md5(code.encode()).hexdigest()
315 else:
316 else:
316 tripcode = ''
317 tripcode = ''
317 return tripcode
318 return tripcode
318
319
319 def get_title(self):
320 def get_title(self):
320 title = self.cleaned_data['title']
321 title = self.cleaned_data['title']
321 if title is not None and TRIPCODE_DELIM in title:
322 if title is not None and TRIPCODE_DELIM in title:
322 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
323 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
323 else:
324 else:
324 return title
325 return title
325
326
326 def get_images(self):
327 def get_images(self):
327 if self.image:
328 if self.image:
328 return [self.image]
329 return [self.image]
329 else:
330 else:
330 return []
331 return []
331
332
332 def is_subscribe(self):
333 def is_subscribe(self):
333 return self.cleaned_data['subscribe']
334 return self.cleaned_data['subscribe']
334
335
335 def _clean_text_file(self):
336 def _clean_text_file(self):
336 text = self.cleaned_data.get('text')
337 text = self.cleaned_data.get('text')
337 file = self.get_file()
338 file = self.get_file()
338 images = self.get_images()
339 images = self.get_images()
339
340
340 if (not text) and (not file) and len(images) == 0:
341 if (not text) and (not file) and len(images) == 0:
341 error_message = _('Either text or file must be entered.')
342 error_message = _('Either text or file must be entered.')
342 self._errors['text'] = self.error_class([error_message])
343 self._errors['text'] = self.error_class([error_message])
343
344
344 def _validate_posting_speed(self):
345 def _validate_posting_speed(self):
345 can_post = True
346 can_post = True
346
347
347 posting_delay = board_settings.get_int('Forms', 'PostingDelay')
348 posting_delay = board_settings.get_int('Forms', 'PostingDelay')
348
349
349 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
350 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
350 now = time.time()
351 now = time.time()
351
352
352 current_delay = 0
353 current_delay = 0
353
354
354 if LAST_POST_TIME not in self.session:
355 if LAST_POST_TIME not in self.session:
355 self.session[LAST_POST_TIME] = now
356 self.session[LAST_POST_TIME] = now
356
357
357 need_delay = True
358 need_delay = True
358 else:
359 else:
359 last_post_time = self.session.get(LAST_POST_TIME)
360 last_post_time = self.session.get(LAST_POST_TIME)
360 current_delay = int(now - last_post_time)
361 current_delay = int(now - last_post_time)
361
362
362 need_delay = current_delay < posting_delay
363 need_delay = current_delay < posting_delay
363
364
364 if need_delay:
365 if need_delay:
365 delay = posting_delay - current_delay
366 delay = posting_delay - current_delay
366 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
367 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
367 delay) % {'delay': delay}
368 delay) % {'delay': delay}
368 self._errors['text'] = self.error_class([error_message])
369 self._errors['text'] = self.error_class([error_message])
369
370
370 can_post = False
371 can_post = False
371
372
372 if can_post:
373 if can_post:
373 self.session[LAST_POST_TIME] = now
374 self.session[LAST_POST_TIME] = now
374
375
375 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
376 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
376 """
377 """
377 Gets an file file from URL.
378 Gets an file file from URL.
378 """
379 """
379
380
380 try:
381 try:
381 return download(url)
382 return download(url)
382 except forms.ValidationError as e:
383 except forms.ValidationError as e:
383 raise e
384 raise e
384 except Exception as e:
385 except Exception as e:
385 raise forms.ValidationError(e)
386 raise forms.ValidationError(e)
386
387
387 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
388 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
388 post_time = timezone.datetime.fromtimestamp(
389 post_time = timezone.datetime.fromtimestamp(
389 int(timestamp[:-3]), tz=timezone.get_current_timezone())
390 int(timestamp[:-3]), tz=timezone.get_current_timezone())
390
391
391 payload = timestamp + message.replace('\r\n', '\n')
392 payload = timestamp + message.replace('\r\n', '\n')
392 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
393 difficulty = board_settings.get_int('Forms', 'PowDifficulty')
393 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
394 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
394 if len(target) < POW_HASH_LENGTH:
395 if len(target) < POW_HASH_LENGTH:
395 target = '0' * (POW_HASH_LENGTH - len(target)) + target
396 target = '0' * (POW_HASH_LENGTH - len(target)) + target
396
397
397 computed_guess = hashlib.sha256((payload + iteration).encode())\
398 computed_guess = hashlib.sha256((payload + iteration).encode())\
398 .hexdigest()[0:POW_HASH_LENGTH]
399 .hexdigest()[0:POW_HASH_LENGTH]
399 if guess != computed_guess or guess > target:
400 if guess != computed_guess or guess > target:
400 self._errors['text'] = self.error_class(
401 self._errors['text'] = self.error_class(
401 [_('Invalid PoW.')])
402 [_('Invalid PoW.')])
402
403
403
404
404
405
405 class ThreadForm(PostForm):
406 class ThreadForm(PostForm):
406
407
407 tags = forms.CharField(
408 tags = forms.CharField(
408 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
409 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
409 max_length=100, label=_('Tags'), required=True)
410 max_length=100, label=_('Tags'), required=True)
410 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
411 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
411
412
412 def clean_tags(self):
413 def clean_tags(self):
413 tags = self.cleaned_data['tags'].strip()
414 tags = self.cleaned_data['tags'].strip()
414
415
415 if not tags or not REGEX_TAGS.match(tags):
416 if not tags or not REGEX_TAGS.match(tags):
416 raise forms.ValidationError(
417 raise forms.ValidationError(
417 _('Inappropriate characters in tags.'))
418 _('Inappropriate characters in tags.'))
418
419
419 required_tag_exists = False
420 required_tag_exists = False
420 tag_set = set()
421 tag_set = set()
421 for tag_string in tags.split():
422 for tag_string in tags.split():
422 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
423 tag, created = Tag.objects.get_or_create(name=tag_string.strip().lower())
423 tag_set.add(tag)
424 tag_set.add(tag)
424
425
425 # If this is a new tag, don't check for its parents because nobody
426 # If this is a new tag, don't check for its parents because nobody
426 # added them yet
427 # added them yet
427 if not created:
428 if not created:
428 tag_set |= set(tag.get_all_parents())
429 tag_set |= set(tag.get_all_parents())
429
430
430 for tag in tag_set:
431 for tag in tag_set:
431 if tag.required:
432 if tag.required:
432 required_tag_exists = True
433 required_tag_exists = True
433 break
434 break
434
435
435 if not required_tag_exists:
436 if not required_tag_exists:
436 raise forms.ValidationError(
437 raise forms.ValidationError(
437 _('Need at least one section.'))
438 _('Need at least one section.'))
438
439
439 return tag_set
440 return tag_set
440
441
441 def clean(self):
442 def clean(self):
442 cleaned_data = super(ThreadForm, self).clean()
443 cleaned_data = super(ThreadForm, self).clean()
443
444
444 return cleaned_data
445 return cleaned_data
445
446
446 def is_monochrome(self):
447 def is_monochrome(self):
447 return self.cleaned_data['monochrome']
448 return self.cleaned_data['monochrome']
448
449
449
450
450 class SettingsForm(NeboardForm):
451 class SettingsForm(NeboardForm):
451
452
452 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
453 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
453 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
454 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
454 username = forms.CharField(label=_('User name'), required=False)
455 username = forms.CharField(label=_('User name'), required=False)
455 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
456 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
456
457
457 def clean_username(self):
458 def clean_username(self):
458 username = self.cleaned_data['username']
459 username = self.cleaned_data['username']
459
460
460 if username and not REGEX_USERNAMES.match(username):
461 if username and not REGEX_USERNAMES.match(username):
461 raise forms.ValidationError(_('Inappropriate characters.'))
462 raise forms.ValidationError(_('Inappropriate characters.'))
462
463
463 return username
464 return username
464
465
465
466
466 class SearchForm(NeboardForm):
467 class SearchForm(NeboardForm):
467 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
468 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