fix(core): use btoa() instead of window.Buffer.from() for base64 encoding

Signed-off-by: Anna Larch <anna@nextcloud.com>
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
This commit is contained in:
Anna Larch 2026-05-05 09:39:26 +02:00 committed by nextcloud-command
parent f721cb2267
commit b56334cd6d
9 changed files with 124 additions and 22 deletions

View file

@ -0,0 +1,114 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { testSupportedBrowser } from '../../utils/RedirectUnsupportedBrowsers.js'
// Mock the router so generateUrl returns a predictable path
vi.mock('@nextcloud/router', () => ({
generateUrl: (path: string) => `/index.php${path}`,
}))
// Mock the logger to suppress output
vi.mock('../../logger.js', () => ({
default: { debug: vi.fn() },
}))
const browserStorage = vi.hoisted(() => ({ getItem: vi.fn(() => null) }))
vi.mock('../../services/BrowserStorageService.js', () => ({ default: browserStorage }))
const supportedUA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
const unsupportedUA = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)'
describe('testSupportedBrowser', () => {
let originalLocation: Location
beforeEach(() => {
originalLocation = window.location
// Reset the override flag
browserStorage.getItem.mockReturnValue(null)
// Default to a path that isn't the unsupported-browser page
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: {
href: 'http://localhost/apps/files',
origin: 'http://localhost',
pathname: '/apps/files',
reload: vi.fn(),
},
})
vi.spyOn(window.history, 'pushState').mockImplementation(() => {})
})
afterEach(() => {
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: originalLocation,
})
vi.restoreAllMocks()
})
it('does nothing for a supported browser', () => {
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: supportedUA })
testSupportedBrowser()
expect(window.history.pushState).not.toHaveBeenCalled()
expect(window.location.reload).not.toHaveBeenCalled()
})
it('redirects an unsupported browser to the warning page', () => {
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: unsupportedUA })
testSupportedBrowser()
expect(window.history.pushState).toHaveBeenCalledOnce()
const [, , url] = (window.history.pushState as ReturnType<typeof vi.fn>).mock.calls[0]
expect(url).toMatch(/^\/index\.php\/unsupported\?redirect_url=/)
expect(window.location.reload).toHaveBeenCalledOnce()
})
it('encodes the redirect URL with btoa, not window.Buffer', () => {
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: unsupportedUA })
testSupportedBrowser()
const [, , url] = (window.history.pushState as ReturnType<typeof vi.fn>).mock.calls[0]
const encoded = new URL(`http://localhost${url}`).searchParams.get('redirect_url')
expect(encoded).toBe(btoa('/apps/files'))
})
it('does not throw regardless of override flag state', () => {
// isBrowserOverridden is read at module-load time so the mock won't flip it
// retroactively — but we can at least assert the function never throws,
// which is the regression guard for the window.Buffer removal.
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: unsupportedUA })
expect(() => testSupportedBrowser()).not.toThrow()
})
it('does not redirect when already on the unsupported-browser page', () => {
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: unsupportedUA })
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: {
href: 'http://localhost/index.php/unsupported',
origin: 'http://localhost',
pathname: '/index.php/unsupported',
reload: vi.fn(),
},
})
testSupportedBrowser()
expect(window.history.pushState).not.toHaveBeenCalled()
expect(window.location.reload).not.toHaveBeenCalled()
})
})

View file

