Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/api/plane/api/serializers/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
State,
User,
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)

from .base import BaseSerializer
from .cycle import CycleLiteSerializer, CycleSerializer
Expand Down Expand Up @@ -75,6 +80,22 @@ def validate(self, data):
except Exception:
raise serializers.ValidationError("Invalid HTML passed")

# Validate description content for security
if data.get("description"):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})

if data.get("description_html"):
is_valid, error_msg = validate_html_content(data["description_html"])
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})

if data.get("description_binary"):
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})

# Validate assignees are from project
if data.get("assignees", []):
data["assignees"] = ProjectMember.objects.filter(
Expand Down
28 changes: 28 additions & 0 deletions apps/api/plane/api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

# Module imports
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)

from .base import BaseSerializer

Expand Down Expand Up @@ -57,6 +62,29 @@ def validate(self, data):
"Default assignee should be a user in the workspace"
)

# Validate description content for security
if "description" in data and data["description"]:
# For Project, description might be text field, not JSON
if isinstance(data["description"], dict):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})

if "description_text" in data and data["description_text"]:
is_valid, error_msg = validate_json_content(data["description_text"])
if not is_valid:
raise serializers.ValidationError({"description_text": error_msg})

if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict):
is_valid, error_msg = validate_json_content(data["description_html"])
else:
is_valid, error_msg = validate_html_content(
str(data["description_html"])
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})

return data

def create(self, validated_data):
Expand Down
1 change: 1 addition & 0 deletions apps/api/plane/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
SubPageSerializer,
PageDetailSerializer,
PageVersionSerializer,
PageBinaryUpdateSerializer,
PageVersionDetailSerializer,
)

Expand Down
22 changes: 22 additions & 0 deletions apps/api/plane/app/serializers/draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
DraftIssueCycle,
DraftIssueModule,
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)


class DraftIssueCreateSerializer(BaseSerializer):
Expand Down Expand Up @@ -64,6 +69,23 @@ def validate(self, data):
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")

# Validate description content for security
if "description" in data and data["description"]:
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})

if "description_html" in data and data["description_html"]:
is_valid, error_msg = validate_html_content(data["description_html"])
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})

if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})

return data

def create(self, validated_data):
Expand Down
21 changes: 21 additions & 0 deletions apps/api/plane/app/serializers/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
IssueDescriptionVersion,
ProjectMember,
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)


class IssueFlatSerializer(BaseSerializer):
Expand Down Expand Up @@ -127,6 +132,22 @@ def validate(self, attrs):
member_id__in=attrs["assignee_ids"],
).values_list("member_id", flat=True)

# Validate description content for security
if "description" in attrs and attrs["description"]:
is_valid, error_msg = validate_json_content(attrs["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})

if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg = validate_html_content(attrs["description_html"])
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})

if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})

return attrs

def create(self, validated_data):
Expand Down
74 changes: 74 additions & 0 deletions apps/api/plane/app/serializers/page.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# Third party imports
from rest_framework import serializers
import base64

# Module imports
from .base import BaseSerializer
from plane.utils.content_validator import (
validate_binary_data,
validate_html_content,
validate_json_content,
)
from plane.db.models import (
Page,
PageLog,
Expand Down Expand Up @@ -186,3 +192,71 @@ class Meta:
"updated_by",
]
read_only_fields = ["workspace", "page"]


class PageBinaryUpdateSerializer(serializers.Serializer):
"""Serializer for updating page binary description with validation"""

description_binary = serializers.CharField(required=False, allow_blank=True)
description_html = serializers.CharField(required=False, allow_blank=True)
description = serializers.JSONField(required=False, allow_null=True)

def validate_description_binary(self, value):
"""Validate the base64-encoded binary data"""
if not value:
return value

try:
# Decode the base64 data
binary_data = base64.b64decode(value)

# Validate the binary data
is_valid, error_message = validate_binary_data(binary_data)
if not is_valid:
raise serializers.ValidationError(
f"Invalid binary data: {error_message}"
)

