Merge lp:~widelands-dev/widelands-website/pybb_attachments into lp:widelands-website
- pybb_attachments
- Merge into trunk
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 549 | ||||||||
Proposed branch: | lp:~widelands-dev/widelands-website/pybb_attachments | ||||||||
Merge into: | lp:widelands-website | ||||||||
Diff against target: |
947 lines (+425/-111) 16 files modified
mainpage/settings.py (+46/-5) mainpage/validators.py (+30/-0) pip_requirements.txt (+1/-0) pybb/admin.py (+9/-2) pybb/forms.py (+20/-13) pybb/models.py (+14/-11) pybb/settings.py (+1/-0) pybb/static/css/forum.css (+4/-1) pybb/templates/pybb/base.html (+1/-1) pybb/templates/pybb/delete_post.html (+7/-2) pybb/templates/pybb/inlines/attachment.html (+11/-0) pybb/templates/pybb/inlines/post.html (+5/-9) pybb/templates/pybb/post_form.html (+39/-1) pybb/util.py (+149/-1) pybb/views.py (+46/-27) wlmaps/forms.py (+42/-38) |
||||||||
To merge this branch: | bzr merge lp:~widelands-dev/widelands-website/pybb_attachments | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
GunChleoc | Approve | ||
kaputtnik (community) | Needs Resubmitting | ||
Review via email:
|
Commit message
Allow attachments in the forum
Description of the change
Made attachments python3 compatible: https:/
Set allowed file size to 5 Mb (as we have for nginx)
Delete attachment when deleting a post: https:/
Added template for showing attachments.
Style tweaks, example: https:/
Autoreload css for the users if this got merged, so the users don't have to hit CTRL+F5
I'll do some explanations in the wiki and announce this change. Probably with the option to remove it again if we get too many problematic attachments.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
GunChleoc (gunchleoc) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
Currently there is no restriction on file types. The problem is that a file may not contain what the extension says it should be. Everyone can rename 'image.js' into 'image.png'...
A list of allowed extension do not suffer. I'll try to implement some checks for validating a files type and probably an 'allowed extension list'.
What about restricting uploads to users who have written x posts prior? From my side this can be implemented.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
GunChleoc (gunchleoc) wrote : | # |
> Everyone can rename 'image.js' into 'image.png'...
That's true. It would still be an advantage though if the browser then doesn't execute it.
Doing a full validation too is definitely a better idea.
> What about restricting uploads to users who have written x posts prior?
Also a good idea
- 553. By kaputtnik
-
make attachments only available to users who had written MAX_HIDDEN_POST
- 554. By kaputtnik
-
added first file validation logic; render the post_form manually; give textarea more height
- 555. By kaputtnik
-
some reafctoring
- 556. By kaputtnik
-
added check for .wgf (widelands savegames); added more comments
- 557. By kaputtnik
-
merged trunk
- 558. By kaputtnik
-
use Image verify()
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
I am a bit lost here...
Some checks are made now, but i am unsure if we need them all. What is implemented:
1. Link of Attachment:
If a user clicks on a link of an attachment, he will be ever prompted for a download (or to open the file with an associated program), even if it is a file which the browser may can display, e.g. an image (png, jpg) or text file. This is not optimal, but i think it is more secure.
2. Restricting upload permission
A user has only permission to upload files, if he has had written 5 posts prior. The number of 5 derives from MAX_HIDDEN_POST at the moment, but i think it's better to have an own value for this. What number do we want?
3. Allowed extensions:
Added a list:
ALLOWED_EXTENSIONS = ['wgf', 'jpg', 'jpeg', 'gif', 'png', 'ogg', 'lua', 'ods', 'zip', 'json', 'txt', 'csv']
I am unsure if this list will be ever complete. E.g. some may want to upload some other types of files (e.g. py, odg, doc, ...)
4. Extension handling:
- Files without extension aren't allowed (can be problematic e.g. with the campvis file)
- For 'wgf' files: Check if some base entries can be found (e.g. '/binary/' or '/minimap.png')
- For image files: Check if this is really an image using the Pillow library. This finds corrupted .png but not corrupted .jpg
Do we need a check for each allowed extension?
5. Comparing Mime-Types:
When uploading a file, a Mime-Type (content-type) is also submitted. The submitted mime-type is mostly derived from the extension of the file. E.g. if you rename a file from 'image.png' to 'image.txt' the OS (or browser) sends the Mime-type 'text/plain', which is obviously wrong. To prevent such mime-type mismatch, a check is made which compares the sent mime-type with the mime-type derived from python-magic. e.g. python magic will show for 'image.txt' the mime-type 'image/png'.
As said, i am a bit lost, not really knowing if we need all this checks, or if they are enough at all :-S Any opinions?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
GunChleoc (gunchleoc) wrote : | # |
2. I have no opinion on this at this time. Maybe 10 or 20? We can easily change the value if it becomes a problem.
> Files without extension aren't allowed (can be problematic e.g. with the campvis file)
campvis will be replaced by campaigns.conf for the next version, so don't worry about it - we can allow .conf
Other extensions that we're using for Widelands & that might be interesting:
.wai - AI
.wmf - Map
.wrpl - Replay - useless with the accompanying .wgf though.
> Do we need a check for each allowed extension?
Since attachments are marked for download anyway, I'd say that checking for those that are easily checked will do for now.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
> Other extensions that we're using for Widelands & that might be interesting:
> .wai - AI
Ok
> .wmf - Map
I forgot to mention: If the extension is wmf, an errormessage is shown: "This seems to be a widelands map file. Please upload it at our maps section."
> .wrpl - Replay - useless with the accompanying .wgf though.
I am planning to make a wikipage describing, beside other things, the usage of replays, e.g. they have to be zipped together before uploading and link that wikipage in the upload form. But i have to check for .wrpl.wgf.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
I have to stop working on this, other wise i fear to change many other things :-D
Anyway i think this works. wai files need some special treatment though.
Please test on alpha: https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
stonerl, can you install and configure clamav on the server, please?
The current branch implemented a check with clamdscan, which needs a clamav daemon. We can also stick with clamscan, which can be run on demand, but this much slow: A file check last 55 sec. on my machine, whereas clamdscan usually needs only 0,1 sec.
The current branch is not running on alpha, yet. Since i am afk for some some days, i'll shut down alpha now.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Toni Förster (stonerl) wrote : | # |
Already installed. ;)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
GunChleoc (gunchleoc) wrote : | # |
1 tiny nit.
I also wonder whether we should disallow map files - it might come in handy if somebody wants some feedback before publishing to the maps section.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
> I also wonder whether we should disallow map files - it might come in handy if somebody wants some feedback before publishing to the maps section.
I just don't want to have map files in the forum. I 'fear' most (new) users will upload a map in the forum instead in the 'right' place. A Feedback can also be done in the map comments, or by linking a map in the forum and ask there for a feedback. Once there is a fix for bug 357914 'Allow submitters of maps to upload new versions' get implement, it should be fine to not allow maps in forum.
But of course we can remove that part of code and if we get in trouble we can add it again.
- 564. By kaputtnik
-
MiB instead of Mb
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
@Toni: Can we have the package 'clamdscan' installed? For some reason it is a recommended package for clamd and we do not install recommended packages by default.
Or is clamd configured to have on-access-scanning enabled? Didn't saw that it is configured like that.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Toni Förster (stonerl) wrote : | # |
How do you intend to access clamav? The current mode is via socket:
LocalSocket /var/run/
If you'd like, I can add a local TCP daemon on port 3310.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Toni Förster (stonerl) wrote : | # |
Stupid me. :-) I installed clamdscan.
- 565. By kaputtnik
-
added attachment to admin site of a post
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
Thanks :-)
But the check for clamdscan doesn't work on the server... i know why, but i don't know the underlying cause. Will talk on IRC about that issue when i have some more time.
Just found that map uploading is broken with the changes in this branch. So the code for map uploading needs adapting too...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
Finally i got it (hopefully)
Please check again on alpha... https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
GunChleoc (gunchleoc) wrote : | # |
I have tested map upload and attaching to forum.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
Manx thanks :)
I wait only for janus to approve the changes regarding python3. He will look at it next weekend.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
kaputtnik (franku) wrote : | # |
Finally this is productive now :-)
Preview Diff
1 | === modified file 'mainpage/settings.py' |
2 | --- mainpage/settings.py 2019-04-11 16:11:21 +0000 |
3 | +++ mainpage/settings.py 2019-09-02 20:01:34 +0000 |
4 | @@ -1,4 +1,5 @@ |
5 | -# Django settings for widelands project. |
6 | +# Django default settings for widelands project. |
7 | +# Overwrite these settings in local_settings.py! |
8 | |
9 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) |
10 | import os |
11 | @@ -162,7 +163,7 @@ |
12 | DEFAULT_FROM_EMAIL = 'noreply@widelands.org' |
13 | ACCOUNT_ACTIVATION_DAYS = 2 # Days an activation token keeps active |
14 | |
15 | -# Franku: SHA1 Needed as compatibility for old passwords |
16 | +# SHA1 Needed as compatibility for old passwords |
17 | # https://docs.djangoproject.com/en/1.11/releases/1.10/#removed-weak-password-hashers-from-the-default-password-hashers-setting |
18 | PASSWORD_HASHERS = [ |
19 | 'django.contrib.auth.hashers.PBKDF2PasswordHasher', |
20 | @@ -190,10 +191,50 @@ |
21 | ###################### |
22 | # Pybb Configuration # |
23 | ###################### |
24 | -# See also pybb defaults in pybb.settings.py |
25 | -PYBB_ATTACHMENT_ENABLE = False # disable gzip middleware when enabling attachments |
26 | + |
27 | +PYBB_ATTACHMENT_ENABLE = True |
28 | + |
29 | +# To prevent sending errors from the webserver, keep |
30 | +# this below the webserver settings |
31 | +PYBB_ATTACHMENT_SIZE_LIMIT = 1024*1024*4 |
32 | PYBB_DEFAULT_MARKUP = 'markdown' |
33 | -INTERNAL_PERM='pybb.can_access_internal' # The permission string derived from pybb.models.category |
34 | +INTERNAL_PERM = 'pybb.can_access_internal' # The permission string derived from pybb.models.category |
35 | + |
36 | +################################## |
37 | +# Uploading files and validation # |
38 | +################################## |
39 | + |
40 | +# Use only this handler to get real a file in /tmp |
41 | +# Some validation checks needs a real file |
42 | +FILE_UPLOAD_HANDLERS = [ |
43 | + 'django.core.files.uploadhandler.TemporaryFileUploadHandler', |
44 | + ] |
45 | + |
46 | +ALLOWED_EXTENSIONS = [ |
47 | + 'wgf', 'jpg', 'jpeg', 'gif', 'png', 'ogg', 'lua', |
48 | + 'ods', 'zip', 'json', 'txt', 'csv', 'wai', |
49 | + ] |
50 | + |
51 | +# Widelands Savegame should contain at least these entries |
52 | +WGF_CONTENT_CHECK = ['/binary/', '/map/', '/minimap.png', '/preload',] |
53 | + |
54 | +# Do not check mime type for these extensions |
55 | +SKIP_MIME_EXTENSIONS = ['wai',] |
56 | + |
57 | +ALLOWED_WAI_SECTIONS = [ |
58 | + 'magic_numbers', 'neuron_values', |
59 | + 'neuron_functions', 'fneurons', |
60 | + ] |
61 | + |
62 | +# Allow attachments only after this amount of posts |
63 | +ALLOW_ATTACHMENTS_AFTER = 5 |
64 | + |
65 | +# Page describing uploads |
66 | +ATTACHMENT_DESCR_PAGE = 'Attachments' |
67 | + |
68 | +# If clamav including clamdscan is installed and running |
69 | +# set this to True |
70 | +VIRUS_CHECK = False |
71 | |
72 | ############################################## |
73 | # Link classification and other Markup stuff # |
74 | |
75 | === added file 'mainpage/validators.py' |
76 | --- mainpage/validators.py 1970-01-01 00:00:00 +0000 |
77 | +++ mainpage/validators.py 2019-09-02 20:01:34 +0000 |
78 | @@ -0,0 +1,30 @@ |
79 | +from django.core.exceptions import ValidationError |
80 | +from django.conf import settings |
81 | + |
82 | +import shutil |
83 | +import subprocess |
84 | + |
85 | + |
86 | +def virus_scan(uploaded_file): |
87 | + '''Scan uploaded_file for viruses with clamav.''' |
88 | + |
89 | + if settings.VIRUS_CHECK: |
90 | + try: |
91 | + tmp_file_path = uploaded_file.temporary_file_path() |
92 | + process_compl = subprocess.run(['/usr/bin/clamdscan', |
93 | + '--multiscan', |
94 | + '--fdpass', |
95 | + tmp_file_path]) |
96 | + if process_compl.returncode == 1: |
97 | + raise ValidationError( |
98 | + 'This file seems to contain malicious code.' |
99 | + ) |
100 | + if process_compl.returncode == 2: |
101 | + raise ValidationError( |
102 | + 'Some error occured during virus scannning...' |
103 | + ) |
104 | + except FileNotFoundError: |
105 | + raise ValidationError( |
106 | + 'Please check the installation of clamav and make \ |
107 | + sure clamdscan is working.' |
108 | + ) |
109 | |
110 | === modified file 'pip_requirements.txt' |
111 | --- pip_requirements.txt 2019-07-05 06:57:46 +0000 |
112 | +++ pip_requirements.txt 2019-09-02 20:01:34 +0000 |
113 | @@ -19,6 +19,7 @@ |
114 | numpy==1.16.4 |
115 | Pillow==6.1.0 |
116 | pydot==1.4.1 |
117 | +python-magic==0.4.15 |
118 | Sphinx==2.1.2 |
119 | Whoosh==2.7.4 |
120 | |
121 | |
122 | === modified file 'pybb/admin.py' |
123 | --- pybb/admin.py 2019-02-27 09:14:05 +0000 |
124 | +++ pybb/admin.py 2019-09-02 20:01:34 +0000 |
125 | @@ -1,7 +1,7 @@ |
126 | # -*- coding: utf-8 |
127 | from django.utils.translation import ugettext_lazy as _ |
128 | from django.contrib import admin |
129 | -from pybb.models import Category, Forum, Topic, Post, Read |
130 | +from pybb.models import Category, Forum, Topic, Post, Read, Attachment |
131 | |
132 | |
133 | def delete_selected(modeladmin, request, queryset): |
134 | @@ -75,6 +75,12 @@ |
135 | ) |
136 | |
137 | |
138 | +class AttachmentInline(admin.StackedInline): |
139 | + model = Attachment |
140 | + extra = 0 |
141 | + exclude = ('hash',) |
142 | + |
143 | + |
144 | class PostAdmin(admin.ModelAdmin): |
145 | list_display = ['summary', 'topic', 'user', 'created', 'hidden'] |
146 | list_per_page = 20 |
147 | @@ -82,12 +88,13 @@ |
148 | date_hierarchy = 'created' |
149 | search_fields = ['body', 'topic__name'] |
150 | actions = [delete_selected, unhide_post] |
151 | + inlines = [AttachmentInline,] |
152 | fieldsets = ( |
153 | (None, { |
154 | 'fields': ('topic', 'user', 'markup', 'hidden') |
155 | } |
156 | ), |
157 | - (_('Additional options'), { |
158 | + (_('Date and Time'), { |
159 | 'classes': ('collapse',), |
160 | 'fields': (('created', 'updated'),) |
161 | } |
162 | |
163 | === modified file 'pybb/forms.py' |
164 | --- pybb/forms.py 2019-03-20 21:32:32 +0000 |
165 | +++ pybb/forms.py 2019-09-02 20:01:34 +0000 |
166 | @@ -1,6 +1,6 @@ |
167 | import re |
168 | from datetime import datetime |
169 | -import os.path |
170 | +import os |
171 | |
172 | from django import forms |
173 | from django.conf import settings |
174 | @@ -10,16 +10,24 @@ |
175 | from pybb.models import Topic, Post, Attachment |
176 | from pybb import settings as pybb_settings |
177 | from django.conf import settings |
178 | +from .util import validate_file |
179 | +from mainpage.validators import virus_scan |
180 | |
181 | |
182 | class AddPostForm(forms.ModelForm): |
183 | name = forms.CharField(label=_('Subject')) |
184 | - attachment = forms.FileField(label=_('Attachment'), required=False) |
185 | + attachment = forms.FileField( |
186 | + label=_('Attachment'), |
187 | + required=False, |
188 | + validators=[virus_scan, validate_file, ]) |
189 | |
190 | class Meta: |
191 | model = Post |
192 | - # Listing fields again to get the the right order; See also the TODO |
193 | + # Listing fields again to get the the right order |
194 | fields = ['name', 'body', 'markup', 'attachment', ] |
195 | + widgets = { |
196 | + 'body': forms.Textarea(attrs={'cols': 80, 'rows': 15}), |
197 | + } |
198 | |
199 | def __init__(self, *args, **kwargs): |
200 | self.user = kwargs.pop('user', None) |
201 | @@ -31,17 +39,10 @@ |
202 | self.fields['name'].widget = forms.HiddenInput() |
203 | self.fields['name'].required = False |
204 | |
205 | - if not pybb_settings.ATTACHMENT_ENABLE: |
206 | + if not pybb_settings.ATTACHMENT_ENABLE or self.user.wlprofile.post_count() < settings.ALLOW_ATTACHMENTS_AFTER: |
207 | self.fields['attachment'].widget = forms.HiddenInput() |
208 | self.fields['attachment'].required = False |
209 | |
210 | - def clean_attachment(self): |
211 | - if self.cleaned_data['attachment']: |
212 | - memfile = self.cleaned_data['attachment'] |
213 | - if memfile.size > pybb_settings.ATTACHMENT_SIZE_LIMIT: |
214 | - raise forms.ValidationError(_('Attachment is too big')) |
215 | - return self.cleaned_data['attachment'] |
216 | - |
217 | def save(self, *args, **kwargs): |
218 | if self.forum: |
219 | topic_is_new = True |
220 | @@ -69,9 +70,15 @@ |
221 | name=memfile.name, post=post) |
222 | dir = os.path.join(settings.MEDIA_ROOT, |
223 | pybb_settings.ATTACHMENT_UPLOAD_TO) |
224 | - fname = '%d.0' % post.id |
225 | + if not os.path.exists(dir): |
226 | + os.makedirs(dir) |
227 | + |
228 | + fname = '{}.0'.format(post.id) |
229 | path = os.path.join(dir, fname) |
230 | - file(path, 'w').write(memfile.read()) |
231 | + |
232 | + with open(path, 'wb') as f: |
233 | + f.write(memfile.read()) |
234 | + |
235 | obj.path = fname |
236 | obj.save() |
237 | |
238 | |
239 | === modified file 'pybb/models.py' |
240 | --- pybb/models.py 2019-04-11 15:20:33 +0000 |
241 | +++ pybb/models.py 2019-09-02 20:01:34 +0000 |
242 | @@ -18,7 +18,7 @@ |
243 | from django.conf import settings |
244 | from notification.models import send |
245 | from check_input.models import SuspiciousInput |
246 | - |
247 | +import magic |
248 | |
249 | try: |
250 | from notification import models as notification |
251 | @@ -345,6 +345,11 @@ |
252 | def delete(self, *args, **kwargs): |
253 | self_id = self.id |
254 | head_post_id = self.topic.posts.order_by('created')[0].id |
255 | + |
256 | + if self.attachments.all(): |
257 | + for attach in self.attachments.all(): |
258 | + attach.delete() |
259 | + |
260 | super(Post, self).delete(*args, **kwargs) |
261 | |
262 | self.topic.save() |
263 | @@ -398,7 +403,7 @@ |
264 | super(Attachment, self).save(*args, **kwargs) |
265 | if not self.hash: |
266 | self.hash = hashlib.sha1( |
267 | - str(self.id) + settings.SECRET_KEY).hexdigest() |
268 | + bytes(self.id) + settings.SECRET_KEY.encode('utf-8')).hexdigest() |
269 | super(Attachment, self).save(*args, **kwargs) |
270 | |
271 | def __str__(self): |
272 | @@ -407,19 +412,17 @@ |
273 | def get_absolute_url(self): |
274 | return reverse('pybb_attachment', args=[self.hash]) |
275 | |
276 | - def size_display(self): |
277 | - size = self.size |
278 | - if size < 1024: |
279 | - return '%b' % size |
280 | - elif size < 1024 * 1024: |
281 | - return '%dKb' % int(size / 1024) |
282 | - else: |
283 | - return '%.2fMb' % (size / float(1024 * 1024)) |
284 | - |
285 | def get_absolute_path(self): |
286 | return os.path.join(settings.MEDIA_ROOT, pybb_settings.ATTACHMENT_UPLOAD_TO, |
287 | self.path) |
288 | |
289 | + def delete(self, *args, **kwargs): |
290 | + try: |
291 | + os.remove(self.get_absolute_path()) |
292 | + except FileNotFoundError: |
293 | + pass |
294 | + super(Attachment, self).delete(*args, **kwargs) |
295 | + |
296 | |
297 | if notification is not None: |
298 | signals.post_save.connect(notification.handle_observations, sender=Post) |
299 | |
300 | === modified file 'pybb/settings.py' |
301 | --- pybb/settings.py 2019-03-15 08:58:09 +0000 |
302 | +++ pybb/settings.py 2019-09-02 20:01:34 +0000 |
303 | @@ -16,6 +16,7 @@ |
304 | DEFAULT_MARKUP = get('PYBB_DEFAULT_MARKUP', 'bbcode') |
305 | ATTACHMENT_UPLOAD_TO = get('PYBB_ATTACHMENT_UPLOAD_TO', 'pybb/attachments') |
306 | ATTACHMENT_SIZE_LIMIT = get('PYBB_ATTACHMENT_SIZE_LIMIT', 1024 * 1024) |
307 | +PYBB_ATTACHMENT_ENABLE = get('PYBB_ATTACHMENT_ENABLE', False) |
308 | ATTACHMENT_ENABLE = get('PYBB_ATTACHMENT_ENABLE', True) |
309 | INTERNAL_PERM = get('INTERNAL_PERM', 'pybb.can_access_internal') |
310 | LAST_POSTS_DAYS = get('LAST_POSTS_DAYS', 30) |
311 | |
312 | === modified file 'pybb/static/css/forum.css' |
313 | --- pybb/static/css/forum.css 2019-02-28 16:09:16 +0000 |
314 | +++ pybb/static/css/forum.css 2019-09-02 20:01:34 +0000 |
315 | @@ -132,6 +132,10 @@ |
316 | border: 1px solid #474444; |
317 | } |
318 | |
319 | +.attachment { |
320 | + display: inline-block |
321 | +} |
322 | + |
323 | /*****************/ |
324 | /* Edit/add Post */ |
325 | /* related */ |
326 | @@ -145,7 +149,6 @@ |
327 | } |
328 | |
329 | .post-form #id_body { |
330 | - height: 150px; |
331 | width: 100%; |
332 | } |
333 | |
334 | |
335 | === modified file 'pybb/templates/pybb/base.html' |
336 | --- pybb/templates/pybb/base.html 2019-01-24 18:03:54 +0000 |
337 | +++ pybb/templates/pybb/base.html 2019-09-02 20:01:34 +0000 |
338 | @@ -7,7 +7,7 @@ |
339 | {% endblock %} |
340 | |
341 | {% block extra_head %} |
342 | -<link rel="stylesheet" type="text/css" media="all" href="{% static 'css/forum.css' %}" /> |
343 | +<link rel="stylesheet" type="text/css" media="all" href="{% static 'css/forum.css' %}?v1" /> |
344 | |
345 | <link rel="alternate" type="application/atom+xml" title="Latest Posts on all forums" href="{% url 'pybb_feed_posts' %}" /> |
346 | <link rel="alternate" type="application/atom+xml" title="Latest Topics on all forums" href="{% url 'pybb_feed_topics' %}" /> |
347 | |
348 | === modified file 'pybb/templates/pybb/delete_post.html' |
349 | --- pybb/templates/pybb/delete_post.html 2018-10-14 13:24:15 +0000 |
350 | +++ pybb/templates/pybb/delete_post.html 2019-09-02 20:01:34 +0000 |
351 | @@ -15,13 +15,18 @@ |
352 | <div class="preview-box"> |
353 | <div class="content post"> |
354 | {{ post.body_html|safe }} |
355 | + |
356 | + {% if post.attachments.all %} |
357 | + {% include 'pybb/inlines/attachment.html' %} |
358 | + {% endif %} |
359 | </div> |
360 | + <br /> |
361 | </div> |
362 | <form method="post"> |
363 | - <div class="info_line"> |
364 | + <p class="info_line"> |
365 | <input type="submit" value="{% trans "Yes, I am sure." %}" /> |
366 | <input type="button" value="{% trans "Cancel" %}" onclick="history.back();" /> |
367 | - </div> |
368 | + </p> |
369 | {% csrf_token %} |
370 | </form> |
371 | </div> |
372 | |
373 | === added file 'pybb/templates/pybb/inlines/attachment.html' |
374 | --- pybb/templates/pybb/inlines/attachment.html 1970-01-01 00:00:00 +0000 |
375 | +++ pybb/templates/pybb/inlines/attachment.html 2019-09-02 20:01:34 +0000 |
376 | @@ -0,0 +1,11 @@ |
377 | +{% load i18n %} |
378 | + |
379 | +{% for attach in post.attachments.all %} |
380 | + <div class="attachment"> |
381 | + <hr /> |
382 | + {% trans "Attachment" %}: |
383 | + <a href="{{ attach.get_absolute_url }}" download="{{attach.name }}" type="{{attach.content_type}}">{{ attach.name }}</a> |
384 | + ({{ attach.size|filesizeformat }}) |
385 | + </div> |
386 | +{% endfor %} |
387 | + |
388 | |
389 | === modified file 'pybb/templates/pybb/inlines/post.html' |
390 | --- pybb/templates/pybb/inlines/post.html 2018-12-21 12:50:32 +0000 |
391 | +++ pybb/templates/pybb/inlines/post.html 2019-09-02 20:01:34 +0000 |
392 | @@ -29,19 +29,16 @@ |
393 | <a id="post-{{ post.id }}" href="{{post.get_absolute_url}}" title="{% trans "Permalink" %}" class="posRight small permalink"> </a> |
394 | <span class="small">Posted at: {{ post.created|custom_date:user}}</span> |
395 | <hr /> |
396 | - <div class="post"> |
397 | {{ post.body_html|safe }} |
398 | - </div> |
399 | + |
400 | + {% if post.updated %} |
401 | + <div class="small">{% trans "Edited" %}: {{ post.updated|custom_date:user|title}}</div> |
402 | + {% endif %} |
403 | |
404 | {% if post.attachment_cache %} |
405 | - {% for attach in post.attachment_cache %} |
406 | - {% trans "Attachment" %}: <a href="{{ attach.get_absolute_url }}">{{ attach.name }}</a> ({{ attach.size_display }}) |
407 | - {% endfor %} |
408 | + {% include 'pybb/inlines/attachment.html' %} |
409 | {% endif %} |
410 | |
411 | - {% if post.updated %} |
412 | - <span class="small">{% trans "Edited" %}: {{ post.updated|custom_date:user|title}}</span> |
413 | - {% endif %} |
414 | <hr /> |
415 | {% if user.is_authenticated %} |
416 | {% ifequal user.wlprofile.show_signatures 1 %} |
417 | @@ -54,7 +51,6 @@ |
418 | {{ post.user.wlprofile.signature|urlize|linebreaks }} |
419 | {% endif %} |
420 | {% endif %} |
421 | - |
422 | <a class="button posRight" href="#top"> |
423 | <img src="{% static 'forum/img/top.png' %}" alt ="" class="middle" /> |
424 | <span class="middle">{% trans "Top" %}</span> |
425 | |
426 | === modified file 'pybb/templates/pybb/post_form.html' |
427 | --- pybb/templates/pybb/post_form.html 2018-11-22 11:08:51 +0000 |
428 | +++ pybb/templates/pybb/post_form.html 2019-09-02 20:01:34 +0000 |
429 | @@ -33,6 +33,11 @@ |
430 | |
431 | }); |
432 | }); |
433 | + $(document).ready(function() { |
434 | + $('#btn-file-reset').on('click', function() { |
435 | + $('#id_attachment').val(''); |
436 | + }); |
437 | +}); |
438 | </script> |
439 | {% endblock %} |
440 | |
441 | @@ -52,7 +57,40 @@ |
442 | </div> |
443 | |
444 | <form class="post-form" action="{{ form_url }}" method="post" enctype="multipart/form-data"> |
445 | - {{ form.as_p }} |
446 | + {% if not form.name.is_hidden %} |
447 | + <p> |
448 | + <p class="errormessage">{{ form.name.errors.as_text }}</p> |
449 | + {{ form.name.label }}: |
450 | + {{ form.name }} |
451 | + </p> |
452 | + {% endif %} |
453 | + <p> |
454 | + <p class="errormessage">{{ form.body.errors.as_text }}</p> |
455 | + {{ form.body.label }}: |
456 | + {{ form.body }} |
457 | + </p> |
458 | + <p> |
459 | + <p class="errormessage">{{ form.markup.errors.as_text }}</p> |
460 | + {{ form.markup.label }}: |
461 | + {{ form.markup }} |
462 | + </p> |
463 | + {% if not form.attachment.is_hidden %} |
464 | + <p> |
465 | + <p> |
466 | + Please refer to the wiki page |
467 | + <a href="{{ site }}/wiki/{{ wikipage }}">{{ wikipage }}</a> |
468 | + for pointers. |
469 | + </p> |
470 | + <p class="errormessage">{{ form.attachment.errors.as_text }}</p> |
471 | + {{ form.attachment.label }} |
472 | + {{ form.attachment }} |
473 | + <button id="btn-file-reset" type="button"> |
474 | + <img src="{% static 'img/delete.png' %}" alt ="" class="middle" /> |
475 | + <span class="middle">{% trans "Reset" %}</span> |
476 | + </button> |
477 | + </p> |
478 | + {% endif %} |
479 | + |
480 | {% csrf_token %} |
481 | <button type="submit"> |
482 | <img src="{% static 'forum/img/send.png' %}" alt ="" class="middle" /> |
483 | |
484 | === modified file 'pybb/util.py' |
485 | --- pybb/util.py 2019-04-11 15:20:33 +0000 |
486 | +++ pybb/util.py 2019-09-02 20:01:34 +0000 |
487 | @@ -3,6 +3,7 @@ |
488 | import traceback |
489 | import json |
490 | import re |
491 | +import subprocess |
492 | |
493 | from bs4 import BeautifulSoup, NavigableString |
494 | from datetime import datetime |
495 | @@ -14,7 +15,14 @@ |
496 | from django import forms |
497 | from django.core.paginator import Paginator, EmptyPage, InvalidPage |
498 | from django.conf import settings |
499 | +from django.core.exceptions import ValidationError |
500 | from pybb import settings as pybb_settings |
501 | +import magic |
502 | +import zipfile |
503 | +import configparser |
504 | +from PIL import Image |
505 | + |
506 | + |
507 | |
508 | |
509 | def render_to(template_path): |
510 | @@ -31,7 +39,6 @@ |
511 | output = func(request, *args, **kwargs) |
512 | if not isinstance(output, dict): |
513 | return output |
514 | - |
515 | |
516 | # TODO(Franku): 'MIME_TYPE' is never in output as i can see for now. |
517 | # But if, this should maybe 'content_type' instead |
518 | @@ -173,3 +180,144 @@ |
519 | text = text.replace('"', '"') |
520 | text = text.replace(''', '\'') |
521 | return text |
522 | + |
523 | + |
524 | +def validate_file(attachment): |
525 | + |
526 | + tmp_file_path = attachment.temporary_file_path() |
527 | + |
528 | + # Helper functions |
529 | + def _split_mime(mime_type): |
530 | + main, sub = mime_type.split('/', maxsplit=1) |
531 | + return {'maintype': main, 'subtype': sub} |
532 | + |
533 | + def _is_image(): |
534 | + # Use PIL to determine if it is a valid image file |
535 | + # works not for corrupted jpg |
536 | + try: |
537 | + with Image.open(tmp_file_path) as im: |
538 | + im.verify() |
539 | + except: |
540 | + return False |
541 | + return True |
542 | + |
543 | + def _is_zip(): |
544 | + try: |
545 | + zip_obj = zipfile.ZipFile(tmp_file_path) |
546 | + except zipfile.BadZipfile: |
547 | + return None |
548 | + return zip_obj |
549 | + |
550 | + def _zip_contains(zip_parts): |
551 | + # Check if each entry in zip_parts is inside the attachment |
552 | + zip_obj = _is_zip() |
553 | + if zip_obj: |
554 | + try: |
555 | + for obj in zip_parts: |
556 | + zip_obj.getinfo(obj) |
557 | + except KeyError: |
558 | + return False |
559 | + return True |
560 | + |
561 | + |
562 | + # Main part of file checks |
563 | + # File size |
564 | + if attachment.size > pybb_settings.ATTACHMENT_SIZE_LIMIT: |
565 | + raise ValidationError( |
566 | + 'Attachment is too big. We allow max %(size)s MiB', |
567 | + params = { |
568 | + 'size': pybb_settings.ATTACHMENT_SIZE_LIMIT/1024/1024, |
569 | + } |
570 | + ) |
571 | + |
572 | + # Checks by file extension |
573 | + splitted_fn = attachment.name.rsplit('.', maxsplit=2) |
574 | + if len(splitted_fn) == 1: |
575 | + raise ValidationError( |
576 | + 'We do not allow uploading files without an extension.' |
577 | + ) |
578 | + |
579 | + ext = splitted_fn[-1] |
580 | + if not ext in settings.ALLOWED_EXTENSIONS: |
581 | + raise ValidationError( |
582 | + 'This type of file is not allowed.' |
583 | + ) |
584 | + |
585 | + # Widelands map file |
586 | + if ext == 'wmf': |
587 | + raise ValidationError( |
588 | + 'This seems to be a widelands map file. Please upload \ |
589 | + it at our maps section.' |
590 | + ) |
591 | + |
592 | + # Widelands savegame (*.wgf) and widelands replay (*.wrpl.wgf) |
593 | + # are not the same. |
594 | + if ext == 'wgf' and not splitted_fn[-2] == 'wrpl': |
595 | + if not _zip_contains(settings.WGF_CONTENT_CHECK): |
596 | + raise ValidationError( |
597 | + 'This is not a valid widelands savegame.' |
598 | + ) |
599 | + |
600 | + # Widelands replay |
601 | + if ext == 'wrpl': |
602 | + raise ValidationError( |
603 | + 'This file is part of a replay. Please zip it together with \ |
604 | + the corresponding .wrpl.wgf file and upload again.' |
605 | + ) |
606 | + |
607 | + if ext == 'zip': |
608 | + if _is_zip() == None: |
609 | + raise ValidationError( |
610 | + 'This is not a valid zip file.' |
611 | + ) |
612 | + |
613 | + # Widelands AI configuration |
614 | + if ext == 'wai': |
615 | + wai = configparser.ConfigParser() |
616 | + try: |
617 | + wai.read(tmp_file_path) |
618 | + wai_sections = wai.sections() |
619 | + if len(settings.ALLOWED_WAI_SECTIONS) == len(wai_sections): |
620 | + for section in settings.ALLOWED_WAI_SECTIONS: |
621 | + if section not in wai_sections: |
622 | + raise |
623 | + else: |
624 | + raise |
625 | + except: |
626 | + raise ValidationError( |
627 | + 'This not a valid wai file.' |
628 | + ) |
629 | + |
630 | + # Checks by MimeType |
631 | + # Get MIME-Type from python-magic |
632 | + magic_mime = magic.from_file(tmp_file_path, mime=True) |
633 | + magic_mime = _split_mime(magic_mime) |
634 | + send_mime = _split_mime(attachment.content_type) |
635 | + |
636 | + # Check for valid image file. Use te mime-type provided by python-magic, |
637 | + # because for a renamed image the wrong mime-type is send by the browser. |
638 | + if magic_mime['maintype'] == 'image': |
639 | + if not _is_image(): |
640 | + raise ValidationError( |
641 | + 'This is not a valid image: %(file)s', |
642 | + params={'file': attachment.name} |
643 | + ) |
644 | + |
645 | + # Compare Mime type send by browser and Mime type from python-magic. |
646 | + # We only compare the main type (the first part) because the second |
647 | + # part may not be recoginzed correctly. E.g. for .lua the submitted |
648 | + # type is 'text/x-lua' but 'x-lua' is not official at all. See: |
649 | + # https://www.iana.org/assignments/media-types/media-types.xhtml |
650 | + # Unrecoginzed extension are always send with mime type |
651 | + # 'application/octet-stream'. Skip if we know them. |
652 | + if not ext in settings.SKIP_MIME_EXTENSIONS: |
653 | + if not magic_mime['maintype'] == send_mime['maintype']: |
654 | + raise ValidationError( |
655 | + 'The file %(file)s looks like %(send_mime)s, \ |
656 | + but we think it is %(magic_mime)s', |
657 | + params={ |
658 | + 'file': attachment.name, |
659 | + 'send_mime': send_mime['maintype'], |
660 | + 'magic_mime': magic_mime['maintype'], |
661 | + }, |
662 | + ) |
663 | |
664 | === modified file 'pybb/views.py' |
665 | --- pybb/views.py 2019-05-27 18:18:14 +0000 |
666 | +++ pybb/views.py 2019-09-02 20:01:34 +0000 |
667 | @@ -11,6 +11,7 @@ |
668 | from django.shortcuts import redirect |
669 | from django.db.models import Q |
670 | from django.http import Http404 |
671 | +from django.conf import settings |
672 | |
673 | from pybb.util import render_to, build_form, quote_text, ajax, urlize |
674 | from pybb.models import Category, Forum, Topic, Post, Attachment,\ |
675 | @@ -77,8 +78,12 @@ |
676 | |
677 | |
678 | def show_topic_ctx(request, topic_id): |
679 | + """View of topic posts including a form to add a Post.""" |
680 | + |
681 | + context = {} |
682 | try: |
683 | topic = Topic.objects.select_related().get(pk=topic_id) |
684 | + context.update({'topic': topic}) |
685 | except Topic.DoesNotExist: |
686 | raise Http404() |
687 | |
688 | @@ -92,25 +97,37 @@ |
689 | topic.update_read(request.user) |
690 | |
691 | last_post = topic.posts.order_by('-created')[0] |
692 | + context.update({'last_post': last_post}) |
693 | |
694 | initial = {} |
695 | + user_is_mod = False |
696 | if request.user.is_authenticated: |
697 | initial = {'markup': 'markdown'} |
698 | - form = AddPostForm(topic=topic, initial=initial) |
699 | - |
700 | - user_is_mod = pybb_moderated_by(topic, request.user) |
701 | - subscribed = (request.user.is_authenticated and |
702 | - request.user in topic.subscribers.all()) |
703 | - |
704 | - is_spam = False |
705 | - if topic.is_hidden: |
706 | - is_spam = topic.posts.first().is_spam() |
707 | + |
708 | + form = AddPostForm(topic=topic, |
709 | + initial=initial, |
710 | + user=request.user, |
711 | + ) |
712 | + context.update({'form': form}) |
713 | + |
714 | + user_is_mod = pybb_moderated_by(topic, request.user) |
715 | + context.update({'user_is_mod': user_is_mod}) |
716 | + |
717 | + subscribed = (request.user.is_authenticated and |
718 | + request.user in topic.subscribers.all()) |
719 | + context.update({'subscribed': subscribed}) |
720 | + |
721 | + is_spam = False |
722 | + if topic.is_hidden: |
723 | + is_spam = topic.posts.first().is_spam() |
724 | + context.update({'is_spam': is_spam}) |
725 | |
726 | if user_is_mod: |
727 | posts = topic.posts.select_related() |
728 | else: |
729 | posts = topic.posts.exclude(hidden=True).select_related() |
730 | - |
731 | + context.update({'posts': posts}) |
732 | + |
733 | # TODO: fetch profiles |
734 | # profiles = Profile.objects.filter(user__pk__in= |
735 | # set(x.user.id for x in page.object_list)) |
736 | @@ -119,23 +136,24 @@ |
737 | # for post in page.object_list: |
738 | # post.user.pybb_profile = profiles[post.user.id] |
739 | |
740 | - load_related(posts, Attachment.objects.all(), 'post') |
741 | - |
742 | - return {'topic': topic, |
743 | - 'last_post': last_post, |
744 | - 'form': form, |
745 | - 'user_is_mod': user_is_mod, |
746 | - 'subscribed': subscribed, |
747 | - 'posts': posts, |
748 | - 'page_size': pybb_settings.TOPIC_PAGE_SIZE, |
749 | - 'form_url': reverse('pybb_add_post', args=[topic.id]), |
750 | - 'is_spam': is_spam, |
751 | - } |
752 | + if pybb_settings.PYBB_ATTACHMENT_ENABLE: |
753 | + load_related(posts, Attachment.objects.all(), 'post') |
754 | + |
755 | + context.update({ |
756 | + 'page_size': pybb_settings.TOPIC_PAGE_SIZE, |
757 | + 'form_url': reverse('pybb_add_post', args=[topic.id]), |
758 | + 'wikipage': settings.ATTACHMENT_DESCR_PAGE, |
759 | + }) |
760 | + |
761 | + return context |
762 | + |
763 | show_topic = render_to('pybb/topic.html')(show_topic_ctx) |
764 | |
765 | |
766 | @login_required |
767 | def add_post_ctx(request, forum_id, topic_id): |
768 | + """ Standalone view for adding posts.""" |
769 | + |
770 | forum = None |
771 | topic = None |
772 | |
773 | @@ -224,6 +242,7 @@ |
774 | 'topic': topic, |
775 | 'forum': forum, |
776 | 'form_url': form_url, |
777 | + 'wikipage': settings.ATTACHMENT_DESCR_PAGE, |
778 | } |
779 | add_post = render_to('pybb/add_post.html')(add_post_ctx) |
780 | |
781 | @@ -297,7 +316,7 @@ |
782 | if not allowed: |
783 | return HttpResponseRedirect(post.get_absolute_url()) |
784 | |
785 | - if 'POST' == request.method: |
786 | + if request.method == 'POST': |
787 | topic = post.topic |
788 | forum = post.topic.forum |
789 | post.delete() |
790 | @@ -352,12 +371,12 @@ |
791 | return HttpResponseRedirect(reverse('pybb_topic', args=[topic.id])) |
792 | |
793 | |
794 | -@login_required |
795 | def show_attachment(request, hash): |
796 | attachment = get_object_or_404(Attachment, hash=hash) |
797 | - file_obj = file(attachment.get_absolute_path()) |
798 | - return HttpResponse(file_obj, content_type=attachment.content_type) |
799 | - |
800 | + |
801 | + with open(attachment.get_absolute_path(), 'rb') as file_obj: |
802 | + return HttpResponse(file_obj, content_type=attachment.content_type) |
803 | + return HTTP404 |
804 | |
805 | @login_required |
806 | @ajax |
807 | |
808 | === modified file 'wlmaps/forms.py' |
809 | --- wlmaps/forms.py 2019-07-12 08:11:24 +0000 |
810 | +++ wlmaps/forms.py 2019-09-02 20:01:34 +0000 |
811 | @@ -6,30 +6,32 @@ |
812 | |
813 | from django import forms |
814 | from django.forms import ModelForm |
815 | +from django.conf import settings |
816 | from django.core.files.storage import default_storage |
817 | -from django.conf import settings |
818 | |
819 | from wlmaps.models import Map |
820 | import os |
821 | import shutil |
822 | +import tempfile |
823 | |
824 | |
825 | class UploadMapForm(ModelForm): |
826 | """ |
827 | + All file operations are done in the temp folder. |
828 | + |
829 | We have to handle here three different kind of files: |
830 | - 1. The map which is uploaded |
831 | - 2. The files created by 'wl_map_info' |
832 | - a. The json file containing infos of the map |
833 | - b. The image of the minimap (png) |
834 | - |
835 | - The filename of uploaded maps may contain bad characters which must be handled. |
836 | - |
837 | - If a map get deleted in the database, the underlying files (.wmf, .png) still exists. Uploading |
838 | - a map with the same name does not overwrite the existing file(s), instead they get a |
839 | - name in form of 'filename_<random_chars>.wmf(.png)'. This is importand for linking the correct |
840 | - minimap. |
841 | - The map file and the minimap (png) have different random characters in such a case, because the map |
842 | - get saved twice: One time for the call to wl_map_info, and when the model is saved. |
843 | + 1. The map which is uploaded, stored as e.g '/tmp/tmp_xyz.upload'. |
844 | + Because 'wl_map_info' can't handle files with this extension and |
845 | + produces a minimap with the same name like the initial name, the |
846 | + uploade file will be copied to a file with the name of the |
847 | + uploaded file and extension, e.g. '/tmp/original_filename.wmf' |
848 | + 2. 'wl_map_info' produces two files which will be stored at the same |
849 | + location as the uploaded file and named like the map file: |
850 | + a. '/tmp/original_filename.wmf.json': Contains infos of the map |
851 | + b. '/tmp/original_filename.wmf.png': The image of the minimap (png). |
852 | + |
853 | + Because we can't be sure the original filename is a valid filename, we |
854 | + may modify it to be a valid filename. |
855 | """ |
856 | |
857 | class Meta: |
858 | @@ -39,44 +41,47 @@ |
859 | def clean(self): |
860 | cleaned_data = super(UploadMapForm, self).clean() |
861 | |
862 | - mem_file_obj = cleaned_data.get('file') |
863 | - if not mem_file_obj: |
864 | + file_obj = cleaned_data.get('file') |
865 | + if not file_obj: |
866 | # no clean file => abort |
867 | return cleaned_data |
868 | |
869 | - try: |
870 | - # Try to make a safe filename |
871 | - safe_name = default_storage.get_valid_name(mem_file_obj.name) |
872 | - file_path = settings.MEDIA_ROOT + 'wlmaps/maps/' + safe_name |
873 | - saved_file = default_storage.save(file_path, mem_file_obj) |
874 | - except UnicodeEncodeError: |
875 | - self._errors['file'] = self.error_class( |
876 | - ['The filename contains characters which cannot be handled. Please rename and upload again.']) |
877 | - del cleaned_data['file'] |
878 | - return cleaned_data |
879 | + # Make a save filename and copy the uploaded file |
880 | + saved_file = file_obj.temporary_file_path() |
881 | + safe_name = default_storage.get_valid_name(file_obj.name) |
882 | + tmpdir = tempfile.gettempdir() |
883 | + copied_file = shutil.copyfile(saved_file, |
884 | + os.path.join(tmpdir, safe_name) |
885 | + ) |
886 | |
887 | try: |
888 | # call map info tool to generate minimap and json info file |
889 | + # change working directory so that the datadir is found |
890 | old_cwd = os.getcwd() |
891 | os.chdir(settings.WIDELANDS_SVN_DIR) |
892 | - check_call(['wl_map_info', saved_file]) |
893 | - |
894 | - # Deleting the file because it will be saved again when |
895 | - # the model is saved. |
896 | - default_storage.delete(saved_file) |
897 | + check_call(['wl_map_info', copied_file]) |
898 | os.chdir(old_cwd) |
899 | except CalledProcessError: |
900 | self._errors['file'] = self.error_class( |
901 | ['The map file could not be processed.']) |
902 | del cleaned_data['file'] |
903 | + os.remove(copied_file) |
904 | return cleaned_data |
905 | |
906 | - mapinfo = json.load(open(saved_file + '.json')) |
907 | + mapinfo = json.load(open(copied_file + '.json')) |
908 | |
909 | if Map.objects.filter(name=mapinfo['name']): |
910 | self._errors['file'] = self.error_class( |
911 | ['A map with the same name already exists.']) |
912 | del cleaned_data['file'] |
913 | + # Delete the file copy and the generated files |
914 | + try: |
915 | + os.remove(copied_file) |
916 | + os.remove(copied_file + '.json') |
917 | + os.remove(copied_file + '.png') |
918 | + except os.FileNotFoundError: |
919 | + pass |
920 | + |
921 | return cleaned_data |
922 | |
923 | # Add information to the map |
924 | @@ -90,19 +95,18 @@ |
925 | self.instance.world_name = mapinfo['world_name'] |
926 | self.instance.wl_version_after = mapinfo['needs_widelands_version_after'] |
927 | |
928 | - # mapinfo["minimap"] is the absolute path containing the path where it |
929 | - # is saved, extract the name |
930 | + # mapinfo["minimap"] is the absolute path to the image file |
931 | + # We move the file to the correct destination |
932 | minimap_name = mapinfo['minimap'].rpartition('/')[2] |
933 | minimap_upload_to = self.instance._meta.get_field('minimap').upload_to |
934 | - # Set the destination relative to MEDIA_ROOT |
935 | minimap_path = os.path.join(minimap_upload_to, minimap_name) |
936 | self.instance.minimap = minimap_path |
937 | - # Move the minimap file (.png) from wlmaps/maps to wlmaps/minimaps |
938 | shutil.move(mapinfo['minimap'], os.path.join( |
939 | settings.MEDIA_ROOT, minimap_path)) |
940 | |
941 | - # the json file is no longer needed |
942 | - default_storage.delete(saved_file + '.json') |
943 | + # Final cleanup |
944 | + os.remove(copied_file + '.json') |
945 | + os.remove(copied_file) |
946 | |
947 | return cleaned_data |
948 |
Code LGTM. Do we have a restriction on file types? We won't want people to upload malicious JavaScript ans stuff lie that, so we should have a list of legal file extensions at least.