mirror of
https://git.tt-rss.org/fox/tt-rss.git
synced 2026-05-04 23:26:09 +02:00
Improve handling of URLs on the frontend.
This commit is contained in:
parent
7b6de15e71
commit
c8b80c4041
25
js/App.js
25
js/App.js
@ -427,6 +427,31 @@ const App = {
|
||||
|
||||
return p.replace(/[&<>"'/]/g, m => map[m]);
|
||||
},
|
||||
/**
|
||||
* Sanitize a URL for safe use in href attributes and window.open()
|
||||
* @param {string} url - URL to sanitize
|
||||
* @param {string} fallback - Optional fallback value if URL is invalid (default: empty string)
|
||||
* @return {string} Safe URL or fallback
|
||||
*/
|
||||
sanitizeUrl: function(url, fallback = '') {
|
||||
if (!url || typeof url !== 'string') return fallback;
|
||||
|
||||
// Remove NULL bytes and other control characters
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const cleaned = url.replace(/[\x00-\x1F\x7F]/g, '');
|
||||
|
||||
const trimmed = cleaned.trim();
|
||||
if (!trimmed) return fallback;
|
||||
|
||||
return /^https?:\/\/.+/i.test(trimmed) ? trimmed : fallback;
|
||||
},
|
||||
openUrl: function(url) {
|
||||
const sanitized = this.sanitizeUrl(url);
|
||||
if (sanitized) {
|
||||
const w = window.open(sanitized);
|
||||
w.opener = null;
|
||||
}
|
||||
},
|
||||
unescapeHtml: function(p) {
|
||||
if (typeof p !== 'string' || p.indexOf('&') === -1)
|
||||
return p;
|
||||
|
||||
@ -76,12 +76,6 @@ const Article = {
|
||||
}
|
||||
}
|
||||
},
|
||||
popupOpenUrl: function(url) {
|
||||
const w = window.open("");
|
||||
|
||||
w.opener = null;
|
||||
w.location = url;
|
||||
},
|
||||
cdmToggleGridSpan: function(id) {
|
||||
const row = document.getElementById(`RROW-${id}`);
|
||||
|
||||
@ -160,26 +154,26 @@ const Article = {
|
||||
<img loading="lazy"
|
||||
width="${enc.width ? enc.width : ''}"
|
||||
height="${enc.height ? enc.height : ''}"
|
||||
src="${App.escapeHtml(enc.content_url)}"
|
||||
src="${App.escapeHtml(App.sanitizeUrl(enc.content_url))}"
|
||||
title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"/>
|
||||
</p>`
|
||||
} else if (enc.content_type && enc.content_type.indexOf("audio/") !== -1 && App.audioCanPlay(enc.content_type)) {
|
||||
return `<p class='inline-player' title="${App.escapeHtml(enc.content_url)}">
|
||||
<audio preload="none" controls="controls">
|
||||
<source type="${App.escapeHtml(enc.content_type)}" src="${App.escapeHtml(enc.content_url)}"/>
|
||||
<source type="${App.escapeHtml(enc.content_type)}" src="${App.escapeHtml(App.sanitizeUrl(enc.content_url))}"/>
|
||||
</audio>
|
||||
</p>
|
||||
`;
|
||||
} else {
|
||||
return `<p>
|
||||
<a target="_blank" href="${App.escapeHtml(enc.content_url)}"
|
||||
<a target="_blank" href="${App.escapeHtml(App.sanitizeUrl(enc.content_url))}"
|
||||
title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"
|
||||
rel="noopener noreferrer">${App.escapeHtml(enc.content_url)}</a>
|
||||
</p>`
|
||||
}
|
||||
} else {
|
||||
return `<p>
|
||||
<a target="_blank" href="${App.escapeHtml(enc.content_url)}"
|
||||
<a target="_blank" href="${App.escapeHtml(App.sanitizeUrl(enc.content_url))}"
|
||||
title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}"
|
||||
rel="noopener noreferrer">${App.escapeHtml(enc.content_url)}</a>
|
||||
</p>`
|
||||
@ -191,7 +185,7 @@ const Article = {
|
||||
<span>${__('Attachments')}</span>
|
||||
<div dojoType="dijit.Menu" style="display: none">
|
||||
${enclosures.entries.map((enc) => `
|
||||
<div onclick='Article.popupOpenUrl("${App.escapeHtml(enc.content_url)}")'
|
||||
<div onclick="App.openUrl('${enc.content_url}')"
|
||||
title="${App.escapeHtml(enc.title ? enc.title : enc.content_url)}" dojoType="dijit.MenuItem">
|
||||
${enc.title ? enc.title : enc.filename}
|
||||
</div>
|
||||
@ -233,7 +227,7 @@ const Article = {
|
||||
comments_msg = hl.num_comments + " " + ngettext("comment", "comments", hl.num_comments)
|
||||
}
|
||||
|
||||
comments = `<a target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(hl.comments ? hl.comments : hl.link)}">(${comments_msg})</a>`;
|
||||
comments = `<a target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(App.sanitizeUrl(hl.comments ? hl.comments : hl.link))}">(${comments_msg})</a>`;
|
||||
}
|
||||
|
||||
return comments;
|
||||
@ -288,7 +282,7 @@ const Article = {
|
||||
<div class="row">
|
||||
<div class="title"><a target="_blank" rel="noopener noreferrer"
|
||||
title="${App.escapeHtml(hl.title)}"
|
||||
href="${App.escapeHtml(hl.link)}">${hl.title}</a></div>
|
||||
href="${App.escapeHtml(App.sanitizeUrl(hl.link))}">${hl.title}</a></div>
|
||||
<div class="date">${hl.updated_long}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@ -609,7 +609,7 @@ const CommonDialogs = {
|
||||
</section>
|
||||
</div>
|
||||
<div dojoType="dijit.layout.ContentPane" title="${__('Icon')}">
|
||||
<div><img class='feedIcon' style="${feed.icon ? "" : "display : none"}" src="${feed.icon ? App.escapeHtml(feed.icon) : ""}"></div>
|
||||
<div><img class='feedIcon' style="${feed.icon ? "" : "display : none"}" src="${feed.icon ? App.escapeHtml(App.sanitizeUrl(feed.icon)) : ""}"></div>
|
||||
|
||||
<label class="dijitButton">
|
||||
${App.FormFields.icon("file_upload")}
|
||||
@ -674,7 +674,7 @@ const CommonDialogs = {
|
||||
<header>${__("%s can be accessed via the following secret URL:").replace("%s", App.escapeHtml(reply.title))}</header>
|
||||
<section>
|
||||
<div class='panel text-center'>
|
||||
<a class='generated_url' href="${App.escapeHtml(reply.link)}" target='_blank'>${App.escapeHtml(reply.link)}</a>
|
||||
<a class='generated_url' href="${App.escapeHtml(App.sanitizeUrl(reply.link))}" target='_blank'>${App.escapeHtml(reply.link)}</a>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
|
||||
@ -741,7 +741,7 @@ const Feeds = {
|
||||
const icon_url = App.getInitParam('icons_url') + '?' + dojo.objectToQuery({op: 'feed_icon', id: feed_id});
|
||||
|
||||
return feed_id && exists ?
|
||||
`<img class='icon' src='${App.escapeHtml(icon_url)}' width='16' height='16' alt='feed icon' onerror='Feeds._handleIconError(this)'>` :
|
||||
`<img class='icon' src='${App.escapeHtml(App.sanitizeUrl(icon_url))}' width='16' height='16' alt='feed icon' onerror='Feeds._handleIconError(this)'>` :
|
||||
`<i class='icon-no-feed material-icons'>rss_feed</i>`;
|
||||
},
|
||||
_handleIconError: (img) => {
|
||||
|
||||
@ -463,7 +463,7 @@ const Headlines = {
|
||||
const vgrhdr = `<div data-feed-id='${hl.feed_id}' class='feed-title'>
|
||||
<div class="pull-right icon-feed" title="${App.escapeHtml(hl.feed_title)}"
|
||||
onclick="Feeds.open({feed:${hl.feed_id}})">${Feeds.renderIcon(hl.feed_id, hl.has_icon)}</div>
|
||||
<a class="title" title="${__('Open site')}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(hl.site_url)}">${hl.feed_title}</a>
|
||||
<a class="title" title="${__('Open site')}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(App.sanitizeUrl(hl.site_url))}">${hl.feed_title}</a>
|
||||
<a class="catchup" title="${__('mark feed as read')}" onclick="Feeds.catchupFeedInGroup(${hl.feed_id})" href="#">
|
||||
<i class="icon-done material-icons">done_all</i>
|
||||
</a>
|
||||
@ -504,7 +504,7 @@ const Headlines = {
|
||||
|
||||
<span onclick="return Headlines.click(event, ${hl.id});" data-article-id="${hl.id}" class="titleWrap hlMenuAttach">
|
||||
${App.getInitParam("debug_headline_ids") ? `<span class="text-muted small">A: ${hl.id} F: ${hl.feed_id}</span>` : ""}
|
||||
<a class="title" title="${App.escapeHtml(hl.title)}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(hl.link)}">
|
||||
<a class="title" title="${App.escapeHtml(hl.title)}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(App.sanitizeUrl(hl.link))}">
|
||||
${hl.title}</a>
|
||||
<span class="author">${hl.author}</span>
|
||||
${Article.renderLabels(hl.id, hl.labels)}
|
||||
@ -512,7 +512,7 @@ const Headlines = {
|
||||
</span>
|
||||
|
||||
<a class="feed vfeedMenuAttach" style="background-color: ${hl.feed_bg_color}" data-feed-id="${hl.feed_id}"
|
||||
title="${__('Open site')}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(hl.site_url)}">${hl.feed_title}</a>
|
||||
title="${__('Open site')}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(App.sanitizeUrl(hl.site_url))}">${hl.feed_title}</a>
|
||||
|
||||
<span class="updated" title="${hl.imported}">${hl.updated}</span>
|
||||
|
||||
@ -575,13 +575,13 @@ const Headlines = {
|
||||
<div onclick="return Headlines.click(event, ${hl.id})" class="title">
|
||||
${App.getInitParam("debug_headline_ids") ? `<span class="text-muted small">A: ${hl.id} F: ${hl.feed_id}</span>` : ""}
|
||||
<span data-article-id="${hl.id}" class="hl-content hlMenuAttach">
|
||||
<a class="title" href="${App.escapeHtml(hl.link)}">${hl.title} <span class="preview">${hl.content_preview}</span></a>
|
||||
<a class="title" href="${App.escapeHtml(App.sanitizeUrl(hl.link))}">${hl.title} <span class="preview">${hl.content_preview}</span></a>
|
||||
<span class="author">${hl.author}</span>
|
||||
${Article.renderLabels(hl.id, hl.labels)}
|
||||
</span>
|
||||
</div>
|
||||
<span class="feed vfeedMenuAttach" data-feed-id="${hl.feed_id}">
|
||||
<a title="${__('Open site')}" style="background : ${hl.feed_bg_color}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(hl.site_url)}">${hl.feed_title}</a>
|
||||
<a title="${__('Open site')}" style="background : ${hl.feed_bg_color}" target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(App.sanitizeUrl(hl.site_url))}">${hl.feed_title}</a>
|
||||
</span>
|
||||
<div title="${hl.imported}">
|
||||
<span class="updated">${hl.updated}</span>
|
||||
@ -637,7 +637,7 @@ const Headlines = {
|
||||
<i class='icon-syndicate material-icons'>rss_feed</i>
|
||||
</a>
|
||||
${tb.site_url ?
|
||||
`<a class="feed_title" target="_blank" href="${App.escapeHtml(tb.site_url)}" title="${tb.last_updated}">${tb.title}</a>` :
|
||||
`<a class="feed_title" target="_blank" href="${App.escapeHtml(App.sanitizeUrl(tb.site_url))}" title="${tb.last_updated}">${tb.title}</a>` :
|
||||
`${search_query ? `<a href="#" onclick="Feeds.search(); return false" class="feed_title" title="${App.escapeHtml(search_query)}">${tb.title}</a>
|
||||
<span class="cancel_search">(<a href="#" onclick="Feeds.cancelSearch(); return false">${__("Cancel search")}</a>)</span>` :
|
||||
`<span class="feed_title">${tb.title}</span>`}`}
|
||||
|
||||
@ -411,7 +411,7 @@ const Helpers = {
|
||||
{disabled: true}) : ''}
|
||||
${plugin.more_info ?
|
||||
App.FormFields.button_tag(App.FormFields.icon("help"), "",
|
||||
{class: 'alt-info', onclick: `window.open("${App.escapeHtml(plugin.more_info)}")`}) : ''}
|
||||
{class: 'alt-info', onclick: `App.openUrl('${plugin.more_info}')`}) : ''}
|
||||
${is_admin && plugin.is_local ?
|
||||
App.FormFields.button_tag(App.FormFields.icon("update"), "",
|
||||
{title: __("Update"), class: 'alt-warning', "data-update-btn-for-plugin": plugin.name, style: 'display : none',
|
||||
@ -578,7 +578,7 @@ const Helpers = {
|
||||
onclick: `App.dialogOf(this).performInstall("${App.escapeHtml(plugin.name)}")`})}
|
||||
|
||||
<h3>${plugin.name}
|
||||
<a target="_blank" href="${App.escapeHtml(plugin.html_url)}">
|
||||
<a target="_blank" rel="noopener noreferrer" href="${App.escapeHtml(App.sanitizeUrl(plugin.html_url))}">
|
||||
${App.FormFields.icon("open_in_new_window")}
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user