##// END OF EJS Templates
Minor import cleanup. Added forgotten fetch command file to mercurial
neko259 -
r1979:b71d904c default
parent child Browse files
Show More
@@ -0,0 +1,16 b''
1 from django.core.management import BaseCommand
2 from django.db import transaction
3
4 from boards.models.source import ThreadSource
5
6 __author__ = 'neko259'
7
8
9 class Command(BaseCommand):
10 help = 'Fetches data from sources and posts into appropriate threads'
11
12 @transaction.atomic
13 def handle(self, *args, **options):
14 for source in ThreadSource.objects.all():
15 source.fetch_latest_posts()
16
@@ -1,590 +1,586 b''
1 import hashlib
2 import logging
1 import logging
3 import re
4 import time
2 import time
5
3
4 import hashlib
6 import pytz
5 import pytz
7
6 import re
8 from PIL import Image
7 from PIL import Image
9
10 from django import forms
8 from django import forms
9 from django.core.cache import cache
10 from django.core.files.images import get_image_dimensions
11 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
11 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
12 from django.forms.utils import ErrorList
12 from django.forms.utils import ErrorList
13 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
13 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
14 from django.core.files.images import get_image_dimensions
15 from django.core.cache import cache
16
14
17 import boards.settings as board_settings
15 import boards.settings as board_settings
18 import neboard
19 from boards import utils
16 from boards import utils
20 from boards.abstracts.constants import REGEX_TAGS
17 from boards.abstracts.constants import REGEX_TAGS
18 from boards.abstracts.settingsmanager import get_settings_manager
21 from boards.abstracts.sticker_factory import get_attachment_by_alias
19 from boards.abstracts.sticker_factory import get_attachment_by_alias
22 from boards.abstracts.settingsmanager import get_settings_manager
23 from boards.forms.fields import UrlFileField
20 from boards.forms.fields import UrlFileField
24 from boards.mdx_neboard import formatters
21 from boards.mdx_neboard import formatters
25 from boards.models import Attachment
22 from boards.models import Attachment
26 from boards.models import Tag
23 from boards.models import Tag
27 from boards.models.attachment import StickerPack
24 from boards.models.attachment import StickerPack
28 from boards.models.attachment.downloaders import download, REGEX_MAGNET
25 from boards.models.attachment.downloaders import download, REGEX_MAGNET
26 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
29 from boards.models.post import TITLE_MAX_LENGTH
27 from boards.models.post import TITLE_MAX_LENGTH
30 from boards.utils import validate_file_size, get_file_mimetype, \
28 from boards.utils import validate_file_size, get_file_mimetype, \
31 FILE_EXTENSION_DELIMITER, get_tripcode_from_text
29 FILE_EXTENSION_DELIMITER, get_tripcode_from_text
32 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
33 from neboard import settings
34
30
35 SECTION_FORMS = 'Forms'
31 SECTION_FORMS = 'Forms'
36
32
37 POW_HASH_LENGTH = 16
33 POW_HASH_LENGTH = 16
38 POW_LIFE_MINUTES = 5
34 POW_LIFE_MINUTES = 5
39
35
40 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
36 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
41 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
37 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
42
38
43 VETERAN_POSTING_DELAY = 5
39 VETERAN_POSTING_DELAY = 5
44
40
45 ATTRIBUTE_PLACEHOLDER = 'placeholder'
41 ATTRIBUTE_PLACEHOLDER = 'placeholder'
46 ATTRIBUTE_ROWS = 'rows'
42 ATTRIBUTE_ROWS = 'rows'
47
43
48 LAST_POST_TIME = 'last_post_time'
44 LAST_POST_TIME = 'last_post_time'
49 LAST_LOGIN_TIME = 'last_login_time'
45 LAST_LOGIN_TIME = 'last_login_time'
50 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
46 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
51 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
47 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
52
48
53 LABEL_TITLE = _('Title')
49 LABEL_TITLE = _('Title')
54 LABEL_TEXT = _('Text')
50 LABEL_TEXT = _('Text')
55 LABEL_TAG = _('Tag')
51 LABEL_TAG = _('Tag')
56 LABEL_SEARCH = _('Search')
52 LABEL_SEARCH = _('Search')
57 LABEL_FILE = _('File')
53 LABEL_FILE = _('File')
58 LABEL_DUPLICATES = _('Check for duplicates')
54 LABEL_DUPLICATES = _('Check for duplicates')
59 LABEL_URL = _('Do not download URLs')
55 LABEL_URL = _('Do not download URLs')
60
56
61 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
57 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
62 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
58 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
63 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
59 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
64 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
60 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
65 ERROR_DUPLICATES = 'Some files are already present on the board.'
61 ERROR_DUPLICATES = 'Some files are already present on the board.'
66
62
67 TAG_MAX_LENGTH = 20
63 TAG_MAX_LENGTH = 20
68
64
69 TEXTAREA_ROWS = 4
65 TEXTAREA_ROWS = 4
70
66
71 TRIPCODE_DELIM = '##'
67 TRIPCODE_DELIM = '##'
72
68
73 # TODO Maybe this may be converted into the database table?
69 # TODO Maybe this may be converted into the database table?
74 MIMETYPE_EXTENSIONS = {
70 MIMETYPE_EXTENSIONS = {
75 'image/jpeg': 'jpeg',
71 'image/jpeg': 'jpeg',
76 'image/png': 'png',
72 'image/png': 'png',
77 'image/gif': 'gif',
73 'image/gif': 'gif',
78 'video/webm': 'webm',
74 'video/webm': 'webm',
79 'application/pdf': 'pdf',
75 'application/pdf': 'pdf',
80 'x-diff': 'diff',
76 'x-diff': 'diff',
81 'image/svg+xml': 'svg',
77 'image/svg+xml': 'svg',
82 'application/x-shockwave-flash': 'swf',
78 'application/x-shockwave-flash': 'swf',
83 'image/x-ms-bmp': 'bmp',
79 'image/x-ms-bmp': 'bmp',
84 'image/bmp': 'bmp',
80 'image/bmp': 'bmp',
85 }
81 }
86
82
87 DOWN_MODE_DOWNLOAD = 'DOWNLOAD'
83 DOWN_MODE_DOWNLOAD = 'DOWNLOAD'
88 DOWN_MODE_DOWNLOAD_UNIQUE = 'DOWNLOAD_UNIQUE'
84 DOWN_MODE_DOWNLOAD_UNIQUE = 'DOWNLOAD_UNIQUE'
89 DOWN_MODE_URL = 'URL'
85 DOWN_MODE_URL = 'URL'
90 DOWN_MODE_TRY = 'TRY'
86 DOWN_MODE_TRY = 'TRY'
91
87
92
88
93 logger = logging.getLogger('boards.forms')
89 logger = logging.getLogger('boards.forms')
94
90
95
91
96 def get_timezones():
92 def get_timezones():
97 timezones = []
93 timezones = []
98 for tz in pytz.common_timezones:
94 for tz in pytz.common_timezones:
99 timezones.append((tz, tz),)
95 timezones.append((tz, tz),)
100 return timezones
96 return timezones
101
97
102
98
103 class FormatPanel(forms.Textarea):
99 class FormatPanel(forms.Textarea):
104 """
100 """
105 Panel for text formatting. Consists of buttons to add different tags to the
101 Panel for text formatting. Consists of buttons to add different tags to the
106 form text area.
102 form text area.
107 """
103 """
108
104
109 def render(self, name, value, attrs=None):
105 def render(self, name, value, attrs=None):
110 output = '<div id="mark-panel">'
106 output = '<div id="mark-panel">'
111 for formatter in formatters:
107 for formatter in formatters:
112 output += '<span class="mark_btn"' + \
108 output += '<span class="mark_btn"' + \
113 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
109 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
114 '\', \'' + formatter.format_right + '\')">' + \
110 '\', \'' + formatter.format_right + '\')">' + \
115 formatter.preview_left + formatter.name + \
111 formatter.preview_left + formatter.name + \
116 formatter.preview_right + '</span>'
112 formatter.preview_right + '</span>'
117
113
118 output += '</div>'
114 output += '</div>'
119 output += super(FormatPanel, self).render(name, value, attrs=attrs)
115 output += super(FormatPanel, self).render(name, value, attrs=attrs)
120
116
121 return output
117 return output
122
118
123
119
124 class PlainErrorList(ErrorList):
120 class PlainErrorList(ErrorList):
125 def __unicode__(self):
121 def __unicode__(self):
126 return self.as_text()
122 return self.as_text()
127
123
128 def as_text(self):
124 def as_text(self):
129 return ''.join(['(!) %s ' % e for e in self])
125 return ''.join(['(!) %s ' % e for e in self])
130
126
131
127
132 class NeboardForm(forms.Form):
128 class NeboardForm(forms.Form):
133 """
129 """
134 Form with neboard-specific formatting.
130 Form with neboard-specific formatting.
135 """
131 """
136 required_css_class = 'required-field'
132 required_css_class = 'required-field'
137
133
138 def as_div(self):
134 def as_div(self):
139 """
135 """
140 Returns this form rendered as HTML <as_div>s.
136 Returns this form rendered as HTML <as_div>s.
141 """
137 """
142
138
143 return self._html_output(
139 return self._html_output(
144 # TODO Do not show hidden rows in the list here
140 # TODO Do not show hidden rows in the list here
145 normal_row='<div class="form-row">'
141 normal_row='<div class="form-row">'
146 '<div class="form-label">'
142 '<div class="form-label">'
147 '%(label)s'
143 '%(label)s'
148 '</div>'
144 '</div>'
149 '<div class="form-input">'
145 '<div class="form-input">'
150 '%(field)s'
146 '%(field)s'
151 '</div>'
147 '</div>'
152 '</div>'
148 '</div>'
153 '<div class="form-row">'
149 '<div class="form-row">'
154 '%(help_text)s'
150 '%(help_text)s'
155 '</div>',
151 '</div>',
156 error_row='<div class="form-row">'
152 error_row='<div class="form-row">'
157 '<div class="form-label"></div>'
153 '<div class="form-label"></div>'
158 '<div class="form-errors">%s</div>'
154 '<div class="form-errors">%s</div>'
159 '</div>',
155 '</div>',
160 row_ender='</div>',
156 row_ender='</div>',
161 help_text_html='%s',
157 help_text_html='%s',
162 errors_on_separate_row=True)
158 errors_on_separate_row=True)
163
159
164 def as_json_errors(self):
160 def as_json_errors(self):
165 errors = []
161 errors = []
166
162
167 for name, field in list(self.fields.items()):
163 for name, field in list(self.fields.items()):
168 if self[name].errors:
164 if self[name].errors:
169 errors.append({
165 errors.append({
170 'field': name,
166 'field': name,
171 'errors': self[name].errors.as_text(),
167 'errors': self[name].errors.as_text(),
172 })
168 })
173
169
174 return errors
170 return errors
175
171
176
172
177 class PostForm(NeboardForm):
173 class PostForm(NeboardForm):
178
174
179 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
175 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
180 label=LABEL_TITLE,
176 label=LABEL_TITLE,
181 widget=forms.TextInput(
177 widget=forms.TextInput(
182 attrs={ATTRIBUTE_PLACEHOLDER: 'Title{}tripcode'.format(TRIPCODE_DELIM)}))
178 attrs={ATTRIBUTE_PLACEHOLDER: 'Title{}tripcode'.format(TRIPCODE_DELIM)}))
183 text = forms.CharField(
179 text = forms.CharField(
184 widget=FormatPanel(attrs={
180 widget=FormatPanel(attrs={
185 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
181 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
186 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
182 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
187 }),
183 }),
188 required=False, label=LABEL_TEXT)
184 required=False, label=LABEL_TEXT)
189 download_mode = forms.ChoiceField(
185 download_mode = forms.ChoiceField(
190 choices=(
186 choices=(
191 (DOWN_MODE_TRY, _('Download or insert as URLs')),
187 (DOWN_MODE_TRY, _('Download or insert as URLs')),
192 (DOWN_MODE_DOWNLOAD, _('Download')),
188 (DOWN_MODE_DOWNLOAD, _('Download')),
193 (DOWN_MODE_DOWNLOAD_UNIQUE, _('Download and check for uniqueness')),
189 (DOWN_MODE_DOWNLOAD_UNIQUE, _('Download and check for uniqueness')),
194 (DOWN_MODE_URL, _('Insert as URLs')),
190 (DOWN_MODE_URL, _('Insert as URLs')),
195 ),
191 ),
196 initial=DOWN_MODE_TRY,
192 initial=DOWN_MODE_TRY,
197 label=_('File process mode'))
193 label=_('File process mode'))
198 file = UrlFileField(required=False, label=LABEL_FILE)
194 file = UrlFileField(required=False, label=LABEL_FILE)
199
195
200 # This field is for spam prevention only
196 # This field is for spam prevention only
201 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
197 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
202 widget=forms.TextInput(attrs={
198 widget=forms.TextInput(attrs={
203 'class': 'form-email'}))
199 'class': 'form-email'}))
204 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
200 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
205
201
206 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
202 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
207 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
203 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
208 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
204 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
209
205
210 session = None
206 session = None
211 need_to_ban = False
207 need_to_ban = False
212
208
213 def clean_title(self):
209 def clean_title(self):
214 title = self.cleaned_data['title']
210 title = self.cleaned_data['title']
215 if title:
211 if title:
216 if len(title) > TITLE_MAX_LENGTH:
212 if len(title) > TITLE_MAX_LENGTH:
217 raise forms.ValidationError(_('Title must have less than %s '
213 raise forms.ValidationError(_('Title must have less than %s '
218 'characters') %
214 'characters') %
219 str(TITLE_MAX_LENGTH))
215 str(TITLE_MAX_LENGTH))
220 return title
216 return title
221
217
222 def clean_text(self):
218 def clean_text(self):
223 text = self.cleaned_data['text'].strip()
219 text = self.cleaned_data['text'].strip()
224 if text:
220 if text:
225 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
221 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
226 if len(text) > max_length:
222 if len(text) > max_length:
227 raise forms.ValidationError(_('Text must have less than %s '
223 raise forms.ValidationError(_('Text must have less than %s '
228 'characters') % str(max_length))
224 'characters') % str(max_length))
229 return text
225 return text
230
226
231 def clean_file(self):
227 def clean_file(self):
232 return self._clean_files(self.cleaned_data['file'])
228 return self._clean_files(self.cleaned_data['file'])
233
229
234 def clean(self):
230 def clean(self):
235 cleaned_data = super(PostForm, self).clean()
231 cleaned_data = super(PostForm, self).clean()
236
232
237 if cleaned_data['email']:
233 if cleaned_data['email']:
238 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
234 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
239 self.need_to_ban = True
235 self.need_to_ban = True
240 raise forms.ValidationError('A human cannot enter a hidden field')
236 raise forms.ValidationError('A human cannot enter a hidden field')
241
237
242 if not self.errors:
238 if not self.errors:
243 self._clean_text_file()
239 self._clean_text_file()
244
240
245 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
241 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
246 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
242 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
247
243
248 settings_manager = get_settings_manager(self)
244 settings_manager = get_settings_manager(self)
249 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
245 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
250 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
246 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
251 if pow_difficulty > 0:
247 if pow_difficulty > 0:
252 # PoW-based
248 # PoW-based
253 if cleaned_data['timestamp'] \
249 if cleaned_data['timestamp'] \
254 and cleaned_data['iteration'] and cleaned_data['guess'] \
250 and cleaned_data['iteration'] and cleaned_data['guess'] \
255 and not settings_manager.get_setting('confirmed_user'):
251 and not settings_manager.get_setting('confirmed_user'):
256 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
252 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
257 else:
253 else:
258 # Time-based
254 # Time-based
259 self._validate_posting_speed()
255 self._validate_posting_speed()
260 settings_manager.set_setting('confirmed_user', True)
256 settings_manager.set_setting('confirmed_user', True)
261 if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE:
257 if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE:
262 self._check_file_duplicates(self.get_files())
258 self._check_file_duplicates(self.get_files())
263
259
264 return cleaned_data
260 return cleaned_data
265
261
266 def get_files(self):
262 def get_files(self):
267 """
263 """
268 Gets file from form or URL.
264 Gets file from form or URL.
269 """
265 """
270
266
271 files = []
267 files = []
272 for file in self.cleaned_data['file']:
268 for file in self.cleaned_data['file']:
273 if isinstance(file, UploadedFile):
269 if isinstance(file, UploadedFile):
274 files.append(file)
270 files.append(file)
275
271
276 return files
272 return files
277
273
278 def get_file_urls(self):
274 def get_file_urls(self):
279 files = []
275 files = []
280 for file in self.cleaned_data['file']:
276 for file in self.cleaned_data['file']:
281 if type(file) == str:
277 if type(file) == str:
282 files.append(file)
278 files.append(file)
283
279
284 return files
280 return files
285
281
286 def get_tripcode(self):
282 def get_tripcode(self):
287 title = self.cleaned_data['title']
283 title = self.cleaned_data['title']
288 if title is not None and TRIPCODE_DELIM in title:
284 if title is not None and TRIPCODE_DELIM in title:
289 tripcode = get_tripcode_from_text(title.split(TRIPCODE_DELIM, maxsplit=1)[1])
285 tripcode = get_tripcode_from_text(title.split(TRIPCODE_DELIM, maxsplit=1)[1])
290 else:
286 else:
291 tripcode = ''
287 tripcode = ''
292 return tripcode
288 return tripcode
293
289
294 def get_title(self):
290 def get_title(self):
295 title = self.cleaned_data['title']
291 title = self.cleaned_data['title']
296 if title is not None and TRIPCODE_DELIM in title:
292 if title is not None and TRIPCODE_DELIM in title:
297 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
293 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
298 else:
294 else:
299 return title
295 return title
300
296
301 def get_images(self):
297 def get_images(self):
302 return self.cleaned_data.get('stickers', [])
298 return self.cleaned_data.get('stickers', [])
303
299
304 def is_subscribe(self):
300 def is_subscribe(self):
305 return self.cleaned_data['subscribe']
301 return self.cleaned_data['subscribe']
306
302
307 def _update_file_extension(self, file):
303 def _update_file_extension(self, file):
308 if file:
304 if file:
309 mimetype = get_file_mimetype(file)
305 mimetype = get_file_mimetype(file)
310 extension = MIMETYPE_EXTENSIONS.get(mimetype)
306 extension = MIMETYPE_EXTENSIONS.get(mimetype)
311 if extension:
307 if extension:
312 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
308 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
313 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
309 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
314
310
315 file.name = new_filename
311 file.name = new_filename
316 else:
312 else:
317 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
313 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
318
314
319 def _clean_files(self, inputs):
315 def _clean_files(self, inputs):
320 files = []
316 files = []
321
317
322 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
318 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
323 if len(inputs) > max_file_count:
319 if len(inputs) > max_file_count:
324 raise forms.ValidationError(
320 raise forms.ValidationError(
325 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
321 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
326 max_file_count) % {'files': max_file_count})
322 max_file_count) % {'files': max_file_count})
327 for file_input in inputs:
323 for file_input in inputs:
328 if isinstance(file_input, UploadedFile):
324 if isinstance(file_input, UploadedFile):
329 files.append(self._clean_file_file(file_input))
325 files.append(self._clean_file_file(file_input))
330 else:
326 else:
331 files.append(self._clean_file_url(file_input))
327 files.append(self._clean_file_url(file_input))
332
328
333 for file in files:
329 for file in files:
334 self._validate_image_dimensions(file)
330 self._validate_image_dimensions(file)
335
331
336 return files
332 return files
337
333
338 def _validate_image_dimensions(self, file):
334 def _validate_image_dimensions(self, file):
339 if isinstance(file, UploadedFile):
335 if isinstance(file, UploadedFile):
340 mimetype = get_file_mimetype(file)
336 mimetype = get_file_mimetype(file)
341 if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
337 if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
342 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
338 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
343 try:
339 try:
344 print(get_image_dimensions(file))
340 print(get_image_dimensions(file))
345 except Exception:
341 except Exception:
346 raise forms.ValidationError('Possible decompression bomb or large image.')
342 raise forms.ValidationError('Possible decompression bomb or large image.')
347
343
348 def _clean_file_file(self, file):
344 def _clean_file_file(self, file):
349 validate_file_size(file.size)
345 validate_file_size(file.size)
350 self._update_file_extension(file)
346 self._update_file_extension(file)
351
347
352 return file
348 return file
353
349
354 def _clean_file_url(self, url):
350 def _clean_file_url(self, url):
355 file = None
351 file = None
356
352
357 if url:
353 if url:
358 mode = self.cleaned_data['download_mode']
354 mode = self.cleaned_data['download_mode']
359 if mode == DOWN_MODE_URL:
355 if mode == DOWN_MODE_URL:
360 return url
356 return url
361
357
362 try:
358 try:
363 image = get_attachment_by_alias(url, self.session)
359 image = get_attachment_by_alias(url, self.session)
364 if image is not None:
360 if image is not None:
365 if 'stickers' not in self.cleaned_data:
361 if 'stickers' not in self.cleaned_data:
366 self.cleaned_data['stickers'] = []
362 self.cleaned_data['stickers'] = []
367 self.cleaned_data['stickers'].append(image)
363 self.cleaned_data['stickers'].append(image)
368 return
364 return
369
365
370 if file is None:
366 if file is None:
371 file = self._get_file_from_url(url)
367 file = self._get_file_from_url(url)
372 if not file:
368 if not file:
373 raise forms.ValidationError(_('Invalid URL'))
369 raise forms.ValidationError(_('Invalid URL'))
374 else:
370 else:
375 validate_file_size(file.size)
371 validate_file_size(file.size)
376 self._update_file_extension(file)
372 self._update_file_extension(file)
377 except forms.ValidationError as e:
373 except forms.ValidationError as e:
378 # Assume we will get the plain URL instead of a file and save it
374 # Assume we will get the plain URL instead of a file and save it
379 if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)):
375 if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)):
380 logger.info('Error in forms: {}'.format(e))
376 logger.info('Error in forms: {}'.format(e))
381 return url
377 return url
382 else:
378 else:
383 raise e
379 raise e
384
380
385 return file
381 return file
386
382
387 def _clean_text_file(self):
383 def _clean_text_file(self):
388 text = self.cleaned_data.get('text')
384 text = self.cleaned_data.get('text')
389 file = self.get_files()
385 file = self.get_files()
390 file_url = self.get_file_urls()
386 file_url = self.get_file_urls()
391 images = self.get_images()
387 images = self.get_images()
392
388
393 if (not text) and (not file) and (not file_url) and len(images) == 0:
389 if (not text) and (not file) and (not file_url) and len(images) == 0:
394 error_message = _('Either text or file must be entered.')
390 error_message = _('Either text or file must be entered.')
395 self._add_general_error(error_message)
391 self._add_general_error(error_message)
396
392
397 def _get_cache_key(self, key):
393 def _get_cache_key(self, key):
398 return '{}_{}'.format(self.session.session_key, key)
394 return '{}_{}'.format(self.session.session_key, key)
399
395
400 def _set_session_cache(self, key, value):
396 def _set_session_cache(self, key, value):
401 cache.set(self._get_cache_key(key), value)
397 cache.set(self._get_cache_key(key), value)
402
398
403 def _get_session_cache(self, key):
399 def _get_session_cache(self, key):
404 return cache.get(self._get_cache_key(key))
400 return cache.get(self._get_cache_key(key))
405
401
406 def _get_last_post_time(self):
402 def _get_last_post_time(self):
407 last = self._get_session_cache(LAST_POST_TIME)
403 last = self._get_session_cache(LAST_POST_TIME)
408 if last is None:
404 if last is None:
409 last = self.session.get(LAST_POST_TIME)
405 last = self.session.get(LAST_POST_TIME)
410 return last
406 return last
411
407
412 def _validate_posting_speed(self):
408 def _validate_posting_speed(self):
413 can_post = True
409 can_post = True
414
410
415 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
411 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
416
412
417 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
413 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
418 now = time.time()
414 now = time.time()
419
415
420 current_delay = 0
416 current_delay = 0
421
417
422 if LAST_POST_TIME not in self.session:
418 if LAST_POST_TIME not in self.session:
423 self.session[LAST_POST_TIME] = now
419 self.session[LAST_POST_TIME] = now
424
420
425 need_delay = True
421 need_delay = True
426 else:
422 else:
427 last_post_time = self._get_last_post_time()
423 last_post_time = self._get_last_post_time()
428 current_delay = int(now - last_post_time)
424 current_delay = int(now - last_post_time)
429
425
430 need_delay = current_delay < posting_delay
426 need_delay = current_delay < posting_delay
431
427
432 self._set_session_cache(LAST_POST_TIME, now)
428 self._set_session_cache(LAST_POST_TIME, now)
433
429
434 if need_delay:
430 if need_delay:
435 delay = posting_delay - current_delay
431 delay = posting_delay - current_delay
436 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
432 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
437 delay) % {'delay': delay}
433 delay) % {'delay': delay}
438 self._add_general_error(error_message)
434 self._add_general_error(error_message)
439
435
440 can_post = False
436 can_post = False
441
437
442 if can_post:
438 if can_post:
443 self.session[LAST_POST_TIME] = now
439 self.session[LAST_POST_TIME] = now
444 else:
440 else:
445 # Reset the time since posting failed
441 # Reset the time since posting failed
446 self._set_session_cache(LAST_POST_TIME, self.session[LAST_POST_TIME])
442 self._set_session_cache(LAST_POST_TIME, self.session[LAST_POST_TIME])
447
443
448 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
444 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
449 """
445 """
450 Gets an file file from URL.
446 Gets an file file from URL.
451 """
447 """
452
448
453 try:
449 try:
454 return download(url)
450 return download(url)
455 except forms.ValidationError as e:
451 except forms.ValidationError as e:
456 raise e
452 raise e
457 except Exception as e:
453 except Exception as e:
458 raise forms.ValidationError(e)
454 raise forms.ValidationError(e)
459
455
460 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
456 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
461 payload = timestamp + message.replace('\r\n', '\n')
457 payload = timestamp + message.replace('\r\n', '\n')
462 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
458 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
463 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
459 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
464 if len(target) < POW_HASH_LENGTH:
460 if len(target) < POW_HASH_LENGTH:
465 target = '0' * (POW_HASH_LENGTH - len(target)) + target
461 target = '0' * (POW_HASH_LENGTH - len(target)) + target
466
462
467 computed_guess = hashlib.sha256((payload + iteration).encode())\
463 computed_guess = hashlib.sha256((payload + iteration).encode())\
468 .hexdigest()[0:POW_HASH_LENGTH]
464 .hexdigest()[0:POW_HASH_LENGTH]
469 if guess != computed_guess or guess > target:
465 if guess != computed_guess or guess > target:
470 self._add_general_error(_('Invalid PoW.'))
466 self._add_general_error(_('Invalid PoW.'))
471
467
472 def _check_file_duplicates(self, files):
468 def _check_file_duplicates(self, files):
473 for file in files:
469 for file in files:
474 file_hash = utils.get_file_hash(file)
470 file_hash = utils.get_file_hash(file)
475 if Attachment.objects.get_existing_duplicate(file_hash, file):
471 if Attachment.objects.get_existing_duplicate(file_hash, file):
476 self._add_general_error(_(ERROR_DUPLICATES))
472 self._add_general_error(_(ERROR_DUPLICATES))
477
473
478 def _add_general_error(self, message):
474 def _add_general_error(self, message):
479 self.add_error('text', forms.ValidationError(message))
475 self.add_error('text', forms.ValidationError(message))
480
476
481
477
482 class ThreadForm(PostForm):
478 class ThreadForm(PostForm):
483
479
484 tags = forms.CharField(
480 tags = forms.CharField(
485 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
481 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
486 max_length=100, label=_('Tags'), required=True)
482 max_length=100, label=_('Tags'), required=True)
487 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
483 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
488 stickerpack = forms.BooleanField(label=_('Sticker Pack'), required=False)
484 stickerpack = forms.BooleanField(label=_('Sticker Pack'), required=False)
489
485
490 def clean_tags(self):
486 def clean_tags(self):
491 tags = self.cleaned_data['tags'].strip()
487 tags = self.cleaned_data['tags'].strip()
492
488
493 if not tags or not REGEX_TAGS.match(tags):
489 if not tags or not REGEX_TAGS.match(tags):
494 raise forms.ValidationError(
490 raise forms.ValidationError(
495 _('Inappropriate characters in tags.'))
491 _('Inappropriate characters in tags.'))
496
492
497 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
493 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
498 .strip().lower()
494 .strip().lower()
499
495
500 required_tag_exists = False
496 required_tag_exists = False
501 tag_set = set()
497 tag_set = set()
502 for tag_string in tags.split():
498 for tag_string in tags.split():
503 tag_name = tag_string.strip().lower()
499 tag_name = tag_string.strip().lower()
504 if tag_name == default_tag_name:
500 if tag_name == default_tag_name:
505 required_tag_exists = True
501 required_tag_exists = True
506 tag, created = Tag.objects.get_or_create_with_alias(
502 tag, created = Tag.objects.get_or_create_with_alias(
507 name=tag_name, required=True)
503 name=tag_name, required=True)
508 else:
504 else:
509 tag, created = Tag.objects.get_or_create_with_alias(name=tag_name)
505 tag, created = Tag.objects.get_or_create_with_alias(name=tag_name)
510 tag_set.add(tag)
506 tag_set.add(tag)
511
507
512 # If this is a new tag, don't check for its parents because nobody
508 # If this is a new tag, don't check for its parents because nobody
513 # added them yet
509 # added them yet
514 if not created:
510 if not created:
515 tag_set |= set(tag.get_all_parents())
511 tag_set |= set(tag.get_all_parents())
516
512
517 for tag in tag_set:
513 for tag in tag_set:
518 if tag.required:
514 if tag.required:
519 required_tag_exists = True
515 required_tag_exists = True
520 break
516 break
521
517
522 # Use default tag if no section exists
518 # Use default tag if no section exists
523 if not required_tag_exists:
519 if not required_tag_exists:
524 default_tag, created = Tag.objects.get_or_create_with_alias(
520 default_tag, created = Tag.objects.get_or_create_with_alias(
525 name=default_tag_name, required=True)
521 name=default_tag_name, required=True)
526 tag_set.add(default_tag)
522 tag_set.add(default_tag)
527
523
528 return tag_set
524 return tag_set
529
525
530 def clean(self):
526 def clean(self):
531 cleaned_data = super(ThreadForm, self).clean()
527 cleaned_data = super(ThreadForm, self).clean()
532
528
533 return cleaned_data
529 return cleaned_data
534
530
535 def is_monochrome(self):
531 def is_monochrome(self):
536 return self.cleaned_data['monochrome']
532 return self.cleaned_data['monochrome']
537
533
538 def clean_stickerpack(self):
534 def clean_stickerpack(self):
539 stickerpack = self.cleaned_data['stickerpack']
535 stickerpack = self.cleaned_data['stickerpack']
540 if stickerpack:
536 if stickerpack:
541 tripcode = self.get_tripcode()
537 tripcode = self.get_tripcode()
542 if not tripcode:
538 if not tripcode:
543 raise forms.ValidationError(_(
539 raise forms.ValidationError(_(
544 'Tripcode should be specified to own a stickerpack.'))
540 'Tripcode should be specified to own a stickerpack.'))
545 title = self.get_title()
541 title = self.get_title()
546 if not title:
542 if not title:
547 raise forms.ValidationError(_(
543 raise forms.ValidationError(_(
548 'Title should be specified as a stickerpack name.'))
544 'Title should be specified as a stickerpack name.'))
549 if not REGEX_TAGS.match(title):
545 if not REGEX_TAGS.match(title):
550 raise forms.ValidationError(_('Inappropriate sticker pack name.'))
546 raise forms.ValidationError(_('Inappropriate sticker pack name.'))
551
547
552 existing_pack = StickerPack.objects.filter(name=title).first()
548 existing_pack = StickerPack.objects.filter(name=title).first()
553 if existing_pack:
549 if existing_pack:
554 if existing_pack.tripcode != tripcode:
550 if existing_pack.tripcode != tripcode:
555 raise forms.ValidationError(_(
551 raise forms.ValidationError(_(
556 'A sticker pack with this name already exists and is'
552 'A sticker pack with this name already exists and is'
557 ' owned by another tripcode.'))
553 ' owned by another tripcode.'))
558 if not existing_pack.tripcode:
554 if not existing_pack.tripcode:
559 raise forms.ValidationError(_(
555 raise forms.ValidationError(_(
560 'This sticker pack can only be updated by an '
556 'This sticker pack can only be updated by an '
561 'administrator.'))
557 'administrator.'))
562
558
563 return stickerpack
559 return stickerpack
564
560
565 def is_stickerpack(self):
561 def is_stickerpack(self):
566 return self.cleaned_data['stickerpack']
562 return self.cleaned_data['stickerpack']
567
563
568
564
569 class SettingsForm(NeboardForm):
565 class SettingsForm(NeboardForm):
570
566
571 theme = forms.ChoiceField(
567 theme = forms.ChoiceField(
572 choices=board_settings.get_list_dict('View', 'Themes'),
568 choices=board_settings.get_list_dict('View', 'Themes'),
573 label=_('Theme'))
569 label=_('Theme'))
574 image_viewer = forms.ChoiceField(
570 image_viewer = forms.ChoiceField(
575 choices=board_settings.get_list_dict('View', 'ImageViewers'),
571 choices=board_settings.get_list_dict('View', 'ImageViewers'),
576 label=_('Image view mode'))
572 label=_('Image view mode'))
577 username = forms.CharField(label=_('User name'), required=False)
573 username = forms.CharField(label=_('User name'), required=False)
578 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
574 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
579
575
580 def clean_username(self):
576 def clean_username(self):
581 username = self.cleaned_data['username']
577 username = self.cleaned_data['username']
582
578
583 if username and not REGEX_USERNAMES.match(username):
579 if username and not REGEX_USERNAMES.match(username):
584 raise forms.ValidationError(_('Inappropriate characters.'))
580 raise forms.ValidationError(_('Inappropriate characters.'))
585
581
586 return username
582 return username
587
583
588
584
589 class SearchForm(NeboardForm):
585 class SearchForm(NeboardForm):
590 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
586 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