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

window.Buffer / Buffer is a Node.js API that is not natively available
in browsers. Replace with the native browser btoa() which has been
universally available since before our minimum browser support baseline.

Backport of #60148

Signed-off-by: Anna Larch <anna@nextcloud.com>
AI-Assisted-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
This commit is contained in:
Anna Larch 2026-05-19 16:44:43 +02:00 committed by nextcloud-command
parent dab6507e27
commit f92bbbadc2
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: Christoph Wurst
SPDX-FileCopyrightText: Ben Briggs
@ -42,9 +39,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.11
- license: Apache-2.0
@ -60,12 +54,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=9d4000663d2aae402ae8",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=06e623e4e4e29be34011
(()=>{"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=4fafd59634333811d007",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=25f0af982a1484fb9558

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