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