Skip to content

Commit

Permalink
feat: add permissions check for file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
hvlads committed Jul 28, 2024
1 parent ee6cc2e commit bfc19eb
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 58 deletions.
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion django_ckeditor_5/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
),
),
],
Expand Down
30 changes: 30 additions & 0 deletions django_ckeditor_5/permissions.py
Original file line number Diff line number Diff line change
@@ -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
39 changes: 23 additions & 16 deletions django_ckeditor_5/views.py
Original file line number Diff line number Diff line change
@@ -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 _
Expand Down Expand Up @@ -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)


Expand All @@ -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)
27 changes: 17 additions & 10 deletions django_ckeditor_5/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand All @@ -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)
Expand Down
27 changes: 1 addition & 26 deletions example/blog/articles/views.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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."))
2 changes: 1 addition & 1 deletion example/blog/blog/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 1 addition & 2 deletions example/blog/blog/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@
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
from django.urls import include, path, re_path

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<path>.*)$", serve, {"document_root": settings.MEDIA_ROOT}),
re_path(r"^static/(?P<path>.*)$", serve, {"document_root": settings.STATIC_ROOT}),
]
26 changes: 26 additions & 0 deletions example/blog/conftest.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)
4 changes: 3 additions & 1 deletion example/blog/tests/test_upload_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
43 changes: 43 additions & 0 deletions example/blog/tests/test_upload_file_permission.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ exclude = '''
| buck-out
| build
| dist
| django_ckeditor_5
| migrations
)/
)
Expand Down

0 comments on commit bfc19eb

Please sign in to comment.