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