Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _scripts/_undefinedDefaultExport.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default undefined
5 changes: 4 additions & 1 deletion _scripts/webpack.renderer.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ const config = {
'shaka-player$': 'shaka-player/dist/shaka-player.ui.js',

// Make @fortawesome/vue-fontawesome use the trimmed down API instead of the original @fortawesome/fontawesome-svg-core
'@fortawesome/fontawesome-svg-core$': path.resolve(__dirname, '../src/renderer/fontawesome-minimal.js')
'@fortawesome/fontawesome-svg-core$': path.resolve(__dirname, '../src/renderer/fontawesome-minimal.js'),

// Fix dompurify not being tree-shaking friendly
dompurify$: path.resolve(__dirname, '_undefinedDefaultExport.mjs')
},
extensions: ['.js', '.vue']
},
Expand Down
15 changes: 14 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,11 @@ export default [
rules: {
'@stylistic/space-before-function-paren': 'off',
'@stylistic/comma-dangle': ['error', 'only-multiline'],
'vue/no-v-html': 'off',

// Ban v-html as it inserts HTML via innerHTML without sanitizing it
// if inserting raw HTML is unavoidable the custom v-safer-html directive should be used
// which sanitizes the HTML before inserting it into the DOM
'vue/no-v-html': 'error',

'no-console': ['error', {
allow: ['warn', 'error'],
Expand Down Expand Up @@ -342,6 +346,15 @@ export default [
}
}
},
{
files: ['src/renderer/directives/vSaferHtml.js'],
languageOptions: {
globals: {
// Fix Sanitizer not being listed in `globals` yet, remove it when it gets added in the future
Sanitizer: 'readable'
}
}
},

...eslintPluginJsonc.configs.base,
{
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@seald-io/nedb": "^4.1.2",
"autolinker": "^4.1.5",
"bgutils-js": "^3.2.0",
"dompurify": "^3.3.3",
"electron-context-menu": "^4.1.1",
"googlevideo": "^4.0.4",
"marked": "^17.0.4",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@
</h1>
</template>
<bdo
v-safer-html.lenient="updateChangelog"
class="changeLogText"
dir="ltr"
lang="en"
v-html="updateChangelog"
/>
<FtFlexBox>
<FtButton
Expand Down Expand Up @@ -129,6 +129,7 @@ import FtPlaylistAddVideoPrompt from './components/FtPlaylistAddVideoPrompt/FtPl
import FtCreatePlaylistPrompt from './components/FtCreatePlaylistPrompt/FtCreatePlaylistPrompt.vue'
import FtKeyboardShortcutPrompt from './components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue'
import FtSearchFilters from './components/FtSearchFilters/FtSearchFilters.vue'
import { vSaferHtml } from './directives/vSaferHtml.js'

import store from './store/index'

Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/ChannelAbout/ChannelAbout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
>
<h2>{{ $t("Channel.About.Channel Description") }}</h2>
<div
v-safer-html="description"
class="aboutInfo"
dir="auto"
v-html="description"
/>
</template>
<template
Expand Down Expand Up @@ -121,6 +121,7 @@ import { useI18n } from '../../composables/use-i18n-polyfill'

import FtChannelBubble from '../../components/FtChannelBubble/FtChannelBubble.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import { vSaferHtml } from '../../directives/vSaferHtml.js'

import store from '../../store/index'

Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/FtCommunityPost/FtCommunityPost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@
</p>
</div>
<p
v-safer-html="postText"
class="postText"
dir="auto"
v-html="postText"
/>
<swiper-container
v-if="postType === 'multiImage' && postContent.content.length > 0"
Expand Down Expand Up @@ -177,6 +177,7 @@ import FtListVideo from '../ft-list-video/ft-list-video.vue'
import FtListPlaylist from '../FtListPlaylist/FtListPlaylist.vue'
import FtCommunityPoll from '../FtCommunityPoll/FtCommunityPoll.vue'
import FtShareButton from '../FtShareButton/FtShareButton.vue'
import { vSaferHtml } from '../../directives/vSaferHtml.js'

import store from '../../store/index'

Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/FtListChannel/FtListChannel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@
</div>
<p
v-if="listType !== 'grid'"
v-safer-html="description"
class="description"
dir="auto"
v-html="description"
/>
</div>
<FtSubscribeButton
Expand All @@ -80,6 +80,7 @@
import { computed } from 'vue'
import FtSubscribeButton from '../FtSubscribeButton/FtSubscribeButton.vue'
import { vSaferHtml } from '../../directives/vSaferHtml'
import store from '../../store/index'
Expand Down
25 changes: 20 additions & 5 deletions src/renderer/components/FtTimestampCatcher.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<template>
<!-- eslint-disable-next-line vuejs-accessibility/click-events-have-key-events -->
<p
v-safer-html="displayText"
dir="auto"
@timestamp-clicked="catchTimestampClick"
v-html="displayText"
@click="catchTimestampClick"
/>
</template>

<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'

import { vSaferHtml } from '../directives/vSaferHtml.js'

const props = defineProps({
inputHtml: {
type: String,
Expand Down Expand Up @@ -40,15 +43,27 @@ const displayText = computed(() => props.inputHtml.replaceAll(/(?:(\d+):)?(\d+):
}).href

// Adding the URL lets the user open the video in a new window at this timestamp
return `<a tabindex="${props.linkTabIndex}" href="${url}" onclick="event.preventDefault();this.dispatchEvent(new CustomEvent('timestamp-clicked',{bubbles:true,detail:${time}}));window.scrollTo(0,0)">${timestamp}</a>`
return `<a tabindex="${props.linkTabIndex}" href="${url}" data-time="${time}">${timestamp}</a>`
}))

const emit = defineEmits(['timestamp-event'])

/**
* @param {CustomEvent} event
* @param {PointerEvent} event
*/
function catchTimestampClick(event) {
emit('timestamp-event', event.detail)
/** @type {HTMLAnchorElement} */
const target = event.target

if (target.tagName === 'A' && target.dataset.time) {
const timeSeconds = parseInt(target.dataset.time)

if (!isNaN(timeSeconds)) {
event.preventDefault()

emit('timestamp-event', timeSeconds)
window.scrollTo(0, 0)
}
}
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@
</p>
</div>
<p
v-safer-html="superChat.message"
class="chatMessage"
dir="auto"
v-html="superChat.message"
/>
</div>
</div>
Expand Down Expand Up @@ -181,9 +181,9 @@
</div>
<p
v-if="comment.message"
v-safer-html="comment.message"
class="chatMessage"
dir="auto"
v-html="comment.message"
/>
</template>
<template
Expand Down Expand Up @@ -219,8 +219,8 @@
>
</span>
<bdi
v-safer-html="comment.message"
class="chatMessage"
v-html="comment.message"
/>
</p>
</template>
Expand Down Expand Up @@ -255,6 +255,7 @@ import { YTNodes } from 'youtubei.js'
import FtLoader from '../FtLoader/FtLoader.vue'
import FtCard from '../ft-card/ft-card.vue'
import FtButton from '../FtButton/FtButton.vue'
import { vSaferHtml } from '../../directives/vSaferHtml.js'

import store from '../../store/index'

Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/ft-list-video/ft-list-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import {
} from '../../helpers/utils'
import { deArrowData, deArrowThumbnail } from '../../helpers/sponsorblock'
import thumbnailPlaceholder from '../../assets/img/thumbnail_placeholder.svg'
import { vSaferHtml } from '../../directives/vSaferHtml.js'

export default defineComponent({
name: 'FtListVideo',
components: {
'ft-icon-button': FtIconButton
},
directives: {
'safer-html': vSaferHtml
},
props: {
data: {
type: Object,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/ft-list-video/ft-list-video.vue
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@
</div>
<p
v-if="description && effectiveListTypeIsList && appearance === 'result'"
v-safer-html="description"
class="description"
dir="auto"
v-html="description"
/>
</div>
</div>
Expand Down
54 changes: 54 additions & 0 deletions src/renderer/directives/vSaferHtml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import DOMPurify from 'dompurify'

const USE_NATIVE_SANITIZER = process.env.IS_ELECTRON || ('Sanitizer' in window && typeof HTMLElement.prototype.setHTML === 'function')

let sanitizer
/** @type {import('dompurify').Config | undefined} */
let domPurifyStrictConfig

/** @type {import('vue').FunctionDirective<HTMLElement, string, 'lenient'>} */
export const vSaferHtml = (element, { value, oldValue, modifiers }) => {
if (oldValue === null || value !== oldValue) {
if (modifiers.lenient) {
// Use the default browser sanitizer configuration e.g. for the changelog
if (USE_NATIVE_SANITIZER) {
element.setHTML(value)
} else {
element.innerHTML = DOMPurify.sanitize(value, { RETURN_TRUSTED_TYPE: false })
}
} else if (USE_NATIVE_SANITIZER) {
// Use a much stricter sanitzer configuration, should be used in most places
if (sanitizer === undefined) {
sanitizer = new Sanitizer({
comments: false,
elements: [
'br',
'b',
'i',
's',
{
name: 'a',
attributes: ['data-time', 'dir', 'href', 'lang', 'tabindex']
},
// live chat emojis (see parseLocalTextRuns)
{
name: 'img',
attributes: ['alt', 'height', 'loading', 'src', 'style', 'title', 'width']
}
]
})
}

element.setHTML(value, { sanitizer })
} else {
if (domPurifyStrictConfig === undefined) {
domPurifyStrictConfig = {
ALLOWED_TAGS: ['br', 'b', 'i', 's', 'a', 'img'],
ALLOWED_ATTR: ['alt', 'data-time', 'dir', 'height', 'href', 'lang', 'loading', 'src', 'style', 'tabindex', 'title', 'width']
}
}

element.innerHTML = DOMPurify.sanitize(value, domPurifyStrictConfig)
}
}
}
8 changes: 2 additions & 6 deletions src/renderer/helpers/api/invidious.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import store from '../../store/index'
import {
calculatePublishedDate,
getRelativeTimeFromDate,
stripHTML,
} from '../utils'
import { calculatePublishedDate, getRelativeTimeFromDate } from '../utils'
import { isNullOrEmpty } from '../strings'
import autolinker from 'autolinker'
import { FormatUtils, Misc, Player } from 'youtubei.js'
Expand Down Expand Up @@ -616,7 +612,7 @@ function parseInvidiousCommentData(response) {
authorThumb: youtubeImageUrlToInvidious(comment.authorThumbnails.at(-1).url),
author: comment.author,
likes: comment.likeCount,
text: autolinker.link(stripHTML(invidiousImageUrlToInvidious(comment.contentHtml, getCurrentInstanceUrl()))),
text: autolinker.link(invidiousImageUrlToInvidious(comment.contentHtml, getCurrentInstanceUrl())),
dataType: 'invidious',
isOwner: comment.authorIsChannelOwner,
isPinned: comment.isPinned,
Expand Down
9 changes: 0 additions & 9 deletions src/renderer/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,15 +431,6 @@ export function createWebURL(path) {
return `${origin}${windowPath}/${path}`
}

/**
* strip html tags but keep <br>, <b>, </b> <s>, </s>, <i>, </i>
* @param {string} value
* @returns {string}
*/
export function stripHTML(value) {
return value.replaceAll(/(<(?!br|\/?[abis]|img>)([^>]+)>)/gi, '')
}

/**
* This formats the duration of a video in seconds into a user friendly timestamp.
* It will return strings like LIVE or UPCOMING, without making any changes
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/views/About/About.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
{{ chunk.title }}
</h3>
<div
v-safer-html="chunk.content"
class="content"
v-html="chunk.content"
/>
</figure>
</section>
Expand All @@ -44,6 +44,7 @@ import { useI18n } from '../../composables/use-i18n-polyfill'

import FtCard from '../../components/ft-card/ft-card.vue'
import FtLogoFull from '../../components/FtLogoFull/FtLogoFull.vue'
import { vSaferHtml } from '../../directives/vSaferHtml.js'

import { ABOUT_BITCOIN_ADDRESS } from '../../../constants'
import packageDetails from '../../../../package.json'
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1984,6 +1984,11 @@
dependencies:
"@types/node" "*"

"@types/trusted-types@^2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==

"@types/verror@^1.10.3":
version "1.10.5"
resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.5.tgz#2a1413aded46e67a1fe2386800e291123ed75eb1"
Expand Down Expand Up @@ -3664,6 +3669,13 @@ domhandler@^5.0.1, domhandler@^5.0.2:
dependencies:
domelementtype "^2.3.0"

dompurify@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6"
integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==
optionalDependencies:
"@types/trusted-types" "^2.0.7"

domutils@^2.5.2, domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
Expand Down