[geany/www.geany.org] 0d8f8d: Add URL shortening service

Enrico Tröger git-noreply at xxxxx
Tue Aug 13 20:28:04 UTC 2019


Branch:      refs/heads/master
Author:      Enrico Tröger <enrico.troeger at uvena.de>
Committer:   Enrico Tröger <enrico.troeger at uvena.de>
Date:        Tue, 13 Aug 2019 20:28:04 UTC
Commit:      0d8f8d74e0abd9519e291d4b8917b65613d7e5c5
             https://github.com/geany/www.geany.org/commit/0d8f8d74e0abd9519e291d4b8917b65613d7e5c5

Log Message:
-----------
Add URL shortening service

Long URLs can be shorted by users using
https://geany.org/s/api/create/. Shortened URLs look like
https://geany.org/s/vLiex/.
This is used by the Git 2 IRC script to get rid of the tiny.cc service.


Modified Paths:
--------------
    geany/settings.py
    geany/urls.py
    requirements.txt
    urlshortener/__init__.py
    urlshortener/apps.py
    urlshortener/urls.py
    urlshortener/views.py

Modified: geany/settings.py
2 lines changed, 2 insertions(+), 0 deletions(-)
===================================================================
@@ -323,11 +323,13 @@
     "static_docs.apps.StaticDocsAppConfig",
     "pastebin.apps.PastebinAppConfig",
     "nightlybuilds.apps.NightlyBuildsAppConfig",
+    "urlshortener.apps.UrlShortenerAppConfig",
 
     # 3rd party
     "honeypot",     # for pastebin
     "mezzanine_pagedown",
     "mezzanine_sync_pages.apps.MezzanineSyncPagesAppConfig",
+    "shortener.apps.ShortenerConfig",
 )
 
 # List of middleware classes to use. Order is important; in the request phase,


Modified: geany/urls.py
3 lines changed, 3 insertions(+), 0 deletions(-)
===================================================================
@@ -62,6 +62,9 @@
     # Pastebin
     url(r"^p/", include("pastebin.urls")),
 
+    # URL Shortener
+    url(r'^s/', include('urlshortener.urls')),
+
     # /news/ News
     url(r"^news/", include("news.urls")),
 


Modified: requirements.txt
1 lines changed, 1 insertions(+), 0 deletions(-)
===================================================================
@@ -4,6 +4,7 @@ mysqlclient
 django-compressor
 django-extensions
 django-honeypot
+django-link-shortener
 django-log-request-id
 django-memcache-status
 mezzanine-pagedown


Modified: urlshortener/__init__.py
0 lines changed, 0 insertions(+), 0 deletions(-)
===================================================================
No diff available, check online


Modified: urlshortener/apps.py
20 lines changed, 20 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,20 @@
+# coding: utf-8
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.apps import AppConfig
+
+
+class UrlShortenerAppConfig(AppConfig):
+    name = 'urlshortener'
+    verbose_name = "URL Shortener"


Modified: urlshortener/urls.py
29 lines changed, 29 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,29 @@
+# coding: utf-8
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.urls import path
+from shortener.views import expand as ShortenerExpandView
+
+from urlshortener.views import UrlShortenerAPIView
+
+
+urlpatterns = (  # pylint: disable=invalid-name
+    path(
+        'api/create/',
+        UrlShortenerAPIView.as_view(),
+        name='url_shortener_api_create'
+    ),
+
+    path('<link>/', ShortenerExpandView, name='url_shortener_expand'),
+)


Modified: urlshortener/views.py
141 lines changed, 141 insertions(+), 0 deletions(-)
===================================================================
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+# LICENCE: This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import logging
+
+from django.contrib.auth.models import User
+from django.core.validators import URLValidator
+from django.http import JsonResponse
+from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import never_cache
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_POST
+from django.views.generic.base import View
+from shortener import shortener
+
+
+logger = logging.getLogger(__name__)  # pylint: disable=invalid-name
+
+
+ at method_decorator(csrf_exempt, name='dispatch')
+ at method_decorator(require_POST, name='dispatch')
+ at method_decorator(never_cache, name='dispatch')
+class UrlShortenerAPIView(View):
+    """
+    Provide a simple create API view for URL shortener app:
+    - JSON API
+    - requires Basic Auth
+    - Input:
+        {
+            "auth": {
+                "username": "username",
+                "password": "secret",
+            },
+            "url": {
+                "fullUrl": "https://..."}
+            }
+        }
+    - Output:
+        {
+            "statusCode": 200,
+            "errorMessage": "...",
+            "url": {
+                "fullUrl": "https://...",
+                "shortUrl": "https://geany.org/s/ABCDEFG/"
+            }
+        }
+    """
+
+    # ----------------------------------------------------------------------
+    def post(self, request):
+        try:
+            request_data = self._parse_request()
+            self._validate_request_data(request_data)
+        except Exception as exc:
+            logger.info('Invalid short url API request: {}'.format(exc))
+            response_body = dict(errorMessage='Invalid JSON: {}'.format(exc), statusCode=400)
+            return JsonResponse(response_body, status=400)
+
+        # authenticate user
+        try:
+            user = self._authenticate_request(request_data)
+        except Exception as exc:
+            # log the exception text but do not pass it to the response
+            logger.info('Unauthorized short url API request: {}'.format(exc))
+            response_body = dict(
+                errorMessage='Unauthorized: Invalid username or password',
+                statusCode=401)
+            return JsonResponse(response_body, status=401)
+
+        full_url = request_data['url']['fullUrl']
+        short_url_code = shortener.create(user, full_url)
+
+        short_url = reverse('url_shortener_expand', args=(short_url_code,))
+        absolute_short_url = request.build_absolute_uri(short_url)
+
+        logger.debug(
+            'Created short URL "{}" for full URL "{}" (user "{}")'.format(
+                absolute_short_url,
+                full_url,
+                user.username))
+        response_body = dict(
+            statusCode=200,
+            url=dict(
+                fullUrl=full_url,
+                shortUrl=absolute_short_url))
+        return JsonResponse(response_body)
+
+    # ----------------------------------------------------------------------
+    def _get_user(self, request_data):
+        username = request_data['auth']['username']
+
+    # ----------------------------------------------------------------------
+    def _parse_request(self):
+        request_body = self.request.body
+        request_data = request_body.decode('utf-8')
+        return json.loads(request_data)
+
+    # ----------------------------------------------------------------------
+    def _validate_request_data(self, request_data):
+        self._validate_request_data_field(request_data, 'auth')
+        self._validate_request_data_field(request_data, 'auth.username')
+        self._validate_request_data_field(request_data, 'auth.password')
+        self._validate_request_data_field(request_data, 'url')
+        self._validate_request_data_field(request_data, 'url.fullUrl')
+
+        validator = URLValidator(schemes=('http', 'https'))
+        validator(request_data['url']['fullUrl'])
+
+    # ----------------------------------------------------------------------
+    def _validate_request_data_field(self, request_data, field_name):
+        field_parts = field_name.split('.')
+        element = request_data
+        for field_part in field_parts:
+            element = element.get(field_part, None)
+            if not element:  # check for None and empty strings on purpose as both are failures
+                raise ValueError('Missing element "{}"'.format(field_name))
+
+    # ----------------------------------------------------------------------
+    def _authenticate_request(self, request_data):
+        username = request_data['auth']['username']
+        password = request_data['auth']['password']
+        # lookup user (throws exception if user does not exist)
+        user = User.objects.get(username=username)
+        # check password
+        if user.check_password(password):
+            return user
+        else:
+            raise ValueError('Invalid password')



--------------
This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).


More information about the Commits mailing list