Sanitize external links in user generated content
parent
a8f518f4b8
commit
55cf127b7e
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,12 +25,25 @@
|
|||
<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">
|
||||
{@html account.note}
|
||||
<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>
|
||||
|
||||
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue