diff --git a/src/app/course/[courseName]/modules/NewItemForm.tsx b/src/app/course/[courseName]/modules/NewItemForm.tsx index b4d0d64..abbe5de 100644 --- a/src/app/course/[courseName]/modules/NewItemForm.tsx +++ b/src/app/course/[courseName]/modules/NewItemForm.tsx @@ -17,6 +17,7 @@ import { getDateFromStringOrThrow, } from "@/features/local/utils/timeUtils"; import { useCreateAssignmentMutation } from "@/features/local/assignments/assignmentHooks"; +import { validateFileName } from "@/services/fileNameValidation"; export default function NewItemForm({ moduleName: defaultModuleName, @@ -41,20 +42,6 @@ export default function NewItemForm({ const [name, setName] = useState(""); const [nameError, setNameError] = useState(""); - const validateFileName = (fileName: string): string => { - // Check for invalid file system characters - const invalidChars = [":", "/", "\\", "*", '"', "<", ">", "|"]; - - for (const char of fileName) { - if (invalidChars.includes(char)) { - return `Name contains invalid character: "${char}". Please avoid: ${invalidChars.join( - " " - )}`; - } - } - return ""; - }; - const handleNameChange = (newName: string) => { setName(newName); const error = validateFileName(newName); diff --git a/src/features/local/assignments/assignmentRouter.ts b/src/features/local/assignments/assignmentRouter.ts index 8bdf173..5df4124 100644 --- a/src/features/local/assignments/assignmentRouter.ts +++ b/src/features/local/assignments/assignmentRouter.ts @@ -10,6 +10,7 @@ import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorage import { promises as fs } from "fs"; import { courseItemFileStorageService } from "../course/courseItemFileStorageService"; import { assignmentMarkdownSerializer } from "./models/utils/assignmentMarkdownSerializer"; +import { assertValidFileName } from "@/services/fileNameValidation"; export const assignmentRouter = router({ getAssignment: publicProcedure @@ -133,16 +134,7 @@ export async function updateOrCreateAssignmentFile({ assignmentName: string; assignment: LocalAssignment; }) { - const illegalCharacters = ["<", ">", ":", '"', "/", "\\", "|", "?", "*"]; - const foundIllegalCharacters = illegalCharacters.filter((char) => - assignmentName.includes(char) - ); - if (foundIllegalCharacters.length > 0) { - throw new Error( - `"${assignmentName}" cannot contain the following characters: ${foundIllegalCharacters.join( - " " - )}` - ); + assertValidFileName(assignmentName); } const courseDirectory = await getCoursePathByName(courseName); diff --git a/src/features/local/pages/pageRouter.ts b/src/features/local/pages/pageRouter.ts index 0290a01..a97d968 100644 --- a/src/features/local/pages/pageRouter.ts +++ b/src/features/local/pages/pageRouter.ts @@ -1,11 +1,16 @@ import publicProcedure from "../../../services/serverFunctions/publicProcedure"; import { z } from "zod"; import { router } from "../../../services/serverFunctions/trpcSetup"; -import { LocalCoursePage, localPageMarkdownUtils, zodLocalCoursePage } from "@/features/local/pages/localCoursePageModels"; +import { + LocalCoursePage, + localPageMarkdownUtils, + zodLocalCoursePage, +} from "@/features/local/pages/localCoursePageModels"; import { courseItemFileStorageService } from "../course/courseItemFileStorageService"; import { promises as fs } from "fs"; import path from "path"; import { getCoursePathByName } from "../globalSettings/globalSettingsFileStorageService"; +import { assertValidFileName } from "@/services/fileNameValidation"; export const pageRouter = router({ getPage: publicProcedure @@ -115,31 +120,32 @@ export const pageRouter = router({ }); export async function updatePageFile({ - courseName, + courseName, + moduleName, + pageName, + page, +}: { + courseName: string; + moduleName: string; + pageName: string; + page: LocalCoursePage; +}) { + assertValidFileName(pageName); + const courseDirectory = await getCoursePathByName(courseName); + const folder = path.join(courseDirectory, moduleName, "pages"); + await fs.mkdir(folder, { recursive: true }); + + const filePath = path.join( + courseDirectory, moduleName, - pageName, - page, - }: { - courseName: string; - moduleName: string; - pageName: string; - page: LocalCoursePage; - }) { - const courseDirectory = await getCoursePathByName(courseName); - const folder = path.join(courseDirectory, moduleName, "pages"); - await fs.mkdir(folder, { recursive: true }); + "pages", + pageName + ".md" + ); - const filePath = path.join( - courseDirectory, - moduleName, - "pages", - pageName + ".md" - ); - - const pageMarkdown = localPageMarkdownUtils.toMarkdown(page); - console.log(`Saving page ${filePath}`); - await fs.writeFile(filePath, pageMarkdown); - } + const pageMarkdown = localPageMarkdownUtils.toMarkdown(page); + console.log(`Saving page ${filePath}`); + await fs.writeFile(filePath, pageMarkdown); +} async function deletePageFile({ courseName, moduleName, diff --git a/src/features/local/quizzes/quizRouter.ts b/src/features/local/quizzes/quizRouter.ts index 0c8d956..c82b33a 100644 --- a/src/features/local/quizzes/quizRouter.ts +++ b/src/features/local/quizzes/quizRouter.ts @@ -10,6 +10,7 @@ import path from "path"; import { promises as fs } from "fs"; import { quizMarkdownUtils } from "./models/utils/quizMarkdownUtils"; import { courseItemFileStorageService } from "../course/courseItemFileStorageService"; +import { assertValidFileName } from "@/services/fileNameValidation"; export const quizRouter = router({ getQuiz: publicProcedure @@ -149,6 +150,7 @@ export async function updateQuizFile({ quizName: string; quiz: LocalQuiz; }) { + assertValidFileName(quizName); const courseDirectory = await getCoursePathByName(courseName); const folder = path.join(courseDirectory, moduleName, "quizzes"); await fs.mkdir(folder, { recursive: true }); diff --git a/src/services/fileNameValidation.ts b/src/services/fileNameValidation.ts new file mode 100644 index 0000000..b3068a5 --- /dev/null +++ b/src/services/fileNameValidation.ts @@ -0,0 +1,28 @@ +export function validateFileName(fileName: string): string { + if (!fileName || fileName.trim() === "") { + return "Name cannot be empty"; + } + + const invalidChars = [":", "/", "\\", "*", "?", '"', "<", ">", "|"]; + + for (const char of fileName) { + if (invalidChars.includes(char)) { + return `Name contains invalid character: "${char}". Please avoid: ${invalidChars.join( + " " + )}`; + } + } + + if (fileName !== fileName.trimEnd()) { + return "Name cannot end with whitespace"; + } + + return ""; +} + +export function assertValidFileName(fileName: string): void { + const error = validateFileName(fileName); + if (error) { + throw new Error(error); + } +}