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