Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 123484f3fe | |||
| 5e810c3682 | |||
| 4a97d7c28c | |||
| 6d33aacab9 | |||
| 67f2094ea6 | |||
| 20e5804cc2 | |||
| 0d3263b0a2 | |||
| 4823a16158 |
@@ -3,11 +3,22 @@ from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
from .base import BaseSerializer
|
||||
|
||||
|
||||
class UserSerializer(BaseSerializer):
|
||||
def validate_first_name(self, value):
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("First name cannot contain a URL.")
|
||||
return value
|
||||
|
||||
def validate_last_name(self, value):
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Last name cannot contain a URL.")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
# Exclude password field from the serializer
|
||||
|
||||
@@ -25,10 +25,12 @@ from plane.db.models import (
|
||||
WorkspaceUserPreference,
|
||||
)
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
# Django imports
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
import re
|
||||
|
||||
|
||||
class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
@@ -36,10 +38,21 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
|
||||
logo_url = serializers.CharField(read_only=True)
|
||||
role = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate_name(self, value):
|
||||
# Check if the name contains a URL
|
||||
if contains_url(value):
|
||||
raise serializers.ValidationError("Name must not contain URLs")
|
||||
return value
|
||||
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
raise serializers.ValidationError("Slug is not valid")
|
||||
# Slug should only contain alphanumeric characters, hyphens, and underscores
|
||||
if not re.match(r"^[a-zA-Z0-9_-]+$", value):
|
||||
raise serializers.ValidationError(
|
||||
"Slug can only contain letters, numbers, hyphens (-), and underscores (_)"
|
||||
)
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -43,6 +43,7 @@ from django.views.decorators.vary import vary_on_cookie
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.bgtasks.workspace_seed_task import workspace_seed
|
||||
from plane.utils.url import contains_url
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
@@ -109,6 +110,12 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if contains_url(name):
|
||||
return Response(
|
||||
{"error": "Name cannot contain a URL"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save(owner=request.user)
|
||||
# Create Workspace member
|
||||
|
||||
@@ -4,6 +4,14 @@ from typing import Optional
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
|
||||
def contains_url(value: str) -> bool:
|
||||
"""
|
||||
Check if the value contains a URL.
|
||||
"""
|
||||
url_pattern = re.compile(r"https?://|www\\.")
|
||||
return bool(url_pattern.search(value))
|
||||
|
||||
|
||||
def is_valid_url(url: str) -> bool:
|
||||
"""
|
||||
Validates whether the given string is a well-formed URL.
|
||||
|
||||
@@ -86,6 +86,10 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
[editor]
|
||||
);
|
||||
|
||||
const handleInvalidFile = useCallback((_error: EFileError, _file: File, message: string) => {
|
||||
alert(message);
|
||||
}, []);
|
||||
|
||||
// hooks
|
||||
const { isUploading: isImageBeingUploaded, uploadFile } = useUploader({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
@@ -94,18 +98,12 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
handleProgressStatus,
|
||||
loadFileFromFileSystem: loadImageFromFileSystem,
|
||||
maxFileSize,
|
||||
onInvalidFile: handleInvalidFile,
|
||||
onUpload,
|
||||
});
|
||||
|
||||
const handleInvalidFile = useCallback((_error: EFileError, message: string) => {
|
||||
alert(message);
|
||||
}, []);
|
||||
|
||||
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
editor,
|
||||
maxFileSize,
|
||||
onInvalidFile: handleInvalidFile,
|
||||
pos: getPos(),
|
||||
type: "image",
|
||||
uploader: uploadFile,
|
||||
@@ -140,11 +138,8 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
return;
|
||||
}
|
||||
await uploadFirstFileAndInsertRemaining({
|
||||
acceptedMimeTypes: ACCEPTED_IMAGE_MIME_TYPES,
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
onInvalidFile: (_error, message) => alert(message),
|
||||
pos: getPos(),
|
||||
type: "image",
|
||||
uploader: uploadFile,
|
||||
|
||||
@@ -9,11 +9,11 @@ import { TEditorCommands } from "@/types";
|
||||
|
||||
type TUploaderArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editorCommand: (file: File) => Promise<string>;
|
||||
editorCommand: (file: File) => Promise<string | undefined>;
|
||||
handleProgressStatus?: (isUploading: boolean) => void;
|
||||
loadFileFromFileSystem?: (file: string) => void;
|
||||
maxFileSize: number;
|
||||
onInvalidFile: (error: EFileError, message: string) => void;
|
||||
onInvalidFile: (error: EFileError, file: File, message: string) => void;
|
||||
onUpload: (url: string, file: File) => void;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
acceptedMimeTypes,
|
||||
file,
|
||||
maxFileSize,
|
||||
onError: onInvalidFile,
|
||||
onError: (error, message) => onInvalidFile(error, file, message),
|
||||
});
|
||||
if (!isValid) {
|
||||
handleProgressStatus?.(false);
|
||||
@@ -60,7 +60,7 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
const url: string = await editorCommand(file);
|
||||
const url = await editorCommand(file);
|
||||
|
||||
if (!url) {
|
||||
throw new Error("Something went wrong while uploading the file.");
|
||||
@@ -89,17 +89,14 @@ export const useUploader = (args: TUploaderArgs) => {
|
||||
};
|
||||
|
||||
type TDropzoneArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editor: Editor;
|
||||
maxFileSize: number;
|
||||
onInvalidFile: (error: EFileError, message: string) => void;
|
||||
pos: number;
|
||||
type: Extract<TEditorCommands, "attachment" | "image">;
|
||||
uploader: (file: File) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useDropZone = (args: TDropzoneArgs) => {
|
||||
const { acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader } = args;
|
||||
const { editor, pos, type, uploader } = args;
|
||||
// states
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [draggedInside, setDraggedInside] = useState<boolean>(false);
|
||||
@@ -126,22 +123,21 @@ export const useDropZone = (args: TDropzoneArgs) => {
|
||||
async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setDraggedInside(false);
|
||||
if (e.dataTransfer.files.length === 0 || !editor.isEditable) {
|
||||
const filesList = e.dataTransfer.files;
|
||||
|
||||
if (filesList.length === 0 || !editor.isEditable) {
|
||||
return;
|
||||
}
|
||||
const filesList = e.dataTransfer.files;
|
||||
|
||||
await uploadFirstFileAndInsertRemaining({
|
||||
acceptedMimeTypes,
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
onInvalidFile,
|
||||
pos,
|
||||
type,
|
||||
uploader,
|
||||
});
|
||||
},
|
||||
[acceptedMimeTypes, editor, maxFileSize, onInvalidFile, pos, type, uploader]
|
||||
[editor, pos, type, uploader]
|
||||
);
|
||||
const onDragEnter = useCallback(() => setDraggedInside(true), []);
|
||||
const onDragLeave = useCallback(() => setDraggedInside(false), []);
|
||||
@@ -156,11 +152,8 @@ export const useDropZone = (args: TDropzoneArgs) => {
|
||||
};
|
||||
|
||||
type TMultipleFileArgs = {
|
||||
acceptedMimeTypes: string[];
|
||||
editor: Editor;
|
||||
filesList: FileList;
|
||||
maxFileSize: number;
|
||||
onInvalidFile: (error: EFileError, message: string) => void;
|
||||
pos: number;
|
||||
type: Extract<TEditorCommands, "attachment" | "image">;
|
||||
uploader: (file: File) => Promise<void>;
|
||||
@@ -168,35 +161,18 @@ type TMultipleFileArgs = {
|
||||
|
||||
// Upload the first file and insert the remaining ones for uploading multiple files
|
||||
export const uploadFirstFileAndInsertRemaining = async (args: TMultipleFileArgs) => {
|
||||
const { acceptedMimeTypes, editor, filesList, maxFileSize, onInvalidFile, pos, type, uploader } = args;
|
||||
const filteredFiles: File[] = [];
|
||||
for (let i = 0; i < filesList.length; i += 1) {
|
||||
const file = filesList.item(i);
|
||||
if (
|
||||
file &&
|
||||
isFileValid({
|
||||
acceptedMimeTypes,
|
||||
file,
|
||||
maxFileSize,
|
||||
onError: onInvalidFile,
|
||||
})
|
||||
) {
|
||||
filteredFiles.push(file);
|
||||
}
|
||||
}
|
||||
if (filteredFiles.length !== filesList.length) {
|
||||
console.warn("Some files were invalid and have been ignored.");
|
||||
}
|
||||
if (filteredFiles.length === 0) {
|
||||
const { editor, filesList, pos, type, uploader } = args;
|
||||
const filesArray = Array.from(filesList);
|
||||
if (filesArray.length === 0) {
|
||||
console.error("No files found to upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the first file
|
||||
const firstFile = filteredFiles[0];
|
||||
const firstFile = filesArray[0];
|
||||
uploader(firstFile);
|
||||
// Insert the remaining files
|
||||
const remainingFiles = filteredFiles.slice(1);
|
||||
const remainingFiles = filesArray.slice(1);
|
||||
if (remainingFiles.length > 0) {
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const posOfNextFileToBeInserted = Math.min(pos + 1, docSize);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||
// constants
|
||||
import { CORE_EDITOR_META } from "@/constants/meta";
|
||||
// plane editor imports
|
||||
import { NODE_FILE_MAP } from "@/plane-editor/constants/utility";
|
||||
// types
|
||||
@@ -32,7 +34,7 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand
|
||||
|
||||
transactions.forEach((transaction) => {
|
||||
// if the transaction has meta of skipFileDeletion set to true, then return (like while clearing the editor content programmatically)
|
||||
if (transaction.getMeta("skipFileDeletion")) return;
|
||||
if (transaction.getMeta(CORE_EDITOR_META.SKIP_FILE_DELETION)) return;
|
||||
|
||||
const removedFiles: TFileNode[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user