Skip to content

Commit

Permalink
2990: Open external urls in new tab
Browse files Browse the repository at this point in the history
  • Loading branch information
steffenkleinle committed Jan 22, 2025
1 parent 96400bb commit adf7d6a
Show file tree
Hide file tree
Showing 15 changed files with 262 additions and 197 deletions.
4 changes: 2 additions & 2 deletions build-configs/BuildConfigType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export type CommonBuildConfigType = {
allowedHostNames: string[]
// Linked hosts that can may look similar https://chromium.googlesource.com/chromium/src/+/master/docs/security/lookalikes/lookalike-domains.md#automated-warning-removal
allowedLookalikes: string[]
// Regex defining which urls to intercept as they are internal ones.
supportedIframeSources: string[]
internalLinksHijackPattern: string
// Regex defining which urls to intercept as they are internal ones.
internalUrlPattern: string
featureFlags: FeatureFlagsType
lightTheme: ThemeType
// Translations deviating from the standard integreat translations.
Expand Down
2 changes: 1 addition & 1 deletion build-configs/aschaffenburg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const commonAschaffenburgBuildConfig: CommonBuildConfigType = {
allowedLookalikes: [],
supportedIframeSources: ['vimeo.com'],
translationsOverride: aschaffenburgOverrideTranslations,
internalLinksHijackPattern:
internalUrlPattern:
'https?:\\/\\/(cms(-test)?\\.integreat-app\\.de|web\\.integreat-app\\.de|integreat\\.app|aschaffenburg\\.app)(?!\\/(media|[^/]*\\/(wp-content|wp-admin|wp-json))\\/.*).*',
featureFlags: {
floss: false,
Expand Down
2 changes: 1 addition & 1 deletion build-configs/integreat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const commonIntegreatBuildConfig: CommonBuildConfigType = {
allowedHostNames: ['cms.integreat-app.de', 'cms-test.integreat-app.de', 'admin.integreat-app.de'],
allowedLookalikes: ['https://integreat.app', 'https://integreat-app.de'],
supportedIframeSources: ['vimeo.com'],
internalLinksHijackPattern:
internalUrlPattern:
'https?:\\/\\/(cms(-test)?\\.integreat-app\\.de|web\\.integreat-app\\.de|integreat\\.app)(?!\\/(media|[^/]*\\/(wp-content|wp-admin|wp-json))\\/.*).*',
featureFlags: {
floss: false,
Expand Down
2 changes: 1 addition & 1 deletion build-configs/malte/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const commonMalteBuildConfig: CommonBuildConfigType = {
allowedLookalikes: [],
supportedIframeSources: ['vimeo.com'],
translationsOverride: malteOverrideTranslations,
internalLinksHijackPattern:
internalUrlPattern:
'https?:\\/\\/((cms\\.)?malteapp\\.de|malte-test\\.tuerantuer\\.org)(?!\\/(media|[^/]*\\/(wp-content|wp-admin|wp-json))\\/.*).*',
hostName: 'malteapp.de',
featureFlags: {
Expand Down
2 changes: 1 addition & 1 deletion build-configs/obdach/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const commonObdachBuildConfig: CommonBuildConfigType = {
allowedLookalikes: [],
supportedIframeSources: ['vimeo.com'],
translationsOverride: obdachOverrideTranslations,
internalLinksHijackPattern:
internalUrlPattern:
'https?:\\/\\/((cms\\.)?netzwerkobdachwohnen\\.de)(?!\\/(media|[^/]*\\/(wp-content|wp-admin|wp-json))\\/.*).*',
featureFlags: {
floss: false,
Expand Down
2 changes: 1 addition & 1 deletion docs/whitelabelling.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Information needed from our CMS.
- Switch cms url
- Share base url
- Allowed host names
- Internal link hijack pattern
- Internal url pattern
- [depends] City name (to skip the landing screen)

## AppTeam TODOs
Expand Down
2 changes: 1 addition & 1 deletion native/src/constants/__mocks__/buildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const buildConfig = jest.fn<CommonBuildConfigType, []>(
allowedHostNames: ['cms.integreat-app.de', 'cms-test.integreat-app.de', 'admin.integreat-app.de'],
allowedLookalikes: ['https://integreat.app', 'https://integreat-app.de'],
supportedIframeSources: ['vimeo.com'],
internalLinksHijackPattern:
internalUrlPattern:
'https?:\\/\\/(cms(-test)?\\.integreat-app\\.de|web\\.integreat-app\\.de|integreat\\.app)(?!\\/[^/]*\\/(wp-content|wp-admin|wp-json)\\/.*).*',
featureFlags: {
floss: false,
Expand Down
4 changes: 2 additions & 2 deletions native/src/hooks/useNavigateToLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import useSnackbar from './useSnackbar'

const SUPPORTED_IMAGE_FILE_TYPES = ['.jpg', '.jpeg', '.png']

const HIJACK = new RegExp(buildConfig().internalLinksHijackPattern)
const internalUrlRegex = new RegExp(buildConfig().internalUrlPattern)

const navigateToLink = <T extends RoutesType>(
url: string,
Expand Down Expand Up @@ -52,7 +52,7 @@ const navigateToLink = <T extends RoutesType>(
url,
shareUrl,
})
} else if (HIJACK.test(url)) {
} else if (internalUrlRegex.test(url)) {
sendTrackingSignal({
signal: {
name: OPEN_INTERNAL_LINK_SIGNAL_NAME,
Expand Down
2 changes: 1 addition & 1 deletion native/src/utils/openExternalUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { reportError } from './sentry'
const openExternalUrl = async (rawUrl: string, showSnackbar: (snackbar: SnackbarType) => void): Promise<void> => {
const encodedUrl = encodeURI(rawUrl)
const { protocol } = new URL(encodedUrl)
const internalLinkRegexp = new RegExp(buildConfig().internalLinksHijackPattern)
const internalLinkRegexp = new RegExp(buildConfig().internalUrlPattern)

const canBeOpenedWithInAppBrowser = (await InAppBrowser.isAvailable()) && ['https:', 'http:'].includes(protocol)
const canBeOpenedWithOtherApp = await Linking.canOpenURL(encodedUrl)
Expand Down
180 changes: 10 additions & 170 deletions web/src/components/RemoteContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@ import Dompurify from 'dompurify'
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import styled, { css } from 'styled-components'

import { ExternalSourcePermissions } from 'shared'

import { ExternalLinkIcon } from '../assets'
import buildConfig from '../constants/buildConfig'
import dimensions from '../constants/dimensions'
import { helpers } from '../constants/theme'
import useLocalStorage from '../hooks/useLocalStorage'
import useWindowDimensions from '../hooks/useWindowDimensions'
import {
Expand All @@ -18,162 +14,15 @@ import {
hideIframe,
preserveIFrameSourcesFromContent,
} from '../utils/iframes'

const SandBox = styled.div<{ $centered: boolean; $smallText: boolean }>`
font-family: ${props => props.theme.fonts.web.contentFont};
font-size: ${props => (props.$smallText ? helpers.adaptiveFontSize : props.theme.fonts.contentFontSize)};
line-height: ${props => props.theme.fonts.contentLineHeight};
display: flow-root; /* clearfix for the img floats */
${props => (props.$centered ? 'text-align: center;' : '')}
${props => (props.$centered ? 'list-style-position: inside;' : '')}
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
&.alignright {
float: inline-end;
}
&.alignleft {
float: inline-start;
}
&.aligncenter {
display: block;
margin: auto;
}
}
figure {
margin-inline-start: 0;
text-align: center;
margin: 15px auto;
@media only screen and (width <= 640px) {
width: 100% !important;
}
}
figcaption {
font-size: ${props => props.theme.fonts.hintFontSize};
font-style: italic;
padding: 0 15px;
}
table {
display: block;
width: 100% !important;
height: auto !important; /* need important because of badly formatted remote content */
overflow: auto;
}
tbody,
thead {
display: table; /* little bit hacky, but works in all browsers, even IE11 :O */
width: 100%;
box-sizing: border-box;
border-collapse: collapse;
}
tbody,
thead,
th,
td {
border: 1px solid ${props => props.theme.colors.backgroundAccentColor};
}
a {
overflow-wrap: break-word;
}
details > * {
padding: 0 25px;
}
details > summary {
padding: 0;
}
pre {
overflow-x: auto;
}
.link-external::after {
content: '';
display: inline-block;
background-image: url('${ExternalLinkIcon}');
width: ${props => props.theme.fonts.contentFontSize};
height: ${props => props.theme.fonts.contentFontSize};
${props =>
props.$smallText &&
css`
${helpers.adaptiveHeight}
${helpers.adaptiveWidth}
`};
background-size: contain;
background-repeat: no-repeat;
vertical-align: middle;
margin: 0 4px;
}
iframe {
border: none;
border-bottom: 1px solid ${props => props.theme.colors.borderColor};
@media ${dimensions.smallViewport} {
max-width: 100%;
}
}
.iframe-container {
display: flex;
padding: 4px;
flex-direction: column;
border: 1px solid ${props => props.theme.colors.borderColor};
border-radius: 4px;
box-shadow:
0 1px 3px rgb(0 0 0 / 10%),
0 1px 2px rgb(0 0 0 / 15%);
}
.iframe-info-text {
display: flex;
padding: 12px;
justify-content: space-between;
font-size: ${props => props.theme.fonts.decorativeFontSizeSmall};
}
.iframe-info-text > input {
margin-inline-start: 12px;
cursor: pointer;
}
.iframe-info-text > label {
cursor: pointer;
}
.iframe-source {
display: contents;
font-weight: bold;
}
#opt-in-settings-link {
margin-inline-start: 12px;
padding: 0;
cursor: pointer;
align-self: center;
}
`
import openLink from '../utils/openLink'
import RemoteContentSandBox from './RemoteContentSandBox'

type RemoteContentProps = {
html: string
centered?: boolean
smallText?: boolean
}

const HIJACK = new RegExp(buildConfig().internalLinksHijackPattern)
const DOMPURIFY_TAG_IFRAME = 'iframe'
const DOMPURIFY_ATTRIBUTE_FULLSCREEN = 'allowfullscreen'
const DOMPURIFY_ATTRIBUTE_TARGET = 'target'
Expand All @@ -193,16 +42,11 @@ const RemoteContent = ({ html, centered = false, smallText = false }: RemoteCont
const { viewportSmall, width: deviceWidth } = useWindowDimensions()
const { t } = useTranslation()

const handleClick = useCallback(
(event: MouseEvent): void => {
const handleAnchorClick = useCallback(
(event: MouseEvent) => {
event.preventDefault()
const target = event.currentTarget

if (target instanceof HTMLAnchorElement) {
const href = target.href
const url = new URL(decodeURIComponent(href))
navigate(decodeURIComponent(`${url.pathname}${url.search}${url.hash}`))
}
const target = event.currentTarget as HTMLAnchorElement
openLink(navigate, target.href)
},
[navigate],
)
Expand All @@ -217,12 +61,8 @@ const RemoteContent = ({ html, centered = false, smallText = false }: RemoteCont
return
}
const currentSandBoxRef = sandBoxRef.current
const collection = currentSandBoxRef.getElementsByTagName('a')
Array.from(collection).forEach(node => {
if (HIJACK.test(node.href)) {
node.addEventListener('click', handleClick)
}
})
const anchors = currentSandBoxRef.getElementsByTagName('a')
Array.from(anchors).forEach(anchor => anchor.addEventListener('click', handleAnchorClick))

const iframes = currentSandBoxRef.getElementsByTagName('iframe')
Array.from(iframes).forEach((iframe: HTMLIFrameElement, index: number) => {
Expand Down Expand Up @@ -253,7 +93,7 @@ const RemoteContent = ({ html, centered = false, smallText = false }: RemoteCont
}, [
t,
html,
handleClick,
handleAnchorClick,
sandBoxRef,
externalSourcePermissions,
contentIframeSources,
Expand All @@ -270,7 +110,7 @@ const RemoteContent = ({ html, centered = false, smallText = false }: RemoteCont
}

return (
<SandBox
<RemoteContentSandBox
dir='auto'
$centered={centered}
dangerouslySetInnerHTML={dangerouslySetInnerHTML}
Expand Down
Loading

0 comments on commit adf7d6a

Please sign in to comment.