diff --git a/.envrc b/.envrc index 5784a18..ba3fcf1 100644 --- a/.envrc +++ b/.envrc @@ -1,7 +1,7 @@ #!/bin/sh if has nix; then - use flake --update-input nixpkgs + use flake fi dotenv_if_exists diff --git a/berrymotes b/berrymotes index 38fb43b..dd64293 160000 --- a/berrymotes +++ b/berrymotes @@ -1 +1 @@ -Subproject commit 38fb43b1d4af4e3f70d27a9f20b447e957bf6723 +Subproject commit dd6429315636fe1b265c6fbeeb933b5caae8c90a diff --git a/docker/node/modules/utils.js b/docker/node/modules/utils.js index 9087b3a..5d78f52 100644 --- a/docker/node/modules/utils.js +++ b/docker/node/modules/utils.js @@ -62,3 +62,12 @@ exports.isUrl = function(maybeUrl) { return false; } }; + +exports.tryDecodeURIComponent = function(str) { + try { + return decodeURIComponent(str); + } catch (err) { + console.warn('Ignored:', err); + return str; + } +}; \ No newline at end of file diff --git a/docker/node/modules/utils.test.js b/docker/node/modules/utils.test.js index 908e4a0..e57ad45 100644 --- a/docker/node/modules/utils.test.js +++ b/docker/node/modules/utils.test.js @@ -1,6 +1,6 @@ const { expect } = require("chai"); -const { parseFormat, parseRawFileUrl } = require("./utils"); +const { parseFormat, parseRawFileUrl, tryDecodeURIComponent } = require("./utils"); describe("parseFormat", function() { it("parses: left {middle} right", function() { @@ -117,3 +117,20 @@ describe("parseRawFileUrl", function() { expect(ret).to.be.null; }); }); + +describe("tryDecodeURIComponent", function() { + it("leaves non-encoded string untouched", function() { + const ret = tryDecodeURIComponent("foobar"); + expect(ret).to.equal("foobar"); + }); + + it("decodes an encoded string", function() { + const ret = tryDecodeURIComponent("Any%25%20Speedrun"); + expect(ret).to.equal("Any% Speedrun"); + }); + + it("doesn't throw on invalid encoding", function() { + const ret = tryDecodeURIComponent("Any% Speedrun"); + expect(ret).to.equal("Any% Speedrun"); + }); +}); \ No newline at end of file diff --git a/docker/node/server.js b/docker/node/server.js index 74414f5..9d11a53 100644 --- a/docker/node/server.js +++ b/docker/node/server.js @@ -5,7 +5,7 @@ const { sanitizeManifest } = require("./modules/playlist"); const { DefaultLog, events, levels, consoleLogger, createStreamLogger } = require("./modules/log"); const { DatabaseService } = require("./modules/database"); const { SessionService, getSocketName, userTypes } = require("./modules/sessions"); -const { parseRawFileUrl } = require("./modules/utils"); +const { parseRawFileUrl, tryDecodeURIComponent } = require("./modules/utils"); const { EventServer } = require("./modules/event-server"); const fetchYoutubeVideoInfo = require("youtube-info"); @@ -730,13 +730,13 @@ function getCommand(msg) { function handleNewVideoChange() { DefaultLog.info(events.EVENT_VIDEO_CHANGE, "changed video to {videoTitle}", - { videoTitle: decodeURI(SERVER.ACTIVE.videotitle) }); + { videoTitle: tryDecodeURIComponent(SERVER.ACTIVE.videotitle) }); eventServer.emit('videoChange', { id: SERVER.ACTIVE.videoid, length: SERVER.ACTIVE.videolength, - title: decodeURI(SERVER.ACTIVE.videotitle), + title: tryDecodeURIComponent(SERVER.ACTIVE.videotitle), type: SERVER.ACTIVE.videotype, volat: SERVER.ACTIVE.volat }); @@ -876,7 +876,7 @@ function setVideoVolatile(socket, pos, isVolat) { DefaultLog.info(events.EVENT_ADMIN_SET_VOLATILE, "{mod} set {title} to {status}", - { mod: getSocketName(socket), type: "playlist", title: decodeURIComponent(elem.videotitle), status: isVolat ? "volatile" : "not volatile" }); + { mod: getSocketName(socket), type: "playlist", title: tryDecodeURIComponent(elem.videotitle), status: isVolat ? "volatile" : "not volatile" }); io.sockets.emit("setVidVolatile", { pos: pos, @@ -1431,12 +1431,12 @@ function delVideo(video, sanity, socket) { DefaultLog.info(events.EVENT_ADMIN_DELETED_VIDEO, "{mod} deleted {title}", - { mod: getSocketName(socket), type: "playlist", title: decodeURIComponent(node.videotitle) }); + { mod: getSocketName(socket), type: "playlist", title: tryDecodeURIComponent(node.videotitle) }); } catch (e) { DefaultLog.error(events.EVENT_ADMIN_DELETED_VIDEO, "{mod} could not delete {title}", - { mod: getSocketName(socket), type: "playlist", title: decodeURIComponent(node.videotitle) }, e); + { mod: getSocketName(socket), type: "playlist", title: tryDecodeURIComponent(node.videotitle) }, e); } } @@ -1602,7 +1602,7 @@ function _addVideoVimeo(socket, data, meta, path, successCallback, failureCallba rawAddVideo({ pos: pos, videoid: jdata.id || jdata.video_id, - videotitle: encodeURI(jdata.title), + videotitle: encodeURIComponent(jdata.title), videolength: jdata.duration, videotype: "vimeo", who: meta.nick, @@ -1797,7 +1797,7 @@ function addVideoYT(socket, data, meta, successCallback, failureCallback) { rawAddVideo({ pos: pos, videoid: videoid, - videotitle: encodeURI(formattedTitle), + videotitle: encodeURIComponent(formattedTitle), videolength: formattedTime, videotype: "yt", who: meta.nick, @@ -1836,7 +1836,7 @@ function addVideoYT(socket, data, meta, successCallback, failureCallback) { rawAddVideo({ pos: pos, videoid: videoid, - videotitle: encodeURI(title), + videotitle: encodeURIComponent(title), videolength: duration, videotype: "yt", who: meta.nick, @@ -1958,7 +1958,7 @@ async function addVideoSoundCloud(socket, data, meta, successCallback, failureCa pos: SERVER.PLAYLIST.length, // Don't collide with vimeo videoid: 'SC' + jdata.id, - videotitle: encodeURI(jdata.user.username + " - " + jdata.title), + videotitle: encodeURIComponent(jdata.user.username + " - " + jdata.title), // soundcloud is millis videolength: jdata.duration / 1000, videotype: "soundcloud", @@ -2080,7 +2080,7 @@ function addVideoDash(socket, data, meta, successCallback, failureCallback) { if (meta.type <= 0) { volat = true; } if (volat === undefined) { volat = false; } const parts = videoid.split('/'); - const videoTitle = data.videotitle ? encodeURI(data.videotitle) : parts[parts.length - 1]; + const videoTitle = data.videotitle ? encodeURIComponent(data.videotitle) : parts[parts.length - 1]; rawAddVideo({ pos: SERVER.PLAYLIST.length, videoid: videoid, @@ -2163,7 +2163,7 @@ function addVideoTwitch(socket, data, meta, successCallback, failureCallback) { rawAddVideo({ pos: SERVER.PLAYLIST.length, videoid: 'videos/' + videoid, - videotitle: encodeURI(response.title), + videotitle: encodeURIComponent(response.title), videolength: parseDuration(response.duration), videotype: "twitch", who: meta.nick, @@ -2188,7 +2188,7 @@ function addVideoTwitch(socket, data, meta, successCallback, failureCallback) { rawAddVideo({ pos: SERVER.PLAYLIST.length, videoid: response.broadcaster_login, - videotitle: encodeURI(response.display_name), + videotitle: encodeURIComponent(response.display_name), videolength: 0, videotype: "twitch", who: meta.nick, @@ -2220,7 +2220,7 @@ function addVideoTwitchClip(socket, data, meta, successCallback, failureCallback rawAddVideo({ pos: SERVER.PLAYLIST.length, videoid: response.id, - videotitle: encodeURI(response.title), + videotitle: encodeURIComponent(response.title), videolength: Math.ceil(response.duration), videotype: "twitchclip", who: meta.nick, @@ -2270,7 +2270,7 @@ function addVideoDailymotion(socket, data, meta, successCallback, failureCallbac rawAddVideo({ pos: SERVER.PLAYLIST.length, videoid: videoId, - videotitle: encodeURI(response.title), + videotitle: encodeURIComponent(response.title), videolength: response.duration, videotype: "dm", who: meta.nick, @@ -2458,7 +2458,7 @@ io.sockets.on('connection', function (ioSocket) { return; } - const pattern = '%' + encodeURI(data.search).replace(/%/g, '\\%') + '%'; + const pattern = '%' + encodeURIComponent(data.search).replace(/%/g, '\\%') + '%'; const { result } = await databaseService.query` SELECT * @@ -2774,7 +2774,7 @@ io.sockets.on('connection', function (ioSocket) { DefaultLog.info(events.EVENT_ADMIN_MOVED_VIDEO, "{mod} moved {title}", - { mod: getSocketName(socket), title: decodeURIComponent(fromelem.videotitle), type: "playlist" }); + { mod: getSocketName(socket), title: tryDecodeURIComponent(fromelem.videotitle), type: "playlist" }); }); socket.on("forceVideoChange", function (data) { if (!authService.can(socket.session, actions.ACTION_CONTROL_PLAYLIST)) { diff --git a/web/js/callbacks.js b/web/js/callbacks.js index daaeee3..51f00c1 100644 --- a/web/js/callbacks.js +++ b/web/js/callbacks.js @@ -452,7 +452,7 @@ socket.on('searchHistoryResults', function (data) { entry.data('plobject', vid); vid.domobj = entry; - $("
").addClass('title').text(decodeURIComponent(vid.videotitle)).appendTo(entry); + $("
").addClass('title').text(tryDecodeURIComponent(vid.videotitle)).appendTo(entry); $("
").addClass('delete').text("X").click(function () { var video = $(this).parent().data('plobject'); diff --git a/web/js/lib.js b/web/js/lib.js index 32f0935..870e55b 100644 --- a/web/js/lib.js +++ b/web/js/lib.js @@ -337,3 +337,12 @@ function onceFunction(fn) { } }; } + +function tryDecodeURIComponent(str) { + try { + return decodeURIComponent(str); + } catch (err) { + console.warn('Ignored:', err); + return str; + } +} diff --git a/web/js/votecallbacks.js b/web/js/votecallbacks.js index 33c3956..fcd4eeb 100644 --- a/web/js/votecallbacks.js +++ b/web/js/votecallbacks.js @@ -93,7 +93,7 @@ socket.on('searchHistoryResults', function (data) { entry.data('plobject', vid); vid.domobj = entry; - $("
").addClass('title').text(decodeURIComponent(vid.videotitle)).appendTo(entry); + $("
").addClass('title').text(tryDecodeURIComponent(vid.videotitle)).appendTo(entry); $("
").addClass('delete').text("X").click(function () { var video = $(this).parent().data('plobject'); diff --git a/web/plugins/toastthemes/toastthemes.js b/web/plugins/toastthemes/toastthemes.js index 3542e18..37f5098 100644 --- a/web/plugins/toastthemes/toastthemes.js +++ b/web/plugins/toastthemes/toastthemes.js @@ -453,7 +453,7 @@ function initToastThemes(data, textStatus, jqxhr) { // Rig up the auto-theme method to the correct socket callback socket.on('forceVideoChange', function(data) { - log('Video loaded: ' + decodeURIComponent(data.video.videotitle) + ' (id=' + data.video.videoid + ', duration=' + data.video.videolength + ')'); + log('Video loaded: ' + tryDecodeURIComponent(data.video.videotitle) + ' (id=' + data.video.videoid + ', duration=' + data.video.videolength + ')'); // Sweetie Belle crawl if (crawlId > -1) { @@ -1128,7 +1128,7 @@ function gakify(node) { var text; if (gakified) { text = node.data('plobject').videotitle; - return decodeURIComponent(text); + return tryDecodeURIComponent(text); } else { text = node.find('.title').text();