diff --git a/swh/web/misc/origin_save.py b/swh/web/misc/origin_save.py index 61ec9fcc..50844a66 100644 --- a/swh/web/misc/origin_save.py +++ b/swh/web/misc/origin_save.py @@ -1,103 +1,108 @@ # Copyright (C) 2018-2019 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import json from django.conf.urls import url from django.core.paginator import Paginator -from django.http import HttpResponse, HttpResponseForbidden +from django.http import ( + HttpResponse, HttpResponseForbidden, HttpResponseServerError +) from django.shortcuts import render from rest_framework.decorators import api_view, authentication_classes from swh.web.common.exc import ForbiddenExc from swh.web.common.models import SaveOriginRequest from swh.web.common.origin_save import ( create_save_origin_request, get_savable_visit_types, get_save_origin_requests_from_queryset ) from swh.web.common.throttling import throttle_scope from swh.web.common.utils import EnforceCSRFAuthentication def _origin_save_view(request): return render(request, 'misc/origin-save.html', {'heading': ('Request the saving of a software origin into ' 'the archive')}) @api_view(['POST']) @authentication_classes((EnforceCSRFAuthentication, )) @throttle_scope('swh_save_origin') def _origin_save_request(request, visit_type, origin_url): """ This view is called through AJAX from the save code now form of swh-web. We use DRF here as we want to rate limit the number of submitted requests per user to avoid being possibly flooded by bots. """ try: response = json.dumps(create_save_origin_request(visit_type, origin_url), separators=(',', ': ')) return HttpResponse(response, content_type='application/json') except ForbiddenExc as exc: return HttpResponseForbidden(json.dumps({'detail': str(exc)}), content_type='application/json') + except Exception as exc: + return HttpResponseServerError(json.dumps({'detail': str(exc)}), + content_type='application/json') def _visit_save_types_list(request): visit_types = json.dumps(get_savable_visit_types(), separators=(',', ': ')) return HttpResponse(visit_types, content_type='application/json') def _origin_save_requests_list(request, status): if status != 'all': save_requests = SaveOriginRequest.objects.filter(status=status) else: save_requests = SaveOriginRequest.objects.all() table_data = {} table_data['recordsTotal'] = save_requests.count() table_data['draw'] = int(request.GET['draw']) search_value = request.GET['search[value]'] column_order = request.GET['order[0][column]'] field_order = request.GET['columns[%s][name]' % column_order] order_dir = request.GET['order[0][dir]'] if order_dir == 'desc': field_order = '-' + field_order save_requests = save_requests.order_by(field_order) length = int(request.GET['length']) page = int(request.GET['start']) / length + 1 save_requests = get_save_origin_requests_from_queryset(save_requests) if search_value: save_requests = \ [sr for sr in save_requests if search_value.lower() in sr['save_request_status'].lower() or search_value.lower() in sr['save_task_status'].lower() or search_value.lower() in sr['visit_type'].lower() or search_value.lower() in sr['origin_url'].lower()] table_data['recordsFiltered'] = len(save_requests) paginator = Paginator(save_requests, length) table_data['data'] = paginator.page(page).object_list table_data_json = json.dumps(table_data, separators=(',', ': ')) return HttpResponse(table_data_json, content_type='application/json') urlpatterns = [ url(r'^save/$', _origin_save_view, name='origin-save'), url(r'^save/(?P.+)/url/(?P.+)/$', _origin_save_request, name='origin-save-request'), url(r'^save/types/list/$', _visit_save_types_list, name='origin-save-types-list'), url(r'^save/requests/list/(?P.+)/$', _origin_save_requests_list, name='origin-save-requests-list'), ] diff --git a/swh/web/tests/misc/test_origin_save.py b/swh/web/tests/misc/test_origin_save.py index 1d824f8a..a1b3d0c9 100644 --- a/swh/web/tests/misc/test_origin_save.py +++ b/swh/web/tests/misc/test_origin_save.py @@ -1,89 +1,104 @@ # Copyright (C) 2019 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import pytest from datetime import datetime from django.test import Client from swh.web.common.origin_save import ( SAVE_REQUEST_ACCEPTED, SAVE_TASK_NOT_YET_SCHEDULED ) from swh.web.common.utils import reverse from swh.web.settings.tests import save_origin_rate_post visit_type = 'git' origin = { 'url': 'https://github.com/python/cpython' } @pytest.fixture def client(): return Client(enforce_csrf_checks=True) def test_save_request_form_csrf_token(client, mocker): mock_create_save_origin_request = mocker.patch( 'swh.web.misc.origin_save.create_save_origin_request') _mock_create_save_origin_request(mock_create_save_origin_request) url = reverse('origin-save-request', url_args={'visit_type': visit_type, 'origin_url': origin['url']}) resp = client.post(url) assert resp.status_code == 403 data = _get_csrf_token(client, reverse('origin-save')) resp = client.post(url, data=data) assert resp.status_code == 200 def test_save_request_form_rate_limit(client, mocker): mock_create_save_origin_request = mocker.patch( 'swh.web.misc.origin_save.create_save_origin_request') _mock_create_save_origin_request(mock_create_save_origin_request) url = reverse('origin-save-request', url_args={'visit_type': visit_type, 'origin_url': origin['url']}) data = _get_csrf_token(client, reverse('origin-save')) for _ in range(save_origin_rate_post): resp = client.post(url, data=data) assert resp.status_code == 200 resp = client.post(url, data=data) assert resp.status_code == 429 +def test_save_request_form_server_error(client, mocker): + mock_create_save_origin_request = mocker.patch( + 'swh.web.misc.origin_save.create_save_origin_request') + mock_create_save_origin_request.side_effect = Exception('Server error') + + url = reverse('origin-save-request', + url_args={'visit_type': visit_type, + 'origin_url': origin['url']}) + + data = _get_csrf_token(client, reverse('origin-save')) + + resp = client.post(url, data=data) + assert resp.status_code == 500 + + def test_old_save_url_redirection(client): url = reverse('browse-origin-save') resp = client.get(url) assert resp.status_code == 302 redirect_url = reverse('origin-save') assert resp['location'] == redirect_url def _get_csrf_token(client, url): resp = client.get(url) return { 'csrfmiddlewaretoken': resp.cookies['csrftoken'].value } def _mock_create_save_origin_request(mock): expected_data = { 'visit_type': visit_type, 'origin_url': origin['url'], 'save_request_date': datetime.now().isoformat(), 'save_request_status': SAVE_REQUEST_ACCEPTED, 'save_task_status': SAVE_TASK_NOT_YET_SCHEDULED, 'visit_date': None } mock.return_value = expected_data