Skip to content

Commit

Permalink
Limit editor file size (#1138)
Browse files Browse the repository at this point in the history
Co-authored-by: Scott Adams <scott.adams@raspberrypi.org>
  • Loading branch information
loiswells97 and sra405 authored Nov 21, 2024
1 parent 1455d32 commit f63926e
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 24 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ jobs:

- name: Build WC bundle
run: |
if [[ "${{ inputs.environment }}" != "production" ]]; then
yarn build:dev
fi
# TODO: Reinitialise when storybook build is fixed
# if [[ "${{ inputs.environment }}" != "production" ]]; then
# yarn build:dev
# fi
yarn build
env:
PUBLIC_URL: ${{ needs.setup-environment.outputs.public_url }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Fixed

- Bug causing py-enigma code to disable stop button
- Crashing caused by excessive file sizes (#1138)

### Changed

Expand Down
14 changes: 10 additions & 4 deletions cypress/e2e/spec-wc-skulpt.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,25 +121,31 @@ describe("Running the code with skulpt", () => {
runCode("import sense_hat");
cy.get("editor-wc")
.shadow()
.find("#root")
.find(".skulptrunner")
.should("contain", "Visual output");
});

it("does not render astro pi component on page load", () => {
cy.get("editor-wc").shadow().find("#root").should("not.contain", "yaw");
cy.get("editor-wc")
.shadow()
.find(".skulptrunner")
.should("not.contain", "yaw");
});

it("renders astro pi component if sense hat imported", () => {
runCode("import sense_hat");
cy.get("editor-wc").shadow().contains("Visual output").click();
cy.get("editor-wc").shadow().find("#root").should("contain", "yaw");
cy.get("editor-wc").shadow().find(".skulptrunner").should("contain", "yaw");
});

it("does not render astro pi component if sense hat unimported", () => {
runCode("import sense_hat");
runCode("import p5");
cy.get("editor-wc").shadow().contains("Visual output").click();
cy.get("editor-wc").shadow().find("#root").should("not.contain", "yaw");
cy.get("editor-wc")
.shadow()
.find(".skulptrunner")
.should("not.contain", "yaw");
});

it("runs a simple turtle program", () => {
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"@hello-pangea/dnd": "^16.2.0",
"@juggle/resize-observer": "^3.3.1",
"@lezer/highlight": "^1.0.0",
"@raspberrypifoundation/design-system-core": "^0.1.9",
"@raspberrypifoundation/design-system-react": "^0.1.5",
"@raspberrypifoundation/design-system-core": "^1.6.0",
"@raspberrypifoundation/design-system-react": "^1.6.0",
"@react-three/drei": "9.114.3",
"@react-three/fiber": "^8.0.13",
"@reduxjs/toolkit": "^1.6.2",
Expand Down Expand Up @@ -45,6 +45,7 @@
"js-convert-case": "^4.2.0",
"jszip": "^3.10.1",
"jszip-utils": "^0.1.0",
"material-symbols": "^0.27.0",
"mime-types": "^2.1.35",
"node-html-parser": "^6.1.5",
"oidc-client": "^1.11.5",
Expand Down
4 changes: 4 additions & 0 deletions src/assets/stylesheets/EditorPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@
@include font-size-2(regular);
}
}

.rpf-alert {
margin: 0;
}
2 changes: 2 additions & 0 deletions src/assets/stylesheets/ExternalStyles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
@use "../../../node_modules/react-toggle/style.css";
@use "../../../node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css";
@use "../../../node_modules/prismjs/plugins/line-highlight/prism-line-highlight.css";
@use "../../../node_modules/@raspberrypifoundation/design-system-core/scss/components/alert.scss";
@use "../../../node_modules/material-symbols/sharp.scss";
2 changes: 2 additions & 0 deletions src/assets/stylesheets/Tabs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
padding-inline-end: 0;
}
}

&__tab-close-btn {
block-size: 100%;
padding: $space-0-25;
Expand Down Expand Up @@ -112,6 +113,7 @@
&__tab-panel--selected {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
}
}
Expand Down
31 changes: 29 additions & 2 deletions src/components/Editor/EditorPanel/EditorPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import "../../../assets/stylesheets/EditorPanel.scss";
import React, { useRef, useEffect, useContext } from "react";
import React, { useRef, useEffect, useContext, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { updateProjectComponent } from "../../../redux/EditorSlice";
import { useCookies } from "react-cookie";
Expand All @@ -11,16 +11,20 @@ import { EditorState } from "@codemirror/state";
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
import { indentationMarkers } from "@replit/codemirror-indentation-markers";
import { indentUnit } from "@codemirror/language";
import "material-symbols";

import { html } from "@codemirror/lang-html";
import { css } from "@codemirror/lang-css";
import { python } from "@codemirror/lang-python";
import { javascript } from "@codemirror/lang-javascript";

import { Alert } from "@raspberrypifoundation/design-system-react";
import { editorLightTheme } from "../../../assets/themes/editorLightTheme";
import { editorDarkTheme } from "../../../assets/themes/editorDarkTheme";
import { SettingsContext } from "../../../utils/settings";

const MAX_CHARACTERS = 8500000;

const EditorPanel = ({ extension = "html", fileName = "index" }) => {
const editor = useRef();
const project = useSelector((state) => state.editor.project);
Expand All @@ -29,6 +33,7 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const settings = useContext(SettingsContext);
const [characterLimitExceeded, setCharacterLimitExceeded] = useState(false);

const updateStoredProject = (content) => {
dispatch(
Expand Down Expand Up @@ -86,6 +91,16 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
customIndentUnit = " ";
}

const limitCharacters = EditorState.transactionFilter.of((transaction) => {
const newDoc = transaction.newDoc;
if (newDoc.length > MAX_CHARACTERS) {
setCharacterLimitExceeded(true);
return [];
}
setCharacterLimitExceeded(false);
return transaction;
});

const startState = EditorState.create({
doc: code,
extensions: [
Expand All @@ -98,6 +113,7 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
indentationMarkers(),
indentUnit.of(customIndentUnit),
EditorView.editable.of(!readOnly),
limitCharacters,
],
});

Expand All @@ -123,7 +139,18 @@ const EditorPanel = ({ extension = "html", fileName = "index" }) => {
}, [cookies]);

return (
<div className={`editor editor--${settings.fontSize}`} ref={editor}></div>
<>
<div className={`editor editor--${settings.fontSize}`} ref={editor}></div>
{characterLimitExceeded && (
<Alert
title={t("editorPanel.characterLimitError")}
type="error"
text={t("editorPanel.characterLimitExplanation", {
maxCharacters: MAX_CHARACTERS,
})}
/>
)}
</>
);
};

Expand Down
47 changes: 46 additions & 1 deletion src/components/Editor/EditorPanel/EditorPanel.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import configureStore from "redux-mock-store";
import { Provider } from "react-redux";
import { SettingsContext } from "../../../utils/settings";
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
import EditorPanel from "./EditorPanel";

Expand Down Expand Up @@ -88,3 +88,48 @@ describe("When read only", () => {
expect(editorInputArea).toHaveAttribute("contenteditable", "false");
});
});

