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,163 @@ +# 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.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' + +_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', + } +} + + +def _get_logo_data() -> str: + """ + Get base64 encoded 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 (django.http.HttpRequest): input http request + object_type (str): The type of swh object to generate a badge for, + either *content*, *directory*, *revision*, *release*, *origin* + or *snapshot* + object_id (str): The id of the swh object, either an url for origin + type or a *sha1* for other object types + object_pid (Optional[str]): If provided, the object persistent + identifier will not be recomputed + + 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. + + """ + right_text = '' + 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: + 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'] + except BadInputExc: + status = 400 + except NotFoundExc: + status = 404 + + badge_data = None + if right_text: + badge_data = badge(left_text='archived', + 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. + """ + object_type = '' + object_id = '' + try: + parsed_pid = parse_persistent_identifier(object_pid) + object_type = parsed_pid.object_type + object_id = parsed_pid.object_id + except Exception: + pass + return _swh_badge(request, object_type, object_id, 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">') + assert_contains(response, _get_logo_data()) + assert_contains(response, _badge_config[object_type]['color']) + assert_contains(response, _badge_config[object_type]['title']) + if object_type == ORIGIN: + link = reverse('browse-origin', url_args={'origin_url': object_id}) + text = 'repository' + else: + 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'] + + assert_contains(response, text) + assert_contains(response, link)