Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Privacy Iframe #18

Merged
merged 4 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .temp/iframe-inner.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<body style="margin:0;overflow:hidden">
<script src="/website/static/child.js"></script>
<style>
</style>
<div id="wrap"></div>

<script>
const wrap = document.querySelector("#wrap");
wrap.onclick = addElement;
// function() {
// wrap.style.height = wrap.style.height === "100px" ? "200px" : "100px";
// }

function addElement() {
const el = document.createElement("div");
el.style.height = "100px";
el.style.backgroundColor = "red";

wrap.appendChild(el);
}

addElement();
</script>
</body>
5 changes: 5 additions & 0 deletions src/Embed/Embed.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Hyvor\Unfold\Embed;

use Hyvor\Unfold\Embed\Iframe\PrivacyIframe;
use Hyvor\Unfold\Exception\EmbedUnableToResolveException;
use Hyvor\Unfold\Exception\EmbedParserException;
use Hyvor\Unfold\Exception\UnfoldException;
Expand Down Expand Up @@ -73,6 +74,10 @@
): Unfolded {
$oembed = self::parse($url, $context->config);

if ($context->config->embedIframeEndpoint && $oembed->html) {

Check failure on line 77 in src/Embed/Embed.php

View workflow job for this annotation

GitHub Actions / PHP Static Analysis

Access to an undefined property Hyvor\Unfold\UnfoldConfig::$embedIframeEndpoint.
$oembed->html = PrivacyIframe::wrap($oembed->html);
}

return Unfolded::fromEmbed(
$oembed,
$url,
Expand Down
21 changes: 21 additions & 0 deletions src/Embed/Iframe/PrivacyIframe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Hyvor\Unfold\Embed\Iframe;

class PrivacyIframe
{
public static function wrap(string $html): string
{
$childJs = (string) file_get_contents(__DIR__ . '/child.js');

return <<<HTML
<html>
<body style="margin:0;overflow:hidden">
$html
$childJs
</body>
</html>
HTML;
}

}
42 changes: 42 additions & 0 deletions src/Embed/Iframe/child.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
(function () {
function sendHeight() {
const height = document.documentElement.scrollHeight;

try {
const iframe = window.frameElement;
if (iframe) {
iframe.style.height = `${height}px`;
} else {
throw new Error("iframe not found");
}
} catch (e) {
window.parent.postMessage(
{
type: "unfold-iframe-resize",
height,
},
"*"
);
}
}

let mutationCallTimeout = null;

function processMutations() {
if (mutationCallTimeout) {
clearTimeout(mutationCallTimeout);
}
mutationCallTimeout = setTimeout(sendHeight, 50);
}

function init() {
const mutation = new window.MutationObserver(processMutations);
mutation.observe(document.body, {
childList: true,
subtree: true,
});
sendHeight();
}

document.addEventListener("DOMContentLoaded", init);
})();
13 changes: 13 additions & 0 deletions src/Embed/Iframe/parent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
(function () {
window.addEventListener("message", function (event) {
const source = event.source;
const iframes = document.querySelectorAll("iframe");
for (let iframe in iframes) {
if (iframes[iframe].contentWindow === source) {
if (event.data.type === "unfold-iframe-resize") {
iframes[iframe].style.height = `${event.data.height}px`;
}
}
}
});
})();
2 changes: 1 addition & 1 deletion src/Unfold.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static function unfold(
} else {
// both
// TODO:
throw new \Exception('Not implemented yet');
throw new \Exception('Not implemented yet'); // @codeCoverageIgnore
}
}
}
13 changes: 0 additions & 13 deletions src/UnfoldConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,6 @@ public function __construct(
*/
public UnfoldMethod $method = UnfoldMethod::LINK,

/**
* Whether to wrap the embed HTML in an iframe with `srcdoc`
* This is useful for security and privacy reasons.
* If set to false, the embed HTML will be directly used, which would give Javascript access to
* the parent page.
*/
public bool $embedWrapInIframe = true,

/**
* If the $method is UnfoldMethod::EMBED or UnfoldMethod::EMBED_LINK,
* and if we cannot find a way to embed the URL using our default parsers,
Expand Down Expand Up @@ -68,11 +60,6 @@ public function __construct(
*/
public string $httpUserAgent = 'Hyvor Unfold PHP Client',

/**
* TODO: Implement this
*/
public ?string $iframeEndpoint = null,

/**
* Meta requires an access_token to access the OEmbed Read Graph API
* This is required for both FacebookPost & Instagram
Expand Down
124 changes: 124 additions & 0 deletions website/src/routes/[[slug]]/Iframe.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script>
import { Callout, CodeBlock } from '@hyvor/design/components';
</script>

<h1>Privacy Iframe</h1>

<p>
Adding embed codes directly to your website is a <strong>privacy concern</strong> since they may
include Javascript that can be used for tracking. Using an
<strong>iframe to wrap the embed code</strong> is the best way to prevent this.
</p>

<h2 id="docker">Docker Image</h2>

<p>
If you are using the <a href="/docker">Docker image</a>, the iframe endpoint is already included.
On your website, use iframes with <code>src</code> set to <code>/iframe?url=my-url</code> to embed
content.
</p>

<CodeBlock
code={`
<iframe
src="https://unfold.example.org/iframe?url=https://url-to-embed.com"
sandbox="allow-scripts allow-modals allow-popups"
></iframe>
`}
/>

<p>
The <code>sandbox</code> attribute is only required if you are hosting the Docker image on the same
domain as the parent website (Same Origin Policy). It's purpose is to prevent the embedded content
from accessing the parent website's window.
</p>

<h2 id="iframe-endpoint">PHP Library</h2>

<p>
If you are using the <a href="/php">PHP Library</a>, you have to set up an endpoint within your
website for the iframe. Here is an example with Laravel, but you can use any PHP framework.
</p>

<CodeBlock
code={`
Route::get('/unfold-iframe', 'IframeController@getIframe');
`}
language="js"
/>

<CodeBlock
code={`
use Hyvor\\Unfold\\Unfold;
use Hyvor\\Unfold\\Exceptions\\UnfoldException;
use Hyvor\\Unfold\\Embed\\Iframe\\PrivacyIframe;
use Illuminate\\Http\\Request;

class IframeController
{

public function getIframe(Request $request)
{
$url = $request->input('url');

try {
$data = Unfold::unfold($url);
} catch (UnfoldException) {
// handle the exception
}

return PrivacyIframe::wrap($data);
}

}
`}
language="js"
/>

<p>
The <code>PrivacyIframe::wrap</code> method will wrap the embed code in a full HTML document with
sensible defaults. It will also include the necessary Javascript for
<a href="#resize">resizing the iframe</a> based on the content.
</p>

<p>You can then use the iframe in your website like this:</p>

<CodeBlock
code={`
<iframe
src="/unfold-iframe?url=https://url-to-embed.com"
sandbox="allow-scripts allow-modals allow-popups"
></iframe>
`}
/>

<h2 id="resize">Resizing the iframe</h2>

<p>
Iframe resizing is a common problem when embedding content. The content inside the iframe may
change its height based on user interactions. Since Javascript inside the iframe cannot access the
parent window, we need to use a <strong>postMessage</strong> to communicate between the parent and
child windows.
</p>

<p>
When you call the <code>PrivacyIframe::wrap</code> method, it will include
<a href="./blob/main/src/Embed/Iframe/child.js"> child.js </a> script that will send the height of
the content to the parent window.
</p>

<p>
On your website, you have to include the <a href="./blob/main/src/Embed/Iframe/parent.js">
parent.js
</a> script. This script will listen to the messages from the child window and resize the iframe accordingly.
</p>

<CodeBlock
code={`
<` +
`script src="/parent.js"><` +
`/script>
`}
/>

<p>Or, since the code is pretty small, feel free to copy it to your source code directly.</p>
2 changes: 1 addition & 1 deletion website/src/routes/[[slug]]/Introduction.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { CodeBlock } from '@hyvor/design/components';
</script>

<h1>Introduction</h1>
<h1>Hyvor Unfold</h1>

<p>
Hyvor Unfold is an open-source API that can fetch metadata from URLs for <strong
Expand Down
14 changes: 9 additions & 5 deletions website/src/routes/[[slug]]/docs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { ComponentType } from "svelte";
import Introduction from "./Introduction.svelte";
import Iframe from "./Iframe.svelte";

export const categories = [
export const categories: Category[] = [
{
name: 'Unfold',
pages: [
Expand All @@ -12,19 +13,22 @@ export const categories = [
},
{
name: 'PHP Library',
slug: 'php'
slug: 'php',
component: Introduction
},
{
name: 'Privacy Iframe',
slug: 'privacy-iframe'
slug: 'iframe',
component: Iframe
},
{
name: 'Docker Hosting',
slug: 'docker'
slug: 'docker',
component: Introduction
}
]
}
] as Category[];
];

export const pages = categories.reduce((acc, category) => acc.concat(category.pages), [] as Page[]);

Expand Down
12 changes: 12 additions & 0 deletions website/src/routes/iframe-test/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<p>This is an iframe that changes its height every time you click on it.</p>

<iframe
src="http://localhost:1234/.temp/iframe-inner.html"
style="margin:30px;border:4px solid black"
width="700"
height="0"
></iframe>

<svelte:head>
<script src="/parent.js"></script>
</svelte:head>
25 changes: 25 additions & 0 deletions website/src/routes/iframe-test/inner/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import { onMount } from 'svelte';

let items = 1;

function handleClick() {
items += 1;
}
</script>

<svelte:head>
<script src="/child.js"></script>
</svelte:head>

<body on:click={handleClick}>
{#each Array(items) as _, i}
<div style="height:150px" />
{/each}
</body>

<style>
body {
background-color: blue;
}
</style>
Loading
Loading