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