return binary_data
except Exception as e:
if isinstance(e, serializers.ValidationError):
raise
raise serializers.ValidationError("Failed to decode base64 data")

def validate_description_html(self, value):
"""Validate the HTML content"""
if not value:
return value

# Use the validation function from utils
is_valid, error_message = validate_html_content(value)
if not is_valid:
raise serializers.ValidationError(error_message)

return value

def validate_description(self, value):
"""Validate the JSON description"""
if not value:
return value

# Use the validation function from utils
is_valid, error_message = validate_json_content(value)
if not is_valid:
raise serializers.ValidationError(error_message)

return value

def update(self, instance, validated_data):
"""Update the page instance with validated data"""
if "description_binary" in validated_data:
instance.description_binary = validated_data.get("description_binary")

if "description_html" in validated_data:
instance.description_html = validated_data.get("description_html")

if "description" in validated_data:
instance.description = validated_data.get("description")

instance.save()
return instance
31 changes: 31 additions & 0 deletions apps/api/plane/app/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
DeployBoard,
ProjectPublicMember,
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)


class ProjectSerializer(BaseSerializer):
Expand Down Expand Up @@ -58,6 +63,32 @@ def validate_identifier(self, identifier):

return identifier

def validate(self, data):
# Validate description content for security
if "description" in data and data["description"]:
# For Project, description might be text field, not JSON
if isinstance(data["description"], dict):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})

if "description_text" in data and data["description_text"]:
is_valid, error_msg = validate_json_content(data["description_text"])
if not is_valid:
raise serializers.ValidationError({"description_text": error_msg})

if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict):
is_valid, error_msg = validate_json_content(data["description_html"])
else:
is_valid, error_msg = validate_html_content(
str(data["description_html"])
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})

return data

def create(self, validated_data):
workspace_id = self.context["workspace_id"]

Expand Down
24 changes: 24 additions & 0 deletions apps/api/plane/app/serializers/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.utils.url import contains_url
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)

# Django imports
from django.core.validators import URLValidator
Expand Down Expand Up @@ -312,6 +317,25 @@ class Meta:
read_only_fields = ["workspace", "owner"]
extra_kwargs = {"name": {"required": False}}

def validate(self, data):
# Validate description content for security
if "description" in data and data["description"]:
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})

if "description_html" in data and data["description_html"]:
is_valid, error_msg = validate_html_content(data["description_html"])
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})

if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})

return data


class WorkspaceUserPreferenceSerializer(BaseSerializer):
class Meta:
Expand Down
28 changes: 12 additions & 16 deletions apps/api/plane/app/views/page/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
PageSerializer,
SubPageSerializer,
PageDetailSerializer,
PageBinaryUpdateSerializer,
)
from plane.db.models import (
Page,
Expand Down Expand Up @@ -538,32 +539,27 @@ def partial_update(self, request, slug, project_id, pk):
{"description_html": page.description_html}, cls=DjangoJSONEncoder
)

# Get the base64 data from the request
base64_data = request.data.get("description_binary")

# If base64 data is provided
if base64_data:
# Decode the base64 data to bytes
new_binary_data = base64.b64decode(base64_data)
# capture the page transaction
# Use serializer for validation and update
serializer = PageBinaryUpdateSerializer(page, data=request.data, partial=True)
if serializer.is_valid():
# Capture the page transaction
if request.data.get("description_html"):
page_transaction.delay(
new_value=request.data, old_value=existing_instance, page_id=pk
)
# Store the updated binary data
page.description_binary = new_binary_data
page.description_html = request.data.get("description_html")
page.description = request.data.get("description")
page.save()
# Return a success response

# Update the page using serializer
updated_page = serializer.save()

# Run background tasks
page_version.delay(
page_id=page.id,
page_id=updated_page.id,
existing_instance=existing_instance,
user_id=request.user.id,
)
return Response({"message": "Updated successfully"})
else:
return Response({"error": "No binary data provided"})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class PageDuplicateEndpoint(BaseAPIView):
Expand Down
Loading
Loading