diff --git a/README.rst b/README.rst index 1811226..bc238e1 100644 --- a/README.rst +++ b/README.rst @@ -118,6 +118,8 @@ Quick start } } + # Define a constant in settings.py to specify file upload permissions + CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any" 3. Include the app URLconf in your `project/urls.py` like this: diff --git a/django_ckeditor_5/forms.py b/django_ckeditor_5/forms.py index 07ea7b4..1f71355 100644 --- a/django_ckeditor_5/forms.py +++ b/django_ckeditor_5/forms.py @@ -10,7 +10,7 @@ class UploadFileForm(forms.Form): getattr( settings, "CKEDITOR_5_UPLOAD_FILE_TYPES", - ["jpeg", "png", "gif", "bmp", "webp", "tiff"], + ["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff"], ), ), ], diff --git a/django_ckeditor_5/permissions.py b/django_ckeditor_5/permissions.py new file mode 100644 index 0000000..2327282 --- /dev/null +++ b/django_ckeditor_5/permissions.py @@ -0,0 +1,30 @@ +from django import get_version +from django.conf import settings +from django.http import JsonResponse + +if get_version() >= "4.0": + from django.utils.translation import gettext_lazy as _ +else: + from django.utils.translation import ugettext_lazy as _ + + +def check_upload_permission(view_func): + def _wrapped_view(request, *args, **kwargs): + permission = getattr(settings, "CKEDITOR_5_FILE_UPLOAD_PERMISSION", "staff") + if permission == "staff" and not request.user.is_staff: + return JsonResponse( + { + "error": { + "message": _("You do not have permission to upload files."), + }, + }, + status=403, + ) + if permission == "authenticated" and not request.user.is_authenticated: + return JsonResponse( + {"error": {"message": _("You must be logged in to upload files.")}}, + status=403, + ) + return view_func(request, *args, **kwargs) + + return _wrapped_view diff --git a/django_ckeditor_5/views.py b/django_ckeditor_5/views.py index 0562d91..05d6602 100644 --- a/django_ckeditor_5/views.py +++ b/django_ckeditor_5/views.py @@ -1,6 +1,8 @@ from django import get_version -from django.http import Http404 from django.utils.module_loading import import_string +from django.views.decorators.http import require_POST + +from .permissions import check_upload_permission if get_version() >= "4.0": from django.utils.translation import gettext_lazy as _ @@ -40,8 +42,10 @@ def get_storage_class(): error_msg = f"Invalid default storage class: {default_storage_name}" raise ImproperlyConfigured(error_msg) else: - error_msg = ("Either CKEDITOR_5_FILE_STORAGE, DEFAULT_FILE_STORAGE, " - "or STORAGES['default'] setting is required.") + error_msg = ( + "Either CKEDITOR_5_FILE_STORAGE, DEFAULT_FILE_STORAGE, " + "or STORAGES['default'] setting is required." + ) raise ImproperlyConfigured(error_msg) @@ -61,17 +65,20 @@ def handle_uploaded_file(f): return fs.url(filename) +@require_POST +@check_upload_permission def upload_file(request): - if request.method == "POST" and request.user.is_staff: - form = UploadFileForm(request.POST, request.FILES) - allow_all_file_types = getattr(settings, "CKEDITOR_5_ALLOW_ALL_FILE_TYPES", False) - - if not allow_all_file_types: - try: - image_verify(request.FILES['upload']) - except NoImageException as ex: - return JsonResponse({"error": {"message": f"{ex}"}}, status=400) - if form.is_valid(): - url = handle_uploaded_file(request.FILES["upload"]) - return JsonResponse({"url": url}) - raise Http404(_("Page not found.")) + form = UploadFileForm(request.POST, request.FILES) + allow_all_file_types = getattr(settings, "CKEDITOR_5_ALLOW_ALL_FILE_TYPES", False) + + if not allow_all_file_types: + try: + image_verify(request.FILES["upload"]) + except NoImageException as ex: + return JsonResponse({"error": {"message": f"{ex}"}}, status=400) + + if form.is_valid(): + url = handle_uploaded_file(request.FILES["upload"]) + return JsonResponse({"url": url}) + + return JsonResponse({"error": {"message": _("Invalid form data")}}, status=400) diff --git a/django_ckeditor_5/widgets.py b/django_ckeditor_5/widgets.py index bb15360..3c43a6a 100644 --- a/django_ckeditor_5/widgets.py +++ b/django_ckeditor_5/widgets.py @@ -40,7 +40,8 @@ def __init__(self, config_name="default", attrs=None): def format_error(self, ex): return "{} {}".format( - _("Check the correct settings.CKEDITOR_5_CONFIGS "), str(ex), + _("Check the correct settings.CKEDITOR_5_CONFIGS "), + str(ex), ) class Media: @@ -56,11 +57,11 @@ class Media: configs = getattr(settings, "CKEDITOR_5_CONFIGS", None) if configs is not None: for config in configs: - language = configs[config].get('language') + language = configs[config].get("language") if language: languages = [] - if isinstance(language, dict) and language.get('ui'): - language = language.get('ui') + if isinstance(language, dict) and language.get("ui"): + language = language.get("ui") elif isinstance(language, str): languages.append(language) elif isinstance(language, list): @@ -83,13 +84,19 @@ def render(self, name, value, attrs=None, renderer=None): context["config"] = self.config context["script_id"] = "{}{}".format(attrs["id"], "_script") context["upload_url"] = reverse( - getattr(settings, "CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME", "ck_editor_5_upload_file"), + getattr( + settings, + "CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME", + "ck_editor_5_upload_file", + ), + ) + context["upload_file_types"] = json.dumps( + getattr( + settings, + "CKEDITOR_5_UPLOAD_FILE_TYPES", + ["jpeg", "png", "gif", "bmp", "webp", "tiff"], + ), ) - context["upload_file_types"] = json.dumps(getattr( - settings, - "CKEDITOR_5_UPLOAD_FILE_TYPES", - ["jpeg", "png", "gif", "bmp", "webp", "tiff"], - )) context["csrf_cookie_name"] = settings.CSRF_COOKIE_NAME if self._config_errors: context["errors"] = ErrorList(self._config_errors) diff --git a/example/blog/articles/views.py b/example/blog/articles/views.py index b095d3d..09b6cab 100644 --- a/example/blog/articles/views.py +++ b/example/blog/articles/views.py @@ -1,14 +1,9 @@ -from django.conf import settings -from django.http import Http404, HttpResponseRedirect, JsonResponse +from django.http import HttpResponseRedirect from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, FormView, TemplateView from django.views.generic.detail import DetailView from django.views.generic.list import ListView -from django_ckeditor_5.forms import UploadFileForm -from django_ckeditor_5.views import NoImageException, handle_uploaded_file, image_verify - from .forms import ArticleForm, CommentForm from .models import Article @@ -57,23 +52,3 @@ def get_success_url(self): class GetEditorView(TemplateView): template_name = "articles/dynamic_editor.html" extra_context = {"form": ArticleForm()} - - -def custom_upload_file(request): - if request.method == "POST" and request.user.is_staff: - form = UploadFileForm(request.POST, request.FILES) - allow_all_file_types = getattr( - settings, - "CKEDITOR_5_ALLOW_ALL_FILE_TYPES", - False, - ) - - if not allow_all_file_types: - try: - image_verify(request.FILES["upload"]) - except NoImageException as ex: - return JsonResponse({"error": {"message": f"{ex}"}}, status=400) - if form.is_valid(): - url = handle_uploaded_file(request.FILES["upload"]) - return JsonResponse({"url": url}) - raise Http404(_("Page not found.")) diff --git a/example/blog/blog/settings.py b/example/blog/blog/settings.py index deee281..aca2b08 100644 --- a/example/blog/blog/settings.py +++ b/example/blog/blog/settings.py @@ -324,5 +324,5 @@ }, } CKEDITOR_5_CUSTOM_CSS = "custom.css" -CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME = "custom_upload_file" CSRF_COOKIE_NAME = "new_csrf_cookie_name" +CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" diff --git a/example/blog/blog/urls.py b/example/blog/blog/urls.py index a144bc1..eb1ce6b 100644 --- a/example/blog/blog/urls.py +++ b/example/blog/blog/urls.py @@ -13,7 +13,6 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from articles.views import custom_upload_file from django.conf import settings from django.conf.urls.static import serve from django.contrib import admin @@ -21,8 +20,8 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("upload/", custom_upload_file, name="custom_upload_file"), path("", include("articles.urls")), + path("ckeditor5/", include("django_ckeditor_5.urls")), re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT}), re_path(r"^static/(?P.*)$", serve, {"document_root": settings.STATIC_ROOT}), ] diff --git a/example/blog/conftest.py b/example/blog/conftest.py index 250322e..954549e 100644 --- a/example/blog/conftest.py +++ b/example/blog/conftest.py @@ -1,6 +1,8 @@ import os import pytest +from django.contrib.auth.models import AnonymousUser, User +from django.test import RequestFactory from django_ckeditor_5.fields import CKEditor5Field from django_ckeditor_5.forms import UploadFileForm @@ -20,3 +22,27 @@ def ckeditor5_field(): @pytest.fixture() def upload_file_form(): return UploadFileForm() + + +@pytest.fixture() +def factory(): + return RequestFactory() + + +@pytest.fixture() +def anonymous_user(): + return AnonymousUser() + + +@pytest.fixture() +def authenticated_user(db): # noqa: ARG001 + return User.objects.create_user(username="testuser", password="12345") # noqa: S106 + + +@pytest.fixture() +def staff_user(db): # noqa: ARG001 + return User.objects.create_user( + username="staffuser", + password="12345", + is_staff=True, # noqa: S106 + ) diff --git a/example/blog/tests/test_upload_file.py b/example/blog/tests/test_upload_file.py index 635de7f..ae39169 100644 --- a/example/blog/tests/test_upload_file.py +++ b/example/blog/tests/test_upload_file.py @@ -4,7 +4,9 @@ def test_upload_file(admin_client, file): with file as upload: - upload_view_name = getattr(settings, "CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME", "") + upload_view_name = getattr( + settings, "CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME", "ck_editor_5_upload_file" + ) response = admin_client.post( reverse(upload_view_name), {"upload": upload}, diff --git a/example/blog/tests/test_upload_file_permission.py b/example/blog/tests/test_upload_file_permission.py new file mode 100644 index 0000000..5d7d15c --- /dev/null +++ b/example/blog/tests/test_upload_file_permission.py @@ -0,0 +1,43 @@ +from django.conf import settings + +from django_ckeditor_5.views import upload_file + + +def test_upload_file_permission_anonymous(factory, anonymous_user, file): + settings.CKEDITOR_5_FILE_UPLOAD_PERMISSION = "authenticated" + request = factory.post("/upload/", {"upload": file}) + request.user = anonymous_user + response = upload_file(request) + assert response.status_code == 403 + + +def test_upload_file_permission_authenticated(factory, authenticated_user, file): + settings.CKEDITOR_5_FILE_UPLOAD_PERMISSION = "authenticated" + request = factory.post("/upload/", {"upload": file}) + request.user = authenticated_user + response = upload_file(request) + assert response.status_code == 200 + + +def test_upload_file_permission_staff(factory, staff_user, file): + settings.CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" + request = factory.post("/upload/", {"upload": file}) + request.user = staff_user + response = upload_file(request) + assert response.status_code == 200 + + +def test_upload_file_permission_any(factory, anonymous_user, file): + settings.CKEDITOR_5_FILE_UPLOAD_PERMISSION = "any" + request = factory.post("/upload/", {"upload": file}) + request.user = anonymous_user + response = upload_file(request) + assert response.status_code == 200 + + +def test_upload_file_permission_authenticated_user(factory, authenticated_user, file): + settings.CKEDITOR_5_FILE_UPLOAD_PERMISSION = "any" + request = factory.post("/upload/", {"upload": file}) + request.user = authenticated_user + response = upload_file(request) + assert response.status_code == 200 diff --git a/pyproject.toml b/pyproject.toml index 3dabde3..b5a995c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,6 @@ exclude = ''' | buck-out | build | dist - | django_ckeditor_5 | migrations )/ )