Skip to content
Open
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
3 changes: 2 additions & 1 deletion core/src/services/BrowsersListService.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ import browserslist from 'browserslist'
import browserslistConfig from '@nextcloud/browserslist-config'

// Generate a regex that matches user agents to detect incompatible browsers
export const supportedBrowsersRegExp = new RegExp(getUserAgentRegex({ allowHigherVersions: true, browsers: browserslistConfig }).source + '|AscDesktopEditor')
// Electron is added explicitly as it is used by Cypress tests and desktop app integrations
export const supportedBrowsersRegExp = new RegExp(getUserAgentRegex({ allowHigherVersions: true, browsers: browserslistConfig }).source + '|AscDesktopEditor|Electron')
export const supportedBrowsers = browserslist(browserslistConfig)
114 changes: 114 additions & 0 deletions core/src/tests/utils/RedirectUnsupportedBrowsers.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
2 changes: 1 addition & 1 deletion core/src/utils/RedirectUnsupportedBrowsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
4 changes: 2 additions & 2 deletions dist/7883-7883.js

Large diffs are not rendered by default.

12 changes: 0 additions & 12 deletions dist/7883-7883.js.license
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dist/7883-7883.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/core-unsupported-browser-redirect.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/core-unsupported-browser-redirect.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/core-unsupported-browser.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/core-unsupported-browser.js.map

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion tests/lib/TextProcessing/TextProcessingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,12 @@ protected function setUp(): void {
$this->taskMapper
->expects($this->any())
->method('deleteOlderThan')
->willReturnCallback(function (int $timeout): void {
->willReturnCallback(function (int $timeout): int {
$before = count($this->tasksDb);
$this->tasksDb = array_filter($this->tasksDb, function (array $task) use ($timeout) {
return $task['last_updated'] >= $this->currentTime->getTimestamp() - $timeout;
});
return $before - count($this->tasksDb);
});

$this->jobList = $this->createPartialMock(DummyJobList::class, ['add']);
Expand Down
Loading