Skip to content
39 changes: 31 additions & 8 deletions src/components/Preview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,49 @@

let loading = true
let preview: Preview | undefined = undefined
let renderNonce = 0

let title = ''

$: {
if (url) {
const decodedUrl = decodeURIComponent(url)
const currentRender = ++renderNonce

if (typeof window !== 'undefined') {
if (!preview) {
preview = new Preview('#site-frame')
}

preview.render(decodedUrl).then(() => {
loading = false

if (preview?.pageData.title) {
// update with page title or URL
title = preview.pageData.title
}
})
loading = true
title = ''

preview
.render(decodedUrl)
.then(() => {
if (currentRender !== renderNonce) {
return
}

if (preview?.pageData.title) {
// update with page title or URL
title = preview.pageData.title
}
})
.catch(() => {
if (currentRender !== renderNonce) {
return
}

title = decodedUrl
})
.finally(() => {
if (currentRender !== renderNonce) {
return
}

loading = false
})
}
}
}
Expand Down
67 changes: 67 additions & 0 deletions src/routes/img/[...path]/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { error } from '@sveltejs/kit'
import {
buildImageFallbackTargets,
decodePreviewUrlFromReferer,
isSupportedImageFallbackSource,
sanitizeRelativePath,
} from '../../../utils/image-fallback'

export const prerender = false

export async function GET({ request, params, fetch }) {
const imagePath = sanitizeRelativePath(params.path)
if (!imagePath) {
throw error(400, 'Invalid image path')
}

const previewUrl = decodePreviewUrlFromReferer(request.headers.get('referer'))
if (!previewUrl) {
throw error(404, 'Preview context unavailable')
}

if (!isSupportedImageFallbackSource(previewUrl)) {
throw error(404, 'Preview source not supported for image fallback')
}

const targets = buildImageFallbackTargets(previewUrl, imagePath)
if (!targets.length) {
throw error(404, 'Could not resolve image target')
}

for (const target of targets) {
let upstream: Response
try {
upstream = await fetch(target, {
redirect: 'follow',
headers: {
Accept: request.headers.get('accept') ?? 'image/*,*/*',
},
})
} catch {
continue
}

if (!upstream.ok) {
continue
}

const payload = await upstream.arrayBuffer()
const headers = new Headers()

const contentType = upstream.headers.get('content-type')
if (contentType) {
headers.set('Content-Type', contentType)
}
headers.set(
'Cache-Control',
upstream.headers.get('cache-control') ?? 'public, max-age=300',
)

return new Response(payload, {
status: upstream.status,
headers,
})
}

throw error(404, 'Image not found')
}
98 changes: 98 additions & 0 deletions src/utils/image-fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { resourceType } from '../types/resources'
import { getRepositoryRoot } from './lang/html'
import { getResourceType, isValidURL, processUrl } from './url'

export function sanitizeRelativePath(
pathParam: string | undefined,
): string | undefined {
if (!pathParam) {
return undefined
}

let decodedPath: string
try {
decodedPath = decodeURIComponent(pathParam)
} catch {
return undefined
}

const segments = decodedPath.split('/').filter(Boolean)
if (!segments.length) {
return undefined
}

if (segments.some((segment) => segment === '.' || segment === '..')) {
return undefined
}

return segments.join('/')
}

export function decodePreviewUrlFromReferer(
refererHeader: string | null,
): string | undefined {
if (!refererHeader) {
return undefined
}

let refererUrl: URL
try {
refererUrl = new URL(refererHeader)
} catch {
return undefined
}

const queryUrl = refererUrl.searchParams.get('url')
if (queryUrl && isValidURL(queryUrl)) {
return queryUrl
}

const slug = refererUrl.pathname.replace(/^\/+/, '')
if (!slug) {
return undefined
}

try {
const decodedSlug = decodeURIComponent(slug)
if (isValidURL(decodedSlug)) {
return decodedSlug
}
} catch {
return undefined
}

return undefined
}

export function buildImageFallbackTargets(
previewUrl: string,
imagePath: string,
): string[] {
const htmlCandidates = processUrl(previewUrl)
const targets: string[] = []

htmlCandidates.forEach((candidate) => {
let candidateUrl: URL
try {
candidateUrl = new URL(candidate)
} catch {
return
}

const repositoryRoot = getRepositoryRoot(candidateUrl)
if (!repositoryRoot) {
return
}

targets.push(new URL(`img/${imagePath}`, repositoryRoot).toString())
})

return [...new Set(targets)]
}

export function isSupportedImageFallbackSource(previewUrl: string): boolean {
const previewType = getResourceType(previewUrl, false)
return (
previewType === resourceType.GITHUB || previewType === resourceType.GITLAB
)
}
Loading
Loading