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