diff --git a/mypy.ini b/mypy.ini --- a/mypy.ini +++ b/mypy.ini @@ -12,6 +12,9 @@ [mypy-bs4.*] ignore_missing_imports = True +[mypy-corsheaders.*] +ignore_missing_imports = True + [mypy-django_js_reverse.*] ignore_missing_imports = True diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ # Runtime dependencies beautifulsoup4 Django >= 1.11.0, < 2.0 +django-cors-headers djangorestframework >= 3.4.0 django_webpack_loader django_js_reverse @@ -18,6 +19,7 @@ pyyaml requests python-memcached +pybadges # Doc dependencies sphinx diff --git a/swh/web/misc/badges.py b/swh/web/misc/badges.py new file mode 100644 --- /dev/null +++ b/swh/web/misc/badges.py @@ -0,0 +1,173 @@ +# 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 + +from base64 import b64encode +from typing import cast, Optional + +from django.conf.urls import url +from django.contrib.staticfiles import finders +from django.http import HttpResponse, HttpRequest + +from pybadges import badge + +from swh.model.exceptions import ValidationError +from swh.model.identifiers import ( + persistent_identifier, parse_persistent_identifier, + CONTENT, DIRECTORY, ORIGIN, RELEASE, REVISION, SNAPSHOT +) +from swh.web.common import service +from swh.web.common.exc import BadInputExc, NotFoundExc +from swh.web.common.utils import reverse, resolve_swh_persistent_id + + +_orange = '#f36a24' +_yellow = '#fac11f' +_red = '#cd5741' + +_swh_logo_data = None + +_badge_config = { + CONTENT: { + 'color': _yellow, + 'title': 'Archived source file', + }, + DIRECTORY: { + 'color': _yellow, + 'title': 'Archived source tree', + }, + ORIGIN: { + 'color': _orange, + 'title': 'Archived software repository', + }, + RELEASE: { + 'color': _yellow, + 'title': 'Archived software release', + }, + REVISION: { + 'color': _yellow, + 'title': 'Archived commit', + }, + SNAPSHOT: { + 'color': _yellow, + 'title': 'Archived software repository snapshot', + }, + 'error': { + 'color': _red, + 'title': 'An error occurred when generating the badge' + } +} + + +def _get_logo_data() -> str: + """ + Get data-URI for Software Heritage SVG logo to embed it in + the generated badges. + """ + global _swh_logo_data + if _swh_logo_data is None: + swh_logo_path = cast(str, finders.find('img/swh-logo.svg')) + with open(swh_logo_path, 'rb') as swh_logo_file: + _swh_logo_data = ('data:image/svg+xml;base64,%s' % + b64encode(swh_logo_file.read()).decode('ascii')) + return _swh_logo_data + + +def _swh_badge(request: HttpRequest, object_type: str, object_id: str, + object_pid: Optional[str] = '') -> HttpResponse: + """ + Generate a Software Heritage badge for a given object type and id. + + Args: + request: input http request + object_type: The type of swh object to generate a badge for, + either *content*, *directory*, *revision*, *release*, *origin* + or *snapshot* + object_id: The id of the swh object, either an url for origin + type or a *sha1* for other object types + object_pid: If provided, the object persistent + identifier will not be recomputed + + Returns: + HTTP response with content type *image/svg+xml* containing the SVG + badge data. If the provided parameters are invalid, HTTP 400 status + code will be returned. If the object can not be found in the archive, + HTTP 404 status code will be returned. + + """ + left_text = 'error' + whole_link = '' + status = 200 + + try: + if object_type == ORIGIN: + service.lookup_origin({'url': object_id}) + right_text = 'repository' + whole_link = reverse('browse-origin', + url_args={'origin_url': object_id}) + else: + # when pid is provided, object type and id will be parsed + # from it + if object_pid: + parsed_pid = parse_persistent_identifier(object_pid) + object_type = parsed_pid.object_type + object_id = parsed_pid.object_id + swh_object = service.lookup_object(object_type, object_id) + if object_pid: + right_text = object_pid + else: + right_text = persistent_identifier(object_type, object_id) + + whole_link = resolve_swh_persistent_id(right_text)['browse_url'] + # remove pid metadata if any for badge text + if object_pid: + right_text = right_text.split(';')[0] + # use release name for badge text + if object_type == RELEASE: + right_text = 'release %s' % swh_object['name'] + left_text = 'archived' + except (BadInputExc, ValidationError): + right_text = f'invalid {object_type if object_type else "object"} id' + status = 400 + object_type = 'error' + except NotFoundExc: + right_text = f'{object_type if object_type else "object"} not found' + status = 404 + object_type = 'error' + + badge_data = badge(left_text=left_text, + right_text=right_text, + right_color=_badge_config[object_type]['color'], + whole_link=request.build_absolute_uri(whole_link), + whole_title=_badge_config[object_type]['title'], + logo=_get_logo_data(), + embed_logo=True) + + return HttpResponse(badge_data, content_type='image/svg+xml', + status=status) + + +def _swh_badge_pid(request: HttpRequest, object_pid: str) -> HttpResponse: + """ + Generate a Software Heritage badge for a given object persistent + identifier. + + Args: + request (django.http.HttpRequest): input http request + object_pid (str): A swh object persistent identifier + + Returns: + django.http.HttpResponse: An http response with content type + *image/svg+xml* containing the SVG badge data. If any error + occurs, a status code of 400 will be returned. + """ + return _swh_badge(request, '', '', object_pid) + + +urlpatterns = [ + url(r'^badge/(?P[a-z]+)/(?P.+)/$', _swh_badge, + name='swh-badge'), + url(r'^badge/(?Pswh:[0-9]+:[a-z]+:[0-9a-f]+.*)/$', + _swh_badge_pid, name='swh-badge-pid'), +] diff --git a/swh/web/misc/urls.py b/swh/web/misc/urls.py --- a/swh/web/misc/urls.py +++ b/swh/web/misc/urls.py @@ -45,6 +45,7 @@ url(r'^jslicenses/$', _jslicenses, name='jslicenses'), url(r'^', include('swh.web.misc.origin_save')), url(r'^stat_counters', _stat_counters, name='stat-counters'), + url(r'^', include('swh.web.misc.badges')), ] diff --git a/swh/web/settings/common.py b/swh/web/settings/common.py --- a/swh/web/settings/common.py +++ b/swh/web/settings/common.py @@ -43,12 +43,14 @@ 'swh.web.api', 'swh.web.browse', 'webpack_loader', - 'django_js_reverse' + 'django_js_reverse', + 'corsheaders' ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -277,3 +279,6 @@ } JS_REVERSE_JS_MINIFY = False + +CORS_ORIGIN_ALLOW_ALL = True +CORS_URLS_REGEX = r'^/badge/.*$' diff --git a/swh/web/static/img/swh-logo.svg b/swh/web/static/img/swh-logo.svg --- a/swh/web/static/img/swh-logo.svg +++ b/swh/web/static/img/swh-logo.svg @@ -7,15 +7,16 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg2" version="1.1" inkscape:version="0.92.1 r15371" xml:space="preserve" - width="78" - height="78" - viewBox="0 0 78.000001 78" + width="78.270248" + height="77.394997" + viewBox="0 0 78.27025 77.394997" sodipodi:docname="swh-logo.svg" inkscape:export-filename="swh-logo.png" inkscape:export-xdpi="630.15387" @@ -34,7 +35,7 @@ x2="1" y2="0" gradientUnits="userSpaceOnUse" - gradientTransform="matrix(61.305366,0,0,-61.305366,13.32883,76.436119)" + gradientTransform="matrix(61.305366,0,0,-61.305366,13.87633,76.436119)" spreadMethod="pad" id="linearGradient24">', status_code=status_code) + assert_contains(response, _get_logo_data(), status_code=status_code) + + if not object_type: + object_type = 'object' + + if object_type == ORIGIN and status_code == 200: + link = reverse('browse-origin', url_args={'origin_url': object_id}) + text = 'repository' + elif status_code == 200: + text = persistent_identifier(object_type, object_id) + link = resolve_swh_persistent_id(text)['browse_url'] + if object_type == RELEASE: + release = service.lookup_release(object_id) + text = release['name'] + elif status_code == 400: + text = 'error' + link = f'invalid {object_type} id' + object_type = 'error' + elif status_code == 404: + text = 'error' + link = f'{object_type} not found' + object_type = 'error' + + assert_contains(response, _badge_config[object_type]['color'], + status_code=status_code) + assert_contains(response, _badge_config[object_type]['title'], + status_code=status_code) + assert_contains(response, text, status_code=status_code) + assert_contains(response, link, status_code=status_code)