[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