##// END OF EJS Templates
Some more constant extracts
neko259 -
r2003:3c9f075c default
parent child Browse files
Show More
@@ -1,227 +1,228 b''
1 1 from boards import settings
2 2 from boards.models import Tag, TagAlias, Attachment
3 3 from boards.models.attachment import AttachmentSticker
4 4 from boards.models.thread import FAV_THREAD_NO_UPDATES
5 5 from boards.models.tag import DEFAULT_LOCALE
6 from boards.settings import SECTION_VIEW
6 7
7 8 MAX_TRIPCODE_COLLISIONS = 50
8 9
9 10 __author__ = 'neko259'
10 11
11 12 SESSION_SETTING = 'setting'
12 13
13 14 # Remove this, it is not used any more cause there is a user's permission
14 15 PERMISSION_MODERATE = 'moderator'
15 16
16 17 SETTING_THEME = 'theme'
17 18 SETTING_FAVORITE_TAGS = 'favorite_tags'
18 19 SETTING_FAVORITE_THREADS = 'favorite_threads'
19 20 SETTING_HIDDEN_TAGS = 'hidden_tags'
20 21 SETTING_PERMISSIONS = 'permissions'
21 22 SETTING_USERNAME = 'username'
22 23 SETTING_LAST_NOTIFICATION_ID = 'last_notification'
23 24 SETTING_IMAGE_VIEWER = 'image_viewer'
24 25 SETTING_TRIPCODE = 'tripcode'
25 26 SETTING_IMAGES = 'images_aliases'
26 27 SETTING_ONLY_FAVORITES = 'only_favorites'
27 28
28 29 DEFAULT_THEME = 'md'
29 30
30 31
31 32 class SettingsManager:
32 33 """
33 34 Base settings manager class. get_setting and set_setting methods should
34 35 be overriden.
35 36 """
36 37 def __init__(self):
37 38 pass
38 39
39 40 def get_theme(self) -> str:
40 41 theme = self.get_setting(SETTING_THEME)
41 42 if not theme:
42 43 theme = DEFAULT_THEME
43 44 self.set_setting(SETTING_THEME, theme)
44 45
45 46 return theme
46 47
47 48 def set_theme(self, theme):
48 49 self.set_setting(SETTING_THEME, theme)
49 50
50 51 def has_permission(self, permission):
51 52 permissions = self.get_setting(SETTING_PERMISSIONS)
52 53 if permissions:
53 54 return permission in permissions
54 55 else:
55 56 return False
56 57
57 58 def get_setting(self, setting, default=None):
58 59 pass
59 60
60 61 def set_setting(self, setting, value):
61 62 pass
62 63
63 64 def add_permission(self, permission):
64 65 permissions = self.get_setting(SETTING_PERMISSIONS)
65 66 if not permissions:
66 67 permissions = [permission]
67 68 else:
68 69 permissions.append(permission)
69 70 self.set_setting(SETTING_PERMISSIONS, permissions)
70 71
71 72 def del_permission(self, permission):
72 73 permissions = self.get_setting(SETTING_PERMISSIONS)
73 74 if not permissions:
74 75 permissions = []
75 76 else:
76 77 permissions.remove(permission)
77 78 self.set_setting(SETTING_PERMISSIONS, permissions)
78 79
79 80 def get_fav_tags(self) -> list:
80 81 tag_names = self.get_setting(SETTING_FAVORITE_TAGS)
81 82 tags = []
82 83 if tag_names:
83 84 tags = list(Tag.objects.filter(aliases__in=TagAlias.objects
84 85 .filter_localized(parent__aliases__name__in=tag_names))
85 86 .order_by('aliases__name'))
86 87 return tags
87 88
88 89 def add_fav_tag(self, tag):
89 90 tags = self.get_setting(SETTING_FAVORITE_TAGS)
90 91 if not tags:
91 92 tags = [tag.get_name()]
92 93 else:
93 94 if not tag.get_name() in tags:
94 95 tags.append(tag.get_name())
95 96
96 97 tags.sort()
97 98 self.set_setting(SETTING_FAVORITE_TAGS, tags)
98 99
99 100 def del_fav_tag(self, tag):
100 101 tags = self.get_setting(SETTING_FAVORITE_TAGS)
101 102 if tag.get_name() in tags:
102 103 tags.remove(tag.get_name())
103 104 self.set_setting(SETTING_FAVORITE_TAGS, tags)
104 105
105 106 def get_hidden_tags(self) -> list:
106 107 tag_names = self.get_setting(SETTING_HIDDEN_TAGS)
107 108 tags = []
108 109 if tag_names:
109 110 tags = list(Tag.objects.filter(aliases__in=TagAlias.objects
110 111 .filter_localized(parent__aliases__name__in=tag_names))
111 112 .order_by('aliases__name'))
112 113
113 114 return tags
114 115
115 116 def add_hidden_tag(self, tag):
116 117 tags = self.get_setting(SETTING_HIDDEN_TAGS)
117 118 if not tags:
118 119 tags = [tag.get_name()]
119 120 else:
120 121 if not tag.get_name() in tags:
121 122 tags.append(tag.get_name())
122 123
123 124 tags.sort()
124 125 self.set_setting(SETTING_HIDDEN_TAGS, tags)
125 126
126 127 def del_hidden_tag(self, tag):
127 128 tags = self.get_setting(SETTING_HIDDEN_TAGS)
128 129 if tag.get_name() in tags:
129 130 tags.remove(tag.get_name())
130 131 self.set_setting(SETTING_HIDDEN_TAGS, tags)
131 132
132 133 def get_fav_threads(self) -> dict:
133 134 return self.get_setting(SETTING_FAVORITE_THREADS, default=dict())
134 135
135 136 def add_or_read_fav_thread(self, opening_post):
136 137 threads = self.get_fav_threads()
137 138
138 max_fav_threads = settings.get_int('View', 'MaxFavoriteThreads')
139 max_fav_threads = settings.get_int(SECTION_VIEW, 'MaxFavoriteThreads')
139 140 if (str(opening_post.id) in threads) or (len(threads) < max_fav_threads):
140 141 thread = opening_post.get_thread()
141 142 # Don't check for new posts if the thread is archived already
142 143 if thread.is_archived():
143 144 last_id = FAV_THREAD_NO_UPDATES
144 145 else:
145 146 last_id = thread.get_replies().last().id
146 147 threads[str(opening_post.id)] = last_id
147 148 self.set_setting(SETTING_FAVORITE_THREADS, threads)
148 149
149 150 def del_fav_thread(self, opening_post):
150 151 threads = self.get_fav_threads()
151 152 if self.thread_is_fav(opening_post):
152 153 del threads[str(opening_post.id)]
153 154 self.set_setting(SETTING_FAVORITE_THREADS, threads)
154 155
155 156 def thread_is_fav(self, opening_post):
156 157 return str(opening_post.id) in self.get_fav_threads()
157 158
158 159 def get_notification_usernames(self):
159 160 names = set()
160 161 name_list = self.get_setting(SETTING_USERNAME)
161 162 if name_list is not None:
162 163 name_list = name_list.strip()
163 164 if len(name_list) > 0:
164 165 names = name_list.lower().split(',')
165 166 names = set(name.strip() for name in names)
166 167 return names
167 168
168 169 def get_attachment_by_alias(self, alias):
169 170 images = self.get_setting(SETTING_IMAGES)
170 171 if images and alias in images:
171 172 try:
172 173 return Attachment.objects.get(id=images.get(alias))
173 174 except Attachment.DoesNotExist:
174 175 self.remove_attachment_alias(alias)
175 176
176 177 def add_attachment_alias(self, alias, attachment):
177 178 images = self.get_setting(SETTING_IMAGES)
178 179 if images is None:
179 180 images = dict()
180 181 images[alias] = attachment.id
181 182 self.set_setting(SETTING_IMAGES, images)
182 183
183 184 def remove_attachment_alias(self, alias):
184 185 images = self.get_setting(SETTING_IMAGES)
185 186 del images[alias]
186 187 self.set_setting(SETTING_IMAGES, images)
187 188
188 189 def get_stickers(self):
189 190 images = self.get_setting(SETTING_IMAGES)
190 191 stickers = []
191 192 if images:
192 193 for key, value in images.items():
193 194 try:
194 195 attachment = Attachment.objects.get(id=value)
195 196 stickers.append(AttachmentSticker(name=key, attachment=attachment))
196 197 except Attachment.DoesNotExist:
197 198 self.remove_attachment_alias(key)
198 199 return stickers
199 200
200 201
201 202 class SessionSettingsManager(SettingsManager):
202 203 """
203 204 Session-based settings manager. All settings are saved to the user's
204 205 session.
205 206 """
206 207 def __init__(self, session):
207 208 SettingsManager.__init__(self)
208 209 self.session = session
209 210
210 211 def get_setting(self, setting, default=None):
211 212 if setting in self.session:
212 213 return self.session[setting]
213 214 else:
214 215 self.set_setting(setting, default)
215 216 return default
216 217
217 218 def set_setting(self, setting, value):
218 219 self.session[setting] = value
219 220
220 221
221 222 def get_settings_manager(request) -> SettingsManager:
222 223 """
223 224 Get settings manager based on the request object. Currently only
224 225 session-based manager is supported. In the future, cookie-based or
225 226 database-based managers could be implemented.
226 227 """
227 228 return SessionSettingsManager(request.session)
@@ -1,588 +1,588 b''
1 1 import logging
2 2 import time
3 3
4 4 import hashlib
5 5 import pytz
6 6 import re
7 7 from PIL import Image
8 8 from django import forms
9 9 from django.core.cache import cache
10 10 from django.core.files.images import get_image_dimensions
11 11 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
12 12 from django.forms.utils import ErrorList
13 13 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
14 14
15 15 import boards.settings as board_settings
16 16 from boards import utils
17 17 from boards.abstracts.constants import REGEX_TAGS
18 18 from boards.abstracts.settingsmanager import get_settings_manager
19 19 from boards.abstracts.sticker_factory import get_attachment_by_alias
20 20 from boards.forms.fields import UrlFileField
21 21 from boards.mdx_neboard import formatters
22 22 from boards.models import Attachment
23 23 from boards.models import Tag
24 24 from boards.models.attachment import StickerPack
25 25 from boards.models.attachment.downloaders import download, REGEX_MAGNET
26 26 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
27 27 from boards.models.post import TITLE_MAX_LENGTH
28 28 from boards.utils import validate_file_size, get_file_mimetype, \
29 29 FILE_EXTENSION_DELIMITER, get_tripcode_from_text
30 from boards.settings import SECTION_FORMS
30 31
31 SECTION_FORMS = 'Forms'
32 32
33 33 POW_HASH_LENGTH = 16
34 34 POW_LIFE_MINUTES = 5
35 35
36 36 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
37 37 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
38 38
39 39 VETERAN_POSTING_DELAY = 5
40 40
41 41 ATTRIBUTE_PLACEHOLDER = 'placeholder'
42 42 ATTRIBUTE_ROWS = 'rows'
43 43
44 44 LAST_POST_TIME = 'last_post_time'
45 45 LAST_LOGIN_TIME = 'last_login_time'
46 46 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
47 47 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
48 48
49 49 LABEL_TITLE = _('Title')
50 50 LABEL_TEXT = _('Text')
51 51 LABEL_TAG = _('Tag')
52 52 LABEL_SEARCH = _('Search')
53 53 LABEL_FILE = _('File')
54 54 LABEL_DUPLICATES = _('Check for duplicates')
55 55 LABEL_URL = _('Do not download URLs')
56 56
57 57 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
58 58 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
59 59 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
60 60 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
61 61 ERROR_DUPLICATES = 'Some files are already present on the board.'
62 62
63 63 TAG_MAX_LENGTH = 20
64 64
65 65 TEXTAREA_ROWS = 4
66 66
67 67 TRIPCODE_DELIM = '##'
68 68
69 69 # TODO Maybe this may be converted into the database table?
70 70 MIMETYPE_EXTENSIONS = {
71 71 'image/jpeg': 'jpeg',
72 72 'image/png': 'png',
73 73 'image/gif': 'gif',
74 74 'video/webm': 'webm',
75 75 'application/pdf': 'pdf',
76 76 'x-diff': 'diff',
77 77 'image/svg+xml': 'svg',
78 78 'application/x-shockwave-flash': 'swf',
79 79 'image/x-ms-bmp': 'bmp',
80 80 'image/bmp': 'bmp',
81 81 }
82 82
83 83 DOWN_MODE_DOWNLOAD = 'DOWNLOAD'
84 84 DOWN_MODE_DOWNLOAD_UNIQUE = 'DOWNLOAD_UNIQUE'
85 85 DOWN_MODE_URL = 'URL'
86 86 DOWN_MODE_TRY = 'TRY'
87 87
88 88
89 89 logger = logging.getLogger('boards.forms')
90 90
91 91
92 92 def get_timezones():
93 93 timezones = []
94 94 for tz in pytz.common_timezones:
95 95 timezones.append((tz, tz),)
96 96 return timezones
97 97
98 98
99 99 class FormatPanel(forms.Textarea):
100 100 """
101 101 Panel for text formatting. Consists of buttons to add different tags to the
102 102 form text area.
103 103 """
104 104
105 105 def render(self, name, value, attrs=None):
106 106 output = '<div id="mark-panel">'
107 107 for formatter in formatters:
108 108 output += '<span class="mark_btn"' + \
109 109 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
110 110 '\', \'' + formatter.format_right + '\')">' + \
111 111 formatter.preview_left + formatter.name + \
112 112 formatter.preview_right + '</span>'
113 113
114 114 output += '</div>'
115 115 output += super(FormatPanel, self).render(name, value, attrs=attrs)
116 116
117 117 return output
118 118
119 119
120 120 class PlainErrorList(ErrorList):
121 121 def __unicode__(self):
122 122 return self.as_text()
123 123
124 124 def as_text(self):
125 125 return ''.join(['(!) %s ' % e for e in self])
126 126
127 127
128 128 class NeboardForm(forms.Form):
129 129 """
130 130 Form with neboard-specific formatting.
131 131 """
132 132 required_css_class = 'required-field'
133 133
134 134 def as_div(self):
135 135 """
136 136 Returns this form rendered as HTML <as_div>s.
137 137 """
138 138
139 139 return self._html_output(
140 140 # TODO Do not show hidden rows in the list here
141 141 normal_row='<div class="form-row">'
142 142 '<div class="form-label">'
143 143 '%(label)s'
144 144 '</div>'
145 145 '<div class="form-input">'
146 146 '%(field)s'
147 147 '</div>'
148 148 '</div>'
149 149 '<div class="form-row">'
150 150 '%(help_text)s'
151 151 '</div>',
152 152 error_row='<div class="form-row">'
153 153 '<div class="form-label"></div>'
154 154 '<div class="form-errors">%s</div>'
155 155 '</div>',
156 156 row_ender='</div>',
157 157 help_text_html='%s',
158 158 errors_on_separate_row=True)
159 159
160 160 def as_json_errors(self):
161 161 errors = []
162 162
163 163 for name, field in list(self.fields.items()):
164 164 if self[name].errors:
165 165 errors.append({
166 166 'field': name,
167 167 'errors': self[name].errors.as_text(),
168 168 })
169 169
170 170 return errors
171 171
172 172
173 173 class PostForm(NeboardForm):
174 174
175 175 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
176 176 label=LABEL_TITLE,
177 177 widget=forms.TextInput(
178 178 attrs={ATTRIBUTE_PLACEHOLDER: 'Title{}tripcode'.format(TRIPCODE_DELIM)}))
179 179 text = forms.CharField(
180 180 widget=FormatPanel(attrs={
181 181 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
182 182 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
183 183 }),
184 184 required=False, label=LABEL_TEXT)
185 185 download_mode = forms.ChoiceField(
186 186 choices=(
187 187 (DOWN_MODE_TRY, _('Download or insert as URLs')),
188 188 (DOWN_MODE_DOWNLOAD, _('Download')),
189 189 (DOWN_MODE_DOWNLOAD_UNIQUE, _('Download and check for uniqueness')),
190 190 (DOWN_MODE_URL, _('Insert as URLs')),
191 191 ),
192 192 initial=DOWN_MODE_TRY,
193 193 label=_('File process mode'))
194 194 file = UrlFileField(required=False, label=LABEL_FILE)
195 195
196 196 # This field is for spam prevention only
197 197 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
198 198 widget=forms.TextInput(attrs={
199 199 'class': 'form-email'}))
200 200 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
201 201
202 202 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
203 203 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
204 204 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
205 205
206 206 session = None
207 207 need_to_ban = False
208 208
209 209 def clean_title(self):
210 210 title = self.cleaned_data['title']
211 211 if title:
212 212 if len(title) > TITLE_MAX_LENGTH:
213 213 raise forms.ValidationError(_('Title must have less than %s '
214 214 'characters') %
215 215 str(TITLE_MAX_LENGTH))
216 216 return title
217 217
218 218 def clean_text(self):
219 219 text = self.cleaned_data['text'].strip()
220 220 if text:
221 221 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
222 222 if len(text) > max_length:
223 223 raise forms.ValidationError(_('Text must have less than %s '
224 224 'characters') % str(max_length))
225 225 return text
226 226
227 227 def clean_file(self):
228 228 return self._clean_files(self.cleaned_data['file'])
229 229
230 230 def clean(self):
231 231 cleaned_data = super(PostForm, self).clean()
232 232
233 233 if cleaned_data['email']:
234 234 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
235 235 self.need_to_ban = True
236 236 raise forms.ValidationError('A human cannot enter a hidden field')
237 237
238 238 if not self.errors:
239 239 self._clean_text_file()
240 240
241 241 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
242 242 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
243 243
244 244 settings_manager = get_settings_manager(self)
245 245 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
246 246 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
247 247 if pow_difficulty > 0:
248 248 # PoW-based
249 249 if cleaned_data['timestamp'] \
250 250 and cleaned_data['iteration'] and cleaned_data['guess'] \
251 251 and not settings_manager.get_setting('confirmed_user'):
252 252 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
253 253 else:
254 254 # Time-based
255 255 self._validate_posting_speed()
256 256 settings_manager.set_setting('confirmed_user', True)
257 257 if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE:
258 258 self._check_file_duplicates(self.get_files())
259 259
260 260 return cleaned_data
261 261
262 262 def get_files(self):
263 263 """
264 264 Gets file from form or URL.
265 265 """
266 266
267 267 files = []
268 268 for file in self.cleaned_data['file']:
269 269 if isinstance(file, UploadedFile):
270 270 files.append(file)
271 271
272 272 return files
273 273
274 274 def get_file_urls(self):
275 275 files = []
276 276 for file in self.cleaned_data['file']:
277 277 if type(file) == str:
278 278 files.append(file)
279 279
280 280 return files
281 281
282 282 def get_tripcode(self):
283 283 title = self.cleaned_data['title']
284 284 if title is not None and TRIPCODE_DELIM in title:
285 285 tripcode = get_tripcode_from_text(title.split(TRIPCODE_DELIM, maxsplit=1)[1])
286 286 else:
287 287 tripcode = ''
288 288 return tripcode
289 289
290 290 def get_title(self):
291 291 title = self.cleaned_data['title']
292 292 if title is not None and TRIPCODE_DELIM in title:
293 293 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
294 294 else:
295 295 return title
296 296
297 297 def get_images(self):
298 298 return self.cleaned_data.get('stickers', [])
299 299
300 300 def is_subscribe(self):
301 301 return self.cleaned_data['subscribe']
302 302
303 303 def _update_file_extension(self, file):
304 304 if file:
305 305 mimetype = get_file_mimetype(file)
306 306 extension = MIMETYPE_EXTENSIONS.get(mimetype)
307 307 if extension:
308 308 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
309 309 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
310 310
311 311 file.name = new_filename
312 312 else:
313 313 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
314 314
315 315 def _clean_files(self, inputs):
316 316 files = []
317 317
318 318 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
319 319 if len(inputs) > max_file_count:
320 320 raise forms.ValidationError(
321 321 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
322 322 max_file_count) % {'files': max_file_count})
323 323
324 324 size = 0
325 325 for file_input in inputs:
326 326 if isinstance(file_input, UploadedFile):
327 327 file = self._clean_file_file(file_input)
328 328 size += file.size
329 329 files.append(file)
330 330 else:
331 331 files.append(self._clean_file_url(file_input))
332 332
333 333 for file in files:
334 334 self._validate_image_dimensions(file)
335 335 validate_file_size(size)
336 336
337 337 return files
338 338
339 339 def _validate_image_dimensions(self, file):
340 340 if isinstance(file, UploadedFile):
341 341 mimetype = get_file_mimetype(file)
342 342 if mimetype.split('/')[-1] in FILE_TYPES_IMAGE:
343 343 Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
344 344 try:
345 345 print(get_image_dimensions(file))
346 346 except Exception:
347 347 raise forms.ValidationError('Possible decompression bomb or large image.')
348 348
349 349 def _clean_file_file(self, file):
350 350 self._update_file_extension(file)
351 351
352 352 return file
353 353
354 354 def _clean_file_url(self, url):
355 355 file = None
356 356
357 357 if url:
358 358 mode = self.cleaned_data['download_mode']
359 359 if mode == DOWN_MODE_URL:
360 360 return url
361 361
362 362 try:
363 363 image = get_attachment_by_alias(url, self.session)
364 364 if image is not None:
365 365 if 'stickers' not in self.cleaned_data:
366 366 self.cleaned_data['stickers'] = []
367 367 self.cleaned_data['stickers'].append(image)
368 368 return
369 369
370 370 if file is None:
371 371 file = self._get_file_from_url(url)
372 372 if not file:
373 373 raise forms.ValidationError(_('Invalid URL'))
374 374 self._update_file_extension(file)
375 375 except forms.ValidationError as e:
376 376 # Assume we will get the plain URL instead of a file and save it
377 377 if mode == DOWN_MODE_TRY and (REGEX_URL.match(url) or REGEX_MAGNET.match(url)):
378 378 logger.info('Error in forms: {}'.format(e))
379 379 return url
380 380 else:
381 381 raise e
382 382
383 383 return file
384 384
385 385 def _clean_text_file(self):
386 386 text = self.cleaned_data.get('text')
387 387 file = self.get_files()
388 388 file_url = self.get_file_urls()
389 389 images = self.get_images()
390 390
391 391 if (not text) and (not file) and (not file_url) and len(images) == 0:
392 392 error_message = _('Either text or file must be entered.')
393 393 self._add_general_error(error_message)
394 394
395 395 def _get_cache_key(self, key):
396 396 return '{}_{}'.format(self.session.session_key, key)
397 397
398 398 def _set_session_cache(self, key, value):
399 399 cache.set(self._get_cache_key(key), value)
400 400
401 401 def _get_session_cache(self, key):
402 402 return cache.get(self._get_cache_key(key))
403 403
404 404 def _get_last_post_time(self):
405 405 last = self._get_session_cache(LAST_POST_TIME)
406 406 if last is None:
407 407 last = self.session.get(LAST_POST_TIME)
408 408 return last
409 409
410 410 def _validate_posting_speed(self):
411 411 can_post = True
412 412
413 413 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
414 414
415 415 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
416 416 now = time.time()
417 417
418 418 current_delay = 0
419 419
420 420 if LAST_POST_TIME not in self.session:
421 421 self.session[LAST_POST_TIME] = now
422 422
423 423 need_delay = True
424 424 else:
425 425 last_post_time = self._get_last_post_time()
426 426 current_delay = int(now - last_post_time)
427 427
428 428 need_delay = current_delay < posting_delay
429 429
430 430 self._set_session_cache(LAST_POST_TIME, now)
431 431
432 432 if need_delay:
433 433 delay = posting_delay - current_delay
434 434 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
435 435 delay) % {'delay': delay}
436 436 self._add_general_error(error_message)
437 437
438 438 can_post = False
439 439
440 440 if can_post:
441 441 self.session[LAST_POST_TIME] = now
442 442 else:
443 443 # Reset the time since posting failed
444 444 self._set_session_cache(LAST_POST_TIME, self.session[LAST_POST_TIME])
445 445
446 446 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
447 447 """
448 448 Gets an file file from URL.
449 449 """
450 450
451 451 try:
452 452 return download(url)
453 453 except forms.ValidationError as e:
454 454 raise e
455 455 except Exception as e:
456 456 raise forms.ValidationError(e)
457 457
458 458 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
459 459 payload = timestamp + message.replace('\r\n', '\n')
460 460 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
461 461 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
462 462 if len(target) < POW_HASH_LENGTH:
463 463 target = '0' * (POW_HASH_LENGTH - len(target)) + target
464 464
465 465 computed_guess = hashlib.sha256((payload + iteration).encode())\
466 466 .hexdigest()[0:POW_HASH_LENGTH]
467 467 if guess != computed_guess or guess > target:
468 468 self._add_general_error(_('Invalid PoW.'))
469 469
470 470 def _check_file_duplicates(self, files):
471 471 for file in files:
472 472 file_hash = utils.get_file_hash(file)
473 473 if Attachment.objects.get_existing_duplicate(file_hash, file):
474 474 self._add_general_error(_(ERROR_DUPLICATES))
475 475
476 476 def _add_general_error(self, message):
477 477 self.add_error('text', forms.ValidationError(message))
478 478
479 479
480 480 class ThreadForm(PostForm):
481 481
482 482 tags = forms.CharField(
483 483 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
484 484 max_length=100, label=_('Tags'), required=True)
485 485 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
486 486 stickerpack = forms.BooleanField(label=_('Sticker Pack'), required=False)
487 487
488 488 def clean_tags(self):
489 489 tags = self.cleaned_data['tags'].strip()
490 490
491 491 if not tags or not REGEX_TAGS.match(tags):
492 492 raise forms.ValidationError(
493 493 _('Inappropriate characters in tags.'))
494 494
495 495 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
496 496 .strip().lower()
497 497
498 498 required_tag_exists = False
499 499 tag_set = set()
500 500 for tag_string in tags.split():
501 501 tag_name = tag_string.strip().lower()
502 502 if tag_name == default_tag_name:
503 503 required_tag_exists = True
504 504 tag, created = Tag.objects.get_or_create_with_alias(
505 505 name=tag_name, required=True)
506 506 else:
507 507 tag, created = Tag.objects.get_or_create_with_alias(name=tag_name)
508 508 tag_set.add(tag)
509 509
510 510 # If this is a new tag, don't check for its parents because nobody
511 511 # added them yet
512 512 if not created:
513 513 tag_set |= set(tag.get_all_parents())
514 514
515 515 for tag in tag_set:
516 516 if tag.required:
517 517 required_tag_exists = True
518 518 break
519 519
520 520 # Use default tag if no section exists
521 521 if not required_tag_exists:
522 522 default_tag, created = Tag.objects.get_or_create_with_alias(
523 523 name=default_tag_name, required=True)
524 524 tag_set.add(default_tag)
525 525
526 526 return tag_set
527 527
528 528 def clean(self):
529 529 cleaned_data = super(ThreadForm, self).clean()
530 530
531 531 return cleaned_data
532 532
533 533 def is_monochrome(self):
534 534 return self.cleaned_data['monochrome']
535 535
536 536 def clean_stickerpack(self):
537 537 stickerpack = self.cleaned_data['stickerpack']
538 538 if stickerpack:
539 539 tripcode = self.get_tripcode()
540 540 if not tripcode:
541 541 raise forms.ValidationError(_(
542 542 'Tripcode should be specified to own a stickerpack.'))
543 543 title = self.get_title()
544 544 if not title:
545 545 raise forms.ValidationError(_(
546 546 'Title should be specified as a stickerpack name.'))
547 547 if not REGEX_TAGS.match(title):
548 548 raise forms.ValidationError(_('Inappropriate sticker pack name.'))
549 549
550 550 existing_pack = StickerPack.objects.filter(name=title).first()
551 551 if existing_pack:
552 552 if existing_pack.tripcode != tripcode:
553 553 raise forms.ValidationError(_(
554 554 'A sticker pack with this name already exists and is'
555 555 ' owned by another tripcode.'))
556 556 if not existing_pack.tripcode:
557 557 raise forms.ValidationError(_(
558 558 'This sticker pack can only be updated by an '
559 559 'administrator.'))
560 560
561 561 return stickerpack
562 562
563 563 def is_stickerpack(self):
564 564 return self.cleaned_data['stickerpack']
565 565
566 566
567 567 class SettingsForm(NeboardForm):
568 568
569 569 theme = forms.ChoiceField(
570 570 choices=board_settings.get_list_dict('View', 'Themes'),
571 571 label=_('Theme'))
572 572 image_viewer = forms.ChoiceField(
573 573 choices=board_settings.get_list_dict('View', 'ImageViewers'),
574 574 label=_('Image view mode'))
575 575 username = forms.CharField(label=_('User name'), required=False)
576 576 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
577 577
578 578 def clean_username(self):
579 579 username = self.cleaned_data['username']
580 580
581 581 if username and not REGEX_USERNAMES.match(username):
582 582 raise forms.ValidationError(_('Inappropriate characters.'))
583 583
584 584 return username
585 585
586 586
587 587 class SearchForm(NeboardForm):
588 588 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
@@ -1,41 +1,42 b''
1 1 import configparser
2 2
3 3
4 4 CONFIG_DEFAULT_SETTINGS = 'boards/config/default_settings.ini'
5 5 CONFIG_SETTINGS = 'boards/config/settings.ini'
6 6
7 7
8 8 SECTION_FORMS = 'Forms'
9 SECTION_VIEW = 'View'
9 10
10 11 VALUE_TRUE = 'true'
11 12
12 13 LIST_DELIMITER = ','
13 14 DICT_DELIMITER = ':'
14 15
15 16
16 17 config = configparser.ConfigParser()
17 18 config.read(CONFIG_DEFAULT_SETTINGS)
18 19 config.read(CONFIG_SETTINGS)
19 20
20 21
21 22 def get(section, name):
22 23 return config[section][name]
23 24
24 25
25 26 def get_int(section, name):
26 27 return int(get(section, name))
27 28
28 29
29 30 def get_bool(section, name):
30 31 return get(section, name) == VALUE_TRUE
31 32
32 33
33 34 def get_list_dict(section, name):
34 35 str_dict = get(section, name)
35 36 return [item.split(DICT_DELIMITER) for item in str_dict.split(LIST_DELIMITER)]
36 37
37 38
38 39 def get_list(section, name):
39 40 str_list = get(section, name)
40 41 return str_list.split(LIST_DELIMITER)
41 42
@@ -1,161 +1,161 b''
1 1 """
2 2 This module contains helper functions and helper classes.
3 3 """
4 4 import time
5 5 import uuid
6 6
7 7 import hashlib
8 8 import magic
9 9 import os
10 10 from django import forms
11 11 from django.core.cache import cache
12 12 from django.db.models import Model
13 13 from django.template.defaultfilters import filesizeformat
14 14 from django.utils import timezone
15 15 from django.utils.translation import ugettext_lazy as _
16 16
17 17 import boards
18 18 from neboard import settings
19 19 from boards.abstracts.constants import FILE_DIRECTORY
20 from boards.settings import get_bool
20 from boards.settings import get_bool, SECTION_FORMS
21 21
22 22 CACHE_KEY_DELIMITER = '_'
23 23
24 24 HTTP_FORWARDED = 'HTTP_X_FORWARDED_FOR'
25 25 META_REMOTE_ADDR = 'REMOTE_ADDR'
26 26
27 27 SETTING_MESSAGES = 'Messages'
28 28 SETTING_ANON_MODE = 'AnonymousMode'
29 29
30 30 ANON_IP = '127.0.0.1'
31 31
32 32 FILE_EXTENSION_DELIMITER = '.'
33 33 URL_DELIMITER = '/'
34 34 CACHE_KEY_DELIMITER = ':'
35 35
36 36 DEFAULT_MIMETYPE = 'application/octet-stream'
37 37
38 38
39 39 def is_anonymous_mode():
40 40 return get_bool(SETTING_MESSAGES, SETTING_ANON_MODE)
41 41
42 42
43 43 def get_client_ip(request):
44 44 if is_anonymous_mode():
45 45 ip = ANON_IP
46 46 else:
47 47 x_forwarded_for = request.META.get(HTTP_FORWARDED)
48 48 if x_forwarded_for:
49 49 ip = x_forwarded_for.split(',')[-1].strip()
50 50 else:
51 51 ip = request.META.get(META_REMOTE_ADDR)
52 52 return ip
53 53
54 54
55 55 # TODO The output format is not epoch because it includes microseconds
56 56 def datetime_to_epoch(datetime):
57 57 return int(time.mktime(timezone.localtime(
58 58 datetime,timezone.get_current_timezone()).timetuple())
59 59 * 1000000 + datetime.microsecond)
60 60
61 61
62 62 # TODO Test this carefully
63 63 def cached_result(key_method=None):
64 64 """
65 65 Caches method result in the Django's cache system, persisted by object name,
66 66 object name, model id if object is a Django model, args and kwargs if any.
67 67 """
68 68 def _cached_result(function):
69 69 def inner_func(obj, *args, **kwargs):
70 70 cache_key_params = [obj.__class__.__name__, function.__name__]
71 71
72 72 cache_key_params += args
73 73 for key, value in kwargs:
74 74 cache_key_params.append(key + CACHE_KEY_DELIMITER + value)
75 75
76 76 if isinstance(obj, Model):
77 77 cache_key_params.append(str(obj.id))
78 78
79 79 if key_method is not None:
80 80 cache_key_params += [str(arg) for arg in key_method(obj)]
81 81
82 82 cache_key = CACHE_KEY_DELIMITER.join(cache_key_params)
83 83
84 84 persisted_result = cache.get(cache_key)
85 85 if persisted_result is not None:
86 86 result = persisted_result
87 87 else:
88 88 result = function(obj, *args, **kwargs)
89 89 if result is not None:
90 90 cache.set(cache_key, result)
91 91
92 92 return result
93 93
94 94 return inner_func
95 95 return _cached_result
96 96
97 97
98 98 def get_file_hash(file) -> str:
99 99 md5 = hashlib.md5()
100 100 for chunk in file.chunks():
101 101 md5.update(chunk)
102 102 return md5.hexdigest()
103 103
104 104
105 105 def validate_file_size(size: int):
106 max_size = boards.settings.get_int('Forms', 'MaxFileSize')
106 max_size = boards.settings.get_int(SECTION_FORMS, 'MaxFileSize')
107 107 if 0 < max_size < size:
108 108 raise forms.ValidationError(
109 109 _('Total file size must be less than %s but is %s.')
110 110 % (filesizeformat(max_size), filesizeformat(size)))
111 111
112 112
113 113 def get_extension(filename):
114 114 return filename.split(FILE_EXTENSION_DELIMITER)[-1:][0]
115 115
116 116
117 117 def get_upload_filename(model_instance, old_filename):
118 118 extension = get_extension(old_filename)
119 119 new_name = '{}.{}'.format(uuid.uuid4(), extension)
120 120
121 121 # Create 2 directories to split the files because holding many files in
122 122 # one directory may impact performance
123 123 dir1 = new_name[0]
124 124 dir2 = new_name[1]
125 125
126 126 return os.path.join(FILE_DIRECTORY, dir1, dir2, new_name)
127 127
128 128
129 129 def get_file_mimetype(file) -> str:
130 130 buf = b''
131 131 for chunk in file.chunks():
132 132 buf += chunk
133 133
134 134 file_type = magic.from_buffer(buf, mime=True)
135 135 if file_type is None:
136 136 file_type = DEFAULT_MIMETYPE
137 137 elif type(file_type) == bytes:
138 138 file_type = file_type.decode()
139 139 return file_type
140 140
141 141
142 142 def get_domain(url: str) -> str:
143 143 """
144 144 Gets domain from an URL with random number of domain levels.
145 145 """
146 146 domain_parts = url.split(URL_DELIMITER)
147 147 if len(domain_parts) >= 2:
148 148 full_domain = domain_parts[2]
149 149 else:
150 150 full_domain = ''
151 151
152 152 return full_domain
153 153
154 154
155 155 def get_tripcode_from_text(text: str) -> str:
156 156 tripcode = ''
157 157 if text:
158 158 code = text + settings.SECRET_KEY
159 159 tripcode = hashlib.md5(code.encode()).hexdigest()
160 160 return tripcode
161 161
@@ -1,132 +1,133 b''
1 1 from django.core.paginator import EmptyPage
2 2 from django.http import Http404
3 3 from django.shortcuts import render, redirect
4 4 from django.urls import reverse
5 5 from django.utils.decorators import method_decorator
6 6 from django.views.decorators.csrf import csrf_protect
7 7
8 8 from boards import settings
9 9 from boards.abstracts.paginator import get_paginator
10 10 from boards.abstracts.settingsmanager import get_settings_manager, \
11 11 SETTING_ONLY_FAVORITES
12 12 from boards.forms import ThreadForm, PlainErrorList
13 13 from boards.models import Post, Thread
14 14 from boards.views.base import BaseBoardView, CONTEXT_FORM
15 15 from boards.views.mixins import FileUploadMixin, PaginatedMixin, \
16 16 DispatcherMixin, PARAMETER_METHOD
17 from boards.settings import SECTION_VIEW, SECTION_FORMS
17 18
18 19 FORM_TAGS = 'tags'
19 20 FORM_TEXT = 'text'
20 21 FORM_TITLE = 'title'
21 22 FORM_IMAGE = 'image'
22 23 FORM_THREADS = 'threads'
23 24
24 25 TAG_DELIMITER = ' '
25 26
26 27 PARAMETER_CURRENT_PAGE = 'current_page'
27 28 PARAMETER_PAGINATOR = 'paginator'
28 29 PARAMETER_THREADS = 'threads'
29 30 PARAMETER_ADDITIONAL = 'additional_params'
30 31 PARAMETER_MAX_FILE_SIZE = 'max_file_size'
31 32 PARAMETER_RSS_URL = 'rss_url'
32 33 PARAMETER_MAX_FILES = 'max_files'
33 34
34 35 TEMPLATE = 'boards/all_threads.html'
35 36 DEFAULT_PAGE = 1
36 37
37 38
38 39 class AllThreadsView(FileUploadMixin, BaseBoardView, PaginatedMixin,
39 40 DispatcherMixin):
40 41
41 42 tag_name = ''
42 43
43 44 def __init__(self):
44 45 self.settings_manager = None
45 46 super(AllThreadsView, self).__init__()
46 47
47 48 @method_decorator(csrf_protect)
48 49 def get(self, request, form: ThreadForm=None):
49 50 page = request.GET.get('page', DEFAULT_PAGE)
50 51
51 52 params = self.get_context_data(request=request)
52 53
53 54 if not form:
54 55 form = ThreadForm(error_class=PlainErrorList,
55 56 initial={FORM_TAGS: self.tag_name})
56 57
57 58 self.settings_manager = get_settings_manager(request)
58 59
59 60 threads = self.get_threads()
60 61
61 62 order = request.GET.get('order', 'bump')
62 63 if order == 'bump':
63 64 threads = threads.order_by('-bump_time')
64 65 else:
65 66 threads = threads.filter(replies__opening=True)\
66 67 .order_by('-replies__pub_time')
67 68 filter = request.GET.get('filter')
68 69 threads = threads.distinct()
69 70
70 71 paginator = get_paginator(threads,
71 settings.get_int('View', 'ThreadsPerPage'))
72 settings.get_int(SECTION_VIEW, 'ThreadsPerPage'))
72 73 paginator.current_page = int(page)
73 74
74 75 try:
75 76 threads = paginator.page(page).object_list
76 77 except EmptyPage:
77 78 raise Http404()
78 79
79 80 params[PARAMETER_THREADS] = threads
80 81 params[CONTEXT_FORM] = form
81 82 params[PARAMETER_MAX_FILE_SIZE] = self.get_max_upload_size()
82 83 params[PARAMETER_RSS_URL] = self.get_rss_url()
83 params[PARAMETER_MAX_FILES] = settings.get_int('Forms', 'MaxFileCount')
84 params[PARAMETER_MAX_FILES] = settings.get_int(SECTION_FORMS, 'MaxFileCount')
84 85
85 86 paginator.set_url(self.get_reverse_url(), request.GET.dict())
86 87 params.update(self.get_page_context(paginator, page))
87 88
88 89 return render(request, TEMPLATE, params)
89 90
90 91 @method_decorator(csrf_protect)
91 92 def post(self, request):
92 93 if PARAMETER_METHOD in request.POST:
93 94 self.dispatch_method(request)
94 95
95 96 return redirect('index') # FIXME Different for different modes
96 97
97 98 form = ThreadForm(request.POST, request.FILES,
98 99 error_class=PlainErrorList)
99 100 form.session = request.session
100 101
101 102 if form.is_valid():
102 103 return Post.objects.create_from_form(request, form, None)
103 104 if form.need_to_ban:
104 105 # Ban user because he is suspected to be a bot
105 106 self._ban_current_user(request)
106 107
107 108 return self.get(request, form)
108 109
109 110 def get_reverse_url(self):
110 111 return reverse('index')
111 112
112 113 def get_threads(self):
113 114 """
114 115 Gets list of threads that will be shown on a page.
115 116 """
116 117
117 118 threads = Thread.objects\
118 119 .exclude(tags__in=self.settings_manager.get_hidden_tags())
119 120 if self.settings_manager.get_setting(SETTING_ONLY_FAVORITES):
120 121 fav_tags = self.settings_manager.get_fav_tags()
121 122 if len(fav_tags) > 0:
122 123 threads = threads.filter(tags__in=fav_tags)
123 124
124 125 return threads
125 126
126 127 def get_rss_url(self):
127 128 return self.get_reverse_url() + 'rss/'
128 129
129 130 def toggle_fav(self, request):
130 131 settings_manager = get_settings_manager(request)
131 132 settings_manager.set_setting(SETTING_ONLY_FAVORITES,
132 133 not settings_manager.get_setting(SETTING_ONLY_FAVORITES, False))
General Comments 0
You need to be logged in to leave comments. Login now