Merge pull request #44761 from nextcloud/fix/deps-webauthn-lib

fix(deps): Bump web-auth/webauthn-lib from 3.3.9 to 4.8.5
This commit is contained in:
Ferdinand Thiessen 2024-04-16 12:57:34 +02:00 committed by GitHub
commit 7eec3b5a72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 296 additions and 331 deletions

@ -1 +1 @@
Subproject commit e2747858e408e4d9dde72a8a7cf99f2d7f750d98
Subproject commit 202c6195d28ac55f08e5b3c31a95fff6a7093659

View File

@ -24,11 +24,11 @@
{{ t('settings', 'Passwordless authentication requires a secure connection.') }}
</div>
<div v-else>
<div v-if="step === RegistrationSteps.READY">
<NcButton @click="start" type="primary">
{{ t('settings', 'Add WebAuthn device') }}
</NcButton>
</div>
<NcButton v-if="step === RegistrationSteps.READY"
type="primary"
@click="start">
{{ t('settings', 'Add WebAuthn device') }}
</NcButton>
<div v-else-if="step === RegistrationSteps.REGISTRATION"
class="new-webauthn-device">
@ -39,13 +39,14 @@
<div v-else-if="step === RegistrationSteps.NAMING"
class="new-webauthn-device">
<span class="icon-loading-small webauthn-loading" />
<input v-model="name"
type="text"
:placeholder="t('settings', 'Name your device')"
@:keyup.enter="submit">
<NcButton @click="submit" type="primary">
{{ t('settings', 'Add') }}
</NcButton>
<NcTextField ref="nameInput"
class="new-webauthn-device__name"
:label="t('settings', 'Device name')"
:value.sync="name"
show-trailing-button
:trailing-button-label="t('settings', 'Add')"
trailing-button-icon="arrowRight"
@trailing-button-click="submit" />
</div>
<div v-else-if="step === RegistrationSteps.PERSIST"
@ -61,15 +62,16 @@
</template>
<script>
import { showError } from '@nextcloud/dialogs'
import { confirmPassword } from '@nextcloud/password-confirmation'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import '@nextcloud/password-confirmation/dist/style.css'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
import logger from '../../logger.ts'
import {
startRegistration,
finishRegistration,
} from '../../service/WebAuthnRegistrationSerice.js'
} from '../../service/WebAuthnRegistrationSerice.ts'
const logAndPass = (text) => (data) => {
logger.debug(text)
@ -88,6 +90,7 @@ export default {
components: {
NcButton,
NcTextField,
},
props: {
@ -101,83 +104,55 @@ export default {
default: false,
},
},
setup() {
// non reactive props
return {
RegistrationSteps,
}
},
data() {
return {
name: '',
credential: {},
RegistrationSteps,
step: RegistrationSteps.READY,
}
},
methods: {
arrayToBase64String(a) {
return btoa(String.fromCharCode(...a))
watch: {
/**
* Auto focus the name input when naming a device
*/
step() {
if (this.step === RegistrationSteps.NAMING) {
this.$nextTick(() => this.$refs.nameInput?.focus())
}
},
start() {
},
methods: {
/**
* Start the registration process by loading the authenticator parameters
* The next step is the naming of the device
*/
async start() {
this.step = RegistrationSteps.REGISTRATION
console.debug('Starting WebAuthn registration')
return confirmPassword()
.then(this.getRegistrationData)
.then(this.register.bind(this))
.then(() => { this.step = RegistrationSteps.NAMING })
.catch(err => {
console.error(err.name, err.message)
this.step = RegistrationSteps.READY
})
},
getRegistrationData() {
console.debug('Fetching webauthn registration data')
const base64urlDecode = function(input) {
// Replace non-url compatible chars with base64 standard chars
input = input
.replace(/-/g, '+')
.replace(/_/g, '/')
// Pad out with standard base64 required padding characters
const pad = input.length % 4
if (pad) {
if (pad === 1) {
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding')
}
input += new Array(5 - pad).join('=')
}
return window.atob(input)
try {
await confirmPassword()
this.credential = await startRegistration()
this.step = RegistrationSteps.NAMING
} catch (err) {
showError(err)
this.step = RegistrationSteps.READY
}
return startRegistration()
.then(publicKey => {
console.debug(publicKey)
publicKey.challenge = Uint8Array.from(base64urlDecode(publicKey.challenge), c => c.charCodeAt(0))
publicKey.user.id = Uint8Array.from(publicKey.user.id, c => c.charCodeAt(0))
return publicKey
})
.catch(err => {
console.error('Error getting webauthn registration data from server', err)
throw new Error(t('settings', 'Server error while trying to add WebAuthn device'))
})
},
register(publicKey) {
console.debug('starting webauthn registration')
return navigator.credentials.create({ publicKey })
.then(data => {
this.credential = {
id: data.id,
type: data.type,
rawId: this.arrayToBase64String(new Uint8Array(data.rawId)),
response: {
clientDataJSON: this.arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
attestationObject: this.arrayToBase64String(new Uint8Array(data.response.attestationObject)),
},
}
})
},
/**
* Save the new device with the given name on the server
*/
submit() {
this.step = RegistrationSteps.PERSIST
@ -187,12 +162,12 @@ export default {
.then(logAndPass('registration data saved'))
.then(() => this.reset())
.then(logAndPass('app reset'))
.catch(console.error.bind(this))
.catch(console.error)
},
async saveRegistrationData() {
try {
const device = await finishRegistration(this.name, JSON.stringify(this.credential))
const device = await finishRegistration(this.name, this.credential)
logger.info('new device added', { device })
@ -212,15 +187,21 @@ export default {
}
</script>
<style scoped>
.webauthn-loading {
display: inline-block;
vertical-align: sub;
margin-left: 2px;
margin-right: 2px;
}
<style scoped lang="scss">
.webauthn-loading {
display: inline-block;
vertical-align: sub;
margin-left: 2px;
margin-right: 2px;
}
.new-webauthn-device {
line-height: 300%;
.new-webauthn-device {
display: flex;
gap: 22px;
align-items: center;
&__name {
max-width: min(100vw, 400px);
}
}
</style>

View File

@ -20,7 +20,7 @@
-->
<template>
<div class="webauthn-device">
<li class="webauthn-device">
<span class="icon-webauthn-device" />
{{ name || t('settings', 'Unnamed device') }}
<NcActions :force-menu="true">
@ -28,7 +28,7 @@
{{ t('settings', 'Delete') }}
</NcActionButton>
</NcActions>
</div>
</li>
</template>
<script>

View File

@ -28,19 +28,22 @@
<NcNoteCard v-if="devices.length === 0" type="info">
{{ t('settings', 'No devices configured.') }}
</NcNoteCard>
<h3 v-else>
<h3 v-else id="security-webauthn__active-devices">
{{ t('settings', 'The following devices are configured for your account:') }}
</h3>
<Device v-for="device in sortedDevices"
:key="device.id"
:name="device.name"
@delete="deleteDevice(device.id)" />
<ul aria-labelledby="security-webauthn__active-devices" class="security-webauthn__device-list">
<Device v-for="device in sortedDevices"
:key="device.id"
:name="device.name"
@delete="deleteDevice(device.id)" />
</ul>
<NcNoteCard v-if="!hasPublicKeyCredential" type="warning">
<NcNoteCard v-if="!supportsWebauthn" type="warning">
{{ t('settings', 'Your browser does not support WebAuthn.') }}
</NcNoteCard>
<AddDevice v-if="hasPublicKeyCredential"
<AddDevice v-if="supportsWebauthn"
:is-https="isHttps"
:is-localhost="isLocalhost"
@added="deviceAdded" />
@ -48,6 +51,7 @@
</template>
<script>
import { browserSupportsWebAuthn } from '@simplewebauthn/browser'
import { confirmPassword } from '@nextcloud/password-confirmation'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import '@nextcloud/password-confirmation/dist/style.css'
@ -79,11 +83,15 @@ export default {
type: Boolean,
default: false,
},
hasPublicKeyCredential: {
type: Boolean,
default: false,
},
},
setup() {
// Non reactive properties
return {
supportsWebauthn: browserSupportsWebAuthn(),
}
},
data() {
return {
devices: this.initialDevices,
@ -115,5 +123,7 @@ export default {
</script>
<style scoped>
.security-webauthn__device-list {
margin-block: 12px 18px;
}
</style>

View File

@ -37,6 +37,5 @@ new View({
initialDevices: devices,
isHttps: window.location.protocol === 'https:',
isLocalhost: window.location.hostname === 'localhost',
hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined',
},
}).$mount('#security-webauthn')

View File

@ -20,34 +20,55 @@
*
*/
import axios from '@nextcloud/axios'
import type { RegistrationResponseJSON } from '@simplewebauthn/types'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { startRegistration as registerWebAuthn } from '@simplewebauthn/browser'
import Axios from 'axios'
import axios from '@nextcloud/axios'
import logger from '../logger'
/**
*
* Start registering a new device
* @return The device attributes
*/
export async function startRegistration() {
const url = generateUrl('/settings/api/personal/webauthn/registration')
const resp = await axios.get(url)
return resp.data
try {
logger.debug('Fetching webauthn registration data')
const { data } = await axios.get(url)
logger.debug('Start webauthn registration')
const attrs = await registerWebAuthn(data)
return attrs
} catch (e) {
logger.error(e as Error)
if (Axios.isAxiosError(e)) {
throw new Error(t('settings', 'Could not register device: Network error'))
} else if ((e as Error).name === 'InvalidStateError') {
throw new Error(t('settings', 'Could not register device: Probably already registered'))
}
throw new Error(t('settings', 'Could not register device'))
}
}
/**
* @param {any} name -
* @param {any} data -
* @param name Name of the device
* @param data Device attributes
*/
export async function finishRegistration(name, data) {
export async function finishRegistration(name: string, data: RegistrationResponseJSON) {
const url = generateUrl('/settings/api/personal/webauthn/registration')
const resp = await axios.post(url, { name, data })
const resp = await axios.post(url, { name, data: JSON.stringify(data) })
return resp.data
}
/**
* @param {any} id -
* @param id Remove registered device with that id
*/
export async function removeRegistration(id) {
export async function removeRegistration(id: string | number) {
const url = generateUrl(`/settings/api/personal/webauthn/registration/${id}`)
await axios.delete(url)

View File

@ -1,5 +1,5 @@
<template>
<form v-if="(isHttps || isLocalhost) && hasPublicKeyCredential"
<form v-if="(isHttps || isLocalhost) && supportsWebauthn"
ref="loginForm"
method="post"
name="login"
@ -20,7 +20,7 @@
@click="authenticate" />
</fieldset>
</form>
<div v-else-if="!hasPublicKeyCredential" class="update">
<div v-else-if="!supportsWebauthn" class="update">
<InformationIcon size="70" />
<h2>{{ t('core', 'Browser not supported') }}</h2>
<p class="infogroup">
@ -37,18 +37,16 @@
</template>
<script>
import { browserSupportsWebAuthn } from '@simplewebauthn/browser'
import {
startAuthentication,
finishAuthentication,
} from '../../services/WebAuthnAuthenticationService.js'
} from '../../services/WebAuthnAuthenticationService.ts'
import LoginButton from './LoginButton.vue'
import InformationIcon from 'vue-material-design-icons/Information.vue'
import LockOpenIcon from 'vue-material-design-icons/LockOpen.vue'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
class NoValidCredentials extends Error {
}
import logger from '../../logger'
export default {
name: 'PasswordLessLoginForm',
@ -79,11 +77,14 @@ export default {
type: Boolean,
default: false,
},
hasPublicKeyCredential: {
type: Boolean,
default: false,
},
},
setup() {
return {
supportsWebauthn: browserSupportsWebAuthn(),
}
},
data() {
return {
user: this.username,
@ -92,7 +93,7 @@ export default {
}
},
methods: {
authenticate() {
async authenticate() {
// check required fields
if (!this.$refs.loginForm.checkValidity()) {
return
@ -100,112 +101,25 @@ export default {
console.debug('passwordless login initiated')
this.getAuthenticationData(this.user)
.then(publicKey => {
console.debug(publicKey)
return publicKey
})
.then(this.sign)
.then(this.completeAuthentication)
.catch(error => {
if (error instanceof NoValidCredentials) {
this.validCredentials = false
return
}
console.debug(error)
})
try {
const params = await startAuthentication(this.user)
await this.completeAuthentication(params)
} catch (error) {
if (error instanceof NoValidCredentials) {
this.validCredentials = false
return
}
logger.debug(error)
}
},
changeUsername(username) {
this.user = username
this.$emit('update:username', this.user)
},
getAuthenticationData(uid) {
const base64urlDecode = function(input) {
// Replace non-url compatible chars with base64 standard chars
input = input
.replace(/-/g, '+')
.replace(/_/g, '/')
// Pad out with standard base64 required padding characters
const pad = input.length % 4
if (pad) {
if (pad === 1) {
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding')
}
input += new Array(5 - pad).join('=')
}
return window.atob(input)
}
return startAuthentication(uid)
.then(publicKey => {
console.debug('Obtained PublicKeyCredentialRequestOptions')
console.debug(publicKey)
if (!Object.prototype.hasOwnProperty.call(publicKey, 'allowCredentials')) {
console.debug('No credentials found.')
throw new NoValidCredentials()
}
publicKey.challenge = Uint8Array.from(base64urlDecode(publicKey.challenge), c => c.charCodeAt(0))
publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) {
return {
...data,
id: Uint8Array.from(base64urlDecode(data.id), c => c.charCodeAt(0)),
}
})
console.debug('Converted PublicKeyCredentialRequestOptions')
console.debug(publicKey)
return publicKey
})
.catch(error => {
console.debug('Error while obtaining data')
throw error
})
},
sign(publicKey) {
const arrayToBase64String = function(a) {
return window.btoa(String.fromCharCode(...a))
}
const arrayToString = function(a) {
return String.fromCharCode(...a)
}
return navigator.credentials.get({ publicKey })
.then(data => {
console.debug(data)
console.debug(new Uint8Array(data.rawId))
console.debug(arrayToBase64String(new Uint8Array(data.rawId)))
return {
id: data.id,
type: data.type,
rawId: arrayToBase64String(new Uint8Array(data.rawId)),
response: {
authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
signature: arrayToBase64String(new Uint8Array(data.response.signature)),
userHandle: data.response.userHandle ? arrayToString(new Uint8Array(data.response.userHandle)) : null,
},
}
})
.then(challenge => {
console.debug(challenge)
return challenge
})
.catch(error => {
console.debug('GOT AN ERROR!')
console.debug(error) // Example: timeout, interaction refused...
})
},
completeAuthentication(challenge) {
console.debug('TIME TO COMPLETE')
const redirectUrl = this.redirectUrl
return finishAuthentication(JSON.stringify(challenge))
return finishAuthentication(challenge)
.then(({ defaultRedirectUrl }) => {
console.debug('Logged in redirecting')
// Redirect url might be false so || should be used instead of ??.

View File

@ -1,44 +0,0 @@
/**
* @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
/**
* @param {any} loginName -
*/
export function startAuthentication(loginName) {
const url = generateUrl('/login/webauthn/start')
return Axios.post(url, { loginName })
.then(resp => resp.data)
}
/**
* @param {any} data -
*/
export function finishAuthentication(data) {
const url = generateUrl('/login/webauthn/finish')
return Axios.post(url, { data })
.then(resp => resp.data)
}

View File

@ -0,0 +1,59 @@
/**
* @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'
import { startAuthentication as startWebauthnAuthentication } from '@simplewebauthn/browser'
import { generateUrl } from '@nextcloud/router'
import Axios from '@nextcloud/axios'
import logger from '../logger'
export class NoValidCredentials extends Error {}
/**
* Start webautn authentication
* This loads the challenge, connects to the authenticator and returns the repose that needs to be sent to the server.
*
* @param loginName Name to login
*/
export async function startAuthentication(loginName: string) {
const url = generateUrl('/login/webauthn/start')
const { data } = await Axios.post<PublicKeyCredentialRequestOptionsJSON>(url, { loginName })
if (!data.allowCredentials || data.allowCredentials.length === 0) {
logger.error('No valid credentials returned for webauthn')
throw new NoValidCredentials()
}
return await startWebauthnAuthentication(data)
}
/**
* Verify webauthn authentication
* @param authData The authentication data to sent to the server
*/
export async function finishAuthentication(authData: AuthenticationResponseJSON) {
const url = generateUrl('/login/webauthn/finish')
const { data } = await Axios.post(url, { data: JSON.stringify(authData) })
return data
}

View File

@ -73,7 +73,6 @@
:auto-complete-allowed="autoCompleteAllowed"
:is-https="isHttps"
:is-localhost="isLocalhost"
:has-public-key-credential="hasPublicKeyCredential"
@submit="loading = true" />
<NcButton type="tertiary"
:aria-label="t('core', 'Back to login form')"
@ -178,7 +177,6 @@ export default {
alternativeLogins: loadState('core', 'alternativeLogins', []),
isHttps: window.location.protocol === 'https:',
isLocalhost: window.location.hostname === 'localhost',
hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined',
hideLoginForm: loadState('core', 'hideLoginForm', false),
emailStates: loadState('core', 'emailStates', []),
}

BIN
dist/core-common.js vendored

Binary file not shown.

Binary file not shown.

BIN
dist/core-login.js vendored

Binary file not shown.

BIN
dist/core-login.js.map vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -83,14 +83,14 @@ class Manager {
public function startRegistration(IUser $user, string $serverHost): PublicKeyCredentialCreationOptions {
$rpEntity = new PublicKeyCredentialRpEntity(
'Nextcloud', //Name
$this->stripPort($serverHost), //ID
$this->stripPort($serverHost), //ID
null //Icon
);
$userEntity = new PublicKeyCredentialUserEntity(
$user->getUID(), //Name
$user->getUID(), //ID
$user->getDisplayName() //Display name
$user->getUID(), // Name
$user->getUID(), // ID
$user->getDisplayName() // Display name
// 'https://foo.example.co/avatar/123e4567-e89b-12d3-a456-426655440000' //Icon
);
@ -107,9 +107,10 @@ class Manager {
];
$authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
null,
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
null,
false,
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED
);
return new PublicKeyCredentialCreationOptions(
@ -117,11 +118,10 @@ class Manager {
$userEntity,
$challenge,
$publicKeyCredentialParametersList,
$timeout,
$excludedPublicKeyDescriptors,
$authenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
null
$excludedPublicKeyDescriptors,
$timeout,
);
}
@ -149,7 +149,7 @@ class Manager {
try {
// Load the data
$publicKeyCredential = $publicKeyCredentialLoader->load($data);
$response = $publicKeyCredential->getResponse();
$response = $publicKeyCredential->response;
// Check if the response is an Authenticator Attestation Response
if (!$response instanceof AuthenticatorAttestationResponse) {
@ -162,7 +162,9 @@ class Manager {
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
$response,
$publicKeyCredentialCreationOptions,
$request);
$request,
['localhost'],
);
} catch (\Throwable $exception) {
throw $exception;
}
@ -180,18 +182,18 @@ class Manager {
$registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) {
$credential = $entity->toPublicKeyCredentialSource();
return new PublicKeyCredentialDescriptor(
$credential->getType(),
$credential->getPublicKeyCredentialId()
$credential->type,
$credential->publicKeyCredentialId,
);
}, $this->credentialMapper->findAllForUid($uid));
// Public Key Credential Request Options
return new PublicKeyCredentialRequestOptions(
random_bytes(32), // Challenge
60000, // Timeout
$this->stripPort($serverHost), // Relying Party ID
$registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED
random_bytes(32), // Challenge
$this->stripPort($serverHost), // Relying Party ID
$registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes
AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
60000, // Timeout
);
}
@ -213,16 +215,15 @@ class Manager {
$tokenBindingHandler,
$extensionOutputCheckerHandler,
$algorithmManager,
null,
$this->logger,
);
$authenticatorAssertionResponseValidator->setLogger($this->logger);
try {
$this->logger->debug('Loading publickey credentials from: ' . $data);
// Load the data
$publicKeyCredential = $publicKeyCredentialLoader->load($data);
$response = $publicKeyCredential->getResponse();
$response = $publicKeyCredential->response;
// Check if the response is an Authenticator Attestation Response
if (!$response instanceof AuthenticatorAssertionResponse) {
@ -233,18 +234,17 @@ class Manager {
$request = ServerRequest::fromGlobals();
$publicKeyCredentialSource = $authenticatorAssertionResponseValidator->check(
$publicKeyCredential->getRawId(),
$publicKeyCredential->rawId,
$response,
$publicKeyCredentialRequestOptions,
$request,
$uid
$uid,
['localhost'],
);
} catch (\Throwable $e) {
throw $e;
}
return true;
}

View File

@ -7,7 +7,7 @@ declare(strict_types=1);
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -31,6 +31,7 @@ use OCP\ILogger;
use OCP\Log\IDataLogger;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Stringable;
use Throwable;
use function array_key_exists;
use function array_merge;
@ -52,19 +53,20 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
/**
* System is unusable.
*
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*/
public function emergency($message, array $context = []): void {
public function emergency(string|Stringable $message, array $context = []): void {
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => ILogger::FATAL,
],
$context
));
} else {
$this->logger->emergency($message, $context);
$this->logger->emergency((string)$message, $context);
}
}
@ -74,19 +76,20 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*/
public function alert($message, array $context = []): void {
public function alert(string|Stringable $message, array $context = []): void {
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => ILogger::ERROR,
],
$context
));
} else {
$this->logger->alert($message, $context);
$this->logger->alert((string)$message, $context);
}
}
@ -95,19 +98,20 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*/
public function critical($message, array $context = []): void {
public function critical(string|Stringable $message, array $context = []): void {
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => ILogger::ERROR,
],
$context
));
} else {
$this->logger->critical($message, $context);
$this->logger->critical((string)$message, $context);
}
}
@ -115,19 +119,20 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*/
public function error($message, array $context = []): void {
public function error(string|Stringable $message, array $context = []): void {
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => ILogger::ERROR,
],
$context
));
} else {
$this->logger->error($message, $context);
$this->logger->error((string)$message, $context);
}
}
@ -137,38 +142,40 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*/
public function warning($message, array $context = []): void {
public function warning(string|Stringable $message, array $context = []): void {
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => ILogger::WARN,
],
$context
));
} else {
$this->logger->warning($message, $context);
$this->logger->warning((string)$message, $context);
}
}
/**
* Normal but significant events.
*
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*/
public function notice($message, array $context = []): void {
public function notice(string|Stringable $message, array $context = []): void {
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => ILogger::INFO,
],
$context
));
} else {
$this->logger->notice($message, $context);
$this->logger->notice((string)$message, $context);
}
}
@ -177,38 +184,40 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*/
public function info($message, array $context = []): void {
public function info(string|Stringable $message, array $context = []): void {
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => ILogger::INFO,
],
$context
));
} else {
$this->logger->info($message, $context);
$this->logger->info((string)$message, $context);
}
}
/**
* Detailed debug information.
*
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*/
public function debug($message, array $context = []): void {
public function debug(string|Stringable $message, array $context = []): void {
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => ILogger::DEBUG,
],
$context
));
} else {
$this->logger->debug($message, $context);
$this->logger->debug((string)$message, $context);
}
}
@ -216,24 +225,25 @@ final class PsrLoggerAdapter implements LoggerInterface, IDataLogger {
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param string|Stringable $message
* @param mixed[] $context
*
* @throws InvalidArgumentException
*/
public function log($level, $message, array $context = []): void {
public function log($level, string|Stringable $message, array $context = []): void {
if (!is_int($level) || $level < ILogger::DEBUG || $level > ILogger::FATAL) {
throw new InvalidArgumentException('Nextcloud allows only integer log levels');
}
if ($this->containsThrowable($context)) {
$this->logger->logException($context['exception'], array_merge(
[
'message' => $message,
'message' => (string)$message,
'level' => $level,
],
$context
));
} else {
$this->logger->log($level, $message, $context);
$this->logger->log($level, (string)$message, $context);
}
}

15
package-lock.json generated
View File

@ -31,6 +31,7 @@
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/upload": "^1.1.1",
"@nextcloud/vue": "^8.11.2",
"@simplewebauthn/browser": "^9.0.1",
"@skjnldsv/sanitize-svg": "^1.0.2",
"@vueuse/components": "^10.7.2",
"@vueuse/core": "^10.7.2",
@ -103,6 +104,7 @@
"@nextcloud/typings": "^1.8.0",
"@nextcloud/webpack-vue-config": "^6.0.1",
"@pinia/testing": "^0.1.2",
"@simplewebauthn/types": "^9.0.1",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^5.8.3",
@ -5061,6 +5063,19 @@
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"dev": true
},
"node_modules/@simplewebauthn/browser": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-9.0.1.tgz",
"integrity": "sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==",
"dependencies": {
"@simplewebauthn/types": "^9.0.1"
}
},
"node_modules/@simplewebauthn/types": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-9.0.1.tgz",
"integrity": "sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w=="
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",

View File

@ -58,6 +58,7 @@
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/upload": "^1.1.1",
"@nextcloud/vue": "^8.11.2",
"@simplewebauthn/browser": "^9.0.1",
"@skjnldsv/sanitize-svg": "^1.0.2",
"@vueuse/components": "^10.7.2",
"@vueuse/core": "^10.7.2",
@ -130,6 +131,7 @@
"@nextcloud/typings": "^1.8.0",
"@nextcloud/webpack-vue-config": "^6.0.1",
"@pinia/testing": "^0.1.2",
"@simplewebauthn/types": "^9.0.1",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^5.8.3",