@ -33,7 +33,7 @@ export const testSupportedBrowser = function() {
// redirect to the unsupported warning page
if (window.location.pathname.indexOf(redirectPath) === -1) {
const redirectUrl = window.location.href.replace(window.location.origin, '')
const base64Param = Buffer.from(redirectUrl).toString('base64')
const base64Param = btoa(redirectUrl)
history.pushState(null, null, `${redirectPath}?redirect_url=${base64Param}`)
window.location.reload()
}

4
dist/7883-7883.js vendored

File diff suppressed because one or more lines are too long

View file

@ -2,18 +2,15 @@ SPDX-License-Identifier: MIT
SPDX-License-Identifier: ISC
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: CC-BY-4.0
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: Apache-2.0
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-FileCopyrightText: dangreen
SPDX-FileCopyrightText: baseline-browser-mapping developers
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Sergey Rubanov <chi187@gmail.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Kilian Valkhof
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Feross Aboukhadijeh
SPDX-FileCopyrightText: Dmitry Soshnikov
SPDX-FileCopyrightText: Ben Briggs
SPDX-FileCopyrightText: Andrey Sitnik <andrey@sitnik.ru>
@ -41,9 +38,6 @@ This file is generated from multiple sources. Included packages:
- @nextcloud/router
- version: 3.1.0
- license: GPL-3.0-or-later
- base64-js
- version: 1.5.1
- license: MIT
- baseline-browser-mapping
- version: 2.9.9
- license: Apache-2.0
@ -59,12 +53,6 @@ This file is generated from multiple sources. Included packages:
- electron-to-chromium
- version: 1.5.267
- license: ISC
- ieee754
- version: 1.2.1
- license: BSD-3-Clause
- buffer
- version: 6.0.3
- license: MIT
- node-releases
- version: 2.0.27
- license: MIT

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,r,t,o={47210(e,r,t){var o=t(21777);t.nc=(0,o.aV)(),window.TESTING||OC?.config?.no_unsupported_browser_warning||window.addEventListener("DOMContentLoaded",async function(){const{testSupportedBrowser:e}=await Promise.all([t.e(4208),t.e(7883)]).then(t.bind(t,77883));e()})}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(s=0;s<e.length;s++){for(var[t,o,n]=e[s],l=!0,d=0;d<t.length;d++)(!1&n||i>=n)&&Object.keys(a.O).every(e=>a.O[e](t[d]))?t.splice(d--,1):(l=!1,n<i&&(i=n));if(l){e.splice(s--,1);var c=o();void 0!==c&&(r=c)}}return r}n=n||0;for(var s=e.length;s>0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((r,t)=>(a.f[t](e,r),r),[])),a.u=e=>e+"-"+e+".js?v=f0b15d22d89c4648c25b",a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,d;if(void 0!==n)for(var c=document.getElementsByTagName("script"),s=0;s<c.length;s++){var u=c[s];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==t+n){l=u;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach(e=>e(o)),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=3604,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={3604:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var n=new Promise((t,n)=>o=e[r]=[t,n]);t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}},"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,[i,l,d]=t,c=0;if(i.some(r=>0!==e[r])){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(d)var s=d(a)}for(r&&r(t);c<i.length;c++)n=i[c],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(s)},t=globalThis.webpackChunknextcloud=globalThis.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var i=a.O(void 0,[4208],()=>a(47210));i=a.O(i)})();
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=6acb671a13676e115e9a
(()=>{"use strict";var e,r,t,o={47210(e,r,t){var o=t(21777);t.nc=(0,o.aV)(),window.TESTING||OC?.config?.no_unsupported_browser_warning||window.addEventListener("DOMContentLoaded",async function(){const{testSupportedBrowser:e}=await Promise.all([t.e(4208),t.e(7883)]).then(t.bind(t,77883));e()})}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var t=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=o,e=[],a.O=(r,t,o,n)=>{if(!t){var i=1/0;for(c=0;c<e.length;c++){for(var[t,o,n]=e[c],l=!0,d=0;d<t.length;d++)(!1&n||i>=n)&&Object.keys(a.O).every(e=>a.O[e](t[d]))?t.splice(d--,1):(l=!1,n<i&&(i=n));if(l){e.splice(c--,1);var s=o();void 0!==s&&(r=s)}}return r}n=n||0;for(var c=e.length;c>0&&e[c-1][2]>n;c--)e[c]=e[c-1];e[c]=[t,o,n]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((r,t)=>(a.f[t](e,r),r),[])),a.u=e=>e+"-"+e+".js?v=c913a312e3fb195d4cf7",a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,o,n,i)=>{if(r[e])r[e].push(o);else{var l,d;if(void 0!==n)for(var s=document.getElementsByTagName("script"),c=0;c<s.length;c++){var u=s[c];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==t+n){l=u;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+n),l.src=e),r[e]=[o];var p=(t,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach(e=>e(o)),t)return t(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),d&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=3604,(()=>{var e;globalThis.importScripts&&(e=globalThis.location+"");var r=globalThis.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var o=t.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=t[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b="undefined"!=typeof document&&document.baseURI||self.location.href;var e={3604:0};a.f.j=(r,t)=>{var o=a.o(e,r)?e[r]:void 0;if(0!==o)if(o)t.push(o[2]);else{var n=new Promise((t,n)=>o=e[r]=[t,n]);t.push(o[2]=n);var i=a.p+a.u(r),l=new Error;a.l(i,t=>{if(a.o(e,r)&&(0!==(o=e[r])&&(e[r]=void 0),o)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}},"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var o,n,[i,l,d]=t,s=0;if(i.some(r=>0!==e[r])){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(d)var c=d(a)}for(r&&r(t);s<i.length;s++)n=i[s],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(c)},t=globalThis.webpackChunknextcloud=globalThis.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var i=a.O(void 0,[4208],()=>a(47210));i=a.O(i)})();
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=f3ea7a1661e7e0cec3ed

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long