diff --git a/swh/web/api/apiurls.py b/swh/web/api/apiurls.py --- a/swh/web/api/apiurls.py +++ b/swh/web/api/apiurls.py @@ -56,12 +56,14 @@ def __init__(self, url_pattern=None, view_name=None, methods=['GET', 'HEAD', 'OPTIONS'], throttle_scope='swh_api', - api_version='1'): + api_version='1', + checksum_args=None): super().__init__() self.url_pattern = '^' + api_version + url_pattern + '$' self.view_name = view_name self.methods = methods self.throttle_scope = throttle_scope + self.checksum_args = checksum_args def __call__(self, f): # create a DRF view from the wrapped function @@ -76,4 +78,9 @@ # register the route and its view in the endpoints index APIUrls.add_url_pattern(self.url_pattern, api_view_f, self.view_name) + + if self.checksum_args: + APIUrls.add_redirect_for_checksum_args(self.view_name, + [self.url_pattern], + self.checksum_args) return f diff --git a/swh/web/api/views/content.py b/swh/web/api/views/content.py --- a/swh/web/api/views/content.py +++ b/swh/web/api/views/content.py @@ -16,7 +16,8 @@ from swh.web.api.views.utils import api_lookup -@api_route(r'/content/(?P.+)/filetype/', 'api-content-filetype') +@api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/filetype/', + 'api-content-filetype', checksum_args=['q']) @api_doc('/content/filetype/') def api_content_filetype(request, q): """ @@ -60,7 +61,8 @@ enrich_fn=utils.enrich_metadata_endpoint) -@api_route(r'/content/(?P.+)/language/', 'api-content-language') +@api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/language/', + 'api-content-language', checksum_args=['q']) @api_doc('/content/language/') def api_content_language(request, q): """ @@ -103,7 +105,8 @@ enrich_fn=utils.enrich_metadata_endpoint) -@api_route(r'/content/(?P.+)/license/', 'api-content-license') +@api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/license/', + 'api-content-license', checksum_args=['q']) @api_doc('/content/license/') def api_content_license(request, q): """ @@ -146,7 +149,7 @@ enrich_fn=utils.enrich_metadata_endpoint) -@api_route(r'/content/(?P.+)/ctags/', 'api-content-ctags') +@api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/ctags/', 'api-content-ctags') @api_doc('/content/ctags/', tags=['hidden']) def api_content_ctags(request, q): """ @@ -159,7 +162,8 @@ enrich_fn=utils.enrich_metadata_endpoint) -@api_route(r'/content/(?P.+)/raw/', 'api-content-raw') +@api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/raw/', 'api-content-raw', + checksum_args=['q']) @api_doc('/content/raw/', handle_response=True) def api_content_raw(request, q): """ @@ -324,7 +328,8 @@ return response -@api_route(r'/content/(?P.+)/', 'api-content') +@api_route(r'/content/(?P[0-9a-z_:]*[0-9a-f]+)/', 'api-content', + checksum_args=['q']) @api_doc('/content/') def api_content_metadata(request, q): """ diff --git a/swh/web/api/views/directory.py b/swh/web/api/views/directory.py --- a/swh/web/api/views/directory.py +++ b/swh/web/api/views/directory.py @@ -10,9 +10,11 @@ from swh.web.api.views.utils import api_lookup -@api_route(r'/directory/(?P[0-9a-f]+)/', 'api-directory') +@api_route(r'/directory/(?P[0-9a-f]+)/', 'api-directory', + checksum_args=['sha1_git']) @api_route(r'/directory/(?P[0-9a-f]+)/(?P.+)/', - 'api-directory') + 'api-directory', + checksum_args=['sha1_git']) @api_doc('/directory/') def api_directory(request, sha1_git, path=None): """ diff --git a/swh/web/api/views/release.py b/swh/web/api/views/release.py --- a/swh/web/api/views/release.py +++ b/swh/web/api/views/release.py @@ -10,7 +10,8 @@ from swh.web.api.views.utils import api_lookup -@api_route(r'/release/(?P[0-9a-f]+)/', 'api-release') +@api_route(r'/release/(?P[0-9a-f]+)/', 'api-release', + checksum_args=['sha1_git']) @api_doc('/release/') def api_release(request, sha1_git): """ diff --git a/swh/web/api/views/revision.py b/swh/web/api/views/revision.py --- a/swh/web/api/views/revision.py +++ b/swh/web/api/views/revision.py @@ -271,7 +271,8 @@ enrich_fn=utils.enrich_revision) -@api_route(r'/revision/(?P[0-9a-f]+)/', 'api-revision') +@api_route(r'/revision/(?P[0-9a-f]+)/', 'api-revision', + checksum_args=['sha1_git']) @api_doc('/revision/') def api_revision(request, sha1_git): """ @@ -327,7 +328,7 @@ @api_route(r'/revision/(?P[0-9a-f]+)/raw/', - 'api-revision-raw-message') + 'api-revision-raw-message', checksum_args=['sha1_git']) @api_doc('/revision/raw/', tags=['hidden'], handle_response=True) def api_revision_raw_message(request, sha1_git): """Return the raw data of the message of revision identified by sha1_git @@ -341,9 +342,9 @@ @api_route(r'/revision/(?P[0-9a-f]+)/directory/', - 'api-revision-directory') + 'api-revision-directory', checksum_args=['sha1_git']) @api_route(r'/revision/(?P[0-9a-f]+)/directory/(?P.+)/', - 'api-revision-directory') + 'api-revision-directory', checksum_args=['sha1_git']) @api_doc('/revision/directory/') def api_revision_directory(request, sha1_git, dir_path=None, @@ -386,10 +387,11 @@ with_data=with_data) -@api_route(r'/revision/(?P[0-9a-f]+)/log/', 'api-revision-log') +@api_route(r'/revision/(?P[0-9a-f]+)/log/', 'api-revision-log', + checksum_args=['sha1_git']) @api_route(r'/revision/(?P[0-9a-f]+)' - r'/prev/(?P[0-9a-f/]+)/log/', - 'api-revision-log') + r'/prev/(?P[0-9a-f]*/*)/log/', + 'api-revision-log', checksum_args=['sha1_git', 'prev_sha1s']) @api_doc('/revision/log/') def api_revision_log(request, sha1_git, prev_sha1s=None): """ diff --git a/swh/web/api/views/snapshot.py b/swh/web/api/views/snapshot.py --- a/swh/web/api/views/snapshot.py +++ b/swh/web/api/views/snapshot.py @@ -12,7 +12,8 @@ from swh.web.api.views.utils import api_lookup -@api_route(r'/snapshot/(?P[0-9a-f]+)/', 'api-snapshot') +@api_route(r'/snapshot/(?P[0-9a-f]+)/', 'api-snapshot', + checksum_args=['snapshot_id']) @api_doc('/snapshot/') def api_snapshot(request, snapshot_id): """ diff --git a/swh/web/browse/browseurls.py b/swh/web/browse/browseurls.py --- a/swh/web/browse/browseurls.py +++ b/swh/web/browse/browseurls.py @@ -24,9 +24,10 @@ reverse the url """ # noqa - def __init__(self, *url_patterns, view_name=None): + def __init__(self, *url_patterns, view_name=None, checksum_args=None): super().__init__() self.url_patterns = [] + self.checksum_args = checksum_args for url_pattern in url_patterns: self.url_patterns.append('^' + url_pattern + '$') self.view_name = view_name @@ -35,4 +36,10 @@ # register the route and its view in the browse endpoints index for url_pattern in self.url_patterns: BrowseUrls.add_url_pattern(url_pattern, f, self.view_name) + + if self.checksum_args: + BrowseUrls.add_redirect_for_checksum_args(self.view_name, + self.url_patterns, + self.checksum_args) + return f diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py --- a/swh/web/browse/views/content.py +++ b/swh/web/browse/views/content.py @@ -27,8 +27,9 @@ from swh.web.browse.browseurls import browse_route -@browse_route(r'content/(?P.+)/raw/', - view_name='browse-content-raw') +@browse_route(r'content/(?P[0-9a-z_:]*[0-9a-f]+.)/raw/', + view_name='browse-content-raw', + checksum_args=['query_string']) def content_raw(request, query_string): """Django view that produces a raw display of a content identified by its hash value. @@ -162,8 +163,9 @@ return HttpResponse(diff_data_json, content_type='application/json') -@browse_route(r'content/(?P.+)/', - view_name='browse-content') +@browse_route(r'content/(?P[0-9a-z_:]*[0-9a-f]+.)/', + view_name='browse-content', + checksum_args=['query_string']) def content_display(request, query_string): """Django view that produces an HTML display of a content identified by its hash value. diff --git a/swh/web/browse/views/directory.py b/swh/web/browse/views/directory.py --- a/swh/web/browse/views/directory.py +++ b/swh/web/browse/views/directory.py @@ -23,7 +23,8 @@ @browse_route(r'directory/(?P[0-9a-f]+)/', r'directory/(?P[0-9a-f]+)/(?P.+)/', - view_name='browse-directory') + view_name='browse-directory', + checksum_args=['sha1_git']) def directory_browse(request, sha1_git, path=None): """Django view for browsing the content of a directory identified by its sha1_git value. diff --git a/swh/web/browse/views/release.py b/swh/web/browse/views/release.py --- a/swh/web/browse/views/release.py +++ b/swh/web/browse/views/release.py @@ -19,7 +19,8 @@ @browse_route(r'release/(?P[0-9a-f]+)/', - view_name='browse-release') + view_name='browse-release', + checksum_args=['sha1_git']) def release_browse(request, sha1_git): """ Django view that produces an HTML display of a release diff --git a/swh/web/browse/views/revision.py b/swh/web/browse/views/revision.py --- a/swh/web/browse/views/revision.py +++ b/swh/web/browse/views/revision.py @@ -137,7 +137,8 @@ @browse_route(r'revision/(?P[0-9a-f]+)/diff/', - view_name='diff-revision') + view_name='diff-revision', + checksum_args=['sha1_git']) def _revision_diff(request, sha1_git): """ Browse internal endpoint to compute revision diff @@ -175,7 +176,8 @@ @browse_route(r'revision/(?P[0-9a-f]+)/log/', - view_name='browse-revision-log') + view_name='browse-revision-log', + checksum_args=['sha1_git']) def revision_log_browse(request, sha1_git): """ Django view that produces an HTML display of the history @@ -254,7 +256,8 @@ @browse_route(r'revision/(?P[0-9a-f]+)/', r'revision/(?P[0-9a-f]+)/(?P.+)/', - view_name='browse-revision') + view_name='browse-revision', + checksum_args=['sha1_git']) def revision_browse(request, sha1_git, extra_path=None): """ Django view that produces an HTML display of a revision diff --git a/swh/web/browse/views/snapshot.py b/swh/web/browse/views/snapshot.py --- a/swh/web/browse/views/snapshot.py +++ b/swh/web/browse/views/snapshot.py @@ -17,7 +17,8 @@ @browse_route(r'snapshot/(?P[0-9a-f]+)/', - view_name='browse-snapshot') + view_name='browse-snapshot', + checksum_args=['snapshot_id']) def snapshot_browse(request, snapshot_id): """Django view for browsing the content of a snapshot. @@ -31,7 +32,8 @@ @browse_route(r'snapshot/(?P[0-9a-f]+)/directory/', r'snapshot/(?P[0-9a-f]+)/directory/(?P.+)/', - view_name='browse-snapshot-directory') + view_name='browse-snapshot-directory', + checksum_args=['snapshot_id']) def snapshot_directory_browse(request, snapshot_id, path=None): """Django view for browsing the content of a directory collected in a snapshot. @@ -48,7 +50,8 @@ @browse_route(r'snapshot/(?P[0-9a-f]+)/content/(?P.+)/', - view_name='browse-snapshot-content') + view_name='browse-snapshot-content', + checksum_args=['snapshot_id']) def snapshot_content_browse(request, snapshot_id, path): """Django view that produces an HTML display of a content collected in a snapshot. @@ -59,7 +62,8 @@ @browse_route(r'snapshot/(?P[0-9a-f]+)/log/', - view_name='browse-snapshot-log') + view_name='browse-snapshot-log', + checksum_args=['snapshot_id']) def snapshot_log_browse(request, snapshot_id): """Django view that produces an HTML display of revisions history (aka the commit log) collected in a snapshot. @@ -70,7 +74,8 @@ @browse_route(r'snapshot/(?P[0-9a-f]+)/branches/', - view_name='browse-snapshot-branches') + view_name='browse-snapshot-branches', + checksum_args=['snapshot_id']) def snapshot_branches_browse(request, snapshot_id): """Django view that produces an HTML display of the list of releases collected in a snapshot. @@ -81,7 +86,8 @@ @browse_route(r'snapshot/(?P[0-9a-f]+)/releases/', - view_name='browse-snapshot-releases') + view_name='browse-snapshot-releases', + checksum_args=['snapshot_id']) def snapshot_releases_browse(request, snapshot_id): """Django view that produces an HTML display of the list of releases collected in a snapshot. diff --git a/swh/web/common/urlsindex.py b/swh/web/common/urlsindex.py --- a/swh/web/common/urlsindex.py +++ b/swh/web/common/urlsindex.py @@ -4,6 +4,7 @@ # See top-level LICENSE file for more information from django.conf.urls import url +from django.shortcuts import redirect class UrlsIndex(object): @@ -19,7 +20,7 @@ scope = 'default' @classmethod - def add_url_pattern(cls, url_pattern, view, view_name): + def add_url_pattern(cls, url_pattern, view, view_name=None): """ Class method that adds an url pattern to the current scope. @@ -37,6 +38,33 @@ cls._urlpatterns[cls.scope].append(url(url_pattern, view)) @classmethod + def add_redirect_for_checksum_args(cls, view_name, url_patterns, + checksum_args): + """ + Class method that redirects to view with lowercase checksums + when upper/mixed case checksums are passed as url arguments. + + Args: + view_name (str): name of the view to redirect requests + url_patterns (List[str]): regexps describing the view urls + checksum_args (List[str]): url argument names corresponding + to checksum values + """ + new_view_name = view_name+'-uppercase-checksum' + for url_pattern in url_patterns: + url_pattern_upper = url_pattern.replace('[0-9a-f]', + '[0-9a-fA-F]') + + def view_redirect(request, *args, **kwargs): + for checksum_arg in checksum_args: + checksum_upper = kwargs[checksum_arg] + kwargs[checksum_arg] = checksum_upper.lower() + return redirect(view_name, *args, **kwargs) + + cls.add_url_pattern(url_pattern_upper, view_redirect, + new_view_name) + + @classmethod def get_url_patterns(cls): """ Class method that returns the list of url pattern associated to diff --git a/swh/web/tests/api/views/test_content.py b/swh/web/tests/api/views/test_content.py --- a/swh/web/tests/api/views/test_content.py +++ b/swh/web/tests/api/views/test_content.py @@ -373,3 +373,16 @@ ], 'search_stats': {'nbfiles': 1, 'pct': 0.0} }) + + @given(content()) + def test_api_content_uppercase(self, content): + url = reverse('api-content-uppercase-checksum', + url_args={'q': content['sha1'].upper()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('api-content', + url_args={'q': content['sha1']}) + + self.assertEqual(resp['location'], redirect_url) diff --git a/swh/web/tests/api/views/test_directory.py b/swh/web/tests/api/views/test_directory.py --- a/swh/web/tests/api/views/test_directory.py +++ b/swh/web/tests/api/views/test_directory.py @@ -74,6 +74,19 @@ 'reason': ('Directory entry with path %s from %s not found' % (path, directory))}) + @given(directory()) + def test_api_directory_uppercase(self, directory): + url = reverse('api-directory-uppercase-checksum', + url_args={'sha1_git': directory.upper()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('api-directory', + url_args={'sha1_git': directory}) + + self.assertEqual(resp['location'], redirect_url) + @classmethod def _enrich_dir_data(cls, dir_data): if dir_data['type'] == 'file': diff --git a/swh/web/tests/api/views/test_release.py b/swh/web/tests/api/views/test_release.py --- a/swh/web/tests/api/views/test_release.py +++ b/swh/web/tests/api/views/test_release.py @@ -109,3 +109,16 @@ 'exception': 'NotFoundExc', 'reason': 'Release with sha1_git %s not found.' % unknown_release }) + + @given(release()) + def test_api_release_uppercase(self, release): + url = reverse('api-release-uppercase-checksum', + url_args={'sha1_git': release.upper()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('api-release-uppercase-checksum', + url_args={'sha1_git': release}) + + self.assertEqual(resp['location'], redirect_url) diff --git a/swh/web/tests/api/views/test_revision.py b/swh/web/tests/api/views/test_revision.py --- a/swh/web/tests/api/views/test_revision.py +++ b/swh/web/tests/api/views/test_revision.py @@ -524,3 +524,16 @@ revision['parents'] = parents_id_url return revision + + @given(revision()) + def test_api_revision_uppercase(self, revision): + url = reverse('api-revision-uppercase-checksum', + url_args={'sha1_git': revision.upper()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('api-revision', + url_args={'sha1_git': revision}) + + self.assertEqual(resp['location'], redirect_url) diff --git a/swh/web/tests/api/views/test_snapshot.py b/swh/web/tests/api/views/test_snapshot.py --- a/swh/web/tests/api/views/test_snapshot.py +++ b/swh/web/tests/api/views/test_snapshot.py @@ -158,3 +158,16 @@ _get_branch_url(alias_target_type, alias_target) return snapshot + + @given(snapshot()) + def test_api_snapshot_uppercase(self, snapshot): + url = reverse('api-snapshot-uppercase-checksum', + url_args={'snapshot_id': snapshot.upper()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('api-snapshot-uppercase-checksum', + url_args={'snapshot_id': snapshot}) + + self.assertEqual(resp['location'], redirect_url) diff --git a/swh/web/tests/browse/views/test_content.py b/swh/web/tests/browse/views/test_content.py --- a/swh/web/tests/browse/views/test_content.py +++ b/swh/web/tests/browse/views/test_content.py @@ -348,3 +348,15 @@ return prepare_content_for_display(content_data, mime_type, content['path']) + + @given(content()) + def test_content_uppercase(self, content): + url = reverse('browse-content-uppercase-checksum', + url_args={'query_string': content['sha1'].upper()}) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('browse-content', + url_args={'query_string': content['sha1']}) + + self.assertEqual(resp['location'], redirect_url) diff --git a/swh/web/tests/browse/views/test_directory.py b/swh/web/tests/browse/views/test_directory.py --- a/swh/web/tests/browse/views/test_directory.py +++ b/swh/web/tests/browse/views/test_directory.py @@ -116,3 +116,16 @@ resp = self.client.get(dir_url) self.assertEqual(resp.status_code, 404) self.assertTemplateUsed('browse/error.html') + + @given(directory()) + def test_directory_uppercase(self, directory): + url = reverse('browse-directory-uppercase-checksum', + url_args={'sha1_git': directory.upper()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('browse-directory', + url_args={'sha1_git': directory}) + + self.assertEqual(resp['location'], redirect_url) diff --git a/swh/web/tests/browse/views/test_release.py b/swh/web/tests/browse/views/test_release.py --- a/swh/web/tests/browse/views/test_release.py +++ b/swh/web/tests/browse/views/test_release.py @@ -98,3 +98,16 @@ url_args={'swh_id': swh_rel_id}) self.assertContains(resp, swh_rel_id) self.assertContains(resp, swh_rel_id_url) + + @given(release()) + def test_release_uppercase(self, release): + url = reverse('browse-release-uppercase-checksum', + url_args={'sha1_git': release.upper()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('browse-release', + url_args={'sha1_git': release}) + + self.assertEqual(resp['location'], redirect_url) diff --git a/swh/web/tests/browse/views/test_revision.py b/swh/web/tests/browse/views/test_revision.py --- a/swh/web/tests/browse/views/test_revision.py +++ b/swh/web/tests/browse/views/test_revision.py @@ -247,3 +247,16 @@ self.assertTemplateUsed('error.html') self.assertContains(resp, 'the origin mentioned in your request' ' appears broken', status_code=404) + + @given(revision()) + def test_revision_uppercase(self, revision): + url = reverse('browse-revision-uppercase-checksum', + url_args={'sha1_git': revision.upper()}) + + resp = self.client.get(url) + self.assertEqual(resp.status_code, 302) + + redirect_url = reverse('browse-revision', + url_args={'sha1_git': revision}) + + self.assertEqual(resp['location'], redirect_url)