Skip to content

Commit

Permalink
Allow links to our own domains (#729)
Browse files Browse the repository at this point in the history
Closing issue:
#713

---------

Co-authored-by: magdalenajadach <[email protected]>
Co-authored-by: Magdalena Jadach <[email protected]>
Co-authored-by: Pete Simonovic <[email protected]>
  • Loading branch information
4 people authored Nov 8, 2023
1 parent 42e70c2 commit 328cc22
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 106 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Changed

- Replace physical properties with logical values
- Allow external rpf.io links (#729)
- Replace physical properties with logical values (#699)
- Moved web component custom events from the `editor-wc` element to the `document` (#710)
- Renamed web component custom events to be prefixed with `editor-` (#710)
- Switch props of `WebComponentLoader` from `snake_case` to `camelCase` (#712)
Expand Down
18 changes: 16 additions & 2 deletions cypress/e2e/spec-html.cy.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const baseUrl = "localhost:3000/en/projects/blank-html-starter";
const baseUrl = "http://localhost:3000/en/projects/blank-html-starter";

const getIframeDocument = () => {
return cy
Expand Down Expand Up @@ -43,7 +43,7 @@ it("updates the preview after a change when you click run", () => {
getIframeBody().find("p").should("include.text", "hello world");
});

it("blocks external links", () => {
it("blocks non-permitted external links", () => {
localStorage.clear();
cy.visit(baseUrl);
cy.get("div[class=cm-content]").invoke(
Expand All @@ -57,6 +57,20 @@ it("blocks external links", () => {
.should("include.text", "An error has occurred");
});

it("allows permitted external links", () => {
localStorage.clear();
cy.visit(baseUrl);
cy.get("div[class=cm-content]").invoke(
"text",
'<a href="https://rpf.io/seefood">some external link</a>',
);
cy.get(".btn--run").click();
const externalLink = getIframeBody().find("a");
externalLink.click();
cy.url().should("be.equals", baseUrl);
cy.get("div[class=modal-content__header]").should("not.exist");
});

it("allows internal links", () => {
localStorage.clear();
cy.visit(baseUrl);
Expand Down
89 changes: 56 additions & 33 deletions cypress/e2e/spec-wc.cy.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,73 @@
const baseUrl = "http://localhost:3001"
const baseUrl = "http://localhost:3001";

beforeEach(() => {
cy.visit(baseUrl)
})
cy.visit(baseUrl);
});

it("renders the web component", () => {
cy.get("editor-wc").shadow().find("button").should("contain", "Run")
})
cy.get("editor-wc").shadow().find("button").should("contain", "Run");
});

it("defaults to the text output tab", () => {
const runnerContainer = cy.get("editor-wc").shadow().find('.proj-runner-container')
runnerContainer.find('.react-tabs__tab--selected').should("contain", "Text output")
})
const runnerContainer = cy
.get("editor-wc")
.shadow()
.find(".proj-runner-container");
runnerContainer
.find(".react-tabs__tab--selected")
.should("contain", "Text output");
});

it("runs the python code", () => {
cy.get("editor-wc").shadow().find("div[class=cm-content]").invoke('text', 'print("Hello world")')
cy.get("editor-wc").shadow().find(".btn--run").click()
cy.get("editor-wc").shadow().find(".pythonrunner-console-output-line").should("contain", "Hello world")
})
cy.get("editor-wc")
.shadow()
.find("div[class=cm-content]")
.invoke("text", 'print("Hello world")');
cy.get("editor-wc").shadow().find(".btn--run").click();
cy.get("editor-wc")
.shadow()
.find(".pythonrunner-console-output-line")
.should("contain", "Hello world");
});

it("does not render visual output tab on page load", () => {
cy.get("editor-wc").shadow().find('#root').should("not.contain", "Visual output")
})
cy.get("editor-wc")
.shadow()
.find("#root")
.should("not.contain", "Visual output");
});

it("renders visual output tab if sense hat imported", () => {
cy.get("editor-wc").shadow().find("div[class=cm-content]").invoke('text', 'import sense_hat')
cy.get("editor-wc").shadow().find(".btn--run").click()
cy.get("editor-wc").shadow().find('#root').should("contain", "Visual output")
})
cy.get("editor-wc")
.shadow()
.find("div[class=cm-content]")
.invoke("text", "import sense_hat");
cy.get("editor-wc").shadow().find(".btn--run").click();
cy.get("editor-wc").shadow().find("#root").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")
})
it("does not render astro pi component on page load", () => {
cy.get("editor-wc").shadow().find("#root").should("not.contain", "yaw");
});

it("renders astro pi component if sense hat imported", () => {
cy.get("editor-wc").shadow().find("div[class=cm-content]").invoke('text', 'import sense_hat')
cy.get("editor-wc").shadow().find(".btn--run").click()
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("div[class=cm-content]")
.invoke("text", "import sense_hat");
cy.get("editor-wc").shadow().find(".btn--run").click();
cy.get("editor-wc").shadow().contains("Visual output").click();
cy.get("editor-wc").shadow().find("#root").should("contain", "yaw");
});

it("does not render astro pi component if sense hat unimported", () => {
cy.get("editor-wc").shadow().find("div[class=cm-content]").invoke('text', 'import sense_hat')
cy.get("editor-wc").shadow().find(".btn--run").click()
cy.get("editor-wc").shadow().find("div[class=cm-content]").invoke('text', '')
cy.get("editor-wc").shadow().find(".btn--run").click()
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("div[class=cm-content]")
.invoke("text", "import sense_hat");
cy.get("editor-wc").shadow().find(".btn--run").click();
cy.get("editor-wc").shadow().find("div[class=cm-content]").invoke("text", "");
cy.get("editor-wc").shadow().find(".btn--run").click();
cy.get("editor-wc").shadow().contains("Visual output").click();
cy.get("editor-wc").shadow().find("#root").should("not.contain", "yaw");
});
177 changes: 119 additions & 58 deletions src/components/Editor/Runners/HtmlRunner/HtmlRunner.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,17 @@ function HtmlRunner() {
const locale = i18n.language;

const dispatch = useDispatch();
const output = useRef();
const output = useRef(null);
const [error, setError] = useState(null);
const allowedHrefs = ["#"];
const domain = `https://rpf.io/`;
const rpfDomain = new RegExp(`^${domain}`);
const allowedInternalLinks = [new RegExp(`^#[a-zA-Z0-9]+`)];
const allowedExternalHrefs = [rpfDomain];

const matchingRegexes = (regexArray, testString) => {
return regexArray.some((reg) => reg.test(testString));
};

const isMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY });

const focussedComponent = (fileName = "index.html") =>
Expand All @@ -71,6 +79,7 @@ function HtmlRunner() {

const [previewFile, setPreviewFile] = useState(defaultPreviewFile);
const [runningFile, setRunningFile] = useState(previewFile);
const [externalLink, setExternalLink] = useState();

const showModal = () => {
dispatch(showErrorModal());
Expand All @@ -83,6 +92,18 @@ function HtmlRunner() {
return URL.createObjectURL(blob);
};

const getFilename = (iframe) => {
let filename;
if (iframe) {
filename = iframe.querySelectorAll("meta[filename]")[0]
? iframe.querySelectorAll("meta[filename]")[0].getAttribute("filename")
: externalLink;
} else {
filename = externalLink;
}
return filename;
};

const cssProjectImgs = (projectFile) => {
var updatedProjectFile = { ...projectFile };
if (projectFile.extension === "css") {
Expand All @@ -104,7 +125,11 @@ function HtmlRunner() {
if (typeof event.data?.msg === "string") {
if (event.data?.msg === "ERROR: External link") {
setError("externalLink");
} else if (event.data?.msg === "Allowed external link") {
setExternalLink(event.data.payload.linkTo);
dispatch(triggerCodeRun());
} else {
setExternalLink(null);
setPreviewFile(`${event.data.payload.linkTo}.html`);
dispatch(triggerCodeRun());
}
Expand All @@ -114,12 +139,25 @@ function HtmlRunner() {

const iframeReload = () => {
const iframe = output.current.contentDocument;
const filename = iframe.querySelectorAll("meta[filename]")[0]
? iframe.querySelectorAll("meta[filename]")[0].getAttribute("filename")
: null;
let filename = getFilename(iframe);

if (runningFile !== filename) {
setRunningFile(filename);
}

if (iframe) {
const linkElement = iframe.querySelector("a");

if (linkElement) {
linkElement.addEventListener("click", (e) => {
e.preventDefault();

output.current.contentDocument.href =
linkElement.getAttribute("href");
});
}
}
setExternalLink(null);
};

useEffect(() => {
Expand All @@ -146,7 +184,11 @@ function HtmlRunner() {
}, [codeRunTriggered]);

useEffect(() => {
if (!isEmbedded && previewable(openFiles[focussedFileIndex])) {
if (
!externalLink &&
!isEmbedded &&
previewable(openFiles[focussedFileIndex])
) {
setPreviewFile(openFiles[focussedFileIndex]);
}
}, [focussedFileIndex, openFiles]);
Expand All @@ -169,69 +211,88 @@ function HtmlRunner() {

const runCode = () => {
setRunningFile(previewFile);
if (!externalLink) {
let indexPage = parse(focussedComponent(previewFile).content);

let indexPage = parse(focussedComponent(previewFile).content);
const body = indexPage.querySelector("body") || indexPage;

const body = indexPage.querySelector("body") || indexPage;
const hrefNodes = indexPage.querySelectorAll("[href]");

const hrefNodes = indexPage.querySelectorAll("[href]");

// replace href's with blob urls
hrefNodes.forEach((hrefNode) => {
const projectFile = projectCode.filter(
(file) => `${file.name}.${file.extension}` === hrefNode.attrs.href,
);

// remove target blanks
if (hrefNode.attrs?.target === "_blank") {
hrefNode.removeAttribute("target");
}
// replace href's with blob urls
hrefNodes.forEach((hrefNode) => {
const projectFile = projectCode.filter(
(file) => `${file.name}.${file.extension}` === hrefNode.attrs.href,
);

let onClick;
// remove target blanks
if (hrefNode.attrs?.target === "_blank") {
hrefNode.removeAttribute("target");
}

if (!!projectFile.length) {
if (parentTag(hrefNode, "head")) {
const projectFileBlob = getBlobURL(
cssProjectImgs(projectFile[0]).content,
`text/${projectFile[0].extension}`,
);
hrefNode.setAttribute("href", projectFileBlob);
let onClick;

if (!!projectFile.length) {
if (parentTag(hrefNode, "head")) {
const projectFileBlob = getBlobURL(
cssProjectImgs(projectFile[0]).content,
`text/${projectFile[0].extension}`,
);
hrefNode.setAttribute("href", projectFileBlob);
} else {
// eslint-disable-next-line no-script-url
hrefNode.setAttribute("href", "javascript:void(0)");
onClick = `window.parent.postMessage({msg: 'RELOAD', payload: { linkTo: '${projectFile[0].name}' }})`;
}
} else {
// eslint-disable-next-line no-script-url
hrefNode.setAttribute("href", "javascript:void(0)");
onClick = `window.parent.postMessage({msg: 'RELOAD', payload: { linkTo: '${projectFile[0].name}' }})`;
const matchingExternalHref = matchingRegexes(
allowedExternalHrefs,
hrefNode.attrs.href,
);
const matchingInternalHref = matchingRegexes(
allowedInternalLinks,
hrefNode.attrs.href,
);
if (
!matchingInternalHref &&
!matchingExternalHref &&
!parentTag(hrefNode, "head")
) {
// eslint-disable-next-line no-script-url
hrefNode.setAttribute("href", "javascript:void(0)");
onClick =
"window.parent.postMessage({msg: 'ERROR: External link'})";
} else if (matchingExternalHref) {
onClick = `window.parent.postMessage({msg: 'Allowed external link', payload: { linkTo: '${hrefNode.attrs.href}' }})`;
}
}
} else {
if (
!allowedHrefs.includes(hrefNode.attrs.href) &&
!parentTag(hrefNode, "head")
) {
// eslint-disable-next-line no-script-url
hrefNode.setAttribute("href", "javascript:void(0)");
onClick = "window.parent.postMessage({msg: 'ERROR: External link'})";

if (onClick) {
hrefNode.removeAttribute("target");
hrefNode.setAttribute("onclick", onClick);
}
}
if (onClick) {
hrefNode.setAttribute("onclick", onClick);
}
});
});

const srcNodes = indexPage.querySelectorAll("[src]");
srcNodes.forEach((srcNode) => {
const projectImage = projectImages.filter(
(component) => component.filename === srcNode.attrs.src,
);
srcNode.setAttribute(
"src",
!!projectImage.length ? projectImage[0].url : "",
);
});
const srcNodes = indexPage.querySelectorAll("[src]");
srcNodes.forEach((srcNode) => {
const projectImage = projectImages.filter(
(component) => component.filename === srcNode.attrs.src,
);
srcNode.setAttribute(
"src",
!!projectImage.length ? projectImage[0].url : "",
);
});

body.appendChild(parse(`<meta filename="${previewFile}" />`));
body.appendChild(parse(`<meta filename="${previewFile}" />`));

const blob = getBlobURL(indexPage.toString(), "text/html");
output.current.src = blob;
if (codeRunTriggered) {
dispatch(codeRunHandled());
}
} else {
output.current.src = externalLink;

const blob = getBlobURL(indexPage.toString(), "text/html");
output.current.src = blob;
if (codeRunTriggered) {
dispatch(codeRunHandled());
}
};
Expand Down
Loading

0 comments on commit 328cc22

Please sign in to comment.