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