Skip to content

Commit

Permalink
add noreferrer, make csp less janky, fix error when no default visibi…
Browse files Browse the repository at this point in the history
…lity is set (looking at you sharkey), max-width on inline images, show tags from wafrn with spaces instead of dashes, don't duplicate hashtag processing work, fix hashtagbar for some posts from sharkey
  • Loading branch information
easrng committed May 26, 2024
1 parent 83da0be commit 1943c42
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 56 deletions.
17 changes: 15 additions & 2 deletions bin/build-inline-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { sapperInlineScriptChecksums } from '../src/server/sapperInlineScriptChe
const __dirname = path.dirname(new URL(import.meta.url).pathname)
const writeFile = promisify(fs.writeFile)

export async function buildInlineScript () {
async function buildInlineScriptAndCSP () {
const inlineScriptPath = path.join(__dirname, '../src/inline-script/inline-script.js')

const bundle = await rollup({
Expand Down Expand Up @@ -63,5 +63,18 @@ export async function buildInlineScript () {
].join(';')
await writeFile(path.resolve(__dirname, '../static/inline-script.js.map'),
map.toString(), 'utf8')
return `<meta http-equiv="Content-Security-Policy" content="${policy}" /></head><body><script>${fullCode}</script>`
return {
inlineScript: `<script>${fullCode}</script>`,
csp: `<meta http-equiv="Content-Security-Policy" content="${policy}" />`
}
}

export function buildInlineScript (context) {
if (context.inlineScript) {
return context.inlineScript
} else {
const promise = buildInlineScriptAndCSP()
context.inlineScript = promise.then(e => e.inlineScript)
return promise.then(e => e.csp)
}
}
13 changes: 8 additions & 5 deletions bin/build-template-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@ const LOCALE_DIRECTION = getLangDir(LOCALE)
const DEBOUNCE = 500

const builders = [
{
watch: 'src/inline-script/inline-script.js',
comment: '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'" />',
rebuild: buildInlineScript
},
{
watch: 'src/scss',
comment: '<!-- inline CSS -->',
rebuild: buildSass
},
{
watch: 'src/inline-script/inline-script.js',
comment: String.raw`<!--( CSP )--
</head>
<body>
--( inline JS )-->`,
comment: '<!-- inline JS -->',
rebuild: buildInlineScript
},
{
Expand Down Expand Up @@ -87,12 +89,13 @@ function doWatch () {

async function buildAll () {
const start = performance.now()
const context = {}
let html = (await Promise.all(partials.map(async partial => {
if (typeof partial === 'string') {
return partial
}
if (!partial.result) {
partial.result = partial.comment + '\n' + (await partial.rebuild())
partial.result = (partial.comment[1] === '!' ? (partial.comment + '\n') : '') + (await partial.rebuild(context))
}
return partial.result
}))).join('')
Expand Down
6 changes: 4 additions & 2 deletions src/build/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<html lang="{process.env.LOCALE}" dir="{process.env.LOCALE_DIRECTION}">
<head>
<meta charset='utf-8' >
<!-- CSP (replacement fails closed) -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'" />
<meta name="referrer" content="no-referrer" />
<meta name="viewport" content="width=device-width">
<meta id='theThemeColor' name='theme-color' content='#4169e1' >
<meta name="description" content="intl.appDescription" >
Expand Down Expand Up @@ -110,10 +113,9 @@
<!-- This contains the contents of the <:Head> component, if
the current page has one -->
%sapper.head%
<!--( CSP )--
<!-- inline JS -->
</head>
<body>
--( inline JS )-->

<!-- The application will be rendered inside this element,
because `templates/client.js` references it -->
Expand Down
2 changes: 1 addition & 1 deletion src/routes/_actions/compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function setReplyVisibility (realm, replyVisibility) {
return // user has already set the postPrivacy
}
const { currentVerifyCredentials } = store.get()
const defaultVisibility = currentVerifyCredentials.source.privacy
const defaultVisibility = currentVerifyCredentials.source.privacy || 'public'
const visibility = PRIVACY_LEVEL[replyVisibility] < PRIVACY_LEVEL[defaultVisibility]
? replyVisibility
: defaultVisibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
return POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey)
},
postPrivacyKey: ({ composeData, $currentVerifyCredentials }) => {
return composeData.postPrivacy || $currentVerifyCredentials.source.privacy
return composeData.postPrivacy || $currentVerifyCredentials.source.privacy || 'public'
},
localOnly: ({ composeData }) => {
return composeData.localOnly
Expand Down
4 changes: 4 additions & 0 deletions src/routes/_components/status/StatusContent.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
text-decoration: underline;
}

:global(.status-content img:not(.inline-custom-emoji)) {
max-width: 100%;
}

@media (max-width: 240px) {
:global(
.status-content p:last-child,
Expand Down
2 changes: 1 addition & 1 deletion src/routes/_components/status/StatusTags.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="status-hashtag-bar {isStatusInNotification ? 'status-in-notification' : '' }">
{#each hashtagsInBar as hashtagInBar}
<a href="/tags/{encodeURIComponent(hashtagInBar)}" class="hashtag" rel="nofollow noopener ugc tag">#{hashtagInBar}</a>
<a href="/tags/{encodeURIComponent(hashtagInBar.value)}" class="hashtag" rel="nofollow noopener ugc tag" title="{hashtagInBar.display ? hashtagInBar.value : ''}">#{hashtagInBar.display || hashtagInBar.value}</a>
{/each}
</div>
<style>
Expand Down
84 changes: 81 additions & 3 deletions src/routes/_utils/renderPostHTML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,69 @@ import {
serialize,
} from 'parse5'
import { Mention } from './types.ts'
import * as hashtag from '../_workers/processContent/hashtagBar.ts'
import {
normalizeHashtag,
localeAwareInclude,
} from '../_workers/processContent/hashtagBar.ts'

const {
NS: { HTML },
} = html

function isNodeLinkHashtag(element: DefaultTreeAdapterMap['element']): boolean {
if (element.tagName === 'a') {
const c = element.attrs.find((attr) => /* */ attr.name === 'class')
const r = element.attrs.find((attr) => attr.name === 'rel')
const h = element.attrs.find((attr) => attr.name === 'href')
return (
!!c?.value.split(/\s+/g).includes('hashtag') ||
!!r?.value.split(/\s+/g).includes('tag') ||
!!h?.value?.match(/\/tags\/[^\/]+$|\/search\?tag=/) // GtS and Friendica, respectively
)
}
return false
}

function textContent(node: DefaultTreeAdapterMap['parentNode']): string[] {
let text = []
for (const child of node.childNodes) {
if (defaultTreeAdapter.isTextNode(child)) {
text.push(child.value)
} else if ('childNodes' in child) {
text.push(...textContent(child))
}
}
return text
}

const isValidHashtagNode = (
node: DefaultTreeAdapterMap['node'],
normalizedTagNames: string[],
) => {
if (!node) {
return false
}
let text: string
if (
defaultTreeAdapter.isElementNode(node) &&
isNodeLinkHashtag(node) &&
(text = textContent(node).join(''))
) {
const normalized = normalizeHashtag(text)
if (!localeAwareInclude(normalizedTagNames, normalized)) {
// stop here, this is not a real hashtag, so consider it as text
return false
}
return normalized
} else if (!defaultTreeAdapter.isTextNode(node) || node.value.trim()) {
// not a space
return false
} else {
// spaces
return true
}
}

function consumeBalanced(
string: string,
open: string,
Expand Down Expand Up @@ -174,10 +231,14 @@ export function renderPostHTMLToDOM({
let tag: string | boolean
if (
href != null &&
(tag = hashtag.isValidHashtagNode(anchor, normalizedTagNames)) &&
(tag = isValidHashtagNode(anchor, normalizedTagNames)) &&
typeof tag === 'string'
) {
let isFriendica = href.value.includes('/search?tag=')
const wafrnMatch = href.value.match(/\/dashboard\/search\/([^\/]+)$/)
const wafrnTag = wafrnMatch
? decodeURIComponent(wafrnMatch[1]!)
: undefined
if (isFriendica) {
isFriendica = false
const parent = anchor.parentNode
Expand All @@ -196,8 +257,25 @@ export function renderPostHTMLToDOM({
href.value = `/tags/${encodeURIComponent(tag)}`
c.value = 'hashtag'
rel.value = 'nofollow noopener ugc tag'
anchor.attrs.push({
name: 'data-tag',
value: tag,
})
if (wafrnTag) {
anchor.attrs.push({
name: 'data-wafrn-tag',
value: wafrnTag,
})
anchor.attrs.push({
name: 'title',
value: tag,
})
}
anchor.childNodes = []
defaultTreeAdapter.insertText(anchor, (isFriendica ? '' : '#') + tag)
defaultTreeAdapter.insertText(
anchor,
(isFriendica ? '' : '#') + (wafrnTag ? wafrnTag : tag),
)
return
} else if (href != null && classList.includes('mention')) {
const mention = mentionsByURL.get(href.value)
Expand Down
Loading

0 comments on commit 1943c42

Please sign in to comment.