Sanitize external links in user generated content

main
Charlotte Som 2022-05-11 14:32:33 +01:00
parent a8f518f4b8
commit 55cf127b7e
7 changed files with 176 additions and 23 deletions

View File

@ -2,9 +2,11 @@
font-family: 'Inter', sans-serif;
}
html, body {
html,
body {
background-color: var(--col-bg-0);
color: var(--col-fg-0);
margin: 0;
}
a {
@ -17,7 +19,9 @@ a.link-muted {
border-bottom: 1px solid var(--col-fg-2);
}
h1, h2, h3 {
h1,
h2,
h3 {
margin: 0;
margin-bottom: 0.25em;
}

View File

@ -4,5 +4,11 @@ export const getSession: GetSession = async () => {
const session: App.Session = {
instance: { url: 'https://trans.enby.town', token: '' }
};
try {
const token = window.localStorage.getItem('instance-token') || '';
session.instance.token = token;
} catch (_) {}
return session;
};

View File

@ -1,7 +1,23 @@
<script lang="ts">
import type { MastodonAccount } from '$lib/mastoapi/account';
import { onMount } from 'svelte';
import type { MastodonAccount } from '$lib/mastoapi/account';
export let account: MastodonAccount;
let fieldsNode: HTMLElement;
onMount(() => {
for (const a of Array.from(fieldsNode.querySelectorAll('dd a'))) {
const href = a.getAttribute('href');
if (href == null) continue;
if (new URL(href, window.location.href).origin != window.location.origin) {
a.setAttribute('title', href);
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'nofollow noopener external');
}
}
});
</script>
<article class="account-profile">
@ -9,13 +25,26 @@
<img class="avatar" alt={account.acct} src={account.avatar} />
<div>
<h1>{account.display_name}</h1>
<a class="link-muted" href={account.url}>@{account.fqn}</a>
<a class="link-muted" href={account.url} target="_blank" rel="nofollow noopener external">
@{account.fqn}
</a>
</div>
</header>
<div class="about">
<div class="fields" bind:this={fieldsNode}>
{#each account.fields as field}
<dl>
<dt>{field.name}</dt>
<dd>{@html field.value}</dd>
</dl>
{/each}
</div>
<div class="bio">
{@html account.note}
</div>
</div>
</article>
<style>
@ -41,4 +70,53 @@
.account-profile header > div {
width: 100%;
}
.about {
display: flex;
flex-direction: column;
}
.about > * {
border: 1px solid var(--col-bg-2);
border-collapse: collapse;
padding: 1em;
}
.bio {
border-block-start: none;
}
.fields dl {
display: flex;
margin: 0;
}
.fields dt {
background: var(--col-bg-2);
font-weight: 500;
max-width: 12ch;
min-width: 12ch;
}
.fields dd {
flex: 1 1 auto;
}
.fields dt,
.fields dd {
padding: 0.8em;
text-overflow: ellipsis;
overflow: hidden;
margin: 0;
max-height: 1em;
white-space: nowrap;
}
:global(.bio > p:first-child) {
margin-top: 0;
}
:global(.bio > p:last-child) {
margin-bottom: 0;
}
</style>

View File

@ -1,36 +1,37 @@
<script lang="ts">
import ago from '$lib/ago';
import { now } from '$lib/global-now';
import { linkAccount } from '$lib/mastoapi/account';
import {
linkStatus,
type MastodonStatus,
type MastodonStatusContext
} from '$lib/mastoapi/status';
import { linkStatus, type MastodonStatus } from '$lib/mastoapi/status';
import StatusContent from './StatusContent.svelte';
import StatusControls from './StatusControls.svelte';
export let status: MastodonStatus;
export let focused: boolean = false;
let createdDate = new Date(status.created_at);
</script>
<article class="status" class:focused>
<div class="status-content">
<aside class="status-author">
<a href={linkAccount(status.account)}>
<a href={`/acc/${status.account.id}`}>
<img class="avatar" alt={status.account.acct} src={status.account.avatar_static} />
</a>
</aside>
<div class="status-main">
<section class="top-line">
<a href={linkAccount(status.account)} class="author-link">
<a href={`/acc/${status.account.id}`} class="author-link">
<strong class="author-display-name">{status.account.display_name}</strong>
<span class="author-handle">@{status.account.fqn}</span>
</a>
<a class="link-muted" href={linkStatus(status)}>
<time>{ago(new Date(status.created_at), $now)}</time>
<time title={createdDate.toLocaleString()}>
{ago(createdDate, $now)}
</time>
</a>
</section>
@ -39,12 +40,12 @@
<p>[TODO: content warning <q>{status.spoiler_text}</q>]</p>
{/if}
{@html status.content}
<StatusContent {status} />
</section>
<section class="status-attachments">
{#each status.media_attachments as attachment}
<p>[TODO: attachment <q>{attachment.description || '(no description)'}</q>]</p>
<div>[TODO: attachment <q>{attachment.description || '(no description)'}</q>]</div>
{/each}
</section>
</div>
@ -70,13 +71,12 @@
}
.status aside {
width: 4rem;
margin-inline-end: 1em;
width: 3em;
margin-inline-end: 0.75em;
}
.status .avatar {
border-radius: 6px;
height: 3em;
border-radius: 4px;
max-width: 3em;
}
@ -99,9 +99,14 @@
flex-direction: row;
justify-content: space-between;
width: 100%;
margin-bottom: 0.5em;
}
.status .status-body {
margin-bottom: 0;
}
:global(.status-body > *:first-child, .status-body > .foreign-content > *:first-child) {
margin-top: 0;
}
</style>

View File

@ -0,0 +1,48 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { MastodonStatus } from '$lib/mastoapi/status';
export let status: MastodonStatus;
let node: HTMLElement;
onMount(() => {
for (let a of Array.from(node.getElementsByTagName('a'))) {
const href = a.getAttribute('href');
if (href == null) continue;
if (a.classList.contains('hashtag')) {
for (const tag of status.tags) {
if (href === tag.url) {
a.setAttribute('href', `/tags/${tag.name}`);
a.removeAttribute('rel');
a.removeAttribute('target');
}
}
} else if (a.classList.contains('mention')) {
for (const mention of status.mentions) {
if (href === mention.url) {
a.setAttribute('href', `/acc/${mention.id}`);
a.setAttribute('title', `@${mention.acct}`);
a.removeAttribute('rel');
a.removeAttribute('target');
}
}
} else if (new URL(href, window.location.href).origin != window.location.origin) {
a.setAttribute('title', href);
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'nofollow noopener external');
}
}
});
</script>
<div class="foreign-content" bind:this={node}>
{@html status.content}
</div>
<style>
.foreign-content {
display: contents;
}
</style>

View File

@ -13,10 +13,12 @@ export interface MastodonAccount extends MastodonObject {
note: string;
bot: boolean;
}
export function linkAccount(account: MastodonAccount) {
return '/acc/' + account.id;
fields: {
name: string;
value: string;
verified_at: string | null;
}[];
}
export async function fetchAccount(instance: InstanceInfo, id: string): Promise<MastodonAccount> {

View File

@ -18,6 +18,16 @@ export interface MastodonStatus extends MastodonObject {
url: string;
in_reply_to_id: string | null;
media_attachments: MastodonMediaAttachment[];
tags: {
name: string;
url: string;
}[];
mentions: {
acct: string;
id: string;
username: string;
url: string;
}[];
}
export interface MastodonStatusContext {