##// END OF EJS Templates
Fixed upload from url
neko259 -
r1276:d07d5464 default
parent child Browse files
Show More
@@ -1,377 +1,377 b''
1 import re
1 import re
2 import time
2 import time
3 import pytz
3 import pytz
4
4
5 from django import forms
5 from django import forms
6 from django.core.files.uploadedfile import SimpleUploadedFile
6 from django.core.files.uploadedfile import SimpleUploadedFile
7 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.exceptions import ObjectDoesNotExist
8 from django.forms.util import ErrorList
8 from django.forms.util import ErrorList
9 from django.utils.translation import ugettext_lazy as _
9 from django.utils.translation import ugettext_lazy as _
10 import requests
10 import requests
11
11
12 from boards.mdx_neboard import formatters
12 from boards.mdx_neboard import formatters
13 from boards.models.post import TITLE_MAX_LENGTH
13 from boards.models.post import TITLE_MAX_LENGTH
14 from boards.models import Tag, Post
14 from boards.models import Tag, Post
15 from neboard import settings
15 from neboard import settings
16 import boards.settings as board_settings
16 import boards.settings as board_settings
17
17
18 HEADER_CONTENT_LENGTH = 'content-length'
18 HEADER_CONTENT_LENGTH = 'content-length'
19 HEADER_CONTENT_TYPE = 'content-type'
19 HEADER_CONTENT_TYPE = 'content-type'
20
20
21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
21 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
22
22
23 VETERAN_POSTING_DELAY = 5
23 VETERAN_POSTING_DELAY = 5
24
24
25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
25 ATTRIBUTE_PLACEHOLDER = 'placeholder'
26 ATTRIBUTE_ROWS = 'rows'
26 ATTRIBUTE_ROWS = 'rows'
27
27
28 LAST_POST_TIME = 'last_post_time'
28 LAST_POST_TIME = 'last_post_time'
29 LAST_LOGIN_TIME = 'last_login_time'
29 LAST_LOGIN_TIME = 'last_login_time'
30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
30 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
31 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
32
32
33 LABEL_TITLE = _('Title')
33 LABEL_TITLE = _('Title')
34 LABEL_TEXT = _('Text')
34 LABEL_TEXT = _('Text')
35 LABEL_TAG = _('Tag')
35 LABEL_TAG = _('Tag')
36 LABEL_SEARCH = _('Search')
36 LABEL_SEARCH = _('Search')
37
37
38 ERROR_SPEED = _('Please wait %s seconds before sending message')
38 ERROR_SPEED = _('Please wait %s seconds before sending message')
39
39
40 TAG_MAX_LENGTH = 20
40 TAG_MAX_LENGTH = 20
41
41
42 FILE_DOWNLOAD_CHUNK_BYTES = 100000
42 FILE_DOWNLOAD_CHUNK_BYTES = 100000
43
43
44 HTTP_RESULT_OK = 200
44 HTTP_RESULT_OK = 200
45
45
46 TEXTAREA_ROWS = 4
46 TEXTAREA_ROWS = 4
47
47
48
48
49 def get_timezones():
49 def get_timezones():
50 timezones = []
50 timezones = []
51 for tz in pytz.common_timezones:
51 for tz in pytz.common_timezones:
52 timezones.append((tz, tz),)
52 timezones.append((tz, tz),)
53 return timezones
53 return timezones
54
54
55
55
56 class FormatPanel(forms.Textarea):
56 class FormatPanel(forms.Textarea):
57 """
57 """
58 Panel for text formatting. Consists of buttons to add different tags to the
58 Panel for text formatting. Consists of buttons to add different tags to the
59 form text area.
59 form text area.
60 """
60 """
61
61
62 def render(self, name, value, attrs=None):
62 def render(self, name, value, attrs=None):
63 output = '<div id="mark-panel">'
63 output = '<div id="mark-panel">'
64 for formatter in formatters:
64 for formatter in formatters:
65 output += '<span class="mark_btn"' + \
65 output += '<span class="mark_btn"' + \
66 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
66 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
67 '\', \'' + formatter.format_right + '\')">' + \
67 '\', \'' + formatter.format_right + '\')">' + \
68 formatter.preview_left + formatter.name + \
68 formatter.preview_left + formatter.name + \
69 formatter.preview_right + '</span>'
69 formatter.preview_right + '</span>'
70
70
71 output += '</div>'
71 output += '</div>'
72 output += super(FormatPanel, self).render(name, value, attrs=None)
72 output += super(FormatPanel, self).render(name, value, attrs=None)
73
73
74 return output
74 return output
75
75
76
76
77 class PlainErrorList(ErrorList):
77 class PlainErrorList(ErrorList):
78 def __unicode__(self):
78 def __unicode__(self):
79 return self.as_text()
79 return self.as_text()
80
80
81 def as_text(self):
81 def as_text(self):
82 return ''.join(['(!) %s ' % e for e in self])
82 return ''.join(['(!) %s ' % e for e in self])
83
83
84
84
85 class NeboardForm(forms.Form):
85 class NeboardForm(forms.Form):
86 """
86 """
87 Form with neboard-specific formatting.
87 Form with neboard-specific formatting.
88 """
88 """
89
89
90 def as_div(self):
90 def as_div(self):
91 """
91 """
92 Returns this form rendered as HTML <as_div>s.
92 Returns this form rendered as HTML <as_div>s.
93 """
93 """
94
94
95 return self._html_output(
95 return self._html_output(
96 # TODO Do not show hidden rows in the list here
96 # TODO Do not show hidden rows in the list here
97 normal_row='<div class="form-row">'
97 normal_row='<div class="form-row">'
98 '<div class="form-label">'
98 '<div class="form-label">'
99 '%(label)s'
99 '%(label)s'
100 '</div>'
100 '</div>'
101 '<div class="form-input">'
101 '<div class="form-input">'
102 '%(field)s'
102 '%(field)s'
103 '</div>'
103 '</div>'
104 '</div>'
104 '</div>'
105 '<div class="form-row">'
105 '<div class="form-row">'
106 '%(help_text)s'
106 '%(help_text)s'
107 '</div>',
107 '</div>',
108 error_row='<div class="form-row">'
108 error_row='<div class="form-row">'
109 '<div class="form-label"></div>'
109 '<div class="form-label"></div>'
110 '<div class="form-errors">%s</div>'
110 '<div class="form-errors">%s</div>'
111 '</div>',
111 '</div>',
112 row_ender='</div>',
112 row_ender='</div>',
113 help_text_html='%s',
113 help_text_html='%s',
114 errors_on_separate_row=True)
114 errors_on_separate_row=True)
115
115
116 def as_json_errors(self):
116 def as_json_errors(self):
117 errors = []
117 errors = []
118
118
119 for name, field in list(self.fields.items()):
119 for name, field in list(self.fields.items()):
120 if self[name].errors:
120 if self[name].errors:
121 errors.append({
121 errors.append({
122 'field': name,
122 'field': name,
123 'errors': self[name].errors.as_text(),
123 'errors': self[name].errors.as_text(),
124 })
124 })
125
125
126 return errors
126 return errors
127
127
128
128
129 class PostForm(NeboardForm):
129 class PostForm(NeboardForm):
130
130
131 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
131 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
132 label=LABEL_TITLE)
132 label=LABEL_TITLE)
133 text = forms.CharField(
133 text = forms.CharField(
134 widget=FormatPanel(attrs={
134 widget=FormatPanel(attrs={
135 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
135 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
136 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
136 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
137 }),
137 }),
138 required=False, label=LABEL_TEXT)
138 required=False, label=LABEL_TEXT)
139 file = forms.FileField(required=False, label=_('File'),
139 file = forms.FileField(required=False, label=_('File'),
140 widget=forms.ClearableFileInput(
140 widget=forms.ClearableFileInput(
141 attrs={'accept': 'file/*'}))
141 attrs={'accept': 'file/*'}))
142 file_url = forms.CharField(required=False, label=_('File URL'),
142 file_url = forms.CharField(required=False, label=_('File URL'),
143 widget=forms.TextInput(
143 widget=forms.TextInput(
144 attrs={ATTRIBUTE_PLACEHOLDER:
144 attrs={ATTRIBUTE_PLACEHOLDER:
145 'http://example.com/image.png'}))
145 'http://example.com/image.png'}))
146
146
147 # This field is for spam prevention only
147 # This field is for spam prevention only
148 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
148 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
149 widget=forms.TextInput(attrs={
149 widget=forms.TextInput(attrs={
150 'class': 'form-email'}))
150 'class': 'form-email'}))
151 threads = forms.CharField(required=False, label=_('Additional threads'),
151 threads = forms.CharField(required=False, label=_('Additional threads'),
152 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
152 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER:
153 '123 456 789'}))
153 '123 456 789'}))
154
154
155 session = None
155 session = None
156 need_to_ban = False
156 need_to_ban = False
157
157
158 def clean_title(self):
158 def clean_title(self):
159 title = self.cleaned_data['title']
159 title = self.cleaned_data['title']
160 if title:
160 if title:
161 if len(title) > TITLE_MAX_LENGTH:
161 if len(title) > TITLE_MAX_LENGTH:
162 raise forms.ValidationError(_('Title must have less than %s '
162 raise forms.ValidationError(_('Title must have less than %s '
163 'characters') %
163 'characters') %
164 str(TITLE_MAX_LENGTH))
164 str(TITLE_MAX_LENGTH))
165 return title
165 return title
166
166
167 def clean_text(self):
167 def clean_text(self):
168 text = self.cleaned_data['text'].strip()
168 text = self.cleaned_data['text'].strip()
169 if text:
169 if text:
170 max_length = board_settings.get_int('Forms', 'MaxTextLength')
170 max_length = board_settings.get_int('Forms', 'MaxTextLength')
171 if len(text) > max_length:
171 if len(text) > max_length:
172 raise forms.ValidationError(_('Text must have less than %s '
172 raise forms.ValidationError(_('Text must have less than %s '
173 'characters') % str(max_length))
173 'characters') % str(max_length))
174 return text
174 return text
175
175
176 def clean_file(self):
176 def clean_file(self):
177 file = self.cleaned_data['file']
177 file = self.cleaned_data['file']
178
178
179 if file:
179 if file:
180 self.validate_file_size(file.size)
180 self.validate_file_size(file.size)
181
181
182 return file
182 return file
183
183
184 def clean_file_url(self):
184 def clean_file_url(self):
185 url = self.cleaned_data['file_url']
185 url = self.cleaned_data['file_url']
186
186
187 file = None
187 file = None
188 if url:
188 if url:
189 file = self._get_file_from_url(url)
189 file = self._get_file_from_url(url)
190
190
191 if not file:
191 if not file:
192 raise forms.ValidationError(_('Invalid URL'))
192 raise forms.ValidationError(_('Invalid URL'))
193 else:
193 else:
194 self.validate_file_size(file.size)
194 self.validate_file_size(file.size)
195
195
196 return file
196 return file
197
197
198 def clean_threads(self):
198 def clean_threads(self):
199 threads_str = self.cleaned_data['threads']
199 threads_str = self.cleaned_data['threads']
200
200
201 if len(threads_str) > 0:
201 if len(threads_str) > 0:
202 threads_id_list = threads_str.split(' ')
202 threads_id_list = threads_str.split(' ')
203
203
204 threads = list()
204 threads = list()
205
205
206 for thread_id in threads_id_list:
206 for thread_id in threads_id_list:
207 try:
207 try:
208 thread = Post.objects.get(id=int(thread_id))
208 thread = Post.objects.get(id=int(thread_id))
209 if not thread.is_opening() or thread.get_thread().archived:
209 if not thread.is_opening() or thread.get_thread().archived:
210 raise ObjectDoesNotExist()
210 raise ObjectDoesNotExist()
211 threads.append(thread)
211 threads.append(thread)
212 except (ObjectDoesNotExist, ValueError):
212 except (ObjectDoesNotExist, ValueError):
213 raise forms.ValidationError(_('Invalid additional thread list'))
213 raise forms.ValidationError(_('Invalid additional thread list'))
214
214
215 return threads
215 return threads
216
216
217 def clean(self):
217 def clean(self):
218 cleaned_data = super(PostForm, self).clean()
218 cleaned_data = super(PostForm, self).clean()
219
219
220 if cleaned_data['email']:
220 if cleaned_data['email']:
221 self.need_to_ban = True
221 self.need_to_ban = True
222 raise forms.ValidationError('A human cannot enter a hidden field')
222 raise forms.ValidationError('A human cannot enter a hidden field')
223
223
224 if not self.errors:
224 if not self.errors:
225 self._clean_text_file()
225 self._clean_text_file()
226
226
227 if not self.errors and self.session:
227 if not self.errors and self.session:
228 self._validate_posting_speed()
228 self._validate_posting_speed()
229
229
230 return cleaned_data
230 return cleaned_data
231
231
232 def get_file(self):
232 def get_file(self):
233 """
233 """
234 Gets file from form or URL.
234 Gets file from form or URL.
235 """
235 """
236
236
237 file = self.cleaned_data['file']
237 file = self.cleaned_data['file']
238 return file or self.cleaned_data['file_url']
238 return file or self.cleaned_data['file_url']
239
239
240 def _clean_text_file(self):
240 def _clean_text_file(self):
241 text = self.cleaned_data.get('text')
241 text = self.cleaned_data.get('text')
242 file = self.get_file()
242 file = self.get_file()
243
243
244 if (not text) and (not file):
244 if (not text) and (not file):
245 error_message = _('Either text or file must be entered.')
245 error_message = _('Either text or file must be entered.')
246 self._errors['text'] = self.error_class([error_message])
246 self._errors['text'] = self.error_class([error_message])
247
247
248 def _validate_posting_speed(self):
248 def _validate_posting_speed(self):
249 can_post = True
249 can_post = True
250
250
251 posting_delay = settings.POSTING_DELAY
251 posting_delay = settings.POSTING_DELAY
252
252
253 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
253 if board_settings.get_bool('Forms', 'LimitPostingSpeed'):
254 now = time.time()
254 now = time.time()
255
255
256 current_delay = 0
256 current_delay = 0
257 need_delay = False
257 need_delay = False
258
258
259 if not LAST_POST_TIME in self.session:
259 if not LAST_POST_TIME in self.session:
260 self.session[LAST_POST_TIME] = now
260 self.session[LAST_POST_TIME] = now
261
261
262 need_delay = True
262 need_delay = True
263 else:
263 else:
264 last_post_time = self.session.get(LAST_POST_TIME)
264 last_post_time = self.session.get(LAST_POST_TIME)
265 current_delay = int(now - last_post_time)
265 current_delay = int(now - last_post_time)
266
266
267 need_delay = current_delay < posting_delay
267 need_delay = current_delay < posting_delay
268
268
269 if need_delay:
269 if need_delay:
270 error_message = ERROR_SPEED % str(posting_delay
270 error_message = ERROR_SPEED % str(posting_delay
271 - current_delay)
271 - current_delay)
272 self._errors['text'] = self.error_class([error_message])
272 self._errors['text'] = self.error_class([error_message])
273
273
274 can_post = False
274 can_post = False
275
275
276 if can_post:
276 if can_post:
277 self.session[LAST_POST_TIME] = now
277 self.session[LAST_POST_TIME] = now
278
278
279 def validate_file_size(self, size: int):
279 def validate_file_size(self, size: int):
280 max_size = board_settings.get_int('Forms', 'MaxFileSize')
280 max_size = board_settings.get_int('Forms', 'MaxFileSize')
281 if size > max_size:
281 if size > max_size:
282 raise forms.ValidationError(
282 raise forms.ValidationError(
283 _('File must be less than %s bytes')
283 _('File must be less than %s bytes')
284 % str(max_size))
284 % str(max_size))
285
285
286 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
286 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
287 """
287 """
288 Gets an file file from URL.
288 Gets an file file from URL.
289 """
289 """
290
290
291 img_temp = None
291 img_temp = None
292
292
293 try:
293 try:
294 # Verify content headers
294 # Verify content headers
295 response_head = requests.head(url, verify=False)
295 response_head = requests.head(url, verify=False)
296 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
296 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
297 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
297 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
298 if length_header:
298 if length_header:
299 length = int(length_header)
299 length = int(length_header)
300 self.validate_file_size(length)
300 self.validate_file_size(length)
301 # Get the actual content into memory
301 # Get the actual content into memory
302 response = requests.get(url, verify=False, stream=True)
302 response = requests.get(url, verify=False, stream=True)
303
303
304 # Download file, stop if the size exceeds limit
304 # Download file, stop if the size exceeds limit
305 size = 0
305 size = 0
306 content = b''
306 content = b''
307 for chunk in response.iter_content(file_DOWNLOAD_CHUNK_BYTES):
307 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
308 size += len(chunk)
308 size += len(chunk)
309 self.validate_file_size(size)
309 self.validate_file_size(size)
310 content += chunk
310 content += chunk
311
311
312 if response.status_code == HTTP_RESULT_OK and content:
312 if response.status_code == HTTP_RESULT_OK and content:
313 # Set a dummy file name that will be replaced
313 # Set a dummy file name that will be replaced
314 # anyway, just keep the valid extension
314 # anyway, just keep the valid extension
315 filename = 'file.' + content_type.split('/')[1]
315 filename = 'file.' + content_type.split('/')[1]
316 img_temp = SimpleUploadedFile(filename, content,
316 img_temp = SimpleUploadedFile(filename, content,
317 content_type)
317 content_type)
318 except Exception:
318 except Exception as e:
319 # Just return no file
319 # Just return no file
320 pass
320 pass
321
321
322 return img_temp
322 return img_temp
323
323
324
324
325 class ThreadForm(PostForm):
325 class ThreadForm(PostForm):
326
326
327 tags = forms.CharField(
327 tags = forms.CharField(
328 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
328 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
329 max_length=100, label=_('Tags'), required=True)
329 max_length=100, label=_('Tags'), required=True)
330
330
331 def clean_tags(self):
331 def clean_tags(self):
332 tags = self.cleaned_data['tags'].strip()
332 tags = self.cleaned_data['tags'].strip()
333
333
334 if not tags or not REGEX_TAGS.match(tags):
334 if not tags or not REGEX_TAGS.match(tags):
335 raise forms.ValidationError(
335 raise forms.ValidationError(
336 _('Inappropriate characters in tags.'))
336 _('Inappropriate characters in tags.'))
337
337
338 required_tag_exists = False
338 required_tag_exists = False
339 for tag in tags.split():
339 for tag in tags.split():
340 try:
340 try:
341 Tag.objects.get(name=tag.strip().lower(), required=True)
341 Tag.objects.get(name=tag.strip().lower(), required=True)
342 required_tag_exists = True
342 required_tag_exists = True
343 break
343 break
344 except ObjectDoesNotExist:
344 except ObjectDoesNotExist:
345 pass
345 pass
346
346
347 if not required_tag_exists:
347 if not required_tag_exists:
348 all_tags = Tag.objects.filter(required=True)
348 all_tags = Tag.objects.filter(required=True)
349 raise forms.ValidationError(
349 raise forms.ValidationError(
350 _('Need at least one section.'))
350 _('Need at least one section.'))
351
351
352 return tags
352 return tags
353
353
354 def clean(self):
354 def clean(self):
355 cleaned_data = super(ThreadForm, self).clean()
355 cleaned_data = super(ThreadForm, self).clean()
356
356
357 return cleaned_data
357 return cleaned_data
358
358
359
359
360 class SettingsForm(NeboardForm):
360 class SettingsForm(NeboardForm):
361
361
362 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
362 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
363 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('image view mode'))
363 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('image view mode'))
364 username = forms.CharField(label=_('User name'), required=False)
364 username = forms.CharField(label=_('User name'), required=False)
365 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
365 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
366
366
367 def clean_username(self):
367 def clean_username(self):
368 username = self.cleaned_data['username']
368 username = self.cleaned_data['username']
369
369
370 if username and not REGEX_TAGS.match(username):
370 if username and not REGEX_TAGS.match(username):
371 raise forms.ValidationError(_('Inappropriate characters.'))
371 raise forms.ValidationError(_('Inappropriate characters.'))
372
372
373 return username
373 return username
374
374
375
375
376 class SearchForm(NeboardForm):
376 class SearchForm(NeboardForm):
377 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
377 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