##// END OF EJS Templates
Added tag 4.11.0 for changeset 2f9d7bc70eef
Added tag 4.11.0 for changeset 2f9d7bc70eef

File last commit:

r2088:09f03b4f default
r2110:208e1fd7 default
Show More
__init__.py
540 lines | 18.5 KiB | text/x-python | PythonLexer
import logging
import pytz
import re
from PIL import Image
from django import forms
from django.core.files.images import get_image_dimensions
from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
from django.forms.utils import ErrorList
from django.utils.translation import ugettext_lazy as _, ungettext_lazy
import boards.settings as board_settings
from boards import utils
from boards.abstracts.constants import REGEX_TAGS
from boards.abstracts.settingsmanager import get_settings_manager, \
SETTING_CONFIRMED_USER
from boards.abstracts.sticker_factory import get_attachment_by_alias
from boards.forms.fields import UrlFileField
from boards.forms.validators import TimeValidator, PowValidator
from boards.mdx_neboard import formatters
from boards.models import Attachment
from boards.models import Tag
from boards.models.attachment import StickerPack
from boards.models.attachment.downloaders import download, REGEX_MAGNET
from boards.models.attachment.viewers import FILE_TYPES_IMAGE
from boards.models.post import TITLE_MAX_LENGTH
from boards.settings import SECTION_FORMS
from boards.utils import validate_file_size, get_file_mimetype, \
FILE_EXTENSION_DELIMITER, get_tripcode_from_text
FORMAT_PANEL_BUTTON = '<span class="mark_btn" ' \
'onClick="addMarkToMsg(\'{}\', \'{}\')">{}{}{}</span>'
REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
VETERAN_POSTING_DELAY = 5
ATTRIBUTE_PLACEHOLDER = 'placeholder'
ATTRIBUTE_ROWS = 'rows'
TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
LABEL_TITLE = _('Title')
LABEL_TEXT = _('Text')
LABEL_TAG = _('Tag')
LABEL_SEARCH = _('Search')
LABEL_FILE = _('File')
LABEL_DUPLICATES = _('Check for duplicates')
LABEL_URL = _('Do not download URLs')
ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
ERROR_DUPLICATES = 'Some files are already present on the board.'
TAG_MAX_LENGTH = 20
TEXTAREA_ROWS = 4
TRIPCODE_DELIM = '##'
# TODO Maybe this may be converted into the database table?
MIMETYPE_EXTENSIONS = {
'image/jpeg': 'jpeg',
'image/png': 'png',
'image/gif': 'gif',
'video/webm': 'webm',
'application/pdf': 'pdf',
'x-diff': 'diff',
'image/svg+xml': 'svg',
'application/x-shockwave-flash': 'swf',
'image/x-ms-bmp': 'bmp',
'image/bmp': 'bmp',
}
DOWN_MODE_DOWNLOAD = 'DOWNLOAD'
DOWN_MODE_DOWNLOAD_UNIQUE = 'DOWNLOAD_UNIQUE'
DOWN_MODE_URL = 'URL'
DOWN_MODE_TRY = 'TRY'
logger = logging.getLogger('boards.forms')
def get_timezones():
timezones = []
for tz in pytz.common_timezones:
timezones.append((tz, tz),)
return timezones
class FormatPanel(forms.Textarea):
"""
Panel for text formatting. Consists of buttons to add different tags to the
form text area.
"""
def render(self, name, value, attrs=None):
output_template = '<div id="mark-panel">{}</div>'
buttons = [self._get_button(formatter) for formatter in formatters]
output = output_template.format(''.join(buttons))
output += super(FormatPanel, self).render(name, value, attrs=attrs)
return output
def _get_button(self, formatter):
return FORMAT_PANEL_BUTTON.format(
formatter.format_left,
formatter.format_right,
formatter.preview_left,
formatter.name,
formatter.preview_right)
class PlainErrorList(ErrorList):
def __unicode__(self):
return self.as_text()
def as_text(self):
return ''.join(['(!) %s ' % e for e in self])
class NeboardForm(forms.Form):
"""
Form with neboard-specific formatting.
"""
required_css_class = 'required-field'
def as_div(self):
"""
Returns this form rendered as HTML <as_div>s.
"""
return self._html_output(
# TODO Do not show hidden rows in the list here
normal_row='<div class="form-row">'
'<div class="form-label">'
'%(label)s'
'</div>'
'<div class="form-input">'
'%(field)s'
'</div>'
'</div>'
'<div class="form-row">'
'%(help_text)s'
'</div>',
error_row='<div class="form-row">'
'<div class="form-label"></div>'
'<div class="form-errors">%s</div>'
'</div>',
row_ender='</div>',
help_text_html='%s',
errors_on_separate_row=True)
def as_json_errors(self):
errors = []
for name, field in list(self.fields.items()):
if self[name].errors:
errors.append({
'field': name,
'errors': self[name].errors.as_text(),
})
return errors
class PostForm(NeboardForm):
title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
label=LABEL_TITLE,
widget=forms.TextInput(
attrs={ATTRIBUTE_PLACEHOLDER: 'Title{}tripcode'.format(TRIPCODE_DELIM)}))
text = forms.CharField(
widget=FormatPanel(attrs={
ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
ATTRIBUTE_ROWS: TEXTAREA_ROWS,
}),
required=False, label=LABEL_TEXT)
download_mode = forms.ChoiceField(
choices=(
(DOWN_MODE_TRY, _('Download or insert as URLs')),
(DOWN_MODE_DOWNLOAD, _('Download')),
(DOWN_MODE_DOWNLOAD_UNIQUE, _('Download and check for uniqueness')),
(DOWN_MODE_URL, _('Insert as URLs')),
),
initial=DOWN_MODE_TRY,
label=_('File process mode'))
file = UrlFileField(required=False, label=LABEL_FILE)
# This field is for spam prevention only
email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
widget=forms.TextInput(attrs={
'class': 'form-email'}))
subscribe = forms.BooleanField(required=False,
label=_('Subscribe to thread'))
guess = forms.CharField(widget=forms.HiddenInput(), required=False)
timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
need_to_ban = False
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, field_order=None,
use_required_attribute=None, renderer=None, session=None):
super().__init__(data, files, auto_id, prefix, initial, error_class,
label_suffix, empty_permitted, field_order,
use_required_attribute, renderer)
self.session = session
def clean_title(self):
title = self.cleaned_data['title']
if title:
if len(title) > TITLE_MAX_LENGTH:
raise forms.ValidationError(_('Title must have less than %s '
'characters') %
str(TITLE_MAX_LENGTH))
return title
def clean_text(self):
text = self.cleaned_data['text'].strip()
if text:
max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
if len(text) > max_length:
raise forms.ValidationError(_('Text must have less than %s '
'characters') % str(max_length))
return text
def clean_file(self):
return self._clean_files(self.cleaned_data['file'])
def clean(self):
cleaned_data = super(PostForm, self).clean()
if cleaned_data['email']:
if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
self.need_to_ban = True
raise forms.ValidationError('A human cannot enter a hidden field')
if not self.errors:
self._clean_text_file()
limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
settings_manager = get_settings_manager(self)
if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting(SETTING_CONFIRMED_USER)):
pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
if pow_difficulty > 0:
validator = PowValidator(
self.session, cleaned_data['timestamp'],
cleaned_data['iteration'], cleaned_data['guess'],
cleaned_data['text'])
else:
validator = TimeValidator(self.session)
validator.validate()
for error in validator.get_errors():
self._add_general_error(error)
settings_manager.set_setting(SETTING_CONFIRMED_USER, True)
if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE:
self._check_file_duplicates(self.get_files())
return cleaned_data
def get_files(self):
"""
Gets file from form or URL.
"""
files = []
for file in self.cleaned_data['file']:
if isinstance(file, UploadedFile):
files.append(file)
return files
def get_file_urls(self):
files = []
for file in self.cleaned_data['file']:
if type(file) == str:
files.append(file)
return files
def get_tripcode(self):
title = self.cleaned_data['title']
if title is not None and TRIPCODE_DELIM in title:
tripcode = get_tripcode_from_text(title.split(TRIPCODE_DELIM, maxsplit=1)[1])
else:
tripcode = ''
return tripcode
def get_title(self):
title = self.cleaned_data['title']
if title is not None and TRIPCODE_DELIM in title:
return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
else:
return title
def get_images(self):
return self.cleaned_data.get('stickers', [])
def is_subscribe(self):
return self.cleaned_data['subscribe']
def _update_file_extension(self, file):
if file:
mimetype = get_file_mimetype(file)
extension = MIMETYPE_EXTENSIONS.get(mimetype)
if extension:
filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
new_filename = filename + FILE_EXTENSION_DELIMITER + extension
file.name = new_filename
else:
logger.info('Unrecognized file mimetype: {}'.format(mimetype))
def _clean_files(self, inputs):
files = []
max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
if len(inputs) > max_file_count:
raise forms.ValidationError(
ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
max_file_count) % {'files': max_file_count})
size = 0
for file_input in inputs:
if isinstance(file_input, UploadedFile):
file = self._clean_file_file(file_input)
size += file.size
files.append(file)
else:
files.append(self._clean_file_url(file_input))
for file in files:
self._validate_image_dimensions(file)
validate_file_size(size)
return files
def _validate_image_dimensions(self, file):
if isinstance(file, UploadedFile):
mimetype = get_file_mimetype(file)
if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
try:
print(get_image_dimensions(file))
except Exception:
raise forms.ValidationError('Possible decompression bomb or large image.')
def _clean_file_file(self, file):
self._update_file_extension(file)
return file
def _clean_file_url(self, url):
file = None
if url:
mode = self.cleaned_data['download_mode']
if mode == DOWN_MODE_URL:
return url
try:
image = get_attachment_by_alias(url, self.session)
if image is not None:
if 'stickers' not in self.cleaned_data:
self.cleaned_data['stickers'] = []
self.cleaned_data['stickers'].append(image)
return
if file is None:
file = self._get_file_from_url(url)
if not file:
raise forms.ValidationError(_('Invalid URL'))
self._update_file_extension(file)
except forms.ValidationError as e:
# Assume we will get the plain URL instead of a file and save it
if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)):
logger.info('Error in forms: {}'.format(e))
return url
else:
raise e
return file
def _clean_text_file(self):
text = self.cleaned_data.get('text')
file = self.get_files()
file_url = self.get_file_urls()
images = self.get_images()
if (not text) and (not file) and (not file_url) and len(images) == 0:
error_message = _('Either text or file must be entered.')
self._add_general_error(error_message)
def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
"""
Gets an file file from URL.
"""
try:
return download(url)
except forms.ValidationError as e:
raise e
except Exception as e:
raise forms.ValidationError(e)
def _check_file_duplicates(self, files):
for file in files:
file_hash = utils.get_file_hash(file)
if Attachment.objects.get_existing_duplicate(file_hash, file):
self._add_general_error(_(ERROR_DUPLICATES))
def _add_general_error(self, message):
self.add_error('text', forms.ValidationError(message))
class ThreadForm(PostForm):
tags = forms.CharField(
widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
max_length=100, label=_('Tags'), required=True)
monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
stickerpack = forms.BooleanField(label=_('Sticker Pack'), required=False)
def clean_tags(self):
tags = self.cleaned_data['tags'].strip()
if not tags or not REGEX_TAGS.match(tags):
raise forms.ValidationError(
_('Inappropriate characters in tags.'))
default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
.strip().lower()
required_tag_exists = False
tag_set = set()
for tag_string in tags.split():
tag_name = tag_string.strip().lower()
if tag_name == default_tag_name:
required_tag_exists = True
tag, created = Tag.objects.get_or_create_with_alias(
name=tag_name, required=True)
else:
tag, created = Tag.objects.get_or_create_with_alias(name=tag_name)
tag_set.add(tag)
# If this is a new tag, don't check for its parents because nobody
# added them yet
if not created:
tag_set |= set(tag.get_all_parents())
for tag in tag_set:
if tag.required:
required_tag_exists = True
break
# Use default tag if no section exists
if not required_tag_exists:
default_tag, created = Tag.objects.get_or_create_with_alias(
name=default_tag_name, required=True)
tag_set.add(default_tag)
return tag_set
def clean(self):
cleaned_data = super(ThreadForm, self).clean()
return cleaned_data
def is_monochrome(self):
return self.cleaned_data['monochrome']
def clean_stickerpack(self):
stickerpack = self.cleaned_data['stickerpack']
if stickerpack:
tripcode = self.get_tripcode()
if not tripcode:
raise forms.ValidationError(_(
'Tripcode should be specified to own a stickerpack.'))
title = self.get_title()
if not title:
raise forms.ValidationError(_(
'Title should be specified as a stickerpack name.'))
if not REGEX_TAGS.match(title):
raise forms.ValidationError(_('Inappropriate sticker pack name.'))
existing_pack = StickerPack.objects.filter(name=title).first()
if existing_pack:
if existing_pack.tripcode != tripcode:
raise forms.ValidationError(_(
'A sticker pack with this name already exists and is'
' owned by another tripcode.'))
if not existing_pack.tripcode:
raise forms.ValidationError(_(
'This sticker pack can only be updated by an '
'administrator.'))
return stickerpack
def is_stickerpack(self):
return self.cleaned_data['stickerpack']
class SettingsForm(NeboardForm):
theme = forms.ChoiceField(
choices=board_settings.get_list_dict('View', 'Themes'),
label=_('Theme'))
image_viewer = forms.ChoiceField(
choices=board_settings.get_list_dict('View', 'ImageViewers'),
label=_('Image view mode'))
username = forms.CharField(label=_('User name'), required=False)
timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
subscribe_by_default = forms.BooleanField(
required=False, label=_('Subscribe to threads by default'))
def clean_username(self):
username = self.cleaned_data['username']
if username and not REGEX_USERNAMES.match(username):
raise forms.ValidationError(_('Inappropriate characters.'))
return username
class SearchForm(NeboardForm):
query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)