diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000000..c847861cf4 Binary files /dev/null and b/dump.rdb differ diff --git a/pybossa/settings_local.py.tmpl b/pybossa/settings_local.py.tmpl index 091db039b8..865acfe20d 100644 --- a/pybossa/settings_local.py.tmpl +++ b/pybossa/settings_local.py.tmpl @@ -500,3 +500,5 @@ WIZARD_STEPS = OrderedDict([ 'visible_checks': {'and': ['project_publish'], 'or': []}, })] ) + +MAX_IMAGE_UPLOAD_SIZE_MB = 5 diff --git a/pybossa/themes/default b/pybossa/themes/default index c9dba9f5f5..1dcd079570 160000 --- a/pybossa/themes/default +++ b/pybossa/themes/default @@ -1 +1 @@ -Subproject commit c9dba9f5f569bae66c490a31c70a311b4e59af38 +Subproject commit 1dcd0795705cfe2544f2fafd4781f90db8be9098 diff --git a/pybossa/view/projects.py b/pybossa/view/projects.py index 0374f7c1dd..0f4724bcde 100644 --- a/pybossa/view/projects.py +++ b/pybossa/view/projects.py @@ -34,11 +34,12 @@ import urllib.parse from rq import Queue from werkzeug.datastructures import MultiDict +from werkzeug.utils import secure_filename import pybossa.sched as sched from pybossa.core import (uploader, signer, sentinel, json_exporter, csv_exporter, importer, db, task_json_exporter, - task_csv_exporter, anonymizer) + task_csv_exporter, anonymizer, csrf) from pybossa.model import make_uuid from pybossa.model.project import Project from pybossa.model.category import Category @@ -517,6 +518,60 @@ def clone(short_name): project=project_sanitized )) +def is_editor_disabled(): + return (not current_user.admin and + current_app.config.get( + 'DISABLE_TASK_PRESENTER_EDITOR')) + +def is_admin_or_owner(project): + return (current_user.admin or + (project.owner_id == current_user.id or + current_user.id in project.owners_ids)) + +@blueprint.route('//tasks/taskpresenterimageupload', methods=['GET', 'POST']) +@login_required +@admin_or_subadmin_required +@csrf.exempt +def upload_task_guidelines_image(short_name): + error = False + return_code = 200 + project = project_by_shortname(short_name) + + imgurls = [] + if is_editor_disabled(): + flash(gettext('Task presenter editor disabled!'), 'error') + error = True + return_code = 400 + elif not is_admin_or_owner(project): + flash(gettext('Ooops! Only project owners can upload files.'), 'error') + error = True + return_code = 400 + else: + for file in request.files.getlist("image"): + file_size_mb = file.seek(0, os.SEEK_END) / 1024 / 1024 + file.seek(0, os.SEEK_SET) + file.filename = secure_filename(file.filename) + if file_size_mb < current_app.config.get('MAX_IMAGE_UPLOAD_SIZE_MB', 5): + container = "user_%s" % current_user.id + uploader.upload_file(file, container=container) + imgurls.append(get_avatar_url( + current_app.config.get('UPLOAD_METHOD'), + file.filename, + container, + current_app.config.get('AVATAR_ABSOLUTE') + )) + else: + flash(gettext('File must be smaller than ' + str(current_app.config.get('MAX_IMAGE_UPLOAD_SIZE_MB', 5)) + ' MB.')) + error = True + return_code = 413 + + response = { + "imgurls" : imgurls, + "error": error + } + + return jsonify(response), return_code + @blueprint.route('//tasks/taskpresentereditor', methods=['GET', 'POST']) @login_required @admin_or_subadmin_required diff --git a/settings_test.py.tmpl b/settings_test.py.tmpl index fd205679ae..bcf8c3a92c 100644 --- a/settings_test.py.tmpl +++ b/settings_test.py.tmpl @@ -245,3 +245,5 @@ COMPLETED_TASK_CLEANUP_DAYS = [ (90, "90 days"), (180, "180 days") ] + +MAX_IMAGE_UPLOAD_SIZE_MB = 5 diff --git a/test/files/small-image1.jpg b/test/files/small-image1.jpg new file mode 100644 index 0000000000..61630bf23d Binary files /dev/null and b/test/files/small-image1.jpg differ diff --git a/test/files/small-image2.jpg b/test/files/small-image2.jpg new file mode 100644 index 0000000000..ea61c63560 Binary files /dev/null and b/test/files/small-image2.jpg differ diff --git a/test/test_web.py b/test/test_web.py index d8997cd57a..2df77511b9 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -28,6 +28,7 @@ from test import db, Fixtures, with_context, with_context_settings, \ FakeResponse, mock_contributions_guard, with_request_context from test.helper import web +from test.test_authorization import mock_current_user from unittest.mock import patch, Mock, call, MagicMock from flask import redirect from itsdangerous import BadSignature @@ -4693,6 +4694,156 @@ def test_48_task_presenter_editor_works_json(self, mock): assert data['form']['guidelines'] == 'Some guidelines!', data + @with_context + @patch('pybossa.view.projects.uploader.upload_file', return_value=True) + def test_task_presenter_large_image_upload(self, mock): + """Test API /tasks/taskpresenterimageupload should not upload images with size > 5 MB""" + print("running test_task_presenter_large_image_upload...") + user = UserFactory.create(id=500) + project = ProjectFactory.create( + short_name='test_project', + name='Test Project', + info={ + 'total': 150, + 'task_presenter': 'foo', + 'data_classification': dict(input_data="L4 - public", output_data="L4 - public"), + 'kpi': 0.5, + 'product': 'abc', + 'subproduct': 'def', + }, + owner=user) + headers = [('Authorization', user.api_key)] + with open('./test/files/small-image1.jpg', 'rb') as img: + imgStringIO = BytesIO(img.read()) + with patch.dict(self.flask_app.config, {'MAX_IMAGE_UPLOAD_SIZE_MB': 0}): + # Call API method to upload image. + res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': (imgStringIO, 'large-image.jpg')}) + res_data = json.loads(res.data) + assert res.status_code == 413, "POST image upload should yield 413" + assert len(res_data['imgurls']) == 0, "Successful count of uploaded images 0." + assert res_data['error'] == True, "There should be an error for a file larger than 5 MB." + + @with_context + @patch('pybossa.view.projects.uploader.upload_file', return_value=True) + def test_task_presenter_image_upload(self, mock): + """Test API /tasks/taskpresenterimageupload to upload a task presenter guidelines image""" + print("running test_task_presenter_image_upload...") + user = UserFactory.create(id=500) + project = ProjectFactory.create( + short_name='test_project', + name='Test Project', + info={ + 'total': 150, + 'task_presenter': 'foo', + 'data_classification': dict(input_data="L4 - public", output_data="L4 - public"), + 'kpi': 0.5, + 'product': 'abc', + 'subproduct': 'def', + }, + owner=user) + headers = [('Authorization', user.api_key)] + with open('./test/files/small-image1.jpg', 'rb') as img: + imgStringIO = BytesIO(img.read()) + # Call API method to upload image. + res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': (imgStringIO, 'large-image.jpg')}) + res_data = json.loads(res.data) + assert res.status_code == 200, "POST image upload should be successful" + assert len(res_data['imgurls']) == 1, "Successful count of uploaded images 1." + assert res_data['error'] == False, "There should be no errors for normal file upload" + + @with_context + @patch('pybossa.view.projects.uploader.upload_file', return_value=True) + def test_task_presenter_multiple_image_upload(self, mock): + """Test API /tasks/taskpresenterimageupload to upload multiple task presenter guidelines images""" + print("running test_task_presenter_multiple_image_upload...") + user = UserFactory.create(id=500) + project = ProjectFactory.create( + short_name='test_project', + name='Test Project', + info={ + 'total': 150, + 'task_presenter': 'foo', + 'data_classification': dict(input_data="L4 - public", output_data="L4 - public"), + 'kpi': 0.5, + 'product': 'abc', + 'subproduct': 'def', + }, + owner=user) + headers = [('Authorization', user.api_key)] + with open('./test/files/small-image1.jpg', 'rb') as img1: + imgStringIO1 = BytesIO(img1.read()) + with open('./test/files/small-image2.jpg', 'rb') as img2: + imgStringIO2 = BytesIO(img2.read()) + # Call API method to upload image. + res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': + [(imgStringIO1, 'img1.jpg'), (imgStringIO2, 'img2.jpg')] + }) + res_data = json.loads(res.data) + assert res.status_code == 200, "POST image upload should be successful" + assert len(res_data['imgurls']) == 2, "Successful count of uploaded images 2." + assert res_data['error'] == False, "There should be no errors for normal file upload" + + @with_context + @patch('pybossa.view.projects.is_admin_or_owner', return_value=False) + def test_task_presenter_image_upload_user_not_owner_or_admin(self, mock): + """Test API /tasks/taskpresenterimageupload to upload a task presenter guidelines image""" + print("running test_task_presenter_image_upload_user_not_owner_or_admin...") + user = UserFactory.create(id=500) + project = ProjectFactory.create( + short_name='test_project', + name='Test Project', + info={ + 'total': 150, + 'task_presenter': 'foo', + 'data_classification': dict(input_data="L4 - public", output_data="L4 - public"), + 'kpi': 0.5, + 'product': 'abc', + 'subproduct': 'def', + }, + owner=user) + headers = [('Authorization', user.api_key)] + with open('./test/files/small-image1.jpg', 'rb') as img: + imgStringIO = BytesIO(img.read()) + # Call API method to upload image. + res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': (imgStringIO, 'large-image.jpg')}) + res_data = json.loads(res.data) + assert res.status_code == 400, "POST image upload should be successful" + assert len(res_data['imgurls']) == 0, "Image should not be uploaded." + assert res_data['error'] == True, "There should be an error since the user is not owner or admin" + + mock_authenticated=mock_current_user(anonymous=False, admin=False, id=2) + + @with_context + @patch('pybossa.view.projects.is_editor_disabled', return_value=True) + def test_task_presenter_image_upload_task_presenter_disabled(self, disable_editor): + + """Test API /tasks/taskpresenterimageupload to upload a task presenter guidelines image""" + print("running test_task_presenter_image_upload_task_presenter_disabled...") + user = UserFactory.create(id=2, admin=False) + project = ProjectFactory.create( + short_name='test_project', + name='Test Project', + info={ + 'total': 150, + 'task_presenter': 'foo', + 'data_classification': dict(input_data="L4 - public", output_data="L4 - public"), + 'kpi': 0.5, + 'product': 'abc', + 'subproduct': 'def', + }, + owner=user) + + headers = [('Authorization', user.api_key)] + with open('./test/files/small-image1.jpg', 'rb') as img: + imgStringIO = BytesIO(img.read()) + with patch.dict(self.flask_app.config, {'DISABLE_TASK_PRESENTER_EDITOR': True}): + # Call API method to upload image. + res = self.app.post('/project/{}/tasks/taskpresenterimageupload'.format(project.short_name), headers=headers, data={'image': (imgStringIO, 'large-image.jpg')}) + res_data = json.loads(res.data) + assert res.status_code == 400, "POST image upload should be successful" + assert len(res_data['imgurls']) == 0, "Image should not be uploaded." + assert res_data['error'] == True, "There should be an error since the task presenter is disabled" + @with_context @patch('pybossa.ckan.requests.get') @patch('pybossa.view.projects.uploader.upload_file', return_value=True)