From 7e55eea41dcdc3a4cb04e568ebc641210dd53bf6 Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Fri, 24 May 2019 17:54:19 +0900 Subject: [PATCH 1/6] check for pdf with python-magic --- app/dashboard/views.py | 42 ++++++++++++++++++++++++++++++++++-------- requirements/base.txt | 1 + 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/app/dashboard/views.py b/app/dashboard/views.py index cb02dbc24f0..1aff955dc4b 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -55,6 +55,7 @@ from git.utils import get_auth_url, get_github_user_data, is_github_token_valid, search_users from kudos.models import KudosTransfer, Token, Wallet from kudos.utils import humanize_name +import magic from marketing.mails import admin_contact_funder, bounty_uninterested from marketing.mails import funder_payout_reminder as funder_payout_reminder_mail from marketing.mails import new_reserved_issue, start_work_approved, start_work_new_applicant, start_work_rejected @@ -1756,6 +1757,11 @@ def profile_job_opportunity(request, handle): Args: handle (str): The profile handle. """ + uploaded_file = request.FILES.get('job_cv') + error_response = invalid_file_response(uploaded_file) + # 400 is ok because file upload is optional here + if error_response and error_response['status'] != '400': + return JsonResponse(error_response) try: profile = profile_helper(handle, True) profile.job_search_status = request.POST.get('job_search_status', None) @@ -1777,6 +1783,28 @@ def profile_job_opportunity(request, handle): return JsonResponse(response) +def invalid_file_response(uploaded_file): + response = None + if not uploaded_file: + response = { + 'status': 400, + 'message': 'No File Found' + } + elif uploaded_file.size > 31457280: + # 30MB max file size + response = { + 'status': 413, + 'message': 'File Too Large' + } + else: + file_mime = magic.from_buffer(uploaded_file.chunks[0]); + if file_mine != 'application/pdf': + response = { + 'status': 415, + 'message': 'Invalid PDF File' + } + return response + @csrf_exempt @require_POST def bounty_upload_nda(request): @@ -1785,9 +1813,11 @@ def bounty_upload_nda(request): Args: bounty_id (int): The bounty id. """ - if request.FILES.get('docs', None): + uploaded_file = request.FILES.get('docs', None) + error_response = invalid_file_response(uploaded_file) + if not error_response: bountydoc = BountyDocuments.objects.create( - doc=request.FILES.get('docs', None), + doc=uploaded_file, doc_type=request.POST.get('doc_type', None) ) response = { @@ -1795,12 +1825,8 @@ def bounty_upload_nda(request): 'bounty_doc_id': bountydoc.pk, 'message': 'NDA saved' } - else: - response = { - 'status': 400, - 'message': 'No File Found' - } - return JsonResponse(response) + + return JsonResponse(error_response) if error_response else JsonResponse(response) diff --git a/requirements/base.txt b/requirements/base.txt index a948ee26df3..0c6523c6851 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -81,3 +81,4 @@ raven==6.9.0 sentry-sdk websocket-client bleach +python-magic From fe3387b23850e7d4669cc85e3fe33e436f530988 Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Fri, 24 May 2019 18:00:28 +0900 Subject: [PATCH 2/6] add libmagic dependency --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f71562acb7b..1e0cf4ec61a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.7-alpine3.8 ENV PYTHONUNBUFFERED 1 ENV PYTHONDONTWRITEBYTECODE 1 -ARG PACKAGES="postgresql-libs libxml2 libxslt freetype libffi jpeg libmaxminddb bash git tar gzip inkscape" +ARG PACKAGES="postgresql-libs libxml2 libxslt freetype libffi jpeg libmaxminddb bash git tar gzip inkscape libmagic" ARG BUILD_DEPS="gcc g++ postgresql-dev libxml2-dev libxslt-dev freetype-dev libffi-dev jpeg-dev linux-headers autoconf automake libtool make dos2unix" WORKDIR /code From a25024dc9e5cc9240b53dcfe224f548282bb894e Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Fri, 24 May 2019 18:03:21 +0900 Subject: [PATCH 3/6] add .doc, .docx support --- app/dashboard/views.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/dashboard/views.py b/app/dashboard/views.py index 1aff955dc4b..e71bef5918c 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -1758,7 +1758,7 @@ def profile_job_opportunity(request, handle): handle (str): The profile handle. """ uploaded_file = request.FILES.get('job_cv') - error_response = invalid_file_response(uploaded_file) + error_response = invalid_file_response(uploaded_file, supported=['application/pdf']) # 400 is ok because file upload is optional here if error_response and error_response['status'] != '400': return JsonResponse(error_response) @@ -1783,7 +1783,7 @@ def profile_job_opportunity(request, handle): return JsonResponse(response) -def invalid_file_response(uploaded_file): +def invalid_file_response(uploaded_file, supported): response = None if not uploaded_file: response = { @@ -1798,10 +1798,10 @@ def invalid_file_response(uploaded_file): } else: file_mime = magic.from_buffer(uploaded_file.chunks[0]); - if file_mine != 'application/pdf': + if file_mime not in supported: response = { 'status': 415, - 'message': 'Invalid PDF File' + 'message': 'Invalid File Type' } return response @@ -1814,7 +1814,10 @@ def bounty_upload_nda(request): bounty_id (int): The bounty id. """ uploaded_file = request.FILES.get('docs', None) - error_response = invalid_file_response(uploaded_file) + error_response = invalid_file_response( + uploaded_file, supported=['application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']) if not error_response: bountydoc = BountyDocuments.objects.create( doc=uploaded_file, From aa7c65a384d0dc9db9418b8d70aa5fdf74e41441 Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Tue, 28 May 2019 19:55:33 +0900 Subject: [PATCH 4/6] better error response and fix --- app/assets/v2/js/jobs.js | 6 +++++- app/dashboard/views.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/v2/js/jobs.js b/app/assets/v2/js/jobs.js index bde9a4fa6ba..b48180cb80a 100644 --- a/app/assets/v2/js/jobs.js +++ b/app/assets/v2/js/jobs.js @@ -53,7 +53,11 @@ const save_job_status = function() { }; $.ajax(profile).done(function(response) { - _alert(response.message, 'info'); + if (response.status == 200) { + _alert(response.message, 'info'); + } else { + _alert(response.message, 'error'); + } }).fail(function(error) { _alert(error, 'error'); }); diff --git a/app/dashboard/views.py b/app/dashboard/views.py index e71bef5918c..c305d3acf87 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -1797,7 +1797,8 @@ def invalid_file_response(uploaded_file, supported): 'message': 'File Too Large' } else: - file_mime = magic.from_buffer(uploaded_file.chunks[0]); + file_mime = magic.from_buffer(next(uploaded_file.chunks()), mime=True) + logger.info('uploaded file: %s' % file_mime) if file_mime not in supported: response = { 'status': 415, From 9a75bc684ad39611760392d00b98e9eddc48e753 Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Tue, 28 May 2019 21:21:16 +0900 Subject: [PATCH 5/6] better error handling for NDAs --- app/assets/v2/js/pages/bounty_details.js | 36 +++++++++++++----------- app/assets/v2/js/pages/new_bounty.js | 14 ++++++--- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/app/assets/v2/js/pages/bounty_details.js b/app/assets/v2/js/pages/bounty_details.js index 29fd9881746..009349d7f12 100644 --- a/app/assets/v2/js/pages/bounty_details.js +++ b/app/assets/v2/js/pages/bounty_details.js @@ -681,22 +681,26 @@ var show_interest_modal = function() { }; $.ajax(ndaSend).done(function(response) { - _alert(response.message, 'info'); - add_interest(document.result['pk'], { - issue_message: msg, - signed_nda: response.bounty_doc_id - }).then(success => { - if (success) { - $(self).attr('href', '/uninterested'); - $(self).find('span').text(gettext('Stop Work')); - $(self).parent().attr('title', '
' + gettext('Notify the funder that you will not be working on this project') + '
'); - modals.bootstrapModal('hide'); - } - }).catch((error) => { - if (error.responseJSON.error === 'You may only work on max of 3 issues at once.') - return; - throw error; - }); + if (response.status == 200) { + _alert(response.message, 'info'); + add_interest(document.result['pk'], { + issue_message: msg, + signed_nda: response.bounty_doc_id + }).then(success => { + if (success) { + $(self).attr('href', '/uninterested'); + $(self).find('span').text(gettext('Stop Work')); + $(self).parent().attr('title', '
' + gettext('Notify the funder that you will not be working on this project') + '
'); + modals.bootstrapModal('hide'); + } + }).catch((error) => { + if (error.responseJSON.error === 'You may only work on max of 3 issues at once.') + return; + throw error; + }); + } else { + _alert(response.message, 'error'); + } }).fail(function(error) { _alert(error, 'error'); }); diff --git a/app/assets/v2/js/pages/new_bounty.js b/app/assets/v2/js/pages/new_bounty.js index f856aa5b397..740ece34563 100644 --- a/app/assets/v2/js/pages/new_bounty.js +++ b/app/assets/v2/js/pages/new_bounty.js @@ -690,10 +690,16 @@ $('#submitBounty').validate({ }; $.ajax(settings).done(function(response) { - _alert(response.message, 'info'); - ipfsBounty.payload.unsigned_nda = response.bounty_doc_id; - if (data.featuredBounty) payFeaturedBounty(); - else do_bounty(); + if (response.status == 200) { + _alert(response.message, 'info'); + ipfsBounty.payload.unsigned_nda = response.bounty_doc_id; + if (data.featuredBounty) payFeaturedBounty(); + else do_bounty(); + } else { + _alert('Unable to upload NDA. ', 'error'); + unloading_button($('.js-submit')); + console.log('NDA error:', response.message); + } }).fail(function(error) { _alert('Unable to upload NDA. ', 'error'); unloading_button($('.js-submit')); From 3749d82f18a354dff0ba513c0a93a9109255c882 Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Tue, 28 May 2019 21:32:10 +0900 Subject: [PATCH 6/6] fix isort --- app/dashboard/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/dashboard/views.py b/app/dashboard/views.py index c305d3acf87..172eff4a920 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -46,6 +46,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET, require_POST +import magic from app.utils import clean_str, ellipses, get_default_network from avatar.utils import get_avatar_context_for_user from dashboard.utils import ProfileHiddenException, ProfileNotFoundException, get_bounty_from_invite_url, profile_helper @@ -55,7 +56,6 @@ from git.utils import get_auth_url, get_github_user_data, is_github_token_valid, search_users from kudos.models import KudosTransfer, Token, Wallet from kudos.utils import humanize_name -import magic from marketing.mails import admin_contact_funder, bounty_uninterested from marketing.mails import funder_payout_reminder as funder_payout_reminder_mail from marketing.mails import new_reserved_issue, start_work_approved, start_work_new_applicant, start_work_rejected