From c03c5a6e39b0dc24605a08e8adb5910c2b5783e5 Mon Sep 17 00:00:00 2001
From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com>
Date: Thu, 16 May 2024 12:10:23 +0300
Subject: [PATCH 1/8] Allow download from QGIS user agent only
---
dockerize/sites-enabled/prod-ssl.conf | 13 +++++++++++++
dockerize/sites-enabled/prod.conf | 6 ++++++
2 files changed, 19 insertions(+)
diff --git a/dockerize/sites-enabled/prod-ssl.conf b/dockerize/sites-enabled/prod-ssl.conf
index 589715f1..bca09fd1 100644
--- a/dockerize/sites-enabled/prod-ssl.conf
+++ b/dockerize/sites-enabled/prod-ssl.conf
@@ -90,6 +90,13 @@ server {
}
}
+ # New rule to allow download from QGIS user agent only
+ location ~* ^/plugins/[^/]+/version/[^/]+/download/$ {
+ if ($http_user_agent !~* "Mozilla/5.0 QGIS") {
+ return 403 "Forbidden: Please use QGIS to download .zip files.";
+ }
+ }
+
location /metabase/ {
# set to webroot path
proxy_pass http://metabase:3000/;
@@ -209,6 +216,12 @@ server {
}
+ # New rule to the download from QGIS user agent only
+ location ~* ^/plugins/[^/]+/version/[^/]+/download/$ {
+ if ($http_user_agent !~* "Mozilla/5.0 QGIS") {
+ return 403 "Forbidden: Please use QGIS to download .zip files.";
+ }
+ }
location /metabase/ {
# set to webroot path
diff --git a/dockerize/sites-enabled/prod.conf b/dockerize/sites-enabled/prod.conf
index 086d6903..830bfe88 100644
--- a/dockerize/sites-enabled/prod.conf
+++ b/dockerize/sites-enabled/prod.conf
@@ -88,6 +88,12 @@ server {
}
+ # New rule to the download from QGIS user agent only
+ location ~* ^/plugins/[^/]+/version/[^/]+/download/$ {
+ if ($http_user_agent !~* "Mozilla/5.0 QGIS") {
+ return 403 "Forbidden: Please use QGIS to download .zip files.";
+ }
+ }
location /metabase/ {
# set to webroot path
From 7c810e82567f9abeb191adce99a32c8a9de34802 Mon Sep 17 00:00:00 2001
From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com>
Date: Thu, 16 May 2024 14:44:57 +0300
Subject: [PATCH 2/8] Set a rate limit for nginx
---
dockerize/sites-enabled/prod-ssl.conf | 12 ++++++++++++
dockerize/sites-enabled/prod.conf | 8 ++++++++
2 files changed, 20 insertions(+)
diff --git a/dockerize/sites-enabled/prod-ssl.conf b/dockerize/sites-enabled/prod-ssl.conf
index bca09fd1..cd79e807 100644
--- a/dockerize/sites-enabled/prod-ssl.conf
+++ b/dockerize/sites-enabled/prod-ssl.conf
@@ -4,6 +4,9 @@ upstream uwsgi {
server uwsgi:8080;
}
+# Define the rate limit zone
+limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
+
server {
# OTF gzip compression
gzip on;
@@ -66,6 +69,11 @@ server {
}
# Finally, send all non-media requests to the Django server.
location / {
+
+ # Apply rate limit
+ limit_req zone=one burst=20 nodelay;
+ limit_req_status 429;
+
uwsgi_pass uwsgi;
# the uwsgi_params file you installed needs to be passed with each
# request.
@@ -191,6 +199,10 @@ server {
}
# Finally, send all non-media requests to the Django server.
location / {
+ # Apply rate limit
+ limit_req zone=one burst=20 nodelay;
+ limit_req_status 429;
+
uwsgi_pass uwsgi;
# the uwsgi_params file you installed needs to be passed with each
# request.
diff --git a/dockerize/sites-enabled/prod.conf b/dockerize/sites-enabled/prod.conf
index 830bfe88..87eb4350 100644
--- a/dockerize/sites-enabled/prod.conf
+++ b/dockerize/sites-enabled/prod.conf
@@ -4,6 +4,9 @@ upstream uwsgi {
server uwsgi:8080;
}
+# Define the rate limit zone
+limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
+
server {
# OTF gzip compression
gzip on;
@@ -63,6 +66,11 @@ server {
}
# Finally, send all non-media requests to the Django server.
location / {
+
+ # Apply rate limit
+ limit_req zone=one burst=20 nodelay;
+ limit_req_status 429;
+
uwsgi_pass uwsgi;
# the uwsgi_params file you installed needs to be passed with each
# request.
From fd7b2df4f98051e1ed4fde8a99c4190de671a414 Mon Sep 17 00:00:00 2001
From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com>
Date: Thu, 16 May 2024 19:03:56 +0300
Subject: [PATCH 3/8] Add an human validation for plugins download
---
.vscode/settings.json | 19 +++
qgis-app/plugins/forms.py | 26 +++-
qgis-app/plugins/models.py | 2 +-
.../templates/plugins/plugin_download.html | 114 ++++++++++++++++++
.../plugins/plugin_download_success.html | 42 +++++++
.../templates/plugins/plugin_list.html | 4 +-
.../templates/plugins/version_detail.html | 2 +-
qgis-app/plugins/urls.py | 6 +
qgis-app/plugins/views.py | 36 +++++-
9 files changed, 244 insertions(+), 7 deletions(-)
create mode 100644 .vscode/settings.json
create mode 100644 qgis-app/plugins/templates/plugins/plugin_download.html
create mode 100644 qgis-app/plugins/templates/plugins/plugin_download_success.html
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..b189a5aa
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,19 @@
+{
+ "files.associations": {
+ "**/*.html": "html",
+ "**/templates/*/*.html": "django-html",
+ "**/templates/*": "django-txt",
+ "**/requirements{/**,*}.{txt,in}": "pip-requirements"
+ },
+
+ "emmet.includeLanguages": {
+ "django-html": "html"
+ },
+ "beautify.language": {
+ "html": [
+ "htm",
+ "html",
+ "django-html"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/qgis-app/plugins/forms.py b/qgis-app/plugins/forms.py
index 9f98f03f..a2b882b1 100644
--- a/qgis-app/plugins/forms.py
+++ b/qgis-app/plugins/forms.py
@@ -269,4 +269,28 @@ class Meta:
model = PluginOutstandingToken
fields = (
"description",
- )
\ No newline at end of file
+ )
+
+class VersionDownloadForm(forms.Form):
+ """Download confirmation for a plugin version"""
+ required_css_class = "required"
+ plugin_name = forms.CharField(
+ label=_("Confirm plugin name"),
+ required=True,
+ help_text=_(
+ "Please insert the plugin name shown above to proceed with the download."
+ ),
+ widget=forms.TextInput,
+ )
+ def __init__(self, *args, original_name=None, **kwargs):
+ super(VersionDownloadForm, self).__init__(*args, **kwargs)
+ self.original_name = original_name
+
+ def clean(self):
+ """
+ Check if plugin name match
+ """
+ super().clean()
+ if self.cleaned_data.get("plugin_name") != self.original_name:
+ raise ValidationError(_("Plugin name mismatch: Please ensure the plugin name matches the one displayed above in order to proceed with the download."))
+ return super(VersionDownloadForm, self).clean()
\ No newline at end of file
diff --git a/qgis-app/plugins/models.py b/qgis-app/plugins/models.py
index 72e8cc5a..b229c9b0 100644
--- a/qgis-app/plugins/models.py
+++ b/qgis-app/plugins/models.py
@@ -844,7 +844,7 @@ def get_absolute_url(self):
def get_download_url(self):
return reverse(
- "version_download",
+ "version_get",
args=(
self.plugin.package_name,
self.version,
diff --git a/qgis-app/plugins/templates/plugins/plugin_download.html b/qgis-app/plugins/templates/plugins/plugin_download.html
new file mode 100644
index 00000000..5c2ce708
--- /dev/null
+++ b/qgis-app/plugins/templates/plugins/plugin_download.html
@@ -0,0 +1,114 @@
+{% extends 'plugins/plugin_base.html' %}{% load i18n %}
+{% load local_timezone %}
+{% block content %}
+
+
+
{% trans "Plugin: " %}
+
+ {% trans "Copy to clipboard" %}
+
+ {{plugin_name}}
+
+
+
+
+
+
+{% if form.errors %}
+
+
+
{% trans "The form contains errors and cannot be submitted, please check the fields highlighted in red." %}
+
+{% endif %}
+{% if form.non_field_errors %}
+
+
+ {% for error in form.non_field_errors %}
+
{{ error }}
+ {% endfor %}
+
+{% endif %}
+
+{% endblock %}
+
+
+{% block extrajs %}
+{{ block.super }}
+
+{% endblock %}
+{% block extracss %}
+{{ block.super }}
+
+{% endblock %}
\ No newline at end of file
diff --git a/qgis-app/plugins/templates/plugins/plugin_download_success.html b/qgis-app/plugins/templates/plugins/plugin_download_success.html
new file mode 100644
index 00000000..dbf50a2b
--- /dev/null
+++ b/qgis-app/plugins/templates/plugins/plugin_download_success.html
@@ -0,0 +1,42 @@
+{% extends 'plugins/plugin_base.html' %}{% load i18n %} {% load local_timezone
+%} {% block content %}
+
+ The download of the plugin {{plugin_name}} will start shortly...
+
+
+{% endblock %} {% block extrajs %} {{ block.super }}
+
+{% endblock %} {% block extracss %} {{ block.super }}
+
+{% endblock %}
diff --git a/qgis-app/plugins/templates/plugins/plugin_list.html b/qgis-app/plugins/templates/plugins/plugin_list.html
index 788053d6..a322ac4a 100644
--- a/qgis-app/plugins/templates/plugins/plugin_list.html
+++ b/qgis-app/plugins/templates/plugins/plugin_list.html
@@ -104,8 +104,8 @@ {% if title %}{{title}}{% else %}{% trans "All plugins" %}{% endif %}
{{ object.latest_version_date|local_timezone:"SHORT_NATURAL_DAY" }} |
{{ object.created_on|local_timezone:"SHORT" }} |
({{ object.rating_votes }}) |
- {% if object.stable %}{{ object.stable.version }}{% else %}—{% endif %} |
- {% if object.experimental %}{{ object.experimental.version }}{% else %}—{% endif %} |
+ {% if object.stable %}{{ object.stable.version }}{% else %}—{% endif %} |
+ {% if object.experimental %}{{ object.experimental.version }}{% else %}—{% endif %} |
{% if user.is_authenticated %}{% if user in object.editors or user.is_staff %}
{% else %}{% endif %} | {% endif %}
diff --git a/qgis-app/plugins/templates/plugins/version_detail.html b/qgis-app/plugins/templates/plugins/version_detail.html
index b9cd4874..847bf572 100644
--- a/qgis-app/plugins/templates/plugins/version_detail.html
+++ b/qgis-app/plugins/templates/plugins/version_detail.html
@@ -2,7 +2,7 @@
{% load local_timezone %}
{% block content %}
{% trans "Version" %}: {{ version }}
-
+
{% if not version.created_by.is_active and not version.is_from_token %}
diff --git a/qgis-app/plugins/urls.py b/qgis-app/plugins/urls.py
index 3d1c5da3..e7ef4f82 100644
--- a/qgis-app/plugins/urls.py
+++ b/qgis-app/plugins/urls.py
@@ -288,6 +288,12 @@
{},
name="version_download",
),
+ url(
+ r"^(?P
[A-Za-z][A-Za-z0-9-_]+)/version/(?P[^\/]+)/get/$",
+ version_get,
+ {},
+ name="version_get",
+ ),
url(
r"^(?P[A-Za-z][A-Za-z0-9-_]+)/version/(?P[^\/]+)/approve/$",
version_approve,
diff --git a/qgis-app/plugins/views.py b/qgis-app/plugins/views.py
index 20dfca1c..7d1c55e4 100644
--- a/qgis-app/plugins/views.py
+++ b/qgis-app/plugins/views.py
@@ -16,7 +16,7 @@
from django.db.models.expressions import RawSQL
from django.db.models.functions import Lower
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
-from django.shortcuts import get_object_or_404, render
+from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.utils.timezone import now
from django.utils.decorators import method_decorator
@@ -1506,8 +1506,38 @@ def version_feedback_delete(request, package_name, version, feedback):
is_update_succeed: bool = True
return JsonResponse({"success": is_update_succeed})
+def version_get(request, package_name, version):
+ """
+ Download a plugin zip from the browser
+ """
+ plugin = get_object_or_404(Plugin, package_name=package_name)
+ if request.method == "POST":
+ form = VersionDownloadForm(request.POST, original_name=plugin.name)
+ if form.is_valid():
+ file_content, file_name = version_download(request, package_name, version, is_from_web=True)
+ return render(
+ request,
+ "plugins/plugin_download_success.html",
+ {
+ "file_content": file_content,
+ "file_name": file_name,
+ "plugin_name": plugin.name
+ }
+ )
+ else:
+ form = VersionDownloadForm(original_name=plugin.name)
+
+ return render(
+ request,
+ "plugins/plugin_download.html",
+ {
+ "form": form,
+ "plugin_name": plugin.name
+ }
+ )
+
-def version_download(request, package_name, version):
+def version_download(request, package_name, version, is_from_web=False):
"""
Update download counter(s)
"""
@@ -1534,6 +1564,8 @@ def version_download(request, package_name, version):
version.package.file.file.close()
zipfile = open(version.package.file.name, "rb")
file_content = zipfile.read()
+ if is_from_web:
+ return [file_content, f"{version.plugin.package_name}-{version.version}"]
response = HttpResponse(file_content, content_type="application/zip")
response["Content-Disposition"] = "attachment; filename=%s-%s.zip" % (
version.plugin.package_name,
From 59443dc3603d0efe52b767f61f1abbc7005011a1 Mon Sep 17 00:00:00 2001
From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com>
Date: Thu, 16 May 2024 19:46:03 +0300
Subject: [PATCH 4/8] Limit download from web to 10 per minute
---
.../plugins/plugin_download_limit_exceed.html | 17 +++++++++++++++++
qgis-app/plugins/views.py | 16 ++++++++++++++++
qgis-app/settings.py | 4 ++--
3 files changed, 35 insertions(+), 2 deletions(-)
create mode 100644 qgis-app/plugins/templates/plugins/plugin_download_limit_exceed.html
diff --git a/qgis-app/plugins/templates/plugins/plugin_download_limit_exceed.html b/qgis-app/plugins/templates/plugins/plugin_download_limit_exceed.html
new file mode 100644
index 00000000..e98a3ac1
--- /dev/null
+++ b/qgis-app/plugins/templates/plugins/plugin_download_limit_exceed.html
@@ -0,0 +1,17 @@
+{% extends 'plugins/plugin_base.html' %}{% load i18n %}
+{% block content %}
+ {% trans "Download rate limit exceeded. Try again later." %}
+{% endblock %}
+
+
+{% block extracss %}
+{{ block.super }}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/qgis-app/plugins/views.py b/qgis-app/plugins/views.py
index 7d1c55e4..c8449dff 100644
--- a/qgis-app/plugins/views.py
+++ b/qgis-app/plugins/views.py
@@ -27,6 +27,7 @@
from django.views.decorators.http import require_POST
from django.views.generic.detail import DetailView
from django.db import transaction
+from django.core.cache import cache
# from sortable_listview import SortableListView
from django.views.generic.list import ListView
@@ -1510,11 +1511,26 @@ def version_get(request, package_name, version):
"""
Download a plugin zip from the browser
"""
+
+ ip = request.META['REMOTE_ADDR']
+ key = f"download_limit_{ip}"
+ downloads = cache.get(key, 0)
+
+ if downloads >= 10:
+ response = render(
+ request,
+ "plugins/plugin_download_limit_exceed.html",
+ {}
+ )
+ response.status_code = 429
+ return response
+
plugin = get_object_or_404(Plugin, package_name=package_name)
if request.method == "POST":
form = VersionDownloadForm(request.POST, original_name=plugin.name)
if form.is_valid():
file_content, file_name = version_download(request, package_name, version, is_from_web=True)
+ cache.set(key, downloads + 1, timeout=60) # 60 seconds timeout for 10 requests per minute
return render(
request,
"plugins/plugin_download_success.html",
diff --git a/qgis-app/settings.py b/qgis-app/settings.py
index 23d50828..a440b0a7 100644
--- a/qgis-app/settings.py
+++ b/qgis-app/settings.py
@@ -203,8 +203,8 @@
# See http://docs.djangoproject.com/en/dev/topics/cache/
CACHES = {
"default": {
- #'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
- "BACKEND": "django.core.cache.backends.dummy.DummyCache",
+ "BACKEND": "django.core.cache.backends.db.DatabaseCache",
+ # "BACKEND": "django.core.cache.backends.dummy.DummyCache",
"LOCATION": "cache_table",
}
}
From 0042e59d98473b671c5e602cc047297977d46f99 Mon Sep 17 00:00:00 2001
From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com>
Date: Fri, 17 May 2024 10:23:25 +0300
Subject: [PATCH 5/8] Refactoring, set rate limit in the environment variable
---
dockerize/.env.template | 5 ++++-
dockerize/docker-compose.yml | 1 +
qgis-app/plugins/views.py | 28 +++++++++++++++-------------
qgis-app/settings_docker.py | 5 ++++-
4 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/dockerize/.env.template b/dockerize/.env.template
index 43f8d6bb..7147655c 100644
--- a/dockerize/.env.template
+++ b/dockerize/.env.template
@@ -31,4 +31,7 @@ DEFAULT_PLUGINS_SITE='https://plugins.qgis.org/'
QGISPLUGINS_ENV=debug
# Ldap
-ENABLE_LDAP=True
\ No newline at end of file
+ENABLE_LDAP=True
+
+# Download limit per minute
+DOWNLOAD_RATE_LIMIT=10
\ No newline at end of file
diff --git a/dockerize/docker-compose.yml b/dockerize/docker-compose.yml
index 225f9784..461b9c48 100644
--- a/dockerize/docker-compose.yml
+++ b/dockerize/docker-compose.yml
@@ -53,6 +53,7 @@ services:
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-automation}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD}
- DEFAULT_PLUGINS_SITE=${DEFAULT_PLUGINS_SITE:-https://plugins.qgis.org/}
+ - DOWNLOAD_RATE_LIMIT=${DOWNLOAD_RATE_LIMIT:-10}
volumes:
- ../qgis-app:/home/web/django_project
- ./docker/uwsgi.conf:/uwsgi.conf
diff --git a/qgis-app/plugins/views.py b/qgis-app/plugins/views.py
index c8449dff..4242eabb 100644
--- a/qgis-app/plugins/views.py
+++ b/qgis-app/plugins/views.py
@@ -1516,7 +1516,7 @@ def version_get(request, package_name, version):
key = f"download_limit_{ip}"
downloads = cache.get(key, 0)
- if downloads >= 10:
+ if downloads >= settings.DOWNLOAD_RATE_LIMIT:
response = render(
request,
"plugins/plugin_download_limit_exceed.html",
@@ -1529,8 +1529,8 @@ def version_get(request, package_name, version):
if request.method == "POST":
form = VersionDownloadForm(request.POST, original_name=plugin.name)
if form.is_valid():
- file_content, file_name = version_download(request, package_name, version, is_from_web=True)
- cache.set(key, downloads + 1, timeout=60) # 60 seconds timeout for 10 requests per minute
+ file_content, file_name = _get_file_content(package_name, version)
+ cache.set(key, downloads + 1, timeout=60) # 60 seconds timeout for limited downloads per minute
return render(
request,
"plugins/plugin_download_success.html",
@@ -1553,9 +1553,18 @@ def version_get(request, package_name, version):
)
-def version_download(request, package_name, version, is_from_web=False):
+def version_download(request, package_name, version):
"""
- Update download counter(s)
+ Download a plugin zip from QGIS
+ """
+ file_content, file_name = _get_file_content(package_name, version)
+ response = HttpResponse(file_content, content_type="application/zip")
+ response["Content-Disposition"] = f"attachment; filename={file_name}.zip"
+ return response
+
+def _get_file_content(package_name, version):
+ """
+ Update download counter(s) and return the file content
"""
plugin = get_object_or_404(Plugin, package_name=package_name)
version = get_object_or_404(PluginVersion, plugin=plugin, version=version)
@@ -1580,14 +1589,7 @@ def version_download(request, package_name, version, is_from_web=False):
version.package.file.file.close()
zipfile = open(version.package.file.name, "rb")
file_content = zipfile.read()
- if is_from_web:
- return [file_content, f"{version.plugin.package_name}-{version.version}"]
- response = HttpResponse(file_content, content_type="application/zip")
- response["Content-Disposition"] = "attachment; filename=%s-%s.zip" % (
- version.plugin.package_name,
- version.version,
- )
- return response
+ return [file_content, f"{version.plugin.package_name}-{version.version}"]
def version_detail(request, package_name, version):
diff --git a/qgis-app/settings_docker.py b/qgis-app/settings_docker.py
index c0b7be1e..995d6a62 100644
--- a/qgis-app/settings_docker.py
+++ b/qgis-app/settings_docker.py
@@ -159,4 +159,7 @@
MATOMO_URL="//matomo.qgis.org/"
# Default primary key type
-DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
\ No newline at end of file
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+
+# Download limit per minute
+DOWNLOAD_RATE_LIMIT = int(os.environ.get("DOWNLOAD_RATE_LIMIT", 10))
\ No newline at end of file
From e0d8b000fdd22101d95275ce6b7c150df94361b9 Mon Sep 17 00:00:00 2001
From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com>
Date: Fri, 17 May 2024 10:49:46 +0300
Subject: [PATCH 6/8] Update unit test for web download
---
qgis-app/plugins/tests/test_download.py | 81 +++++++++++++++++++++++++
1 file changed, 81 insertions(+)
diff --git a/qgis-app/plugins/tests/test_download.py b/qgis-app/plugins/tests/test_download.py
index f18f0874..d8ab96be 100644
--- a/qgis-app/plugins/tests/test_download.py
+++ b/qgis-app/plugins/tests/test_download.py
@@ -6,6 +6,11 @@
from plugins.models import Plugin, PluginVersion, PluginVersionDownload
from plugins.views import version_download
+from django.conf import settings
+from django.core.cache import cache
+from django.urls import reverse
+from unittest.mock import patch
+from plugins.forms import VersionDownloadForm
class TestVersionDownloadView(TestCase):
def setUp(self):
@@ -50,3 +55,79 @@ def test_version_download(self):
self.assertEqual(self.version.downloads, 1)
self.assertEqual(self.plugin.downloads, 1)
self.assertEqual(download_record.download_count, 1)
+
+
+class VersionGetViewTest(TestCase):
+ fixtures = [
+ "fixtures/styles.json",
+ "fixtures/auth.json",
+ "fixtures/simplemenu.json",
+ ]
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ self.user = User.objects.create_user(
+ username='testuser',
+ password='12345'
+ )
+
+ self.plugin = Plugin.objects.create(
+ package_name="test-package",
+ created_by=self.user,
+ name="Test Package"
+ )
+
+ self.version = PluginVersion.objects.create(
+ plugin=self.plugin,
+ version="1.0.0",
+ downloads=0,
+ created_by=self.user,
+ package=SimpleUploadedFile("test.zip", b"file_content"),
+ min_qg_version='3.1.1',
+ max_qg_version='3.3.0'
+ )
+ self.url = reverse(
+ 'version_get',
+ kwargs={
+ 'package_name': self.plugin.package_name,
+ 'version': self.version.version
+ }
+ )
+
+ self.ip = '127.0.0.1'
+ self.cache_key = f'download_limit_{self.ip}'
+ settings.DOWNLOAD_RATE_LIMIT = 5 # Set a rate limit for testing
+
+ def tearDown(self):
+ # Clear cache after each test
+ cache.clear()
+
+ def test_rate_limit_not_exceeded(self):
+ response = self.client.get(self.url)
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'plugins/plugin_download.html')
+
+ def test_rate_limit_exceeded(self):
+ # Simulate exceeding the rate limit
+ cache.set(self.cache_key, settings.DOWNLOAD_RATE_LIMIT, timeout=60)
+ response = self.client.get(self.url)
+ self.assertEqual(response.status_code, 429)
+ self.assertTemplateUsed(response, 'plugins/plugin_download_limit_exceed.html')
+
+ def test_successful_download_post(self):
+ # Mocking _get_file_content function
+ with patch('plugins.views._get_file_content') as mock_get_file_content:
+ mock_get_file_content.return_value = ('file_content', 'file.zip')
+ response = self.client.post(self.url, {'plugin_name': 'Test Package'}) # Adjust form data as needed
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'plugins/plugin_download_success.html')
+ self.assertIn('file_content', response.context)
+ self.assertIn('file_name', response.context)
+ self.assertIn('plugin_name', response.context)
+
+ def test_form_display_on_get_request(self):
+ response = self.client.get(self.url)
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'plugins/plugin_download.html')
+ self.assertIsInstance(response.context['form'], VersionDownloadForm)
+ self.assertEqual(response.context['plugin_name'], self.plugin.name)
\ No newline at end of file
From e2e73f33c81c681396b6a4d0bd1af668cbd3d6a3 Mon Sep 17 00:00:00 2001
From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com>
Date: Fri, 17 May 2024 11:51:42 +0300
Subject: [PATCH 7/8] Disable ldap authentication in the env template
---
dockerize/.env.template | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dockerize/.env.template b/dockerize/.env.template
index 7147655c..87086092 100644
--- a/dockerize/.env.template
+++ b/dockerize/.env.template
@@ -31,7 +31,7 @@ DEFAULT_PLUGINS_SITE='https://plugins.qgis.org/'
QGISPLUGINS_ENV=debug
# Ldap
-ENABLE_LDAP=True
+ENABLE_LDAP=False
# Download limit per minute
DOWNLOAD_RATE_LIMIT=10
\ No newline at end of file
From b6fd056d730dbd473a9321d790f31d8f74f1f302 Mon Sep 17 00:00:00 2001
From: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com>
Date: Fri, 17 May 2024 14:15:56 +0300
Subject: [PATCH 8/8] Fix plugin feedback test, clear django cache before next
request
---
.../plugins/tests/test_plugin_version_feedback.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/qgis-app/plugins/tests/test_plugin_version_feedback.py b/qgis-app/plugins/tests/test_plugin_version_feedback.py
index 8f7c7702..bdf17a26 100644
--- a/qgis-app/plugins/tests/test_plugin_version_feedback.py
+++ b/qgis-app/plugins/tests/test_plugin_version_feedback.py
@@ -10,6 +10,7 @@
from plugins.models import Plugin, PluginVersion, PluginVersionFeedback
from plugins.views import version_feedback_notify
from django.conf import settings
+from django.core.cache import cache
class SetupMixin:
fixtures = ["fixtures/auth.json", "fixtures/simplemenu.json"]
@@ -150,6 +151,9 @@ def test_staff_should_see_plugin_feedback_received(self):
self.assertContains(response, "test plugin 1")
self.assertNotContains(response, "test plugin 2")
+ # Clear django cache before sending another request
+ cache.clear()
+
# add feedback for plugin 2
PluginVersionFeedback.objects.create(
version=self.version_2,
@@ -170,6 +174,10 @@ def test_approved_plugin_should_not_show_in_feedback_received_list(self):
list(response.context['object_list']),
[self.plugin_1]
)
+
+ # Clear django cache before sending another request
+ cache.clear()
+
self.version_1.approved = True
self.version_1.save()
response = self.client.get(self.url)
@@ -186,6 +194,10 @@ def setUp(self):
super().setUp()
self.url = reverse("feedback_pending_plugins")
+ def tearDown(self):
+ # Clear cache after each test
+ cache.clear()
+
def test_non_staff_should_not_see_plugin_feedback_pending_list(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
@@ -208,6 +220,9 @@ def test_staff_should_see_plugin_feedback_pending_list(self):
self.assertContains(response, "test plugin 2")
self.assertNotContains(response, "test plugin 1")
+ # Clear django cache before sending another request
+ cache.clear()
+
# add feedback for plugin 2
PluginVersionFeedback.objects.create(
version=self.version_2,