snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Request, Response } from "express";
import { Effect, Schema, Cause } from "effect";
import { Controller, Post } from "@plane/decorators";
import { logger } from "@plane/logger";
import { AppError } from "@/lib/errors";
import { PdfExportRequestBody, PdfValidationError, PdfAuthenticationError } from "@/schema/pdf-export";
import { PdfExportService, exportToPdf } from "@/services/pdf-export";
import type { PdfExportInput } from "@/services/pdf-export";
@Controller("/pdf-export")
export class PdfExportController {
/**
* Parses and validates the request, returning a typed input object
*/
private parseRequest(
req: Request,
requestId: string
): Effect.Effect<PdfExportInput, PdfValidationError | PdfAuthenticationError> {
return Effect.gen(function* () {
const cookie = req.headers.cookie || "";
if (!cookie) {
return yield* Effect.fail(
new PdfAuthenticationError({
message: "Authentication required",
})
);
}
const body = yield* Schema.decodeUnknown(PdfExportRequestBody)(req.body).pipe(
Effect.mapError(
(cause) =>
new PdfValidationError({
message: "Invalid request body",
cause,
})
)
);
return {
pageId: body.pageId,
workspaceSlug: body.workspaceSlug,
projectId: body.projectId,
title: body.title,
author: body.author,
subject: body.subject,
pageSize: body.pageSize,
pageOrientation: body.pageOrientation,
fileName: body.fileName,
noAssets: body.noAssets,
cookie,
requestId,
};
});
}
/**
* Maps domain errors to HTTP responses
*/
private mapErrorToHttpResponse(error: unknown): { status: number; error: string } {
if (error && typeof error === "object" && "_tag" in error) {
const tag = (error as { _tag: string })._tag;
const message = (error as { message?: string }).message || "Unknown error";
switch (tag) {
case "PdfValidationError":
return { status: 400, error: message };
case "PdfAuthenticationError":
return { status: 401, error: message };
case "PdfContentFetchError":
return {
status: message.includes("not found") ? 404 : 502,
error: message,
};
case "PdfTimeoutError":
return { status: 504, error: message };
case "PdfGenerationError":
return { status: 500, error: message };
case "PdfMetadataFetchError":
case "PdfImageProcessingError":
return { status: 502, error: message };
default:
return { status: 500, error: message };
}
}
return { status: 500, error: "Failed to generate PDF" };
}
@Post("/")
async exportToPdf(req: Request, res: Response) {
const requestId = crypto.randomUUID();
const effect = Effect.gen(this, function* () {
// Parse request
const input = yield* this.parseRequest(req, requestId);
// Delegate to service
return yield* exportToPdf(input);
}).pipe(
// Log errors before catching them
Effect.tapError((error) => Effect.logError("PDF_EXPORT: Export failed", { requestId, error })),
// Map all tagged errors to HTTP responses
Effect.catchAll((error) => Effect.succeed(this.mapErrorToHttpResponse(error))),
// Handle unexpected defects
Effect.catchAllDefect((defect) => {
const appError = new AppError(Cause.pretty(Cause.die(defect)), {
context: { requestId, operation: "exportToPdf" },
});
logger.error("PDF_EXPORT: Unexpected failure", appError);
return Effect.succeed({ status: 500, error: "Failed to generate PDF" });
})
);
const result = await Effect.runPromise(Effect.provide(effect, PdfExportService.Default));
// Check if result is an error response
if ("error" in result && "status" in result) {
return res.status(result.status).json({ message: result.error });
}
// Success - send PDF
const { pdfBuffer, outputFileName } = result;
// Sanitize filename for Content-Disposition header to prevent header injection
const sanitizedFileName = outputFileName
.replace(/["\\\r\n]/g, "") // Remove quotes, backslashes, and CRLF
.replace(/[^\x20-\x7E]/g, "_"); // Replace non-ASCII with underscore
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename="${sanitizedFileName}"; filename*=UTF-8''${encodeURIComponent(outputFileName)}`
);
res.setHeader("Content-Length", pdfBuffer.length);
return res.send(pdfBuffer);
}
}