import re import time import pytz from django import forms from django.core.files.uploadedfile import SimpleUploadedFile from django.core.exceptions import ObjectDoesNotExist from django.forms.util import ErrorList from django.utils.translation import ugettext_lazy as _ import requests from boards.mdx_neboard import formatters from boards.models.post import TITLE_MAX_LENGTH from boards.models import Tag, Post from neboard import settings import boards.settings as board_settings HEADER_CONTENT_LENGTH = 'content-length' HEADER_CONTENT_TYPE = 'content-type' CONTENT_TYPE_IMAGE = ( 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', ) REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE) VETERAN_POSTING_DELAY = 5 ATTRIBUTE_PLACEHOLDER = 'placeholder' ATTRIBUTE_ROWS = 'rows' LAST_POST_TIME = 'last_post_time' LAST_LOGIN_TIME = 'last_login_time' 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') ERROR_SPEED = _('Please wait %s seconds before sending message') TAG_MAX_LENGTH = 20 IMAGE_DOWNLOAD_CHUNK_BYTES = 100000 HTTP_RESULT_OK = 200 TEXTAREA_ROWS = 4 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 = '
' for formatter in formatters: output += '' + \ formatter.preview_left + formatter.name + \ formatter.preview_right + '' output += '
' output += super(FormatPanel, self).render(name, value, attrs=None) return output 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. """ def as_div(self): """ Returns this form rendered as HTML s. """ return self._html_output( # TODO Do not show hidden rows in the list here normal_row='
' '
' '%(label)s' '
' '
' '%(field)s' '
' '
' '
' '%(help_text)s' '
', error_row='
' '
' '
%s
' '
', row_ender='', 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) text = forms.CharField( widget=FormatPanel(attrs={ ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER, ATTRIBUTE_ROWS: TEXTAREA_ROWS, }), required=False, label=LABEL_TEXT) image = forms.ImageField(required=False, label=_('Image'), widget=forms.ClearableFileInput( attrs={'accept': 'image/*'})) image_url = forms.CharField(required=False, label=_('Image URL'), widget=forms.TextInput( attrs={ATTRIBUTE_PLACEHOLDER: 'http://example.com/image.png'})) # 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'})) threads = forms.CharField(required=False, label=_('Additional threads'), widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: '123 456 789'})) session = None need_to_ban = False 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('Forms', 'MaxTextLength') if len(text) > max_length: raise forms.ValidationError(_('Text must have less than %s ' 'characters') % str(max_length)) return text def clean_image(self): image = self.cleaned_data['image'] if image: self.validate_image_size(image.size) return image def clean_image_url(self): url = self.cleaned_data['image_url'] image = None if url: image = self._get_image_from_url(url) if not image: raise forms.ValidationError(_('Invalid URL')) else: self.validate_image_size(image.size) return image def clean_threads(self): threads_str = self.cleaned_data['threads'] if len(threads_str) > 0: threads_id_list = threads_str.split(' ') threads = list() for thread_id in threads_id_list: try: thread = Post.objects.get(id=int(thread_id)) if not thread.is_opening() or thread.get_thread().archived: raise ObjectDoesNotExist() threads.append(thread) except (ObjectDoesNotExist, ValueError): raise forms.ValidationError(_('Invalid additional thread list')) return threads def clean(self): cleaned_data = super(PostForm, self).clean() if cleaned_data['email']: self.need_to_ban = True raise forms.ValidationError('A human cannot enter a hidden field') if not self.errors: self._clean_text_image() if not self.errors and self.session: self._validate_posting_speed() return cleaned_data def get_image(self): """ Gets image from file or URL. """ image = self.cleaned_data['image'] return image if image else self.cleaned_data['image_url'] def _clean_text_image(self): text = self.cleaned_data.get('text') image = self.get_image() if (not text) and (not image): error_message = _('Either text or image must be entered.') self._errors['text'] = self.error_class([error_message]) def _validate_posting_speed(self): can_post = True posting_delay = settings.POSTING_DELAY if board_settings.get_bool('Forms', 'LimitPostingSpeed'): now = time.time() current_delay = 0 need_delay = False if not LAST_POST_TIME in self.session: self.session[LAST_POST_TIME] = now need_delay = True else: last_post_time = self.session.get(LAST_POST_TIME) current_delay = int(now - last_post_time) need_delay = current_delay < posting_delay if need_delay: error_message = ERROR_SPEED % str(posting_delay - current_delay) self._errors['text'] = self.error_class([error_message]) can_post = False if can_post: self.session[LAST_POST_TIME] = now def validate_image_size(self, size: int): max_size = board_settings.get_int('Forms', 'MaxImageSize') if size > max_size: raise forms.ValidationError( _('Image must be less than %s bytes') % str(max_size)) def _get_image_from_url(self, url: str) -> SimpleUploadedFile: """ Gets an image file from URL. """ img_temp = None try: # Verify content headers response_head = requests.head(url, verify=False) content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0] if content_type in CONTENT_TYPE_IMAGE: length_header = response_head.headers.get(HEADER_CONTENT_LENGTH) if length_header: length = int(length_header) self.validate_image_size(length) # Get the actual content into memory response = requests.get(url, verify=False, stream=True) # Download image, stop if the size exceeds limit size = 0 content = b'' for chunk in response.iter_content(IMAGE_DOWNLOAD_CHUNK_BYTES): size += len(chunk) self.validate_image_size(size) content += chunk if response.status_code == HTTP_RESULT_OK and content: # Set a dummy file name that will be replaced # anyway, just keep the valid extension filename = 'image.' + content_type.split('/')[1] img_temp = SimpleUploadedFile(filename, content, content_type) except Exception: # Just return no image pass return img_temp class ThreadForm(PostForm): tags = forms.CharField( widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}), max_length=100, label=_('Tags'), required=True) 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.')) required_tag_exists = False for tag in tags.split(): try: Tag.objects.get(name=tag.strip().lower(), required=True) required_tag_exists = True break except ObjectDoesNotExist: pass if not required_tag_exists: all_tags = Tag.objects.filter(required=True) raise forms.ValidationError( _('Need at least one section.')) return tags def clean(self): cleaned_data = super(ThreadForm, self).clean() return cleaned_data class SettingsForm(NeboardForm): theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme')) image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode')) username = forms.CharField(label=_('User name'), required=False) timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone')) def clean_username(self): username = self.cleaned_data['username'] if username and not REGEX_TAGS.match(username): raise forms.ValidationError(_('Inappropriate characters.')) return username class SearchForm(NeboardForm): query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)