##// END OF EJS Templates
Show domain next to URL if available
neko259 -
r1765:7a6a61e1 default
parent child Browse files
Show More
@@ -1,483 +1,483
1 1 import hashlib
2 2 import logging
3 3 import re
4 4 import time
5 5
6 6 import pytz
7 7 from django import forms
8 8 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
9 9 from django.forms.utils import ErrorList
10 10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 11
12 12 import boards.settings as board_settings
13 13 import neboard
14 14 from boards.abstracts.attachment_alias import get_image_by_alias
15 15 from boards.abstracts.settingsmanager import get_settings_manager
16 16 from boards.forms.fields import UrlFileField
17 17 from boards.mdx_neboard import formatters
18 18 from boards.models import Tag
19 from boards.models.attachment.downloaders import download
19 from boards.models.attachment.downloaders import download, REGEX_MAGNET
20 20 from boards.models.post import TITLE_MAX_LENGTH
21 21 from boards.utils import validate_file_size, get_file_mimetype, \
22 22 FILE_EXTENSION_DELIMITER
23 23 from neboard import settings
24 24
25 25 SECTION_FORMS = 'Forms'
26 26
27 27 POW_HASH_LENGTH = 16
28 28 POW_LIFE_MINUTES = 5
29 29
30 30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
31 31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
32 REGEX_URL = re.compile(r'^(http|https|ftp|magnet):\/\/', re.UNICODE)
32 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
33 33
34 34 VETERAN_POSTING_DELAY = 5
35 35
36 36 ATTRIBUTE_PLACEHOLDER = 'placeholder'
37 37 ATTRIBUTE_ROWS = 'rows'
38 38
39 39 LAST_POST_TIME = 'last_post_time'
40 40 LAST_LOGIN_TIME = 'last_login_time'
41 41 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
42 42 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
43 43
44 44 LABEL_TITLE = _('Title')
45 45 LABEL_TEXT = _('Text')
46 46 LABEL_TAG = _('Tag')
47 47 LABEL_SEARCH = _('Search')
48 48 LABEL_FILE = _('File')
49 49
50 50 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
51 51 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
52 52 ERROR_MANY_FILES = _('Too many files.')
53 53
54 54 TAG_MAX_LENGTH = 20
55 55
56 56 TEXTAREA_ROWS = 4
57 57
58 58 TRIPCODE_DELIM = '#'
59 59
60 60 # TODO Maybe this may be converted into the database table?
61 61 MIMETYPE_EXTENSIONS = {
62 62 'image/jpeg': 'jpeg',
63 63 'image/png': 'png',
64 64 'image/gif': 'gif',
65 65 'video/webm': 'webm',
66 66 'application/pdf': 'pdf',
67 67 'x-diff': 'diff',
68 68 'image/svg+xml': 'svg',
69 69 'application/x-shockwave-flash': 'swf',
70 70 'image/x-ms-bmp': 'bmp',
71 71 'image/bmp': 'bmp',
72 72 }
73 73
74 74
75 75 logger = logging.getLogger('boards.forms')
76 76
77 77
78 78 def get_timezones():
79 79 timezones = []
80 80 for tz in pytz.common_timezones:
81 81 timezones.append((tz, tz),)
82 82 return timezones
83 83
84 84
85 85 class FormatPanel(forms.Textarea):
86 86 """
87 87 Panel for text formatting. Consists of buttons to add different tags to the
88 88 form text area.
89 89 """
90 90
91 91 def render(self, name, value, attrs=None):
92 92 output = '<div id="mark-panel">'
93 93 for formatter in formatters:
94 94 output += '<span class="mark_btn"' + \
95 95 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
96 96 '\', \'' + formatter.format_right + '\')">' + \
97 97 formatter.preview_left + formatter.name + \
98 98 formatter.preview_right + '</span>'
99 99
100 100 output += '</div>'
101 101 output += super(FormatPanel, self).render(name, value, attrs=attrs)
102 102
103 103 return output
104 104
105 105
106 106 class PlainErrorList(ErrorList):
107 107 def __unicode__(self):
108 108 return self.as_text()
109 109
110 110 def as_text(self):
111 111 return ''.join(['(!) %s ' % e for e in self])
112 112
113 113
114 114 class NeboardForm(forms.Form):
115 115 """
116 116 Form with neboard-specific formatting.
117 117 """
118 118 required_css_class = 'required-field'
119 119
120 120 def as_div(self):
121 121 """
122 122 Returns this form rendered as HTML <as_div>s.
123 123 """
124 124
125 125 return self._html_output(
126 126 # TODO Do not show hidden rows in the list here
127 127 normal_row='<div class="form-row">'
128 128 '<div class="form-label">'
129 129 '%(label)s'
130 130 '</div>'
131 131 '<div class="form-input">'
132 132 '%(field)s'
133 133 '</div>'
134 134 '</div>'
135 135 '<div class="form-row">'
136 136 '%(help_text)s'
137 137 '</div>',
138 138 error_row='<div class="form-row">'
139 139 '<div class="form-label"></div>'
140 140 '<div class="form-errors">%s</div>'
141 141 '</div>',
142 142 row_ender='</div>',
143 143 help_text_html='%s',
144 144 errors_on_separate_row=True)
145 145
146 146 def as_json_errors(self):
147 147 errors = []
148 148
149 149 for name, field in list(self.fields.items()):
150 150 if self[name].errors:
151 151 errors.append({
152 152 'field': name,
153 153 'errors': self[name].errors.as_text(),
154 154 })
155 155
156 156 return errors
157 157
158 158
159 159 class PostForm(NeboardForm):
160 160
161 161 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
162 162 label=LABEL_TITLE,
163 163 widget=forms.TextInput(
164 164 attrs={ATTRIBUTE_PLACEHOLDER: 'title#tripcode'}))
165 165 text = forms.CharField(
166 166 widget=FormatPanel(attrs={
167 167 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
168 168 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
169 169 }),
170 170 required=False, label=LABEL_TEXT)
171 171 file = UrlFileField(required=False, label=LABEL_FILE)
172 172
173 173 # This field is for spam prevention only
174 174 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
175 175 widget=forms.TextInput(attrs={
176 176 'class': 'form-email'}))
177 177 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
178 178
179 179 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
180 180 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
181 181 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
182 182
183 183 session = None
184 184 need_to_ban = False
185 185 image = None
186 186
187 187 def clean_title(self):
188 188 title = self.cleaned_data['title']
189 189 if title:
190 190 if len(title) > TITLE_MAX_LENGTH:
191 191 raise forms.ValidationError(_('Title must have less than %s '
192 192 'characters') %
193 193 str(TITLE_MAX_LENGTH))
194 194 return title
195 195
196 196 def clean_text(self):
197 197 text = self.cleaned_data['text'].strip()
198 198 if text:
199 199 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
200 200 if len(text) > max_length:
201 201 raise forms.ValidationError(_('Text must have less than %s '
202 202 'characters') % str(max_length))
203 203 return text
204 204
205 205 def clean_file(self):
206 206 return self._clean_files(self.cleaned_data['file'])
207 207
208 208 def clean(self):
209 209 cleaned_data = super(PostForm, self).clean()
210 210
211 211 if cleaned_data['email']:
212 212 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
213 213 self.need_to_ban = True
214 214 raise forms.ValidationError('A human cannot enter a hidden field')
215 215
216 216 if not self.errors:
217 217 self._clean_text_file()
218 218
219 219 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
220 220 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
221 221
222 222 settings_manager = get_settings_manager(self)
223 223 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
224 224 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
225 225 if pow_difficulty > 0:
226 226 # PoW-based
227 227 if cleaned_data['timestamp'] \
228 228 and cleaned_data['iteration'] and cleaned_data['guess'] \
229 229 and not settings_manager.get_setting('confirmed_user'):
230 230 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
231 231 else:
232 232 # Time-based
233 233 self._validate_posting_speed()
234 234 settings_manager.set_setting('confirmed_user', True)
235 235
236 236 return cleaned_data
237 237
238 238 def get_files(self):
239 239 """
240 240 Gets file from form or URL.
241 241 """
242 242
243 243 files = []
244 244 for file in self.cleaned_data['file']:
245 245 if isinstance(file, UploadedFile):
246 246 files.append(file)
247 247
248 248 return files
249 249
250 250 def get_file_urls(self):
251 251 files = []
252 252 for file in self.cleaned_data['file']:
253 253 if type(file) == str:
254 254 files.append(file)
255 255
256 256 return files
257 257
258 258 def get_tripcode(self):
259 259 title = self.cleaned_data['title']
260 260 if title is not None and TRIPCODE_DELIM in title:
261 261 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
262 262 tripcode = hashlib.md5(code.encode()).hexdigest()
263 263 else:
264 264 tripcode = ''
265 265 return tripcode
266 266
267 267 def get_title(self):
268 268 title = self.cleaned_data['title']
269 269 if title is not None and TRIPCODE_DELIM in title:
270 270 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
271 271 else:
272 272 return title
273 273
274 274 def get_images(self):
275 275 if self.image:
276 276 return [self.image]
277 277 else:
278 278 return []
279 279
280 280 def is_subscribe(self):
281 281 return self.cleaned_data['subscribe']
282 282
283 283 def _update_file_extension(self, file):
284 284 if file:
285 285 mimetype = get_file_mimetype(file)
286 286 extension = MIMETYPE_EXTENSIONS.get(mimetype)
287 287 if extension:
288 288 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
289 289 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
290 290
291 291 file.name = new_filename
292 292 else:
293 293 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
294 294
295 295 def _clean_files(self, inputs):
296 296 files = []
297 297
298 298 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
299 299 if len(inputs) > max_file_count:
300 300 raise forms.ValidationError(ERROR_MANY_FILES)
301 301 for file_input in inputs:
302 302 if isinstance(file_input, UploadedFile):
303 303 files.append(self._clean_file_file(file_input))
304 304 else:
305 305 files.append(self._clean_file_url(file_input))
306 306
307 307 return files
308 308
309 309 def _clean_file_file(self, file):
310 310 validate_file_size(file.size)
311 311 self._update_file_extension(file)
312 312
313 313 return file
314 314
315 315 def _clean_file_url(self, url):
316 316 file = None
317 317
318 318 if url:
319 319 try:
320 320 file = get_image_by_alias(url, self.session)
321 321 self.image = file
322 322
323 323 if file is not None:
324 324 return
325 325
326 326 if file is None:
327 327 file = self._get_file_from_url(url)
328 328 if not file:
329 329 raise forms.ValidationError(_('Invalid URL'))
330 330 else:
331 331 validate_file_size(file.size)
332 332 self._update_file_extension(file)
333 333 except forms.ValidationError as e:
334 334 # Assume we will get the plain URL instead of a file and save it
335 if REGEX_URL.match(url):
335 if REGEX_URL.match(url) or REGEX_MAGNET.match(url):
336 336 logger.info('Error in forms: {}'.format(e))
337 337 return url
338 338 else:
339 339 raise e
340 340
341 341 return file
342 342
343 343 def _clean_text_file(self):
344 344 text = self.cleaned_data.get('text')
345 345 file = self.get_files()
346 346 file_url = self.get_file_urls()
347 347 images = self.get_images()
348 348
349 349 if (not text) and (not file) and (not file_url) and len(images) == 0:
350 350 error_message = _('Either text or file must be entered.')
351 351 self._errors['text'] = self.error_class([error_message])
352 352
353 353 def _validate_posting_speed(self):
354 354 can_post = True
355 355
356 356 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
357 357
358 358 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
359 359 now = time.time()
360 360
361 361 current_delay = 0
362 362
363 363 if LAST_POST_TIME not in self.session:
364 364 self.session[LAST_POST_TIME] = now
365 365
366 366 need_delay = True
367 367 else:
368 368 last_post_time = self.session.get(LAST_POST_TIME)
369 369 current_delay = int(now - last_post_time)
370 370
371 371 need_delay = current_delay < posting_delay
372 372
373 373 if need_delay:
374 374 delay = posting_delay - current_delay
375 375 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
376 376 delay) % {'delay': delay}
377 377 self._errors['text'] = self.error_class([error_message])
378 378
379 379 can_post = False
380 380
381 381 if can_post:
382 382 self.session[LAST_POST_TIME] = now
383 383
384 384 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
385 385 """
386 386 Gets an file file from URL.
387 387 """
388 388
389 389 try:
390 390 return download(url)
391 391 except forms.ValidationError as e:
392 392 raise e
393 393 except Exception as e:
394 394 raise forms.ValidationError(e)
395 395
396 396 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
397 397 payload = timestamp + message.replace('\r\n', '\n')
398 398 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
399 399 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
400 400 if len(target) < POW_HASH_LENGTH:
401 401 target = '0' * (POW_HASH_LENGTH - len(target)) + target
402 402
403 403 computed_guess = hashlib.sha256((payload + iteration).encode())\
404 404 .hexdigest()[0:POW_HASH_LENGTH]
405 405 if guess != computed_guess or guess > target:
406 406 self._errors['text'] = self.error_class(
407 407 [_('Invalid PoW.')])
408 408
409 409
410 410 class ThreadForm(PostForm):
411 411
412 412 tags = forms.CharField(
413 413 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
414 414 max_length=100, label=_('Tags'), required=True)
415 415 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
416 416
417 417 def clean_tags(self):
418 418 tags = self.cleaned_data['tags'].strip()
419 419
420 420 if not tags or not REGEX_TAGS.match(tags):
421 421 raise forms.ValidationError(
422 422 _('Inappropriate characters in tags.'))
423 423
424 424 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
425 425 .strip().lower()
426 426
427 427 required_tag_exists = False
428 428 tag_set = set()
429 429 for tag_string in tags.split():
430 430 if tag_string.strip().lower() == default_tag_name:
431 431 required_tag_exists = True
432 432 tag, created = Tag.objects.get_or_create(
433 433 name=tag_string.strip().lower(), required=True)
434 434 else:
435 435 tag, created = Tag.objects.get_or_create(
436 436 name=tag_string.strip().lower())
437 437 tag_set.add(tag)
438 438
439 439 # If this is a new tag, don't check for its parents because nobody
440 440 # added them yet
441 441 if not created:
442 442 tag_set |= set(tag.get_all_parents())
443 443
444 444 for tag in tag_set:
445 445 if tag.required:
446 446 required_tag_exists = True
447 447 break
448 448
449 449 # Use default tag if no section exists
450 450 if not required_tag_exists:
451 451 default_tag, created = Tag.objects.get_or_create(
452 452 name=default_tag_name, required=True)
453 453 tag_set.add(default_tag)
454 454
455 455 return tag_set
456 456
457 457 def clean(self):
458 458 cleaned_data = super(ThreadForm, self).clean()
459 459
460 460 return cleaned_data
461 461
462 462 def is_monochrome(self):
463 463 return self.cleaned_data['monochrome']
464 464
465 465
466 466 class SettingsForm(NeboardForm):
467 467
468 468 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
469 469 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
470 470 username = forms.CharField(label=_('User name'), required=False)
471 471 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
472 472
473 473 def clean_username(self):
474 474 username = self.cleaned_data['username']
475 475
476 476 if username and not REGEX_USERNAMES.match(username):
477 477 raise forms.ValidationError(_('Inappropriate characters.'))
478 478
479 479 return username
480 480
481 481
482 482 class SearchForm(NeboardForm):
483 483 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,35 +1,34
1 1 from django.core.management import BaseCommand
2 2 from django.db.models import Count
3 3
4 4 from boards.models import Attachment
5 5 from boards.utils import get_domain
6 6
7 7
8 8 class Command(BaseCommand):
9 9 help = 'Gather board statistics'
10 10
11 11 def handle(self, *args, **options):
12 12 print('* Domains and their usage')
13 13 domains = {}
14 14 for attachment in Attachment.objects.exclude(url=''):
15 full_domain = attachment.url.split('/')[2]
16 domain = get_domain(full_domain)
15 domain = get_domain(attachment.url)
17 16 if domain in domains:
18 17 domains[domain] += 1
19 18 else:
20 19 domains[domain] = 1
21 20
22 21 for domain in domains:
23 22 print('{}: {}'.format(domain, domains[domain]))
24 23
25 24 print('* Overall numbers')
26 25 print('{} attachments in the system, {} of them as URLs'.format(
27 26 Attachment.objects.count(),
28 27 Attachment.objects.exclude(url='').count()))
29 28
30 29 print('* File types')
31 30 mimetypes = Attachment.objects.filter(url='')\
32 .values('mimetype').annotate(count=Count('id'))\
33 .order_by('-count')
31 .values('mimetype').annotate(count=Count('id'))\
32 .order_by('-count')
34 33 for mimetype in mimetypes:
35 34 print('{}: {}'.format(mimetype['mimetype'], mimetype['count']))
@@ -1,95 +1,97
1 import os
2 1 import re
3 2
4 from django.core.files.uploadedfile import SimpleUploadedFile, \
5 TemporaryUploadedFile
3 import requests
4 from django.core.files.uploadedfile import TemporaryUploadedFile
6 5 from pytube import YouTube
7 import requests
8 6
9 7 from boards.utils import validate_file_size
10 8
11 9 YOUTUBE_VIDEO_FORMAT = 'webm'
12 10
13 11 HTTP_RESULT_OK = 200
14 12
15 13 HEADER_CONTENT_LENGTH = 'content-length'
16 14 HEADER_CONTENT_TYPE = 'content-type'
17 15
18 16 FILE_DOWNLOAD_CHUNK_BYTES = 200000
19 17
20 YOUTUBE_URL = re.compile(r'https?://((www\.)?youtube\.com/watch\?v=|youtu.be/)[-\w]+')
18 REGEX_YOUTUBE_URL = re.compile(r'https?://((www\.)?youtube\.com/watch\?v=|youtu.be/)[-\w]+')
19 REGEX_MAGNET = re.compile(r'magnet:\?xt=urn:(btih:)?[a-z0-9]{20,50}.*')
21 20
22 21 TYPE_URL_ONLY = (
23 22 'application/xhtml+xml',
24 23 'text/html',
25 24 )
26 25
27 26
28 27 class Downloader:
29 28 @staticmethod
30 29 def handles(url: str) -> bool:
31 30 return False
32 31
33 32 @staticmethod
34 33 def download(url: str):
35 34 # Verify content headers
36 35 response_head = requests.head(url, verify=False)
37 36 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
38 37 length_header = response_head.headers.get(HEADER_CONTENT_LENGTH)
39 38 if length_header:
40 39 length = int(length_header)
41 40 validate_file_size(length)
42 41 # Get the actual content into memory
43 42 response = requests.get(url, verify=False, stream=True)
44 43
45 44 # Download file, stop if the size exceeds limit
46 45 size = 0
47 46
48 47 # Set a dummy file name that will be replaced
49 48 # anyway, just keep the valid extension
50 49 filename = 'file.' + content_type.split('/')[1]
51 50
52 51 file = TemporaryUploadedFile(filename, content_type, 0, None, None)
53 52 for chunk in response.iter_content(FILE_DOWNLOAD_CHUNK_BYTES):
54 53 size += len(chunk)
55 54 validate_file_size(size)
56 55 file.write(chunk)
57 56
58 57 if response.status_code == HTTP_RESULT_OK:
59 58 return file
60 59
61 60
62 61 def download(url):
63 62 for downloader in Downloader.__subclasses__():
64 63 if downloader.handles(url):
65 64 return downloader.download(url)
66 65 # If nobody of the specific downloaders handles this, use generic
67 66 # one
68 67 return Downloader.download(url)
69 68
70 69
71 70 class YouTubeDownloader(Downloader):
72 71 @staticmethod
73 72 def download(url: str):
74 73 yt = YouTube()
75 74 yt.from_url(url)
76 75 videos = yt.filter(YOUTUBE_VIDEO_FORMAT)
77 76 if len(videos) > 0:
78 77 video = videos[0]
79 78 return Downloader.download(video.url)
80 79
81 80 @staticmethod
82 81 def handles(url: str) -> bool:
83 return YOUTUBE_URL.match(url)
82 return REGEX_YOUTUBE_URL.match(url) is not None
84 83
85 84
86 85 class NothingDownloader(Downloader):
87 86 @staticmethod
88 87 def handles(url: str) -> bool:
88 if REGEX_MAGNET.match(url) or REGEX_YOUTUBE_URL.match(url):
89 return True
90
89 91 response_head = requests.head(url, verify=False)
90 92 content_type = response_head.headers[HEADER_CONTENT_TYPE].split(';')[0]
91 return content_type in TYPE_URL_ONLY and not YOUTUBE_URL.match(url)
93 return content_type in TYPE_URL_ONLY
92 94
93 95 @staticmethod
94 96 def download(url: str):
95 97 return None
@@ -1,193 +1,201
1 1 import re
2 2
3 3 from django.contrib.staticfiles import finders
4 4 from django.contrib.staticfiles.templatetags.staticfiles import static
5 5 from django.core.files.images import get_image_dimensions
6 6 from django.template.defaultfilters import filesizeformat
7 7
8 8 from boards.utils import get_domain
9 9
10 10
11 11 FILE_STUB_IMAGE = 'images/file.png'
12 12 FILE_STUB_URL = 'url'
13 13
14 14
15 15 FILE_TYPES_VIDEO = (
16 16 'webm',
17 17 'mp4',
18 18 'mpeg',
19 19 'ogv',
20 20 )
21 21 FILE_TYPE_SVG = 'svg'
22 22 FILE_TYPES_AUDIO = (
23 23 'ogg',
24 24 'mp3',
25 25 'opus',
26 26 )
27 27 FILE_TYPES_IMAGE = (
28 28 'jpeg',
29 29 'jpg',
30 30 'png',
31 31 'bmp',
32 32 'gif',
33 33 )
34 34
35 35 PLAIN_FILE_FORMATS = {
36 36 'pdf': 'pdf',
37 37 'djvu': 'djvu',
38 38 'txt': 'txt',
39 39 'tex': 'tex',
40 40 'xcf': 'xcf',
41 41 'zip': 'archive',
42 42 'tar': 'archive',
43 43 'gz': 'archive',
44 44 }
45 45
46 46 URL_PROTOCOLS = {
47 47 'magnet': 'magnet',
48 48 }
49 49
50 50 CSS_CLASS_IMAGE = 'image'
51 51 CSS_CLASS_THUMB = 'thumb'
52 52
53 53
54 54 def get_viewers():
55 55 return AbstractViewer.__subclasses__()
56 56
57 57
58 58 def get_static_dimensions(filename):
59 59 file_path = finders.find(filename)
60 60 return get_image_dimensions(file_path)
61 61
62 62
63 63 # TODO Move this to utils
64 64 def file_exists(filename):
65 65 return finders.find(filename) is not None
66 66
67 67
68 68 class AbstractViewer:
69 69 def __init__(self, file, file_type, hash, url):
70 70 self.file = file
71 71 self.file_type = file_type
72 72 self.hash = hash
73 73 self.url = url
74 74
75 75 @staticmethod
76 76 def supports(file_type):
77 77 return True
78 78
79 79 def get_view(self):
80 80 return '<div class="image">'\
81 81 '{}'\
82 82 '<div class="image-metadata"><a href="{}" download >{}, {}</a></div>'\
83 83 '</div>'.format(self.get_format_view(), self.file.url,
84 84 self.file_type, filesizeformat(self.file.size))
85 85
86 86 def get_format_view(self):
87 87 if self.file_type in PLAIN_FILE_FORMATS:
88 88 image = 'images/fileformats/{}.png'.format(
89 89 PLAIN_FILE_FORMATS[self.file_type])
90 90 else:
91 91 image = FILE_STUB_IMAGE
92 92
93 93 w, h = get_static_dimensions(image)
94 94
95 95 return '<a href="{}">'\
96 96 '<img class="url-image" src="{}" width="{}" height="{}"/>'\
97 97 '</a>'.format(self.file.url, static(image), w, h)
98 98
99 99
100 100 class VideoViewer(AbstractViewer):
101 101 @staticmethod
102 102 def supports(file_type):
103 103 return file_type in FILE_TYPES_VIDEO
104 104
105 105 def get_format_view(self):
106 106 return '<video width="200" height="150" controls src="{}"></video>'\
107 107 .format(self.file.url)
108 108
109 109
110 110 class AudioViewer(AbstractViewer):
111 111 @staticmethod
112 112 def supports(file_type):
113 113 return file_type in FILE_TYPES_AUDIO
114 114
115 115 def get_format_view(self):
116 116 return '<audio controls src="{}"></audio>'.format(self.file.url)
117 117
118 118
119 119 class SvgViewer(AbstractViewer):
120 120 @staticmethod
121 121 def supports(file_type):
122 122 return file_type == FILE_TYPE_SVG
123 123
124 124 def get_format_view(self):
125 125 return '<a class="thumb" href="{}">'\
126 126 '<img class="post-image-preview" width="200" height="150" src="{}" />'\
127 127 '</a>'.format(self.file.url, self.file.url)
128 128
129 129
130 130 class ImageViewer(AbstractViewer):
131 131 @staticmethod
132 132 def supports(file_type):
133 133 return file_type in FILE_TYPES_IMAGE
134 134
135 135 def get_format_view(self):
136 136 metadata = '{}, {}'.format(self.file.name.split('.')[-1],
137 137 filesizeformat(self.file.size))
138 138 width, height = get_image_dimensions(self.file.file)
139 139 preview_path = self.file.path.replace('.', '.200x150.')
140 140 pre_width, pre_height = get_image_dimensions(preview_path)
141 141
142 142 split = self.file.url.rsplit('.', 1)
143 143 w, h = 200, 150
144 144 thumb_url = '%s.%sx%s.%s' % (split[0], w, h, split[1])
145 145
146 146 return '<a class="{}" href="{full}">' \
147 147 '<img class="post-image-preview"' \
148 148 ' src="{}"' \
149 149 ' alt="{}"' \
150 150 ' width="{}"' \
151 151 ' height="{}"' \
152 152 ' data-width="{}"' \
153 153 ' data-height="{}" />' \
154 154 '</a>' \
155 155 .format(CSS_CLASS_THUMB,
156 156 thumb_url,
157 157 self.hash,
158 158 str(pre_width),
159 159 str(pre_height), str(width), str(height),
160 160 full=self.file.url, image_meta=metadata)
161 161
162 162
163 163 class UrlViewer(AbstractViewer):
164 164 @staticmethod
165 165 def supports(file_type):
166 166 return file_type is None
167 167
168 168 def get_view(self):
169 169 return '<div class="image">' \
170 170 '{}' \
171 '</div>'.format(self.get_format_view())
171 '<div class="image-metadata">{}</div>' \
172 '</div>'.format(self.get_format_view(), get_domain(self.url))
172 173
173 174 def get_format_view(self):
174 175 protocol = self.url.split('://')[0]
175 full_domain = self.url.split('/')[2]
176 domain = get_domain(full_domain)
176
177 domain = get_domain(self.url)
177 178
178 179 if protocol in URL_PROTOCOLS:
179 180 url_image_name = URL_PROTOCOLS.get(protocol)
180 else:
181 elif domain:
181 182 filename = 'images/domains/{}.png'.format(domain)
182 183 if file_exists(filename):
183 184 url_image_name = 'domains/' + domain
184 185 else:
185 186 url_image_name = FILE_STUB_URL
187 else:
188 url_image_name = FILE_STUB_URL
186 189
187 190 image_path = 'images/{}.png'.format(url_image_name)
188 191 image = static(image_path)
189 192 w, h = get_static_dimensions(image_path)
190 193
191 194 return '<a href="{}">' \
192 195 '<img class="url-image" src="{}" width="{}" height="{}"/>' \
193 196 '</a>'.format(self.url, image, w, h)
197
198
199 def _get_protocol(self):
200 pass
201
@@ -1,173 +1,179
1 1 """
2 2 This module contains helper functions and helper classes.
3 3 """
4 4 import hashlib
5 5 from boards.abstracts.constants import FILE_DIRECTORY
6 6 from random import random
7 7 import time
8 8 import hmac
9 9
10 10 from django.core.cache import cache
11 11 from django.db.models import Model
12 12 from django import forms
13 13 from django.template.defaultfilters import filesizeformat
14 14 from django.utils import timezone
15 15 from django.utils.translation import ugettext_lazy as _
16 16 import magic
17 17 import os
18 18
19 19 import boards
20 20 from boards.settings import get_bool
21 21 from neboard import settings
22 22
23 23 CACHE_KEY_DELIMITER = '_'
24 24
25 25 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
26 26 META_REMOTE_ADDR = 'REMOTE_ADDR'
27 27
28 28 SETTING_MESSAGES = 'Messages'
29 29 SETTING_ANON_MODE = 'AnonymousMode'
30 30
31 31 ANON_IP = '127.0.0.1'
32 32
33 33 FILE_EXTENSION_DELIMITER = '.'
34 34
35 35 KNOWN_DOMAINS = (
36 36 'org.ru',
37 37 )
38 38
39 39
40 40 def is_anonymous_mode():
41 41 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
42 42
43 43
44 44 def get_client_ip(request):
45 45 if is_anonymous_mode():
46 46 ip = ANON_IP
47 47 else:
48 48 x_forwarded_for = request.META.get(HTTP_FORWARDED)
49 49 if x_forwarded_for:
50 50 ip = x_forwarded_for.split(',')[-1].strip()
51 51 else:
52 52 ip = request.META.get(META_REMOTE_ADDR)
53 53 return ip
54 54
55 55
56 56 # TODO The output format is not epoch because it includes microseconds
57 57 def datetime_to_epoch(datetime):
58 58 return int(time.mktime(timezone.localtime(
59 59 datetime,timezone.get_current_timezone()).timetuple())
60 60 * 1000000 + datetime.microsecond)
61 61
62 62
63 63 def get_websocket_token(user_id='', timestamp=''):
64 64 """
65 65 Create token to validate information provided by new connection.
66 66 """
67 67
68 68 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
69 69 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
70 70 sign.update(user_id.encode())
71 71 sign.update(timestamp.encode())
72 72 token = sign.hexdigest()
73 73
74 74 return token
75 75
76 76
77 77 # TODO Test this carefully
78 78 def cached_result(key_method=None):
79 79 """
80 80 Caches method result in the Django's cache system, persisted by object name,
81 81 object name, model id if object is a Django model, args and kwargs if any.
82 82 """
83 83 def _cached_result(function):
84 84 def inner_func(obj, *args, **kwargs):
85 85 cache_key_params = [obj.__class__.__name__, function.__name__]
86 86
87 87 cache_key_params += args
88 88 for key, value in kwargs:
89 89 cache_key_params.append(key + ':' + value)
90 90
91 91 if isinstance(obj, Model):
92 92 cache_key_params.append(str(obj.id))
93 93
94 94 if key_method is not None:
95 95 cache_key_params += [str(arg) for arg in key_method(obj)]
96 96
97 97 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
98 98
99 99 persisted_result = cache.get(cache_key)
100 100 if persisted_result is not None:
101 101 result = persisted_result
102 102 else:
103 103 result = function(obj, *args, **kwargs)
104 104 if result is not None:
105 105 cache.set(cache_key, result)
106 106
107 107 return result
108 108
109 109 return inner_func
110 110 return _cached_result
111 111
112 112
113 113 def get_file_hash(file) -> str:
114 114 md5 = hashlib.md5()
115 115 for chunk in file.chunks():
116 116 md5.update(chunk)
117 117 return md5.hexdigest()
118 118
119 119
120 120 def validate_file_size(size: int):
121 121 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
122 122 if size > max_size:
123 123 raise forms.ValidationError(
124 124 _('File must be less than %s but is %s.')
125 125 % (filesizeformat(max_size), filesizeformat(size)))
126 126
127 127
128 128 def get_extension(filename):
129 129 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
130 130
131 131
132 132 def get_upload_filename(model_instance, old_filename):
133 133 # TODO Use something other than random number in file name
134 134 extension = get_extension(old_filename)
135 135 new_name = '{}{}.{}'.format(
136 136 str(int(time.mktime(time.gmtime()))),
137 137 str(int(random() * 1000)),
138 138 extension)
139 139
140 140 return os.path.join(FILE_DIRECTORY, new_name)
141 141
142 142
143 143 def get_file_mimetype(file) -> str:
144 144 file_type = magic.from_buffer(file.chunks().__next__(), mime=True)
145 145 if file_type is None:
146 146 file_type = 'application/octet-stream'
147 147 elif type(file_type) == bytes:
148 148 file_type = file_type.decode()
149 149 return file_type
150 150
151 151
152 152 def get_domain(url: str) -> str:
153 153 """
154 154 Gets domain from an URL with random number of domain levels.
155 155 """
156 levels = url.split('.')
157 if len(levels) < 2:
158 return url
159
160 top = levels[-1]
161 second = levels[-2]
156 domain_parts = url.split('/')
157 if len(domain_parts) >= 2:
158 full_domain = domain_parts[2]
159 else:
160 full_domain = ''
162 161
163 has_third_level = len(levels) > 2
164 if has_third_level:
165 third = levels[-3]
162 result = full_domain
163 if full_domain:
164 levels = full_domain.split('.')
165 if len(levels) >= 2:
166 top = levels[-1]
167 second = levels[-2]
166 168
167 if has_third_level and ('{}.{}'.format(second, top) in KNOWN_DOMAINS):
168 result = '{}.{}.{}'.format(third, second, top)
169 else:
170 result = '{}.{}'.format(second, top)
169 has_third_level = len(levels) > 2
170 if has_third_level:
171 third = levels[-3]
172
173 if has_third_level and ('{}.{}'.format(second, top) in KNOWN_DOMAINS):
174 result = '{}.{}.{}'.format(third, second, top)
175 else:
176 result = '{}.{}'.format(second, top)
171 177
172 178 return result
173 179
General Comments 0
You need to be logged in to leave comments. Login now