fix: substitution of content when sending an image to a profile (#3869)
* fix: Limit public uploads to basic image types * test
This commit is contained in:
@@ -8,12 +8,10 @@ import { Optional } from "utility-types";
|
||||
import insertFiles from "@shared/editor/commands/insertFiles";
|
||||
import embeds from "@shared/editor/embeds";
|
||||
import { Heading } from "@shared/editor/lib/getHeadings";
|
||||
import {
|
||||
getDataTransferFiles,
|
||||
supportedImageMimeTypes,
|
||||
} from "@shared/utils/files";
|
||||
import { getDataTransferFiles } from "@shared/utils/files";
|
||||
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
|
||||
import { isInternalUrl } from "@shared/utils/urls";
|
||||
import { AttachmentValidation } from "@shared/validations";
|
||||
import Document from "~/models/Document";
|
||||
import ClickablePadding from "~/components/ClickablePadding";
|
||||
import ErrorBoundary from "~/components/ErrorBoundary";
|
||||
@@ -212,7 +210,7 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
|
||||
|
||||
// Insert all files as attachments if any of the files are not images.
|
||||
const isAttachment = files.some(
|
||||
(file) => !supportedImageMimeTypes.includes(file.type)
|
||||
(file) => !AttachmentValidation.imageContentTypes.includes(file.type)
|
||||
);
|
||||
|
||||
insertFiles(view, event, pos, files, {
|
||||
|
||||
@@ -11,7 +11,8 @@ import { CommandFactory } from "@shared/editor/lib/Extension";
|
||||
import filterExcessSeparators from "@shared/editor/lib/filterExcessSeparators";
|
||||
import { EmbedDescriptor, MenuItem } from "@shared/editor/types";
|
||||
import { depths } from "@shared/styles";
|
||||
import { supportedImageMimeTypes, getEventFiles } from "@shared/utils/files";
|
||||
import { getEventFiles } from "@shared/utils/files";
|
||||
import { AttachmentValidation } from "@shared/validations";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import { Dictionary } from "~/hooks/useDictionary";
|
||||
import Input from "./Input";
|
||||
@@ -181,7 +182,9 @@ class CommandMenu<T = MenuItem> extends React.Component<Props<T>, State> {
|
||||
insertItem = (item: any) => {
|
||||
switch (item.name) {
|
||||
case "image":
|
||||
return this.triggerFilePick(supportedImageMimeTypes.join(", "));
|
||||
return this.triggerFilePick(
|
||||
AttachmentValidation.imageContentTypes.join(", ")
|
||||
);
|
||||
case "attachment":
|
||||
return this.triggerFilePick("*");
|
||||
case "embed":
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as React from "react";
|
||||
import AvatarEditor from "react-avatar-editor";
|
||||
import Dropzone from "react-dropzone";
|
||||
import styled from "styled-components";
|
||||
import { AttachmentValidation } from "@shared/validations";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
@@ -134,7 +135,7 @@ class ImageUpload extends React.Component<RootStore & Props> {
|
||||
|
||||
return (
|
||||
<Dropzone
|
||||
accept="image/png, image/jpeg"
|
||||
accept={AttachmentValidation.avatarContentTypes.join(", ")}
|
||||
onDropAccepted={this.onDropAccepted}
|
||||
>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
|
||||
@@ -13,9 +13,46 @@ import { flushdb } from "@server/test/support";
|
||||
const app = webService();
|
||||
const server = new TestServer(app.callback());
|
||||
|
||||
jest.mock("@server/utils/s3");
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe("#attachments.create", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/attachments.create");
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
|
||||
it("should allow simple image upload for public attachments", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/attachments.create", {
|
||||
body: {
|
||||
name: "test.png",
|
||||
contentType: "image/png",
|
||||
size: 1000,
|
||||
public: true,
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(200);
|
||||
});
|
||||
|
||||
it("should not allow file upload for public attachments", async () => {
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/attachments.create", {
|
||||
body: {
|
||||
name: "test.pdf",
|
||||
contentType: "application/pdf",
|
||||
size: 1000,
|
||||
public: true,
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#attachments.delete", () => {
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/attachments.delete");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Router from "koa-router";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { bytesToHumanReadable } from "@shared/utils/files";
|
||||
import { AttachmentValidation } from "@shared/validations";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import { AuthorizationError, ValidationError } from "@server/errors";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
@@ -11,12 +12,13 @@ import {
|
||||
publicS3Endpoint,
|
||||
getSignedUrl,
|
||||
} from "@server/utils/s3";
|
||||
import { assertPresent, assertUuid } from "@server/validation";
|
||||
import { assertIn, assertPresent, assertUuid } from "@server/validation";
|
||||
|
||||
const router = new Router();
|
||||
const AWS_S3_ACL = process.env.AWS_S3_ACL || "private";
|
||||
|
||||
router.post("attachments.create", auth(), async (ctx) => {
|
||||
const isPublic = ctx.body.public;
|
||||
const {
|
||||
name,
|
||||
documentId,
|
||||
@@ -25,9 +27,15 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
} = ctx.body;
|
||||
assertPresent(name, "name is required");
|
||||
assertPresent(size, "size is required");
|
||||
|
||||
const { user } = ctx.state;
|
||||
authorize(user, "createAttachment", user.team);
|
||||
|
||||
// Public attachments are only used for avatars, so this is loosely coupled.
|
||||
if (isPublic) {
|
||||
assertIn(contentType, AttachmentValidation.avatarContentTypes);
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.AWS_S3_UPLOAD_MAX_SIZE &&
|
||||
size > process.env.AWS_S3_UPLOAD_MAX_SIZE
|
||||
@@ -39,7 +47,6 @@ router.post("attachments.create", auth(), async (ctx) => {
|
||||
);
|
||||
}
|
||||
|
||||
const isPublic = ctx.body.public;
|
||||
const s3Key = uuidv4();
|
||||
const acl =
|
||||
isPublic === undefined ? AWS_S3_ACL : isPublic ? "public-read" : "private";
|
||||
|
||||
@@ -5,3 +5,5 @@ export const publicS3Endpoint = jest.fn().mockReturnValue("http://mock");
|
||||
export const getSignedUrl = jest.fn().mockReturnValue("http://s3mock");
|
||||
|
||||
export const getSignedUrlPromise = jest.fn().mockResolvedValue("http://s3mock");
|
||||
|
||||
export const getPresignedPost = jest.fn().mockReturnValue({});
|
||||
|
||||
@@ -11,11 +11,8 @@ import {
|
||||
import * as React from "react";
|
||||
import ImageZoom from "react-medium-image-zoom";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
getDataTransferFiles,
|
||||
supportedImageMimeTypes,
|
||||
getEventFiles,
|
||||
} from "../../utils/files";
|
||||
import { getDataTransferFiles, getEventFiles } from "../../utils/files";
|
||||
import { AttachmentValidation } from "../../validations";
|
||||
import insertFiles, { Options } from "../commands/insertFiles";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import uploadPlaceholderPlugin from "../lib/uploadPlaceholder";
|
||||
@@ -413,7 +410,7 @@ export default class Image extends Node {
|
||||
// create an input element and click to trigger picker
|
||||
const inputElement = document.createElement("input");
|
||||
inputElement.type = "file";
|
||||
inputElement.accept = supportedImageMimeTypes.join(", ");
|
||||
inputElement.accept = AttachmentValidation.imageContentTypes.join(", ");
|
||||
inputElement.onchange = (event) => {
|
||||
const files = getEventFiles(event);
|
||||
insertFiles(view, event, state.selection.from, files, {
|
||||
|
||||
@@ -83,21 +83,3 @@ export function getEventFiles(
|
||||
? Array.prototype.slice.call(event.target.files)
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of image mimetypes commonly supported by modern browsers
|
||||
*/
|
||||
export const supportedImageMimeTypes = [
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"image/pjpeg",
|
||||
"image/png",
|
||||
"image/apng",
|
||||
"image/avif",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/svg",
|
||||
"image/svg+xml",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
];
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
export const AttachmentValidation = {
|
||||
/** The limited allowable mime-types for user and team avatars */
|
||||
avatarContentTypes: ["image/jpg", "image/jpeg", "image/png"],
|
||||
|
||||
/** Image mime-types commonly supported by modern browsers */
|
||||
imageContentTypes: [
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"image/pjpeg",
|
||||
"image/png",
|
||||
"image/apng",
|
||||
"image/avif",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/svg",
|
||||
"image/svg+xml",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
],
|
||||
};
|
||||
|
||||
export const CollectionValidation = {
|
||||
/** The maximum length of the collection description */
|
||||
maxDescriptionLength: 1000,
|
||||
|
||||
Reference in New Issue
Block a user