describe("When excessive file content is pasted into the editor", () => {
beforeEach(() => {
renderEditorPanel({ readOnly: false });
const editorInputArea = screen.getByLabelText("editorPanel.ariaLabel");
const massiveFileContent = "mango".repeat(2000000);
fireEvent.paste(editorInputArea, {
clipboardData: {
getData: () => massiveFileContent,
},
});
});

test("It does not display the file content", () => {
expect(screen.queryByText(/mango/)).not.toBeInTheDocument();
});

test("Character limit exceeded message is displayed", () => {
expect(
screen.getByText("editorPanel.characterLimitError"),
).toBeInTheDocument();
});

test("It allows the user to input text below the limit", () => {
const editorInputArea = screen.getByLabelText("editorPanel.ariaLabel");
fireEvent.paste(editorInputArea, {
clipboardData: {
getData: () => "mango",
},
});
expect(screen.getByText("mango")).toBeInTheDocument();
});

test("It removes the character limit exceeded message when the user inputs text below the limit", () => {
const editorInputArea = screen.getByLabelText("editorPanel.ariaLabel");
fireEvent.paste(editorInputArea, {
clipboardData: {
getData: () => "mango",
},
});
expect(
screen.queryByText("editorPanel.characterLimitError"),
).not.toBeInTheDocument();
});
});
11 changes: 10 additions & 1 deletion src/hooks/useProjectPersistence.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
} from "../redux/EditorSlice";
import { showLoginPrompt, showSavePrompt } from "../utils/Notifications";

const COMBINED_FILE_SIZE_SOFT_LIMIT = 1000000;

export const useProjectPersistence = ({
user,
project = {},
Expand All @@ -18,6 +20,13 @@ export const useProjectPersistence = ({
}) => {
const dispatch = useDispatch();

const combinedFileSize = project.components?.reduce(
(sum, component) => sum + component.content.length,
0,
);
const autoSaveInterval =
combinedFileSize > COMBINED_FILE_SIZE_SOFT_LIMIT ? 10000 : 2000;

const saveToLocalStorage = (project) => {
localStorage.setItem(
project.identifier || "project",
Expand Down Expand Up @@ -90,7 +99,7 @@ export const useProjectPersistence = ({
}
}
}
}, 2000);
}, autoSaveInterval);

return () => clearTimeout(debouncer);
}, [dispatch, project, user, hasShownSavePrompt]); // eslint-disable-line react-hooks/exhaustive-deps
Expand Down
28 changes: 28 additions & 0 deletions src/hooks/useProjectPersistence.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,34 @@ describe("When logged in", () => {
});
});

test("Increases save interval for large projects", async () => {
const largeProject = {
...project,
components: [
{
name: "main",
extension: "py",
content: "mango".repeat(200001),
},
],
};
renderHook(() =>
useProjectPersistence({
user: user1,
project: largeProject,
saveTriggered: false,
}),
);
jest.advanceTimersByTime(2500);
expect(saveProject).not.toHaveBeenCalled();
jest.runAllTimers();
expect(saveProject).toHaveBeenCalledWith({
project: largeProject,
accessToken: user1.access_token,
autosave: true,
});
});

test("Saves project to database if save triggered", async () => {
renderHook(() =>
useProjectPersistence({
Expand Down
3 changes: 3 additions & 0 deletions src/utils/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ i18n
},
editorPanel: {
ariaLabel: "editor text input",
characterLimitError: "Error: Character limit reached",
characterLimitExplanation:
"Files in the editor are limited to {{maxCharacters}} characters",
viewOnly: "View only",
},
filePanel: {
Expand Down
2 changes: 2 additions & 0 deletions storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"build-storybook": "build-storybook"
},
"dependencies": {
"@raspberrypifoundation/design-system-core": "^1.6.0",
"@raspberrypifoundation/design-system-react": "^1.6.0",
"@reduxjs/toolkit": "^1.8.3",
"@storybook/addon-actions": "6.5.10",
"@storybook/addon-essentials": "6.5.10",
Expand Down
Loading

0 comments on commit f63926e

Please sign in to comment.