From 757e553dbd250c7fb284f693973ad059e5ecd48f Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Mon, 17 Nov 2025 14:49:29 +0000 Subject: [PATCH 01/38] make prod: Add test users and groups --- scripts/run-app.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/run-app.sh b/scripts/run-app.sh index 2a0c0b3..89eb95a 100755 --- a/scripts/run-app.sh +++ b/scripts/run-app.sh @@ -82,6 +82,12 @@ docker exec --user www-data -i nextcloud-container php occ config:app:set gdatav docker exec --user www-data -i nextcloud-container php occ config:app:set gdatavaas authMethod --value=ClientCredentials docker exec --user www-data -i nextcloud-container php occ config:app:set gdatavaas autoScanFiles --value=true +# Add groups and users +docker exec -e OC_PASS=gdatavaas-user --user www-data -i nextcloud-container php occ user:add user --password-from-env +docker exec --user www-data -i nextcloud-container php occ group:add vaas-operators +docker exec -e OC_PASS=gdatavaas-operator --user www-data -i nextcloud-container php occ user:add vaas-operator --password-from-env --group vaas-operators +docker exec --user www-data -i nextcloud-container php occ admin-delegation:add 'OCA\GDataVaas\Settings\VaasOperator' vaas-operators + # Configure Nextcloud to send emails docker exec --user www-data -i nextcloud-container php occ config:app:set gdatavaas notifyMails --value="test@example.com" docker exec --user www-data -i nextcloud-container php occ config:app:set gdatavaas sendMailOnVirusUpload --value=true From cba59c365d8a448b428b02f688253ff910a73468 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Mon, 17 Nov 2025 14:52:28 +0000 Subject: [PATCH 02/38] lib/Settings: extract operator settings --- appinfo/info.xml | 1 + lib/Settings/VaasAdmin.php | 11 ------- lib/Settings/VaasOperator.php | 61 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 lib/Settings/VaasOperator.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 3718079..54ca3df 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -70,6 +70,7 @@ Please make sure the "Authentication Method" "Resource Owner Password Flow" is s OCA\GDataVaas\Settings\VaasAdmin + OCA\GDataVaas\Settings\VaasOperator OCA\GDataVaas\Settings\VaasAdminSection diff --git a/lib/Settings/VaasAdmin.php b/lib/Settings/VaasAdmin.php index f3ea696..7bcc5d6 100644 --- a/lib/Settings/VaasAdmin.php +++ b/lib/Settings/VaasAdmin.php @@ -42,17 +42,6 @@ public function getForm(): TemplateResponse { 'vaasUrl', 'https://gateway.staging.vaas.gdatasecurity.de' ), - 'quarantineFolder' - => $this->config->getValueString(Application::APP_ID, 'quarantineFolder', 'Quarantine'), - 'autoScanFiles' => $this->config->getValueBool(Application::APP_ID, 'autoScanFiles'), - 'prefixMalicious' - => $this->config->getValueBool(Application::APP_ID, 'prefixMalicious', true), - 'disableUnscannedTag' => $this->config->getValueBool(Application::APP_ID, 'disableUnscannedTag'), - 'scanOnlyThis' => $this->config->getValueString(Application::APP_ID, 'scanOnlyThis'), - 'doNotScanThis' => $this->config->getValueString(Application::APP_ID, 'doNotScanThis'), - 'notifyMail' => $this->config->getValueString(Application::APP_ID, 'notifyMails'), - 'sendMailOnVirusUpload' - => $this->config->getValueBool(Application::APP_ID, 'sendMailOnVirusUpload'), 'maxScanSizeInMB' => $this->config->getValueInt(Application::APP_ID, 'maxScanSizeInMB', 256), 'timeout' => $this->config->getValueInt(Application::APP_ID, 'timeout', 300), diff --git a/lib/Settings/VaasOperator.php b/lib/Settings/VaasOperator.php new file mode 100644 index 0000000..4ae1b30 --- /dev/null +++ b/lib/Settings/VaasOperator.php @@ -0,0 +1,61 @@ + +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\GDataVaas\Settings; + +use OCA\GDataVaas\AppInfo\Application; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IAppConfig; +use OCP\IL10N; +use OCP\Settings\IDelegatedSettings; + +class VaasOperator implements IDelegatedSettings { + + private IAppConfig $config; + private IL10N $l; + + public function __construct(IAppConfig $config, IL10N $l) { + $this->config = $config; + $this->l = $l; + } + + #[\Override] + public function getForm(): TemplateResponse { + $params = [ + 'quarantineFolder' + => $this->config->getValueString(Application::APP_ID, 'quarantineFolder', 'Quarantine'), + 'autoScanFiles' => $this->config->getValueBool(Application::APP_ID, 'autoScanFiles'), + 'prefixMalicious' + => $this->config->getValueBool(Application::APP_ID, 'prefixMalicious', true), + 'disableUnscannedTag' => $this->config->getValueBool(Application::APP_ID, 'disableUnscannedTag'), + 'scanOnlyThis' => $this->config->getValueString(Application::APP_ID, 'scanOnlyThis'), + 'doNotScanThis' => $this->config->getValueString(Application::APP_ID, 'doNotScanThis'), + 'notifyMail' => $this->config->getValueString(Application::APP_ID, 'notifyMails'), + 'sendMailOnVirusUpload' + => $this->config->getValueBool(Application::APP_ID, 'sendMailOnVirusUpload'), + ]; + + return new TemplateResponse(Application::APP_ID, 'operator', $params); + } + + #[\Override] + public function getSection(): string { + return 'vaas'; + } + + #[\Override] + public function getPriority(): int { + return 20; + } + + public function getName(): ?string { + return $this->l->t('Operator Settings'); + } + + public function getAuthorizedAppConfig(): array { + return []; + } +} From b4d91a454b6d7fc6b21ae5af7b9412919f6b66d9 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Tue, 18 Nov 2025 15:17:31 +0000 Subject: [PATCH 03/38] Templates + js --- src/admin-settings.js | 8 -- src/operator-settings.js | 155 +++++++++++++++++++++++++++++++++++++++ templates/admin.php | 83 +++------------------ templates/operator.php | 75 +++++++++++++++++++ 4 files changed, 241 insertions(+), 80 deletions(-) create mode 100644 src/operator-settings.js create mode 100644 templates/operator.php diff --git a/src/admin-settings.js b/src/admin-settings.js index 8804b74..d33b6a7 100644 --- a/src/admin-settings.js +++ b/src/admin-settings.js @@ -59,10 +59,6 @@ document.addEventListener('DOMContentLoaded', async () => { const password = document.querySelector('#password').value; const clientId = document.querySelector('#clientId').value; const clientSecret = document.querySelector('#clientSecret').value; - const quarantineFolder = document.querySelector('#quarantine_folder').value; - const scanOnlyThis = document.querySelector('#scanOnlyThis').value; - const doNotScanThis = document.querySelector('#doNotScanThis').value; - const notifyMails = document.querySelector('#notify_mails').value; const maxScanSize = document.querySelector('#max-scan-size').value; const timeout = document.querySelector('#timeout').value; const cache = document.querySelector('#cache').checked; @@ -74,10 +70,6 @@ document.addEventListener('DOMContentLoaded', async () => { clientId: clientId, clientSecret: clientSecret, authMethod: authMethod.value, - quarantineFolder, - scanOnlyThis, - doNotScanThis, - notifyMails, maxScanSize, timeout, cache, diff --git a/src/operator-settings.js b/src/operator-settings.js new file mode 100644 index 0000000..2ce38e1 --- /dev/null +++ b/src/operator-settings.js @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2025 Lennart Dohmann +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +document.addEventListener('DOMContentLoaded', async () => { + + async function postData(url = '', data = {}) { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'requesttoken': oc_requesttoken + }, + body: JSON.stringify(data) + }); + return response.json(); + } + + async function getData(url = '') { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'requesttoken': oc_requesttoken + } + }); + return response.json(); + } + + const authSubmit = document.querySelector('#auth_submit'); + const authSubmitAdvanced = document.querySelector('#auth_submit_advanced'); + const resetAllTags = document.querySelector('#reset'); + const autoScanFiles = document.querySelector('#auto_scan_files'); + const prefixMalicious = document.querySelector('#prefixMalicious'); + const authMethod = document.querySelector('#authMethod'); + const disableUnscannedTag = document.querySelector('#disable_tag_unscanned'); + const scanCounter = document.querySelector('#scan_counter'); + const sendMailOnVirusUpload = document.querySelector('#send_mail_on_virus_upload'); + + authMethod.addEventListener('change', (e) => { + hideUnneccessaryFields(e.target.value); + }); + + authSubmit.addEventListener('click', async (e) => { + e.preventDefault(); + const quarantineFolder = document.querySelector('#quarantine_folder').value; + const scanOnlyThis = document.querySelector('#scanOnlyThis').value; + const doNotScanThis = document.querySelector('#doNotScanThis').value; + const notifyMails = document.querySelector('#notify_mails').value; + + // TODO: Use other controller + const response = await postData(OC.generateUrl('apps/gdatavaas/setconfig'), { + quarantineFolder, + scanOnlyThis, + doNotScanThis, + notifyMails, + }); + const msgElement = document.querySelector('#auth_save_msg'); + + if (response.status === "success") { + msgElement.textContent = 'Data saved successfully.'; + } else { + if (response.message) { + msgElement.textContent = response.message; + } else { + msgElement.textContent = 'An error occurred when saving the data.'; + } + } + }); + + authSubmitAdvanced.addEventListener('click', async (e) => { + e.preventDefault(); + const tokenEndpoint = document.querySelector('#token_endpoint').value; + const vaasUrl = document.querySelector('#vaas_url').value; + + const response = await postData(OC.generateUrl('apps/gdatavaas/setadvancedconfig'), { + tokenEndpoint, + vaasUrl + }); + const msgElement = document.querySelector('#auth_save_msg_advanced'); + + if (response.status === "success") { + msgElement.textContent = 'Data saved successfully.'; + } else { + msgElement.textContent = 'An error occurred when saving the data.'; + } + }); + + resetAllTags.addEventListener('click', async (e) => { + e.preventDefault(); + const response = await postData(OC.generateUrl('apps/gdatavaas/resetalltags'), {}); + const msgElement = document.querySelector('#auth_save_msg_advanced'); + + if (response.status === "success") { + msgElement.textContent = 'All tags have been reset successfully.'; + } else { + msgElement.textContent = 'An error occurred when resetting the tags.'; + } + }); + + autoScanFiles.addEventListener('click', async () => { + await toggleAutoScan(autoScanFiles.checked); + }); + + prefixMalicious.addEventListener('click', async () => { + await postData( + OC.generateUrl('apps/gdatavaas/setPrefixMalicious'), + {prefixMalicious: prefixMalicious.checked} + ); + }); + + disableUnscannedTag.addEventListener('click', async () => { + await postData( + OC.generateUrl('apps/gdatavaas/setDisableUnscannedTag'), + {disableUnscannedTag: disableUnscannedTag.checked} + ); + }); + + sendMailOnVirusUpload.addEventListener('click', async () => { + await postData( + OC.generateUrl('apps/gdatavaas/setSendMailOnVirusUpload'), + {sendMailOnVirusUpload: sendMailOnVirusUpload.checked} + ); + }); + + // Activate or deactivate automatic file scanning + const toggleAutoScan = async (enable) => { + autoScanFiles.checked = enable; + const response = await postData(OC.generateUrl('apps/gdatavaas/setAutoScan'), {autoScanFiles: enable}); + if (response.status !== "success") { + OC.Notification.showTemporary( + `An Error occurred when ${enable ? 'activating' : 'deactivating'} automatic file scanning.` + ); + } + } + + // Set values on page load + const autoScanResponse = await getData(OC.generateUrl('apps/gdatavaas/getAutoScan')); + if (autoScanResponse.status) { + autoScanFiles.checked = true; + } else { + autoScanFiles.checked = false; + } + prefixMalicious.checked = (await getData(OC.generateUrl('apps/gdatavaas/getPrefixMalicious'))).status; + disableUnscannedTag.checked = (await getData(OC.generateUrl('apps/gdatavaas/getDisableUnscannedTag'))).status; + sendMailOnVirusUpload.checked = (await getData(OC.generateUrl('apps/gdatavaas/getSendMailOnVirusUpload'))).status; + + let filesCounter = await getData(OC.generateUrl('apps/gdatavaas/getCounters')); + if (filesCounter['status'] === 'success') { + scanCounter.textContent = filesCounter["scanned"] + ' / ' + filesCounter["all"]; + } else { + scanCounter.textContent = ' N/A'; + console.log('Error getting files counter:', filesCounter['message']); + } +}); diff --git a/templates/admin.php b/templates/admin.php index 7ac9612..f160411 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -1,21 +1,17 @@ - - - - - - - - - G DATA Verdict-as-a-Service - -
-

G DATA Antivirus

+

Administrator Settings

You may use self registration and create a new username and password by yourself here for free.
@@ -46,22 +42,6 @@ - - - - - - - - - - - - - - - - @@ -85,9 +65,6 @@
" class="visible">
-
- Caution: The use of the "Scan only this" and "Do not scan this" settings should be approached with caution. Using these settings allows malicious users to upload and distribute malicious content via the Nextcloud instance. It is recommended that you carefully consider the implications of these settings and use them in a way that does not jeopardize the security of your system and data. -

t('Advanced Settings'));?>

t('If you are not sure about this, you can just leave it blank.'));?>
@@ -112,41 +89,3 @@
-
- - - - - - - - - - - - - - - - - -
- - -
- - -
" class="visible">
- - -
" class="visible">
- - -
-

- - -

-
- - diff --git a/templates/operator.php b/templates/operator.php new file mode 100644 index 0000000..f16edb3 --- /dev/null +++ b/templates/operator.php @@ -0,0 +1,75 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +style('gdatavaas', 'style'); +\OCP\Util::addScript('gdatavaas', 'gdatavaas-operator-settings'); + +?> + +
+
+

Operator Settings

+ + + + + + + + + + + + + + + + + +
" class="visible">
+ + +
+ Caution: The use of the "Scan only this" and "Do not scan this" settings should be approached with caution. Using these settings allows malicious users to upload and distribute malicious content via the Nextcloud instance. It is recommended that you carefully consider the implications of these settings and use them in a way that does not jeopardize the security of your system and data. +
+
+
+
+ + + + + + + + + + + + + + + + + +
+ + +
+ + +
" class="visible">
+ + +
" class="visible">
+ + +
+

+ + +

+
From 1b512569a40afcd9b1497066fb0a644746884817 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Tue, 18 Nov 2025 15:20:39 +0000 Subject: [PATCH 04/38] routes + Controller --- appinfo/routes.php | 18 ++++++---- lib/Controller/SettingsController.php | 52 ++++++++++++++++++--------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 17b8d0d..2017adc 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -7,22 +7,26 @@ return [ 'resources' => [], 'routes' => [ + // user ['name' => 'scan#scan', 'url' => '/scan', 'verb' => 'POST'], - ['name' => 'settings#setconfig', 'url' => '/setconfig', 'verb' => 'POST'], - ['name' => 'settings#setadvancedconfig', 'url' => '/setadvancedconfig', 'verb' => 'POST'], + ['name' => 'settings#getCounters', 'url' => '/getCounters', 'verb' => 'GET'], + // operator + ['name' => 'settings#setOperatorSettings', 'url' => '/operatorSettings', 'verb' => 'POST'], + ['name' => 'settings#getSendMailOnVirusUpload', 'url' => '/getSendMailOnVirusUpload', 'verb' => 'GET'], + ['name' => 'settings#setSendMailOnVirusUpload', 'url' => '/setSendMailOnVirusUpload', 'verb' => 'POST'], ['name' => 'settings#setAutoScan', 'url' => '/setAutoScan', 'verb' => 'POST'], ['name' => 'settings#getAutoScan', 'url' => '/getAutoScan', 'verb' => 'GET'], ['name' => 'settings#setPrefixMalicious', 'url' => '/setPrefixMalicious', 'verb' => 'POST'], ['name' => 'settings#getPrefixMalicious', 'url' => '/getPrefixMalicious', 'verb' => 'GET'], - ['name' => 'settings#getAuthMethod', 'url' => '/getAuthMethod', 'verb' => 'GET'], ['name' => 'settings#setDisableUnscannedTag', 'url' => '/setDisableUnscannedTag', 'verb' => 'POST'], ['name' => 'settings#getDisableUnscannedTag', 'url' => '/getDisableUnscannedTag', 'verb' => 'GET'], + // admin + ['name' => 'settings#setAdminSettings', 'url' => '/adminSettings', 'verb' => 'POST'], + ['name' => 'settings#setadvancedconfig', 'url' => '/setadvancedconfig', 'verb' => 'POST'], + ['name' => 'settings#getAuthMethod', 'url' => '/getAuthMethod', 'verb' => 'GET'], ['name' => 'settings#resetAllTags', 'url' => '/resetalltags', 'verb' => 'POST'], - ['name' => 'settings#getCounters', 'url' => '/getCounters', 'verb' => 'GET'], - ['name' => 'settings#getSendMailOnVirusUpload', 'url' => '/getSendMailOnVirusUpload', 'verb' => 'GET'], - ['name' => 'settings#setSendMailOnVirusUpload', 'url' => '/setSendMailOnVirusUpload', 'verb' => 'POST'], ['name' => 'settings#testsettings', 'url' => '/testsettings', 'verb' => 'POST'], ['name' => 'settings#getCache', 'url' => '/getCache', 'verb' => 'GET'], - ['name' => 'settings#getHashlookup', 'url' => '/getHashlookup', 'verb' => 'GET'] + ['name' => 'settings#getHashlookup', 'url' => '/getHashlookup', 'verb' => 'GET'], ] ]; diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 8068117..7acd27a 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -8,8 +8,10 @@ use OCA\GDataVaas\Service\TagService; use OCA\GDataVaas\Service\VerdictService; +use OCA\GDataVaas\Settings\VaasOperator; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\DB\Exception; use OCP\IAppConfig; use OCP\IRequest; @@ -40,29 +42,18 @@ public function __construct( $this->verdictService = $verdictService; } - public function setconfig( + #[AuthorizedAdminSetting(settings: VaasOperator::class)] + public function setAdminSettings( $username, $password, $clientId, $clientSecret, $authMethod, - $quarantineFolder, - $scanOnlyThis, - $doNotScanThis, - $notifyMails, $maxScanSize, $timeout, bool $cache, bool $hashlookup, ): JSONResponse { - if (!empty($notifyMails)) { - $mails = explode(',', preg_replace('/\s+/', '', $notifyMails)); - foreach ($mails as $mail) { - if ($this->mailer->validateMailAddress($mail) === false) { - return new JSONResponse(['status' => 'error', 'message' => 'Invalid email address: ' . $mail]); - } - } - } if ((int)$maxScanSize < 1) { return new JSONResponse(['status' => 'error', 'message' => 'Invalid max scan size: ' . $maxScanSize]); } @@ -74,10 +65,6 @@ public function setconfig( $this->config->setValueString($this->appName, 'clientId', $clientId); $this->config->setValueString($this->appName, 'clientSecret', $clientSecret); $this->config->setValueString($this->appName, 'authMethod', $authMethod); - $this->config->setValueString($this->appName, 'quarantineFolder', $quarantineFolder); - $this->config->setValueString($this->appName, 'scanOnlyThis', $scanOnlyThis); - $this->config->setValueString($this->appName, 'doNotScanThis', $doNotScanThis); - $this->config->setValueString($this->appName, 'notifyMails', $notifyMails); $this->config->setValueInt($this->appName, 'maxScanSizeInMB', (int)$maxScanSize); $this->config->setValueInt($this->appName, 'timeout', (int)$timeout); $this->config->setValueBool($this->appName, 'cache', $cache); @@ -91,20 +78,46 @@ public function setadvancedconfig($tokenEndpoint, $vaasUrl): JSONResponse { return new JSONResponse(['status' => 'success']); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] + public function setOperatorSettings( + $quarantineFolder, + $scanOnlyThis, + $doNotScanThis, + $notifyMails, + ): JSONResponse { + if (!empty($notifyMails)) { + $mails = explode(',', preg_replace('/\s+/', '', $notifyMails)); + foreach ($mails as $mail) { + if ($this->mailer->validateMailAddress($mail) === false) { + return new JSONResponse(['status' => 'error', 'message' => 'Invalid email address: ' . $mail]); + } + } + } + $this->config->setValueString($this->appName, 'quarantineFolder', $quarantineFolder); + $this->config->setValueString($this->appName, 'scanOnlyThis', $scanOnlyThis); + $this->config->setValueString($this->appName, 'doNotScanThis', $doNotScanThis); + $this->config->setValueString($this->appName, 'notifyMails', $notifyMails); + return new JSONResponse(['status' => 'success']); + } + + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function setAutoScan(bool $autoScanFiles): JSONResponse { $this->config->setValueBool($this->appName, 'autoScanFiles', $autoScanFiles); return new JSONResponse(['status' => 'success']); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function getAutoScan(): JSONResponse { return new JSONResponse(['status' => $this->config->getValueBool($this->appName, 'autoScanFiles')]); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function setPrefixMalicious(bool $prefixMalicious): JSONResponse { $this->config->setValueBool($this->appName, 'prefixMalicious', $prefixMalicious); return new JSONResponse(['status' => 'success']); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function getPrefixMalicious(): JSONResponse { return new JSONResponse(['status' => $this->config->getValueBool($this->appName, 'prefixMalicious')]); } @@ -113,11 +126,13 @@ public function getAuthMethod(): JSONResponse { return new JSONResponse(['status' => $this->config->getValueString($this->appName, 'authMethod')]); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function setDisableUnscannedTag(bool $disableUnscannedTag): JSONResponse { $this->config->setValueBool($this->appName, 'disableUnscannedTag', $disableUnscannedTag); return new JSONResponse(['status' => 'success']); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function getDisableUnscannedTag(): JSONResponse { return new JSONResponse(['status' => $this->config->getValueBool($this->appName, 'disableUnscannedTag')]); } @@ -127,6 +142,7 @@ public function resetAllTags(): JSONResponse { return new JSONResponse(['status' => 'success']); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function getCounters(): JSONResponse { try { $filesCount = $this->tagService->getScannedFilesCount(); @@ -143,12 +159,14 @@ public function getCounters(): JSONResponse { ]); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function getSendMailOnVirusUpload(): JSONResponse { return new JSONResponse( ['status' => $this->config->getValueBool($this->appName, 'sendMailOnVirusUpload')] ); } + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function setSendMailOnVirusUpload(bool $sendMailOnVirusUpload): JSONResponse { $this->config->setValueBool($this->appName, 'sendMailOnVirusUpload', $sendMailOnVirusUpload); return new JSONResponse(['status' => 'success']); From a6e643f4ba1a3959804e19146cff0eb9af24610d Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Tue, 18 Nov 2025 15:36:37 +0000 Subject: [PATCH 05/38] Fix controller? --- src/admin-settings.js | 2 +- src/operator-settings.js | 43 +++------------------------------------- templates/operator.php | 4 ++-- 3 files changed, 6 insertions(+), 43 deletions(-) diff --git a/src/admin-settings.js b/src/admin-settings.js index d33b6a7..a45f3b0 100644 --- a/src/admin-settings.js +++ b/src/admin-settings.js @@ -64,7 +64,7 @@ document.addEventListener('DOMContentLoaded', async () => { const cache = document.querySelector('#cache').checked; const hashlookup = document.querySelector('#hashlookup').checked; - const response = await postData(OC.generateUrl('apps/gdatavaas/setconfig'), { + const response = await postData(OC.generateUrl('apps/gdatavaas/adminSettings'), { username: username, password: password, clientId: clientId, diff --git a/src/operator-settings.js b/src/operator-settings.js index 2ce38e1..5694af5 100644 --- a/src/operator-settings.js +++ b/src/operator-settings.js @@ -27,21 +27,14 @@ document.addEventListener('DOMContentLoaded', async () => { return response.json(); } - const authSubmit = document.querySelector('#auth_submit'); - const authSubmitAdvanced = document.querySelector('#auth_submit_advanced'); - const resetAllTags = document.querySelector('#reset'); + const operatorSubmit = document.querySelector('#operator_submit'); const autoScanFiles = document.querySelector('#auto_scan_files'); const prefixMalicious = document.querySelector('#prefixMalicious'); - const authMethod = document.querySelector('#authMethod'); const disableUnscannedTag = document.querySelector('#disable_tag_unscanned'); const scanCounter = document.querySelector('#scan_counter'); const sendMailOnVirusUpload = document.querySelector('#send_mail_on_virus_upload'); - authMethod.addEventListener('change', (e) => { - hideUnneccessaryFields(e.target.value); - }); - - authSubmit.addEventListener('click', async (e) => { + operatorSubmit.addEventListener('click', async (e) => { e.preventDefault(); const quarantineFolder = document.querySelector('#quarantine_folder').value; const scanOnlyThis = document.querySelector('#scanOnlyThis').value; @@ -49,7 +42,7 @@ document.addEventListener('DOMContentLoaded', async () => { const notifyMails = document.querySelector('#notify_mails').value; // TODO: Use other controller - const response = await postData(OC.generateUrl('apps/gdatavaas/setconfig'), { + const response = await postData(OC.generateUrl('apps/gdatavaas/operatorSettings'), { quarantineFolder, scanOnlyThis, doNotScanThis, @@ -68,36 +61,6 @@ document.addEventListener('DOMContentLoaded', async () => { } }); - authSubmitAdvanced.addEventListener('click', async (e) => { - e.preventDefault(); - const tokenEndpoint = document.querySelector('#token_endpoint').value; - const vaasUrl = document.querySelector('#vaas_url').value; - - const response = await postData(OC.generateUrl('apps/gdatavaas/setadvancedconfig'), { - tokenEndpoint, - vaasUrl - }); - const msgElement = document.querySelector('#auth_save_msg_advanced'); - - if (response.status === "success") { - msgElement.textContent = 'Data saved successfully.'; - } else { - msgElement.textContent = 'An error occurred when saving the data.'; - } - }); - - resetAllTags.addEventListener('click', async (e) => { - e.preventDefault(); - const response = await postData(OC.generateUrl('apps/gdatavaas/resetalltags'), {}); - const msgElement = document.querySelector('#auth_save_msg_advanced'); - - if (response.status === "success") { - msgElement.textContent = 'All tags have been reset successfully.'; - } else { - msgElement.textContent = 'An error occurred when resetting the tags.'; - } - }); - autoScanFiles.addEventListener('click', async () => { await toggleAutoScan(autoScanFiles.checked); }); diff --git a/templates/operator.php b/templates/operator.php index f16edb3..54711da 100644 --- a/templates/operator.php +++ b/templates/operator.php @@ -30,8 +30,8 @@ - - + +
Caution: The use of the "Scan only this" and "Do not scan this" settings should be approached with caution. Using these settings allows malicious users to upload and distribute malicious content via the Nextcloud instance. It is recommended that you carefully consider the implications of these settings and use them in a way that does not jeopardize the security of your system and data.
From 87d9267cf1b1a32472a6eddf0680ffdaf9fde310 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Tue, 18 Nov 2025 15:57:37 +0000 Subject: [PATCH 06/38] Pack operator-settings, cleanup admin.js --- src/admin-settings.js | 60 ------------------------------------------- webpack.config.js | 1 + 2 files changed, 1 insertion(+), 60 deletions(-) diff --git a/src/admin-settings.js b/src/admin-settings.js index a45f3b0..a4d3e92 100644 --- a/src/admin-settings.js +++ b/src/admin-settings.js @@ -40,12 +40,7 @@ document.addEventListener('DOMContentLoaded', async () => { const authSubmitAdvanced = document.querySelector('#auth_submit_advanced'); const testSettings = document.querySelector('#test-settings'); const resetAllTags = document.querySelector('#reset'); - const autoScanFiles = document.querySelector('#auto_scan_files'); - const prefixMalicious = document.querySelector('#prefixMalicious'); const authMethod = document.querySelector('#authMethod'); - const disableUnscannedTag = document.querySelector('#disable_tag_unscanned'); - const scanCounter = document.querySelector('#scan_counter'); - const sendMailOnVirusUpload = document.querySelector('#send_mail_on_virus_upload'); hideUnneccessaryFields(authMethod.value); @@ -136,61 +131,6 @@ document.addEventListener('DOMContentLoaded', async () => { } }); - autoScanFiles.addEventListener('click', async () => { - await toggleAutoScan(autoScanFiles.checked); - }); - - prefixMalicious.addEventListener('click', async () => { - await postData( - OC.generateUrl('apps/gdatavaas/setPrefixMalicious'), - {prefixMalicious: prefixMalicious.checked} - ); - }); - - disableUnscannedTag.addEventListener('click', async () => { - await postData( - OC.generateUrl('apps/gdatavaas/setDisableUnscannedTag'), - {disableUnscannedTag: disableUnscannedTag.checked} - ); - }); - - sendMailOnVirusUpload.addEventListener('click', async () => { - await postData( - OC.generateUrl('apps/gdatavaas/setSendMailOnVirusUpload'), - {sendMailOnVirusUpload: sendMailOnVirusUpload.checked} - ); - }); - - // Activate or deactivate automatic file scanning - const toggleAutoScan = async (enable) => { - autoScanFiles.checked = enable; - const response = await postData(OC.generateUrl('apps/gdatavaas/setAutoScan'), {autoScanFiles: enable}); - if (response.status !== "success") { - OC.Notification.showTemporary( - `An Error occurred when ${enable ? 'activating' : 'deactivating'} automatic file scanning.` - ); - } - } - - // Set values on page load - const autoScanResponse = await getData(OC.generateUrl('apps/gdatavaas/getAutoScan')); - if (autoScanResponse.status) { - autoScanFiles.checked = true; - } else { - autoScanFiles.checked = false; - } - prefixMalicious.checked = (await getData(OC.generateUrl('apps/gdatavaas/getPrefixMalicious'))).status; - disableUnscannedTag.checked = (await getData(OC.generateUrl('apps/gdatavaas/getDisableUnscannedTag'))).status; - sendMailOnVirusUpload.checked = (await getData(OC.generateUrl('apps/gdatavaas/getSendMailOnVirusUpload'))).status; - - let filesCounter = await getData(OC.generateUrl('apps/gdatavaas/getCounters')); - if (filesCounter['status'] === 'success') { - scanCounter.textContent = filesCounter["scanned"] + ' / ' + filesCounter["all"]; - } else { - scanCounter.textContent = ' N/A'; - console.log('Error getting files counter:', filesCounter['message']); - } - document.querySelector('#cache').checked = (await getData(OC.generateUrl('apps/gdatavaas/getCache'))).status; document.querySelector('#hashlookup').checked = (await getData(OC.generateUrl('apps/gdatavaas/getHashlookup'))).status; }); diff --git a/webpack.config.js b/webpack.config.js index 5ad41b2..325ebdb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,5 +13,6 @@ webpackConfig.module.rules = Object.values(webpackRules) webpackConfig.entry['admin-settings'] = path.join(__dirname, 'src', 'admin-settings.js') webpackConfig.entry['files-action'] = path.join(__dirname, 'src', 'files-action.js') +webpackConfig.entry['operator-settings'] = path.join(__dirname, 'src', 'operator-settings.js') module.exports = webpackConfig From a131c7ed550f04edafbb4842f971e1a9b45b7180 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Wed, 19 Nov 2025 12:59:25 +0100 Subject: [PATCH 07/38] Prettier, remove TODO, fix message element id --- .prettierrc | 2 + src/operator-settings.js | 115 +++++++++++++++++++-------------------- 2 files changed, 57 insertions(+), 60 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1d2127c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,2 @@ +semi: false +singleQuote: true diff --git a/src/operator-settings.js b/src/operator-settings.js index 5694af5..8a5349e 100644 --- a/src/operator-settings.js +++ b/src/operator-settings.js @@ -3,17 +3,16 @@ // SPDX-License-Identifier: AGPL-3.0-or-later document.addEventListener('DOMContentLoaded', async () => { - async function postData(url = '', data = {}) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'requesttoken': oc_requesttoken + requesttoken: oc_requesttoken, }, - body: JSON.stringify(data) - }); - return response.json(); + body: JSON.stringify(data), + }) + return response.json() } async function getData(url = '') { @@ -21,98 +20,94 @@ document.addEventListener('DOMContentLoaded', async () => { method: 'GET', headers: { 'Content-Type': 'application/json', - 'requesttoken': oc_requesttoken - } - }); - return response.json(); + requesttoken: oc_requesttoken, + }, + }) + return response.json() } - const operatorSubmit = document.querySelector('#operator_submit'); - const autoScanFiles = document.querySelector('#auto_scan_files'); - const prefixMalicious = document.querySelector('#prefixMalicious'); - const disableUnscannedTag = document.querySelector('#disable_tag_unscanned'); - const scanCounter = document.querySelector('#scan_counter'); - const sendMailOnVirusUpload = document.querySelector('#send_mail_on_virus_upload'); + const operatorSubmit = document.querySelector('#operator_submit') + const autoScanFiles = document.querySelector('#auto_scan_files') + const prefixMalicious = document.querySelector('#prefixMalicious') + const disableUnscannedTag = document.querySelector('#disable_tag_unscanned') + const scanCounter = document.querySelector('#scan_counter') + const sendMailOnVirusUpload = document.querySelector('#send_mail_on_virus_upload') operatorSubmit.addEventListener('click', async (e) => { - e.preventDefault(); - const quarantineFolder = document.querySelector('#quarantine_folder').value; - const scanOnlyThis = document.querySelector('#scanOnlyThis').value; - const doNotScanThis = document.querySelector('#doNotScanThis').value; - const notifyMails = document.querySelector('#notify_mails').value; + e.preventDefault() + const quarantineFolder = document.querySelector('#quarantine_folder').value + const scanOnlyThis = document.querySelector('#scanOnlyThis').value + const doNotScanThis = document.querySelector('#doNotScanThis').value + const notifyMails = document.querySelector('#notify_mails').value - // TODO: Use other controller const response = await postData(OC.generateUrl('apps/gdatavaas/operatorSettings'), { quarantineFolder, scanOnlyThis, doNotScanThis, notifyMails, - }); - const msgElement = document.querySelector('#auth_save_msg'); + }) + const msgElement = document.querySelector('#operator_save_msg') - if (response.status === "success") { - msgElement.textContent = 'Data saved successfully.'; + if (response.status === 'success') { + msgElement.textContent = 'Data saved successfully.' } else { if (response.message) { - msgElement.textContent = response.message; + msgElement.textContent = response.message } else { - msgElement.textContent = 'An error occurred when saving the data.'; + msgElement.textContent = 'An error occurred when saving the data.' } } - }); + }) autoScanFiles.addEventListener('click', async () => { - await toggleAutoScan(autoScanFiles.checked); - }); + await toggleAutoScan(autoScanFiles.checked) + }) prefixMalicious.addEventListener('click', async () => { - await postData( - OC.generateUrl('apps/gdatavaas/setPrefixMalicious'), - {prefixMalicious: prefixMalicious.checked} - ); - }); + await postData(OC.generateUrl('apps/gdatavaas/setPrefixMalicious'), { + prefixMalicious: prefixMalicious.checked, + }) + }) disableUnscannedTag.addEventListener('click', async () => { - await postData( - OC.generateUrl('apps/gdatavaas/setDisableUnscannedTag'), - {disableUnscannedTag: disableUnscannedTag.checked} - ); - }); + await postData(OC.generateUrl('apps/gdatavaas/setDisableUnscannedTag'), { + disableUnscannedTag: disableUnscannedTag.checked, + }) + }) sendMailOnVirusUpload.addEventListener('click', async () => { - await postData( - OC.generateUrl('apps/gdatavaas/setSendMailOnVirusUpload'), - {sendMailOnVirusUpload: sendMailOnVirusUpload.checked} - ); - }); + await postData(OC.generateUrl('apps/gdatavaas/setSendMailOnVirusUpload'), { + sendMailOnVirusUpload: sendMailOnVirusUpload.checked, + }) + }) // Activate or deactivate automatic file scanning const toggleAutoScan = async (enable) => { - autoScanFiles.checked = enable; - const response = await postData(OC.generateUrl('apps/gdatavaas/setAutoScan'), {autoScanFiles: enable}); - if (response.status !== "success") { + autoScanFiles.checked = enable + const response = await postData(OC.generateUrl('apps/gdatavaas/setAutoScan'), { autoScanFiles: enable }) + if (response.status !== 'success') { OC.Notification.showTemporary( `An Error occurred when ${enable ? 'activating' : 'deactivating'} automatic file scanning.` - ); + ) } } // Set values on page load - const autoScanResponse = await getData(OC.generateUrl('apps/gdatavaas/getAutoScan')); + const autoScanResponse = await getData(OC.generateUrl('apps/gdatavaas/getAutoScan')) if (autoScanResponse.status) { - autoScanFiles.checked = true; + autoScanFiles.checked = true } else { - autoScanFiles.checked = false; + autoScanFiles.checked = false } - prefixMalicious.checked = (await getData(OC.generateUrl('apps/gdatavaas/getPrefixMalicious'))).status; - disableUnscannedTag.checked = (await getData(OC.generateUrl('apps/gdatavaas/getDisableUnscannedTag'))).status; - sendMailOnVirusUpload.checked = (await getData(OC.generateUrl('apps/gdatavaas/getSendMailOnVirusUpload'))).status; + prefixMalicious.checked = (await getData(OC.generateUrl('apps/gdatavaas/getPrefixMalicious'))).status + disableUnscannedTag.checked = (await getData(OC.generateUrl('apps/gdatavaas/getDisableUnscannedTag'))).status + sendMailOnVirusUpload.checked = (await getData(OC.generateUrl('apps/gdatavaas/getSendMailOnVirusUpload'))).status - let filesCounter = await getData(OC.generateUrl('apps/gdatavaas/getCounters')); + let filesCounter = await getData(OC.generateUrl('apps/gdatavaas/getCounters')) if (filesCounter['status'] === 'success') { - scanCounter.textContent = filesCounter["scanned"] + ' / ' + filesCounter["all"]; + scanCounter.textContent = filesCounter['scanned'] + ' / ' + filesCounter['all'] } else { - scanCounter.textContent = ' N/A'; - console.log('Error getting files counter:', filesCounter['message']); + scanCounter.textContent = ' N/A' + console.log('Error getting files counter:', filesCounter['message']) } -}); +}) From c47c5a92df57c3bf9748d6fb0c196252c9764e05 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Wed, 19 Nov 2025 14:03:32 +0100 Subject: [PATCH 08/38] Last second changes --- appinfo/routes.php | 2 +- lib/Controller/SettingsController.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 2017adc..589f07a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -9,7 +9,6 @@ 'routes' => [ // user ['name' => 'scan#scan', 'url' => '/scan', 'verb' => 'POST'], - ['name' => 'settings#getCounters', 'url' => '/getCounters', 'verb' => 'GET'], // operator ['name' => 'settings#setOperatorSettings', 'url' => '/operatorSettings', 'verb' => 'POST'], ['name' => 'settings#getSendMailOnVirusUpload', 'url' => '/getSendMailOnVirusUpload', 'verb' => 'GET'], @@ -20,6 +19,7 @@ ['name' => 'settings#getPrefixMalicious', 'url' => '/getPrefixMalicious', 'verb' => 'GET'], ['name' => 'settings#setDisableUnscannedTag', 'url' => '/setDisableUnscannedTag', 'verb' => 'POST'], ['name' => 'settings#getDisableUnscannedTag', 'url' => '/getDisableUnscannedTag', 'verb' => 'GET'], + ['name' => 'settings#getCounters', 'url' => '/getCounters', 'verb' => 'GET'], // admin ['name' => 'settings#setAdminSettings', 'url' => '/adminSettings', 'verb' => 'POST'], ['name' => 'settings#setadvancedconfig', 'url' => '/setadvancedconfig', 'verb' => 'POST'], diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 7acd27a..504b2d8 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -42,7 +42,6 @@ public function __construct( $this->verdictService = $verdictService; } - #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function setAdminSettings( $username, $password, From 7ff2a5533709bfc77f5feef28803072f63eacb13 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Thu, 20 Nov 2025 09:19:55 +0100 Subject: [PATCH 09/38] WIP --- tests/bats/functionality-parallel.bats | 116 ++++++++- tests/integration/ApiEndpointTest.php | 222 ++++++++++++++++ tests/integration/BaseIntegrationTest.php | 292 ++++++++++++++++++++++ tests/integration/FileUploadTest.php | 140 +++++++++++ tests/integration/README.md | 185 ++++++++++++++ tests/integration/TagManagementTest.php | 188 ++++++++++++++ tests/integration/bootstrap.php | 96 +++++++ tests/integration/phpunit.xml | 22 ++ 8 files changed, 1260 insertions(+), 1 deletion(-) create mode 100644 tests/integration/ApiEndpointTest.php create mode 100644 tests/integration/BaseIntegrationTest.php create mode 100644 tests/integration/FileUploadTest.php create mode 100644 tests/integration/README.md create mode 100644 tests/integration/TagManagementTest.php create mode 100644 tests/integration/bootstrap.php create mode 100644 tests/integration/phpunit.xml diff --git a/tests/bats/functionality-parallel.bats b/tests/bats/functionality-parallel.bats index 0f6edf3..c2b3352 100755 --- a/tests/bats/functionality-parallel.bats +++ b/tests/bats/functionality-parallel.bats @@ -103,6 +103,120 @@ setup_file() { } -@tearddown_file() { +# Controller endpoint tests using admin user + +# Helper function for testing GET endpoints +test_get_endpoint() { + local endpoint="$1" + local description="$2" + local expected_http_status="${3:-200}" # Default to 200 if not provided + local user_name="${4:-admin}" # Default to admin if not provided + local password="${5:-admin}" # Default to admin if not provided + + RESULT=$(curl --silent -w "%{http_code}" -u "$user_name:$password" -X GET \ + http://$HOSTNAME/apps/gdatavaas/$endpoint) + + echo "$description result: $RESULT" + [[ $RESULT -eq $expected_http_status ]] +} + +# Helper function for testing POST endpoints +test_post_endpoint() { + local endpoint="$1" + local data="$2" + local description="$3" + local expected_http_status="${4:-200}" # Default to 200 if not provided + local user_name="${5:-admin}" # Default to admin if not provided + local password="${6:-admin}" # Default to admin if not provided + + RESULT=$(curl --silent -w "%{http_code}" -u "$user_name:$password" -X POST \ + -H "Content-Type: application/json" \ + -d "$data" \ + http://$HOSTNAME/apps/gdatavaas/$endpoint) + + echo "$description result: $RESULT" + [[ $RESULT -eq $expected_http_status ]] +} + +@test "test scan endpoint (POST /scan)" { + # Create a test file first + echo $CLEAN_STRING > $FOLDER_PREFIX/test-scan.txt + docker cp $FOLDER_PREFIX/test-scan.txt nextcloud-container:/var/www/html/data/admin/files/test-scan.txt + $DOCKER_EXEC_WITH_USER nextcloud-container php occ files:scan admin + + # Test scan endpoint + test_post_endpoint "scan" '{"path":"test-scan.txt"}' "Scan endpoint" "200" + + $DOCKER_EXEC_WITH_USER nextcloud-container rm -f /var/www/html/data/admin/files/test-scan.txt +} + + + +# Parameterized tests for GET endpoints +@test "test GET endpoints" { + # Array of GET endpoints to test + declare -a get_endpoints=( + "getCounters:Get counters" + "getAuthMethod:Get auth method" + "getCache:Get cache" + "getHashlookup:Get hash lookup" + "getSendMailOnVirusUpload:Get send mail on virus upload" + "getAutoScan:Get auto scan" + "getPrefixMalicious:Get prefix malicious" + "getDisableUnscannedTag:Get disable unscanned tag" + ) + + for endpoint_info in "${get_endpoints[@]}"; do + IFS=':' read -r endpoint description <<< "$endpoint_info" + echo "Testing $endpoint..." + test_get_endpoint "$endpoint" "$description" "200" + done +} + +# Parameterized tests for POST endpoints with settings +@test "test POST settings endpoints" { + # Array of POST endpoints with their test data + declare -A post_endpoints=( + ["setAutoScan"]='{"autoScan":"true"}' + ["setPrefixMalicious"]='{"prefixMalicious":"[VIRUS] "}' + ["setSendMailOnVirusUpload"]='{"sendMailOnVirusUpload":"false"}' + ["setDisableUnscannedTag"]='{"disableUnscannedTag":"false"}' + ["setadvancedconfig"]='{"maxScanFileSize":"104857600","maxUploadFileSize":"104857600"}' + ) + + for endpoint in "${!post_endpoints[@]}"; do + echo "Testing $endpoint..." + test_post_endpoint "$endpoint" "${post_endpoints[$endpoint]}" "Set ${endpoint#set}" "200" + done +} + +@test "test operator settings endpoint" { + test_post_endpoint "operatorSettings" \ + '{"autoScan":"true","prefixMalicious":"[VIRUS]","sendMailOnVirusUpload":"false","disableUnscannedTag":"false"}' \ + "Set operator settings" "200" +} + +@test "test admin settings endpoint" { + test_post_endpoint "adminSettings" \ + '{"authMethod":"client-credentials","clientId":"test","clientSecret":"test","username":"","password":"","url":"https://gateway.production.vaas.gdatasecurity.de"}' \ + "Set admin settings" "200" +} + +@test "test reset all tags endpoint" { + test_post_endpoint "resetalltags" '{}' "Reset all tags" "200" +} + +@test "test settings validation endpoint" { + # Skip test if CLIENT_ID or CLIENT_SECRET are not set + if [[ -z "$CLIENT_ID" || -z "$CLIENT_SECRET" ]]; then + skip "CLIENT_ID and CLIENT_SECRET environment variables are required for this test" + fi + + test_post_endpoint "testsettings" \ + '{"authMethod":"client-credentials","clientId":"'$CLIENT_ID'","clientSecret":"'$CLIENT_SECRET'","username":"","password":"","url":"https://gateway.production.vaas.gdatasecurity.de"}' \ + "Test settings" "200" +} + +teardown_file() { rm -rf $FOLDER_PREFIX/ } diff --git a/tests/integration/ApiEndpointTest.php b/tests/integration/ApiEndpointTest.php new file mode 100644 index 0000000..8b32cf7 --- /dev/null +++ b/tests/integration/ApiEndpointTest.php @@ -0,0 +1,222 @@ + +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Integration; + +class ApiEndpointTest extends BaseIntegrationTest +{ + /** + * Test scan endpoint (POST /scan) + */ + public function testScanEndpoint(): void + { + // Create a test file first + $filename = 'test-scan.txt'; + $containerPath = "/var/www/html/data/admin/files/{$filename}"; + $testFile = $this->folderPrefix . '/test-scan.txt'; + + // Create local test file + file_put_contents($testFile, $this->cleanString); + + try { + // Copy to container + exec("docker cp {$testFile} nextcloud-container:{$containerPath}", $output, $returnCode); + if ($returnCode !== 0) { + $this->markTestSkipped('Could not copy test file to container'); + } + + // Set proper ownership + $this->executeDockerCommand("chown www-data:www-data {$containerPath}"); + + // Scan admin files + $this->executeDockerCommand("php occ files:scan admin"); + + // Test scan endpoint + $this->testPostEndpoint('scan', ['path' => $filename], 'Scan endpoint', 200); + + } finally { + // Clean up + $this->executeDockerCommand("rm -f {$containerPath}"); + if (file_exists($testFile)) { + unlink($testFile); + } + } + } + + /** + * Test all GET endpoints + * @dataProvider getEndpointsProvider + */ + public function testGetEndpoints(string $endpoint, string $description): void + { + echo "Testing {$endpoint}...\n"; + $this->testGetEndpoint($endpoint, $description, 200); + } + + /** + * Data provider for GET endpoints + */ + public static function getEndpointsProvider(): array + { + return [ + ['getCounters', 'Get counters'], + ['getAuthMethod', 'Get auth method'], + ['getCache', 'Get cache'], + ['getHashlookup', 'Get hash lookup'], + ['getSendMailOnVirusUpload', 'Get send mail on virus upload'], + ['getAutoScan', 'Get auto scan'], + ['getPrefixMalicious', 'Get prefix malicious'], + ['getDisableUnscannedTag', 'Get disable unscanned tag'], + ]; + } + + /** + * Test all POST settings endpoints + * @dataProvider postEndpointsProvider + */ + public function testPostSettingsEndpoints(string $endpoint, array $data, string $description): void + { + echo "Testing {$endpoint}...\n"; + $this->testPostEndpoint($endpoint, $data, $description, 200); + } + + /** + * Data provider for POST settings endpoints + */ + public static function postEndpointsProvider(): array + { + return [ + ['setAutoScan', ['autoScan' => 'true'], 'Set auto scan'], + ['setPrefixMalicious', ['prefixMalicious' => '[VIRUS] '], 'Set prefix malicious'], + ['setSendMailOnVirusUpload', ['sendMailOnVirusUpload' => 'false'], 'Set send mail on virus upload'], + ['setDisableUnscannedTag', ['disableUnscannedTag' => 'false'], 'Set disable unscanned tag'], + ['setadvancedconfig', ['maxScanFileSize' => '104857600', 'maxUploadFileSize' => '104857600'], 'Set advanced config'], + ]; + } + + /** + * Test operator settings endpoint + */ + public function testOperatorSettingsEndpoint(): void + { + $data = [ + 'autoScan' => 'true', + 'prefixMalicious' => '[VIRUS]', + 'sendMailOnVirusUpload' => 'false', + 'disableUnscannedTag' => 'false' + ]; + + $this->testPostEndpoint('operatorSettings', $data, 'Set operator settings', 200); + } + + /** + * Test admin settings endpoint + */ + public function testAdminSettingsEndpoint(): void + { + $data = [ + 'authMethod' => 'client-credentials', + 'clientId' => 'test', + 'clientSecret' => 'test', + 'username' => '', + 'password' => '', + 'url' => 'https://gateway.production.vaas.gdatasecurity.de' + ]; + + $this->testPostEndpoint('adminSettings', $data, 'Set admin settings', 200); + } + + /** + * Test reset all tags endpoint + */ + public function testResetAllTagsEndpoint(): void + { + $this->testPostEndpoint('resetalltags', [], 'Reset all tags', 200); + } + + /** + * Test settings validation endpoint + * This test requires valid CLIENT_ID and CLIENT_SECRET environment variables + */ + public function testSettingsValidationEndpoint(): void + { + if (empty($this->clientId) || empty($this->clientSecret)) { + $this->markTestSkipped('CLIENT_ID and CLIENT_SECRET environment variables are required for this test'); + } + + $data = [ + 'authMethod' => 'client-credentials', + 'clientId' => $this->clientId, + 'clientSecret' => $this->clientSecret, + 'username' => '', + 'password' => '', + 'url' => 'https://gateway.production.vaas.gdatasecurity.de' + ]; + + $this->testPostEndpoint('testsettings', $data, 'Test settings', 200); + } + + /** + * Test unauthorized access to admin endpoints + * Tests that non-admin users get appropriate error responses + */ + public function testUnauthorizedAccess(): void + { + // Test with regular user credentials (should fail for admin endpoints) + $this->testGetEndpoint('getCounters', 'Unauthorized get counters', 401, $this->testUser, $this->testUserPassword); + } + + /** + * Test invalid endpoint + * Tests that non-existent endpoints return 404 + */ + public function testInvalidEndpoint(): void + { + $url = "http://{$this->hostname}/apps/gdatavaas/nonexistent"; + + $result = $this->makeHttpRequest('GET', $url, [ + 'auth' => ['username' => 'admin', 'password' => 'admin'] + ]); + + echo "Invalid endpoint result: {$result['http_code']}\n"; + $this->assertEquals(404, $result['http_code'], 'Expected 404 for non-existent endpoint'); + } + + /** + * Test malformed JSON in POST requests + */ + public function testMalformedJsonPost(): void + { + $url = "http://{$this->hostname}/apps/gdatavaas/setAutoScan"; + + $result = $this->makeHttpRequest('POST', $url, [ + 'auth' => ['username' => 'admin', 'password' => 'admin'], + 'body' => '{"invalid": json}', // Malformed JSON + 'headers' => ['Content-Type: application/json'] + ]); + + echo "Malformed JSON result: {$result['http_code']}\n"; + // Depending on implementation, this might return 400 or 500 + $this->assertContains($result['http_code'], [400, 500], 'Expected error status for malformed JSON'); + } + + /** + * Test endpoint with missing required parameters + */ + public function testMissingParameters(): void + { + // Test scan endpoint without required 'path' parameter + $result = $this->makeHttpRequest('POST', "http://{$this->hostname}/apps/gdatavaas/scan", [ + 'auth' => ['username' => 'admin', 'password' => 'admin'], + 'body' => json_encode([]), // Empty data + 'headers' => ['Content-Type: application/json'] + ]); + + echo "Missing parameters result: {$result['http_code']}\n"; + // Should return an error status + $this->assertGreaterThanOrEqual(400, $result['http_code'], 'Expected error status for missing parameters'); + } +} \ No newline at end of file diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php new file mode 100644 index 0000000..bb8a452 --- /dev/null +++ b/tests/integration/BaseIntegrationTest.php @@ -0,0 +1,292 @@ + +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Integration; + +use PHPUnit\Framework\TestCase; +use RuntimeException; + +abstract class BaseIntegrationTest extends TestCase +{ + protected string $hostname; + protected string $folderPrefix; + protected string $testUser; + protected string $testUserPassword; + protected string $eicarString; + protected string $cleanString; + protected string $dockerExecWithUser; + protected string $clientSecret; + protected string $clientId; + + protected static bool $setupCompleted = false; + + protected function setUp(): void + { + parent::setUp(); + + // Load environment variables + $this->hostname = $_ENV['HOSTNAME'] ?? '127.0.0.1:8080'; + $this->folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; + $this->testUser = $_ENV['TESTUSER'] ?? 'testuser'; + $this->testUserPassword = $_ENV['TESTUSER_PASSWORD'] ?? 'myfancysecurepassword234'; + $this->eicarString = $_ENV['EICAR_STRING'] ?? 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; + $this->cleanString = $_ENV['CLEAN_STRING'] ?? 'nothingwronghere'; + $this->dockerExecWithUser = $_ENV['DOCKER_EXEC_WITH_USER'] ?? 'docker exec --env XDEBUG_MODE=off --user www-data'; + $this->clientSecret = $_ENV['CLIENT_SECRET'] ?? ''; + $this->clientId = $_ENV['CLIENT_ID'] ?? ''; + + if (!self::$setupCompleted) { + $this->setupEnvironment(); + self::$setupCompleted = true; + } + } + + protected function setupEnvironment(): void + { + // Create temporary folder + if (!is_dir($this->folderPrefix)) { + mkdir($this->folderPrefix, 0755, true); + } + + // Download PUP file + $pupFile = $this->folderPrefix . '/pup.exe'; + if (!file_exists($pupFile)) { + $pupContent = file_get_contents('http://amtso.eicar.org/PotentiallyUnwanted.exe'); + if ($pupContent === false) { + $this->markTestSkipped('Could not download PUP file from AMTSO'); + } + file_put_contents($pupFile, $pupContent); + } + + // Create test user + $this->executeDockerCommand("php occ user:add {$this->testUser} --password-from-env", [ + 'OC_PASS' => $this->testUserPassword + ]); + + // Create user directory + $this->executeDockerCommand("mkdir -p /var/www/html/data/{$this->testUser}/files"); + + // Set client secret if available + if (!empty($this->clientSecret)) { + $this->executeDockerCommand("php occ config:app:set gdatavaas clientSecret --value=\"{$this->clientSecret}\""); + } + + // Cache busting and enable app + $this->executeDockerCommand("php occ files:scan --all"); + sleep(2); + $this->executeDockerCommand("php occ app:enable gdatavaas"); + } + + protected function executeDockerCommand(string $command, array $env = []): array + { + $envString = ''; + foreach ($env as $key => $value) { + $envString .= " --env {$key}=\"{$value}\""; + } + + $fullCommand = "{$this->dockerExecWithUser}{$envString} nextcloud-container {$command}"; + + exec($fullCommand . ' 2>&1', $output, $returnCode); + + return [ + 'output' => $output, + 'return_code' => $returnCode, + 'command' => $fullCommand + ]; + } + + protected function makeHttpRequest(string $method, string $url, array $options = []): array + { + $ch = curl_init(); + + $defaultOptions = [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HEADER => false, + CURLOPT_NOBODY => false, + CURLOPT_VERBOSE => false, + ]; + + // Add authentication if provided + if (isset($options['auth'])) { + $defaultOptions[CURLOPT_USERPWD] = $options['auth']['username'] . ':' . $options['auth']['password']; + unset($options['auth']); + } + + // Add request body if provided + if (isset($options['body'])) { + $defaultOptions[CURLOPT_POSTFIELDS] = $options['body']; + unset($options['body']); + } + + // Add headers if provided + if (isset($options['headers'])) { + $defaultOptions[CURLOPT_HTTPHEADER] = $options['headers']; + unset($options['headers']); + } + + // Merge with provided options + foreach ($options as $key => $value) { + $defaultOptions[$key] = $value; + } + + curl_setopt_array($ch, $defaultOptions); + + $body = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + + curl_close($ch); + + if ($body === false) { + throw new RuntimeException("cURL error: " . $error); + } + + return [ + 'body' => $body, + 'http_code' => $httpCode, + 'error' => $error + ]; + } + + protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array + { + $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; + + return $this->makeHttpRequest('PUT', $url, [ + 'auth' => ['username' => $username, 'password' => $password], + 'body' => $content, + 'headers' => ['Content-Type: text/plain'] + ]); + } + + protected function deleteFileViaWebDAV(string $username, string $password, string $filename): array + { + $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; + + return $this->makeHttpRequest('DELETE', $url, [ + 'auth' => ['username' => $username, 'password' => $password] + ]); + } + + protected function uploadFileFromDisk(string $username, string $password, string $filename, string $localPath): array + { + if (!file_exists($localPath)) { + throw new RuntimeException("File not found: {$localPath}"); + } + + $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; + $fileHandle = fopen($localPath, 'r'); + + if (!$fileHandle) { + throw new RuntimeException("Could not open file: {$localPath}"); + } + + try { + return $this->makeHttpRequest('PUT', $url, [ + 'auth' => ['username' => $username, 'password' => $password], + CURLOPT_INFILE => $fileHandle, + CURLOPT_INFILESIZE => filesize($localPath), + ]); + } finally { + fclose($fileHandle); + } + } + + protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void + { + $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; + + $result = $this->makeHttpRequest('GET', $url, [ + 'auth' => ['username' => $username, 'password' => $password] + ]); + + echo "{$description} result: {$result['http_code']}\n"; + $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); + } + + protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void + { + $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; + + $result = $this->makeHttpRequest('POST', $url, [ + 'auth' => ['username' => $username, 'password' => $password], + 'body' => json_encode($data), + 'headers' => ['Content-Type: application/json'] + ]); + + echo "{$description} result: {$result['http_code']}\n"; + $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); + } + + protected function assertContainsVirusFound(array $response): void + { + $this->assertStringContainsString('Virus found', $response['body'], 'Expected "Virus found" in response body'); + } + + protected function assertHttpCodeInRange(int $httpCode, int $min = 200, int $max = 299): void + { + $this->assertGreaterThanOrEqual($min, $httpCode, "HTTP code {$httpCode} is below expected range {$min}-{$max}"); + $this->assertLessThan($max + 1, $httpCode, "HTTP code {$httpCode} is above expected range {$min}-{$max}"); + } + + protected function getTagsForFile(string $filePath): array + { + $result = $this->executeDockerCommand("php occ gdatavaas:get-tags-for-file {$filePath}"); + return $result['output']; + } + + protected function assertHasTag(string $filePath, string $expectedTag): void + { + $tags = $this->getTagsForFile($filePath); + $tagString = implode("\n", $tags); + $this->assertStringContainsString($expectedTag, $tagString, "Expected tag '{$expectedTag}' not found in file tags"); + } + + protected function assertTagCount(string $filePath, int $expectedCount): void + { + $tags = $this->getTagsForFile($filePath); + // Filter out empty lines + $nonEmptyTags = array_filter($tags, function($line) { + return trim($line) !== ''; + }); + $this->assertCount($expectedCount, $nonEmptyTags, "Expected {$expectedCount} tags, got " . count($nonEmptyTags)); + } + + protected static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + // Clean up temporary files + $folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; + if (is_dir($folderPrefix)) { + self::removeDirectory($folderPrefix); + } + } + + private static function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + + foreach ($files as $file) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (is_dir($path)) { + self::removeDirectory($path); + } else { + unlink($path); + } + } + + rmdir($dir); + } +} \ No newline at end of file diff --git a/tests/integration/FileUploadTest.php b/tests/integration/FileUploadTest.php new file mode 100644 index 0000000..5d87c8f --- /dev/null +++ b/tests/integration/FileUploadTest.php @@ -0,0 +1,140 @@ + +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Integration; + +class FileUploadTest extends BaseIntegrationTest +{ + /** + * Test admin EICAR upload - should be blocked with virus found message + */ + public function testAdminEicarUpload(): void + { + $filename = 'functionality-parallel.eicar.com.txt'; + + try { + $result = $this->uploadFileViaWebDAV('admin', 'admin', $filename, $this->eicarString); + + echo "EICAR upload result: {$result['http_code']}\n"; + echo "Response body: {$result['body']}\n"; + + $this->assertContainsVirusFound($result); + } finally { + // Clean up - attempt to delete file if it exists + $this->deleteFileViaWebDAV('admin', 'admin', $filename); + } + } + + /** + * Test admin clean file upload - should succeed + */ + public function testAdminCleanUpload(): void + { + $filename = 'functionality-parallel.clean.txt'; + + try { + $result = $this->uploadFileViaWebDAV('admin', 'admin', $filename, $this->cleanString); + + echo "Clean file upload result: {$result['http_code']}\n"; + + $this->assertHttpCodeInRange($result['http_code'], 200, 299); + } finally { + // Clean up + $this->deleteFileViaWebDAV('admin', 'admin', $filename); + } + } + + /** + * Test admin PUP (Potentially Unwanted Program) upload - should succeed + */ + public function testAdminPupUpload(): void + { + $filename = 'functionality-parallel.pup.exe'; + $pupFilePath = $this->folderPrefix . '/pup.exe'; + + if (!file_exists($pupFilePath)) { + $this->markTestSkipped('PUP file not available'); + } + + try { + $result = $this->uploadFileFromDisk('admin', 'admin', $filename, $pupFilePath); + + echo "PUP upload result: {$result['http_code']}\n"; + + $this->assertHttpCodeInRange($result['http_code'], 200, 299); + } finally { + // Clean up + $this->deleteFileViaWebDAV('admin', 'admin', $filename); + } + } + + /** + * Test testuser EICAR upload - should be blocked with virus found message + */ + public function testTestUserEicarUpload(): void + { + $filename = 'functionality-parallel.eicar.com.txt'; + + try { + $result = $this->uploadFileViaWebDAV($this->testUser, $this->testUserPassword, $filename, $this->eicarString); + + echo "Testuser EICAR upload result: {$result['http_code']}\n"; + echo "Response body: {$result['body']}\n"; + + // Log client secret for debugging + $configResult = $this->executeDockerCommand("php occ config:app:get gdatavaas clientSecret"); + echo "Client secret configured: " . (empty($configResult['output']) ? 'No' : 'Yes') . "\n"; + + $this->assertContainsVirusFound($result); + } finally { + // Clean up + $this->deleteFileViaWebDAV($this->testUser, $this->testUserPassword, $filename); + } + } + + /** + * Test testuser clean file upload - should succeed + */ + public function testTestUserCleanUpload(): void + { + $filename = 'functionality-parallel.clean.txt'; + + try { + $result = $this->uploadFileViaWebDAV($this->testUser, $this->testUserPassword, $filename, $this->cleanString); + + echo "Testuser clean file upload result: {$result['http_code']}\n"; + + $this->assertHttpCodeInRange($result['http_code'], 200, 299); + } finally { + // Clean up + $this->deleteFileViaWebDAV($this->testUser, $this->testUserPassword, $filename); + } + } + + /** + * Test testuser PUP upload - should succeed + */ + public function testTestUserPupUpload(): void + { + $filename = 'functionality-parallel.pup.exe'; + $pupFilePath = $this->folderPrefix . '/pup.exe'; + + if (!file_exists($pupFilePath)) { + $this->markTestSkipped('PUP file not available'); + } + + try { + $result = $this->uploadFileFromDisk($this->testUser, $this->testUserPassword, $filename, $pupFilePath); + + echo "Testuser PUP upload result: {$result['http_code']}\n"; + + $this->assertHttpCodeInRange($result['http_code'], 200, 299); + } finally { + // Clean up + $this->deleteFileViaWebDAV($this->testUser, $this->testUserPassword, $filename); + } + } +} \ No newline at end of file diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 0000000..1e717e0 --- /dev/null +++ b/tests/integration/README.md @@ -0,0 +1,185 @@ +# Integration Tests + +This directory contains PHPUnit integration tests ported from the original BATS (Bash Automated Testing System) tests. These tests verify the functionality of the G DATA Antivirus app by testing real HTTP requests and Docker interactions. + +## Overview + +The integration tests are organized into the following test classes: + +- **BaseIntegrationTest.php**: Base class providing common functionality for HTTP requests, Docker interactions, and environment setup +- **FileUploadTest.php**: Tests for file uploads via WebDAV (EICAR, clean files, PUP files) +- **TagManagementTest.php**: Tests for file tagging functionality (won't scan, unscanned tags) +- **ApiEndpointTest.php**: Tests for REST API endpoints (GET/POST operations, settings validation) + +## Features Tested + +### File Upload Tests + +- EICAR virus file uploads (should be blocked) +- Clean file uploads (should succeed) +- PUP (Potentially Unwanted Program) file uploads (should succeed) +- Tests performed for both admin and test user accounts + +### Tag Management Tests + +- "Won't scan" tags for files exceeding size limits +- "Unscanned" tags for files that haven't been processed +- Tag verification and counting + +### API Endpoint Tests + +- GET endpoints for retrieving configuration and status +- POST endpoints for updating settings +- Authentication and authorization testing +- Error handling (invalid endpoints, malformed requests, missing parameters) + +## Prerequisites + +1. **Docker Environment**: Tests require a running Nextcloud container named `nextcloud-container` +2. **Environment Variables**: Configure the following environment variables (see `.env-test` file): + + - `HOSTNAME`: Nextcloud server hostname (default: `127.0.0.1:8080`) + - `TESTUSER`: Test user name (default: `testuser`) + - `TESTUSER_PASSWORD`: Test user password + - `CLIENT_SECRET`: G DATA VaaS client secret (optional, required for some tests) + - `CLIENT_ID`: G DATA VaaS client ID (optional, required for validation tests) + +3. **Dependencies**: Ensure all Composer dependencies are installed: + ```bash + composer install + ``` + +## Running the Tests + +### Run All Integration Tests + +```bash +cd tests/integration +../vendor/bin/phpunit +``` + +### Run Specific Test Class + +```bash +cd tests/integration +../vendor/bin/phpunit FileUploadTest.php +../vendor/bin/phpunit TagManagementTest.php +../vendor/bin/phpunit ApiEndpointTest.php +``` + +### Run Individual Test Method + +```bash +cd tests/integration +../vendor/bin/phpunit --filter testAdminEicarUpload FileUploadTest.php +``` + +### Verbose Output + +```bash +cd tests/integration +../vendor/bin/phpunit --verbose +``` + +## Test Environment Setup + +The tests automatically perform the following setup operations: + +1. Create temporary directory for test files +2. Download PUP test file from AMTSO +3. Create test user in Nextcloud +4. Configure G DATA VaaS client secret (if available) +5. Enable the G DATA VaaS app +6. Perform initial file scan + +## Environment Configuration + +The tests load configuration from multiple sources in this order: + +1. `tests/bats/.env-test` (main test configuration) +2. `.env-local` (local overrides) +3. `.env` (fallback) +4. Built-in defaults + +### Key Environment Variables + +```bash +# Nextcloud server configuration +HOSTNAME=127.0.0.1:8080 +MAIL_HOSTNAME=127.0.0.1:8081 + +# Test data configuration +FOLDER_PREFIX=./tmp/functionality-parallel +TESTUSER=testuser +TESTUSER_PASSWORD=myfancysecurepassword234 + +# Test files +EICAR_STRING='X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' +CLEAN_STRING='nothingwronghere' + +# Docker configuration +DOCKER_EXEC_WITH_USER='docker exec --env XDEBUG_MODE=off --user www-data' + +# G DATA VaaS configuration (optional) +CLIENT_SECRET='your-client-secret' +CLIENT_ID='your-client-id' +``` + +## Test Data and Cleanup + +- **Temporary Files**: Tests create temporary files in `./tmp/functionality-parallel/` +- **Test Files**: Downloaded PUP file and generated large files are created as needed +- **Automatic Cleanup**: Tests automatically clean up created files after execution +- **Container Files**: Files uploaded to the Docker container are removed after each test + +## Troubleshooting + +### Common Issues + +1. **Docker Container Not Found**: + + - Ensure the Nextcloud container is running: `docker ps | grep nextcloud` + - Check container name matches `nextcloud-container` + +2. **Permission Errors**: + + - Verify Docker daemon is accessible + - Check file permissions in temporary directory + +3. **Network Connection Issues**: + + - Verify Nextcloud is accessible at configured hostname + - Check firewall and port configuration + +4. **Test File Download Failures**: + - Check internet connection for PUP file download + - Verify AMTSO test file availability + +### Debug Output + +Tests provide verbose output including: + +- HTTP response codes and bodies +- Docker command execution results +- File creation and cleanup status +- Tag verification results + +## Comparison with BATS Tests + +These PHPUnit tests provide equivalent functionality to the original BATS tests with additional benefits: + +- **Better Error Handling**: More detailed error messages and debugging information +- **Object-Oriented Structure**: Reusable components and cleaner test organization +- **IDE Integration**: Better support for debugging and code navigation +- **Type Safety**: PHP type hints and better parameter validation +- **Extensibility**: Easier to add new tests and modify existing ones + +## Contributing + +When adding new integration tests: + +1. Extend the appropriate test class or create a new one inheriting from `BaseIntegrationTest` +2. Follow the existing naming conventions +3. Include proper cleanup in `finally` blocks +4. Add appropriate assertions and error messages +5. Document any new environment variables or prerequisites diff --git a/tests/integration/TagManagementTest.php b/tests/integration/TagManagementTest.php new file mode 100644 index 0000000..287a6c2 --- /dev/null +++ b/tests/integration/TagManagementTest.php @@ -0,0 +1,188 @@ + +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Integration; + +class TagManagementTest extends BaseIntegrationTest +{ + /** + * Test "Won't scan" tag for testuser with large file + * Files larger than the configured scan limit should get the "Won't scan" tag + */ + public function testWontScanTagForTestUser(): void + { + $largeFileName = "{$this->testUser}.too-large.dat"; + $localLargeFile = $this->folderPrefix . '/too-large.dat'; + $containerPath = "/var/www/html/data/{$this->testUser}/files/{$largeFileName}"; + + // Create large file (256MB + 1 byte = 268,435,457 bytes) + // This uses dd command equivalent in PHP + if (!file_exists($localLargeFile)) { + $this->createLargeFile($localLargeFile, 268435457); + } + + try { + // Copy large file to container + $copyResult = $this->executeDockerCommand("cp {$localLargeFile} nextcloud-container:{$containerPath}"); + if ($copyResult['return_code'] !== 0) { + // Alternative approach: use docker cp command directly + exec("docker cp {$localLargeFile} nextcloud-container:{$containerPath}", $output, $returnCode); + if ($returnCode !== 0) { + $this->markTestSkipped('Could not copy large file to container'); + } + } + + // Scan all files + $this->executeDockerCommand("php occ files:scan --all"); + + // Run GDATA VaaS scan + $this->executeDockerCommand("php occ gdatavaas:scan"); + + // Get tags for the file + $filePath = "{$this->testUser}/files/{$largeFileName}"; + + // Verify "Won't scan" tag is present + $this->assertHasTag($filePath, "Won't scan"); + + // Verify only one tag is present + $this->assertTagCount($filePath, 1); + + } finally { + // Clean up - remove file from container + $this->executeDockerCommand("rm -f {$containerPath}"); + + // Clean up local large file + if (file_exists($localLargeFile)) { + unlink($localLargeFile); + } + } + } + + /** + * Test unscanned job for admin user + * Files that haven't been scanned should get the "Unscanned" tag when running the tag-unscanned command + */ + public function testUnscannedJobForAdmin(): void + { + $filename = 'admin.unscanned.pup.exe'; + $pupFilePath = $this->folderPrefix . '/pup.exe'; + $containerPath = "/var/www/html/data/admin/files/{$filename}"; + + if (!file_exists($pupFilePath)) { + $this->markTestSkipped('PUP file not available'); + } + + try { + // Copy PUP file directly to container (bypassing normal upload process) + exec("docker cp {$pupFilePath} nextcloud-container:{$containerPath}", $output, $returnCode); + if ($returnCode !== 0) { + $this->markTestSkipped('Could not copy PUP file to container for admin'); + } + + // Set proper ownership + $this->executeDockerCommand("chown www-data:www-data {$containerPath}"); + + // Scan files for admin user only + $this->executeDockerCommand("php occ files:scan admin"); + + // Tag unscanned files + $this->executeDockerCommand("php occ gdatavaas:tag-unscanned"); + + // Verify "Unscanned" tag is present + $filePath = "admin/files/{$filename}"; + $this->assertHasTag($filePath, "Unscanned"); + + // Verify only one tag is present + $this->assertTagCount($filePath, 1); + + } finally { + // Clean up + $this->executeDockerCommand("rm -f {$containerPath}"); + } + } + + /** + * Test unscanned job for test user + */ + public function testUnscannedJobForTestUser(): void + { + $filename = "{$this->testUser}.unscanned.pup.exe"; + $pupFilePath = $this->folderPrefix . '/pup.exe'; + $containerPath = "/var/www/html/data/{$this->testUser}/files/{$filename}"; + + if (!file_exists($pupFilePath)) { + $this->markTestSkipped('PUP file not available'); + } + + try { + // Copy PUP file directly to container (bypassing normal upload process) + exec("docker cp {$pupFilePath} nextcloud-container:{$containerPath}", $output, $returnCode); + if ($returnCode !== 0) { + $this->markTestSkipped('Could not copy PUP file to container for testuser'); + } + + // Set proper ownership (this is done automatically in the BATS test by the docker exec user) + $this->executeDockerCommand("chown www-data:www-data {$containerPath}"); + + // Scan files for test user only + $this->executeDockerCommand("php occ files:scan {$this->testUser}"); + + // Tag unscanned files + $this->executeDockerCommand("php occ gdatavaas:tag-unscanned"); + + // Verify "Unscanned" tag is present + $filePath = "{$this->testUser}/files/{$filename}"; + $this->assertHasTag($filePath, "Unscanned"); + + // Verify only one tag is present + $this->assertTagCount($filePath, 1); + + } finally { + // Clean up + $this->executeDockerCommand("rm -f {$containerPath}"); + } + } + + /** + * Creates a large file with specified size + */ + private function createLargeFile(string $filePath, int $size): void + { + $chunkSize = 1024 * 1024; // 1MB chunks + $handle = fopen($filePath, 'wb'); + + if (!$handle) { + throw new \RuntimeException("Cannot create file: {$filePath}"); + } + + try { + $written = 0; + $zeroChunk = str_repeat("\0", $chunkSize); + + while ($written < $size) { + $remaining = $size - $written; + $writeSize = min($chunkSize, $remaining); + + if ($writeSize < $chunkSize) { + $chunk = str_repeat("\0", $writeSize); + } else { + $chunk = $zeroChunk; + } + + $bytesWritten = fwrite($handle, $chunk); + if ($bytesWritten === false) { + throw new \RuntimeException("Failed to write to file: {$filePath}"); + } + + $written += $bytesWritten; + } + } finally { + fclose($handle); + } + + echo "Created large file: {$filePath} ({$size} bytes)\n"; + } +} \ No newline at end of file diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php new file mode 100644 index 0000000..0f1c065 --- /dev/null +++ b/tests/integration/bootstrap.php @@ -0,0 +1,96 @@ + +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +require_once __DIR__ . '/../../vendor/autoload.php'; + +// Define constants for integration testing +define('INTEGRATION_TEST_ROOT', __DIR__); +define('PROJECT_ROOT', __DIR__ . '/../..'); + +// Load environment variables for integration testing +$envFile = __DIR__ . '/../bats/.env-test'; +if (file_exists($envFile)) { + $envContent = file_get_contents($envFile); + $envLines = explode("\n", $envContent); + + foreach ($envLines as $line) { + $line = trim($line); + if (empty($line) || strpos($line, '#') === 0 || strpos($line, 'export') !== 0) { + continue; + } + + // Remove 'export ' prefix and parse key=value + $line = substr($line, 7); // Remove 'export ' + if (strpos($line, '=') !== false) { + list($key, $value) = explode('=', $line, 2); + // Remove quotes if present + $value = trim($value, '"\''); + $_ENV[$key] = $value; + putenv("$key=$value"); + } + } +} + +// Load local environment files +$localEnvFiles = [PROJECT_ROOT . '/.env-local', PROJECT_ROOT . '/.env']; +foreach ($localEnvFiles as $envFile) { + if (file_exists($envFile)) { + $envContent = file_get_contents($envFile); + $envLines = explode("\n", $envContent); + + foreach ($envLines as $line) { + $line = trim($line); + if (empty($line) || strpos($line, '#') === 0) { + continue; + } + + if (strpos($line, '=') !== false) { + list($key, $value) = explode('=', $line, 2); + // Remove quotes if present + $value = trim($value, '"\''); + $_ENV[$key] = $value; + putenv("$key=$value"); + } + } + break; // Use first found env file + } +} + +// Set default values if not defined +if (!isset($_ENV['HOSTNAME'])) { + $_ENV['HOSTNAME'] = '127.0.0.1:8080'; + putenv('HOSTNAME=127.0.0.1:8080'); +} + +if (!isset($_ENV['FOLDER_PREFIX'])) { + $_ENV['FOLDER_PREFIX'] = './tmp/functionality-parallel'; + putenv('FOLDER_PREFIX=./tmp/functionality-parallel'); +} + +if (!isset($_ENV['TESTUSER'])) { + $_ENV['TESTUSER'] = 'testuser'; + putenv('TESTUSER=testuser'); +} + +if (!isset($_ENV['TESTUSER_PASSWORD'])) { + $_ENV['TESTUSER_PASSWORD'] = 'myfancysecurepassword234'; + putenv('TESTUSER_PASSWORD=myfancysecurepassword234'); +} + +if (!isset($_ENV['EICAR_STRING'])) { + $_ENV['EICAR_STRING'] = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; + putenv('EICAR_STRING=X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'); +} + +if (!isset($_ENV['CLEAN_STRING'])) { + $_ENV['CLEAN_STRING'] = 'nothingwronghere'; + putenv('CLEAN_STRING=nothingwronghere'); +} + +if (!isset($_ENV['DOCKER_EXEC_WITH_USER'])) { + $_ENV['DOCKER_EXEC_WITH_USER'] = 'docker exec --env XDEBUG_MODE=off --user www-data'; + putenv('DOCKER_EXEC_WITH_USER=docker exec --env XDEBUG_MODE=off --user www-data'); +} \ No newline at end of file diff --git a/tests/integration/phpunit.xml b/tests/integration/phpunit.xml new file mode 100644 index 0000000..7e96c8e --- /dev/null +++ b/tests/integration/phpunit.xml @@ -0,0 +1,22 @@ + + + + + + + . + + + + + \ No newline at end of file From 5bd095f467573f4b5de3f81914acb9a54dec31fa Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Fri, 21 Nov 2025 10:29:46 +0000 Subject: [PATCH 10/38] Make tests runnable --- tests/integration/ApiEndpointTest.php | 2 ++ tests/integration/BaseIntegrationTest.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/ApiEndpointTest.php b/tests/integration/ApiEndpointTest.php index 8b32cf7..23df1aa 100644 --- a/tests/integration/ApiEndpointTest.php +++ b/tests/integration/ApiEndpointTest.php @@ -6,6 +6,8 @@ namespace Integration; +require_once __DIR__ . '/BaseIntegrationTest.php'; + class ApiEndpointTest extends BaseIntegrationTest { /** diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index bb8a452..c32e203 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -259,7 +259,7 @@ protected function assertTagCount(string $filePath, int $expectedCount): void $this->assertCount($expectedCount, $nonEmptyTags, "Expected {$expectedCount} tags, got " . count($nonEmptyTags)); } - protected static function tearDownAfterClass(): void + public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); From e125b35c0715d60b5c67e513b5fe6560dac06fba Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Fri, 21 Nov 2025 10:59:10 +0000 Subject: [PATCH 11/38] SettingsControllerTest --- composer.json | 6 +- tests/integration/BaseIntegrationTest.php | 122 ++++----- tests/integration/SettingsControllerTest.php | 257 +++++++++++++++++++ 3 files changed, 322 insertions(+), 63 deletions(-) create mode 100644 tests/integration/SettingsControllerTest.php diff --git a/composer.json b/composer.json index 56f2e8c..3ff7e64 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,9 @@ "nextcloud/coding-standard": "v1.4.0", "colinodell/psr-testlogger": "1.3.0", "phpunit/phpunit": "10.5.52", - "symfony/console": "v6.4.24" + "symfony/console": "v6.4.24", + "amphp/http-client": "^5.3", + "amphp/amp": "^3.1" }, "autoload": { "psr-4": { @@ -45,4 +47,4 @@ "php": "8.1" } } -} \ No newline at end of file +} diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index c32e203..311535b 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -6,9 +6,15 @@ namespace Integration; +use Amp\Http\Client\HttpClient; +use Amp\Http\Client\HttpClientBuilder; +use Amp\Http\Client\Request; +use Amp\Http\Client\Response; use PHPUnit\Framework\TestCase; use RuntimeException; +use function Amp\async; + abstract class BaseIntegrationTest extends TestCase { protected string $hostname; @@ -100,59 +106,58 @@ protected function executeDockerCommand(string $command, array $env = []): array protected function makeHttpRequest(string $method, string $url, array $options = []): array { - $ch = curl_init(); - - $defaultOptions = [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_TIMEOUT => 30, - CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_HEADER => false, - CURLOPT_NOBODY => false, - CURLOPT_VERBOSE => false, - ]; - - // Add authentication if provided - if (isset($options['auth'])) { - $defaultOptions[CURLOPT_USERPWD] = $options['auth']['username'] . ':' . $options['auth']['password']; - unset($options['auth']); - } - - // Add request body if provided - if (isset($options['body'])) { - $defaultOptions[CURLOPT_POSTFIELDS] = $options['body']; - unset($options['body']); - } - - // Add headers if provided - if (isset($options['headers'])) { - $defaultOptions[CURLOPT_HTTPHEADER] = $options['headers']; - unset($options['headers']); - } + return async(function () use ($method, $url, $options) { + $client = HttpClientBuilder::buildDefault(); + $request = new Request($url, $method); + + // Add authentication if provided + if (isset($options['auth'])) { + $credentials = base64_encode($options['auth']['username'] . ':' . $options['auth']['password']); + $request->setHeader('Authorization', 'Basic ' . $credentials); + unset($options['auth']); + } - // Merge with provided options - foreach ($options as $key => $value) { - $defaultOptions[$key] = $value; - } + // Add request body if provided + if (isset($options['body'])) { + $request->setBody($options['body']); + unset($options['body']); + } - curl_setopt_array($ch, $defaultOptions); - - $body = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); - - curl_close($ch); + // Add headers if provided + if (isset($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && strpos($header, ':') !== false) { + [$name, $value] = explode(':', $header, 2); + $request->setHeader(trim($name), trim($value)); + } + } + unset($options['headers']); + } - if ($body === false) { - throw new RuntimeException("cURL error: " . $error); - } + // Handle file upload for PUT requests + if (isset($options['file_handle'])) { + $request->setBody($options['file_handle']); + unset($options['file_handle']); + } - return [ - 'body' => $body, - 'http_code' => $httpCode, - 'error' => $error - ]; + + + try { + $response = $client->request($request); + + return [ + 'body' => $response->getBody()->buffer(), + 'http_code' => $response->getStatus(), + 'error' => '' + ]; + } catch (\Throwable $e) { + return [ + 'body' => '', + 'http_code' => 0, + 'error' => $e->getMessage() + ]; + } + })->await(); } protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array @@ -182,21 +187,16 @@ protected function uploadFileFromDisk(string $username, string $password, string } $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; - $fileHandle = fopen($localPath, 'r'); + $fileContent = file_get_contents($localPath); - if (!$fileHandle) { - throw new RuntimeException("Could not open file: {$localPath}"); + if ($fileContent === false) { + throw new RuntimeException("Could not read file: {$localPath}"); } - try { - return $this->makeHttpRequest('PUT', $url, [ - 'auth' => ['username' => $username, 'password' => $password], - CURLOPT_INFILE => $fileHandle, - CURLOPT_INFILESIZE => filesize($localPath), - ]); - } finally { - fclose($fileHandle); - } + return $this->makeHttpRequest('PUT', $url, [ + 'auth' => ['username' => $username, 'password' => $password], + 'body' => $fileContent, + ]); } protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php new file mode 100644 index 0000000..3df66ed --- /dev/null +++ b/tests/integration/SettingsControllerTest.php @@ -0,0 +1,257 @@ + +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Integration; + +use PHPUnit\Framework\Attributes\DataProvider; + +require_once __DIR__ . '/BaseIntegrationTest.php'; + +class SettingsControllerTest extends BaseIntegrationTest +{ + public static function adminGetRouteProvider(): array + { + return [ + ['getCounters'], + ['getAuthMethod'], + ['getCache'], + ['getHashlookup'], + ['getSendMailOnVirusUpload'], + ['getAutoScan'], + ['getPrefixMalicious'], + ['getDisableUnscannedTag'], + // ['setAutoScan'], + // ['setPrefixMalicious'], + // ['setSendMailOnVirusUpload'], + // ['setDisableUnscannedTag'], + // ['setadvancedconfig'], + ['operatorSettings'], + ['adminSettings'], + // ['resetalltags'], + // ['testsettings'], + // ['scan'] + ]; + } + + #[DataProvider('adminGetRouteProvider')] + public function testAdminCanAccessAllRoutes(string $route): void + { + echo "Testing admin access to {$route}...\n"; + $this->testGetEndpoint($route, "Admin access to {$route}", 200); + } + + // /** + // * Test scan endpoint (POST /scan) + // */ + // public function testScanEndpoint(): void + // { + // // Create a test file first + // $filename = 'test-scan.txt'; + // $containerPath = "/var/www/html/data/admin/files/{$filename}"; + // $testFile = $this->folderPrefix . '/test-scan.txt'; + + // // Create local test file + // file_put_contents($testFile, $this->cleanString); + + // try { + // // Copy to container + // exec("docker cp {$testFile} nextcloud-container:{$containerPath}", $output, $returnCode); + // if ($returnCode !== 0) { + // $this->markTestSkipped('Could not copy test file to container'); + // } + + // // Set proper ownership + // $this->executeDockerCommand("chown www-data:www-data {$containerPath}"); + + // // Scan admin files + // $this->executeDockerCommand("php occ files:scan admin"); + + // // Test scan endpoint + // $this->testPostEndpoint('scan', ['path' => $filename], 'Scan endpoint', 200); + + // } finally { + // // Clean up + // $this->executeDockerCommand("rm -f {$containerPath}"); + // if (file_exists($testFile)) { + // unlink($testFile); + // } + // } + // } + + // /** + // * Test all GET endpoints + // * @dataProvider getEndpointsProvider + // */ + // public function testGetEndpoints(string $endpoint, string $description): void + // { + // echo "Testing {$endpoint}...\n"; + // $this->testGetEndpoint($endpoint, $description, 200); + // } + + // /** + // * Data provider for GET endpoints + // */ + // public static function getEndpointsProvider(): array + // { + // return [ + // ['getCounters', 'Get counters'], + // ['getAuthMethod', 'Get auth method'], + // ['getCache', 'Get cache'], + // ['getHashlookup', 'Get hash lookup'], + // ['getSendMailOnVirusUpload', 'Get send mail on virus upload'], + // ['getAutoScan', 'Get auto scan'], + // ['getPrefixMalicious', 'Get prefix malicious'], + // ['getDisableUnscannedTag', 'Get disable unscanned tag'], + // ]; + // } + + // /** + // * Test all POST settings endpoints + // * @dataProvider postEndpointsProvider + // */ + // public function testPostSettingsEndpoints(string $endpoint, array $data, string $description): void + // { + // echo "Testing {$endpoint}...\n"; + // $this->testPostEndpoint($endpoint, $data, $description, 200); + // } + + // /** + // * Data provider for POST settings endpoints + // */ + // public static function postEndpointsProvider(): array + // { + // return [ + // ['setAutoScan', ['autoScan' => 'true'], 'Set auto scan'], + // ['setPrefixMalicious', ['prefixMalicious' => '[VIRUS] '], 'Set prefix malicious'], + // ['setSendMailOnVirusUpload', ['sendMailOnVirusUpload' => 'false'], 'Set send mail on virus upload'], + // ['setDisableUnscannedTag', ['disableUnscannedTag' => 'false'], 'Set disable unscanned tag'], + // ['setadvancedconfig', ['maxScanFileSize' => '104857600', 'maxUploadFileSize' => '104857600'], 'Set advanced config'], + // ]; + // } + + // /** + // * Test operator settings endpoint + // */ + // public function testOperatorSettingsEndpoint(): void + // { + // $data = [ + // 'autoScan' => 'true', + // 'prefixMalicious' => '[VIRUS]', + // 'sendMailOnVirusUpload' => 'false', + // 'disableUnscannedTag' => 'false' + // ]; + + // $this->testPostEndpoint('operatorSettings', $data, 'Set operator settings', 200); + // } + + // /** + // * Test admin settings endpoint + // */ + // public function testAdminSettingsEndpoint(): void + // { + // $data = [ + // 'authMethod' => 'client-credentials', + // 'clientId' => 'test', + // 'clientSecret' => 'test', + // 'username' => '', + // 'password' => '', + // 'url' => 'https://gateway.production.vaas.gdatasecurity.de' + // ]; + + // $this->testPostEndpoint('adminSettings', $data, 'Set admin settings', 200); + // } + + // /** + // * Test reset all tags endpoint + // */ + // public function testResetAllTagsEndpoint(): void + // { + // $this->testPostEndpoint('resetalltags', [], 'Reset all tags', 200); + // } + + // /** + // * Test settings validation endpoint + // * This test requires valid CLIENT_ID and CLIENT_SECRET environment variables + // */ + // public function testSettingsValidationEndpoint(): void + // { + // if (empty($this->clientId) || empty($this->clientSecret)) { + // $this->markTestSkipped('CLIENT_ID and CLIENT_SECRET environment variables are required for this test'); + // } + + // $data = [ + // 'authMethod' => 'client-credentials', + // 'clientId' => $this->clientId, + // 'clientSecret' => $this->clientSecret, + // 'username' => '', + // 'password' => '', + // 'url' => 'https://gateway.production.vaas.gdatasecurity.de' + // ]; + + // $this->testPostEndpoint('testsettings', $data, 'Test settings', 200); + // } + + // /** + // * Test unauthorized access to admin endpoints + // * Tests that non-admin users get appropriate error responses + // */ + // public function testUnauthorizedAccess(): void + // { + // // Test with regular user credentials (should fail for admin endpoints) + // $this->testGetEndpoint('getCounters', 'Unauthorized get counters', 401, $this->testUser, $this->testUserPassword); + // } + + // /** + // * Test invalid endpoint + // * Tests that non-existent endpoints return 404 + // */ + // public function testInvalidEndpoint(): void + // { + // $url = "http://{$this->hostname}/apps/gdatavaas/nonexistent"; + + // $result = $this->makeHttpRequest('GET', $url, [ + // 'auth' => ['username' => 'admin', 'password' => 'admin'] + // ]); + + // echo "Invalid endpoint result: {$result['http_code']}\n"; + // $this->assertEquals(404, $result['http_code'], 'Expected 404 for non-existent endpoint'); + // } + + // /** + // * Test malformed JSON in POST requests + // */ + // public function testMalformedJsonPost(): void + // { + // $url = "http://{$this->hostname}/apps/gdatavaas/setAutoScan"; + + // $result = $this->makeHttpRequest('POST', $url, [ + // 'auth' => ['username' => 'admin', 'password' => 'admin'], + // 'body' => '{"invalid": json}', // Malformed JSON + // 'headers' => ['Content-Type: application/json'] + // ]); + + // echo "Malformed JSON result: {$result['http_code']}\n"; + // // Depending on implementation, this might return 400 or 500 + // $this->assertContains($result['http_code'], [400, 500], 'Expected error status for malformed JSON'); + // } + + // /** + // * Test endpoint with missing required parameters + // */ + // public function testMissingParameters(): void + // { + // // Test scan endpoint without required 'path' parameter + // $result = $this->makeHttpRequest('POST', "http://{$this->hostname}/apps/gdatavaas/scan", [ + // 'auth' => ['username' => 'admin', 'password' => 'admin'], + // 'body' => json_encode([]), // Empty data + // 'headers' => ['Content-Type: application/json'] + // ]); + + // echo "Missing parameters result: {$result['http_code']}\n"; + // // Should return an error status + // $this->assertGreaterThanOrEqual(400, $result['http_code'], 'Expected error status for missing parameters'); + // } +} From 5df48b05153887777e0f697ce768bc26ef8f12f5 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Fri, 21 Nov 2025 13:15:07 +0000 Subject: [PATCH 12/38] No async --- tests/integration/BaseIntegrationTest.php | 126 +++++++++---------- tests/integration/SettingsControllerTest.php | 18 ++- 2 files changed, 78 insertions(+), 66 deletions(-) diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index 311535b..4aa7103 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -32,7 +32,7 @@ abstract class BaseIntegrationTest extends TestCase protected function setUp(): void { parent::setUp(); - + // Load environment variables $this->hostname = $_ENV['HOSTNAME'] ?? '127.0.0.1:8080'; $this->folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; @@ -71,7 +71,7 @@ protected function setupEnvironment(): void $this->executeDockerCommand("php occ user:add {$this->testUser} --password-from-env", [ 'OC_PASS' => $this->testUserPassword ]); - + // Create user directory $this->executeDockerCommand("mkdir -p /var/www/html/data/{$this->testUser}/files"); @@ -92,11 +92,11 @@ protected function executeDockerCommand(string $command, array $env = []): array foreach ($env as $key => $value) { $envString .= " --env {$key}=\"{$value}\""; } - + $fullCommand = "{$this->dockerExecWithUser}{$envString} nextcloud-container {$command}"; - + exec($fullCommand . ' 2>&1', $output, $returnCode); - + return [ 'output' => $output, 'return_code' => $returnCode, @@ -106,64 +106,62 @@ protected function executeDockerCommand(string $command, array $env = []): array protected function makeHttpRequest(string $method, string $url, array $options = []): array { - return async(function () use ($method, $url, $options) { - $client = HttpClientBuilder::buildDefault(); - $request = new Request($url, $method); - - // Add authentication if provided - if (isset($options['auth'])) { - $credentials = base64_encode($options['auth']['username'] . ':' . $options['auth']['password']); - $request->setHeader('Authorization', 'Basic ' . $credentials); - unset($options['auth']); - } + $client = HttpClientBuilder::buildDefault(); + $request = new Request($url, $method); + + // Add authentication if provided + if (isset($options['auth'])) { + $credentials = base64_encode($options['auth']['username'] . ':' . $options['auth']['password']); + $request->setHeader('Authorization', 'Basic ' . $credentials); + unset($options['auth']); + } - // Add request body if provided - if (isset($options['body'])) { - $request->setBody($options['body']); - unset($options['body']); - } + // Add request body if provided + if (isset($options['body'])) { + $request->setBody($options['body']); + unset($options['body']); + } - // Add headers if provided - if (isset($options['headers'])) { - foreach ($options['headers'] as $header) { - if (is_string($header) && strpos($header, ':') !== false) { - [$name, $value] = explode(':', $header, 2); - $request->setHeader(trim($name), trim($value)); - } + // Add headers if provided + if (isset($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && strpos($header, ':') !== false) { + [$name, $value] = explode(':', $header, 2); + $request->setHeader(trim($name), trim($value)); } - unset($options['headers']); } + unset($options['headers']); + } - // Handle file upload for PUT requests - if (isset($options['file_handle'])) { - $request->setBody($options['file_handle']); - unset($options['file_handle']); - } + // Handle file upload for PUT requests + if (isset($options['file_handle'])) { + $request->setBody($options['file_handle']); + unset($options['file_handle']); + } - - - try { - $response = $client->request($request); - - return [ - 'body' => $response->getBody()->buffer(), - 'http_code' => $response->getStatus(), - 'error' => '' - ]; - } catch (\Throwable $e) { - return [ - 'body' => '', - 'http_code' => 0, - 'error' => $e->getMessage() - ]; - } - })->await(); + + + try { + $response = $client->request($request); + + return [ + 'body' => $response->getBody()->buffer(), + 'http_code' => $response->getStatus(), + 'error' => '' + ]; + } catch (\Throwable $e) { + return [ + 'body' => '', + 'http_code' => 0, + 'error' => $e->getMessage() + ]; + } } protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array { $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; - + return $this->makeHttpRequest('PUT', $url, [ 'auth' => ['username' => $username, 'password' => $password], 'body' => $content, @@ -174,7 +172,7 @@ protected function uploadFileViaWebDAV(string $username, string $password, strin protected function deleteFileViaWebDAV(string $username, string $password, string $filename): array { $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; - + return $this->makeHttpRequest('DELETE', $url, [ 'auth' => ['username' => $username, 'password' => $password] ]); @@ -188,7 +186,7 @@ protected function uploadFileFromDisk(string $username, string $password, string $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; $fileContent = file_get_contents($localPath); - + if ($fileContent === false) { throw new RuntimeException("Could not read file: {$localPath}"); } @@ -202,11 +200,11 @@ protected function uploadFileFromDisk(string $username, string $password, string protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; - + $result = $this->makeHttpRequest('GET', $url, [ 'auth' => ['username' => $username, 'password' => $password] ]); - + echo "{$description} result: {$result['http_code']}\n"; $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); } @@ -214,13 +212,13 @@ protected function testGetEndpoint(string $endpoint, string $description, int $e protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; - + $result = $this->makeHttpRequest('POST', $url, [ 'auth' => ['username' => $username, 'password' => $password], 'body' => json_encode($data), 'headers' => ['Content-Type: application/json'] ]); - + echo "{$description} result: {$result['http_code']}\n"; $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); } @@ -253,7 +251,7 @@ protected function assertTagCount(string $filePath, int $expectedCount): void { $tags = $this->getTagsForFile($filePath); // Filter out empty lines - $nonEmptyTags = array_filter($tags, function($line) { + $nonEmptyTags = array_filter($tags, function ($line) { return trim($line) !== ''; }); $this->assertCount($expectedCount, $nonEmptyTags, "Expected {$expectedCount} tags, got " . count($nonEmptyTags)); @@ -262,7 +260,7 @@ protected function assertTagCount(string $filePath, int $expectedCount): void public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); - + // Clean up temporary files $folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; if (is_dir($folderPrefix)) { @@ -275,9 +273,9 @@ private static function removeDirectory(string $dir): void if (!is_dir($dir)) { return; } - + $files = array_diff(scandir($dir), ['.', '..']); - + foreach ($files as $file) { $path = $dir . DIRECTORY_SEPARATOR . $file; if (is_dir($path)) { @@ -286,7 +284,7 @@ private static function removeDirectory(string $dir): void unlink($path); } } - + rmdir($dir); } -} \ No newline at end of file +} diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 3df66ed..953eec1 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -37,12 +37,26 @@ public static function adminGetRouteProvider(): array } #[DataProvider('adminGetRouteProvider')] - public function testAdminCanAccessAllRoutes(string $route): void + public function testAdminCanAccessAdminGetRoutes(string $route): void { - echo "Testing admin access to {$route}...\n"; $this->testGetEndpoint($route, "Admin access to {$route}", 200); } + // testAdminCanAccessAdminGetRoutes + // testAdminCanAccessAdminPostRoutes + // testAdminCanAccessOperatorGetRoutes + // testAdminCanAccessOperatorPostRoutes + + // testOperatorCannotAccessAdminGetRoutes + // testOperatorCannotAccessAdminPostRoutes + // testOperatorCanAccessOperatorGetRoutes + // testOperatorCanAccessOperatorPostRoutes + + // testUserCannotAccessAdminGetRoutes + // testUserCannotAccessAdminPostRoutes + // testUserCannotAccessOperatorGetRoutes + // testUserCannotAccessOperatorPostRoutes + // /** // * Test scan endpoint (POST /scan) // */ From d06c15deb8e3db28e30c106fbb770bf980a01a84 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Fri, 21 Nov 2025 13:29:07 +0000 Subject: [PATCH 13/38] unset, removeHeader --- tests/integration/BaseIntegrationTest.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index 4aa7103..ddbc70a 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -112,14 +112,13 @@ protected function makeHttpRequest(string $method, string $url, array $options = // Add authentication if provided if (isset($options['auth'])) { $credentials = base64_encode($options['auth']['username'] . ':' . $options['auth']['password']); + $request->removeHeader('Authorization'); $request->setHeader('Authorization', 'Basic ' . $credentials); - unset($options['auth']); } // Add request body if provided if (isset($options['body'])) { $request->setBody($options['body']); - unset($options['body']); } // Add headers if provided @@ -130,16 +129,14 @@ protected function makeHttpRequest(string $method, string $url, array $options = $request->setHeader(trim($name), trim($value)); } } - unset($options['headers']); } // Handle file upload for PUT requests if (isset($options['file_handle'])) { $request->setBody($options['file_handle']); - unset($options['file_handle']); } - + var_dump($request); try { $response = $client->request($request); From e5a7155b209691091c3a2f4d939ef83cd61ead4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=B6hling=2C=20Maximilian?= Date: Fri, 21 Nov 2025 14:45:57 +0100 Subject: [PATCH 14/38] set OCS-APIRequest --- tests/integration/BaseIntegrationTest.php | 527 +++++++++++----------- 1 file changed, 254 insertions(+), 273 deletions(-) diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index ddbc70a..80c9972 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -6,282 +6,263 @@ namespace Integration; -use Amp\Http\Client\HttpClient; use Amp\Http\Client\HttpClientBuilder; use Amp\Http\Client\Request; -use Amp\Http\Client\Response; use PHPUnit\Framework\TestCase; use RuntimeException; -use function Amp\async; - -abstract class BaseIntegrationTest extends TestCase -{ - protected string $hostname; - protected string $folderPrefix; - protected string $testUser; - protected string $testUserPassword; - protected string $eicarString; - protected string $cleanString; - protected string $dockerExecWithUser; - protected string $clientSecret; - protected string $clientId; - - protected static bool $setupCompleted = false; - - protected function setUp(): void - { - parent::setUp(); - - // Load environment variables - $this->hostname = $_ENV['HOSTNAME'] ?? '127.0.0.1:8080'; - $this->folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; - $this->testUser = $_ENV['TESTUSER'] ?? 'testuser'; - $this->testUserPassword = $_ENV['TESTUSER_PASSWORD'] ?? 'myfancysecurepassword234'; - $this->eicarString = $_ENV['EICAR_STRING'] ?? 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; - $this->cleanString = $_ENV['CLEAN_STRING'] ?? 'nothingwronghere'; - $this->dockerExecWithUser = $_ENV['DOCKER_EXEC_WITH_USER'] ?? 'docker exec --env XDEBUG_MODE=off --user www-data'; - $this->clientSecret = $_ENV['CLIENT_SECRET'] ?? ''; - $this->clientId = $_ENV['CLIENT_ID'] ?? ''; - - if (!self::$setupCompleted) { - $this->setupEnvironment(); - self::$setupCompleted = true; - } - } - - protected function setupEnvironment(): void - { - // Create temporary folder - if (!is_dir($this->folderPrefix)) { - mkdir($this->folderPrefix, 0755, true); - } - - // Download PUP file - $pupFile = $this->folderPrefix . '/pup.exe'; - if (!file_exists($pupFile)) { - $pupContent = file_get_contents('http://amtso.eicar.org/PotentiallyUnwanted.exe'); - if ($pupContent === false) { - $this->markTestSkipped('Could not download PUP file from AMTSO'); - } - file_put_contents($pupFile, $pupContent); - } - - // Create test user - $this->executeDockerCommand("php occ user:add {$this->testUser} --password-from-env", [ - 'OC_PASS' => $this->testUserPassword - ]); - - // Create user directory - $this->executeDockerCommand("mkdir -p /var/www/html/data/{$this->testUser}/files"); - - // Set client secret if available - if (!empty($this->clientSecret)) { - $this->executeDockerCommand("php occ config:app:set gdatavaas clientSecret --value=\"{$this->clientSecret}\""); - } - - // Cache busting and enable app - $this->executeDockerCommand("php occ files:scan --all"); - sleep(2); - $this->executeDockerCommand("php occ app:enable gdatavaas"); - } - - protected function executeDockerCommand(string $command, array $env = []): array - { - $envString = ''; - foreach ($env as $key => $value) { - $envString .= " --env {$key}=\"{$value}\""; - } - - $fullCommand = "{$this->dockerExecWithUser}{$envString} nextcloud-container {$command}"; - - exec($fullCommand . ' 2>&1', $output, $returnCode); - - return [ - 'output' => $output, - 'return_code' => $returnCode, - 'command' => $fullCommand - ]; - } - - protected function makeHttpRequest(string $method, string $url, array $options = []): array - { - $client = HttpClientBuilder::buildDefault(); - $request = new Request($url, $method); - - // Add authentication if provided - if (isset($options['auth'])) { - $credentials = base64_encode($options['auth']['username'] . ':' . $options['auth']['password']); - $request->removeHeader('Authorization'); - $request->setHeader('Authorization', 'Basic ' . $credentials); - } - - // Add request body if provided - if (isset($options['body'])) { - $request->setBody($options['body']); - } - - // Add headers if provided - if (isset($options['headers'])) { - foreach ($options['headers'] as $header) { - if (is_string($header) && strpos($header, ':') !== false) { - [$name, $value] = explode(':', $header, 2); - $request->setHeader(trim($name), trim($value)); - } - } - } - - // Handle file upload for PUT requests - if (isset($options['file_handle'])) { - $request->setBody($options['file_handle']); - } - - var_dump($request); - - try { - $response = $client->request($request); - - return [ - 'body' => $response->getBody()->buffer(), - 'http_code' => $response->getStatus(), - 'error' => '' - ]; - } catch (\Throwable $e) { - return [ - 'body' => '', - 'http_code' => 0, - 'error' => $e->getMessage() - ]; - } - } - - protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array - { - $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; - - return $this->makeHttpRequest('PUT', $url, [ - 'auth' => ['username' => $username, 'password' => $password], - 'body' => $content, - 'headers' => ['Content-Type: text/plain'] - ]); - } - - protected function deleteFileViaWebDAV(string $username, string $password, string $filename): array - { - $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; - - return $this->makeHttpRequest('DELETE', $url, [ - 'auth' => ['username' => $username, 'password' => $password] - ]); - } - - protected function uploadFileFromDisk(string $username, string $password, string $filename, string $localPath): array - { - if (!file_exists($localPath)) { - throw new RuntimeException("File not found: {$localPath}"); - } - - $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; - $fileContent = file_get_contents($localPath); - - if ($fileContent === false) { - throw new RuntimeException("Could not read file: {$localPath}"); - } - - return $this->makeHttpRequest('PUT', $url, [ - 'auth' => ['username' => $username, 'password' => $password], - 'body' => $fileContent, - ]); - } - - protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void - { - $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; - - $result = $this->makeHttpRequest('GET', $url, [ - 'auth' => ['username' => $username, 'password' => $password] - ]); - - echo "{$description} result: {$result['http_code']}\n"; - $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); - } - - protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void - { - $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; - - $result = $this->makeHttpRequest('POST', $url, [ - 'auth' => ['username' => $username, 'password' => $password], - 'body' => json_encode($data), - 'headers' => ['Content-Type: application/json'] - ]); - - echo "{$description} result: {$result['http_code']}\n"; - $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); - } - - protected function assertContainsVirusFound(array $response): void - { - $this->assertStringContainsString('Virus found', $response['body'], 'Expected "Virus found" in response body'); - } - - protected function assertHttpCodeInRange(int $httpCode, int $min = 200, int $max = 299): void - { - $this->assertGreaterThanOrEqual($min, $httpCode, "HTTP code {$httpCode} is below expected range {$min}-{$max}"); - $this->assertLessThan($max + 1, $httpCode, "HTTP code {$httpCode} is above expected range {$min}-{$max}"); - } - - protected function getTagsForFile(string $filePath): array - { - $result = $this->executeDockerCommand("php occ gdatavaas:get-tags-for-file {$filePath}"); - return $result['output']; - } - - protected function assertHasTag(string $filePath, string $expectedTag): void - { - $tags = $this->getTagsForFile($filePath); - $tagString = implode("\n", $tags); - $this->assertStringContainsString($expectedTag, $tagString, "Expected tag '{$expectedTag}' not found in file tags"); - } - - protected function assertTagCount(string $filePath, int $expectedCount): void - { - $tags = $this->getTagsForFile($filePath); - // Filter out empty lines - $nonEmptyTags = array_filter($tags, function ($line) { - return trim($line) !== ''; - }); - $this->assertCount($expectedCount, $nonEmptyTags, "Expected {$expectedCount} tags, got " . count($nonEmptyTags)); - } - - public static function tearDownAfterClass(): void - { - parent::tearDownAfterClass(); - - // Clean up temporary files - $folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; - if (is_dir($folderPrefix)) { - self::removeDirectory($folderPrefix); - } - } - - private static function removeDirectory(string $dir): void - { - if (!is_dir($dir)) { - return; - } - - $files = array_diff(scandir($dir), ['.', '..']); - - foreach ($files as $file) { - $path = $dir . DIRECTORY_SEPARATOR . $file; - if (is_dir($path)) { - self::removeDirectory($path); - } else { - unlink($path); - } - } - - rmdir($dir); - } +abstract class BaseIntegrationTest extends TestCase { + protected string $hostname; + protected string $folderPrefix; + protected string $testUser; + protected string $testUserPassword; + protected string $eicarString; + protected string $cleanString; + protected string $dockerExecWithUser; + protected string $clientSecret; + protected string $clientId; + + protected static bool $setupCompleted = false; + + protected function setUp(): void { + parent::setUp(); + + // Load environment variables + $this->hostname = $_ENV['HOSTNAME'] ?? '127.0.0.1:8080'; + $this->folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; + $this->testUser = $_ENV['TESTUSER'] ?? 'testuser'; + $this->testUserPassword = $_ENV['TESTUSER_PASSWORD'] ?? 'myfancysecurepassword234'; + $this->eicarString = $_ENV['EICAR_STRING'] ?? 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; + $this->cleanString = $_ENV['CLEAN_STRING'] ?? 'nothingwronghere'; + $this->dockerExecWithUser = $_ENV['DOCKER_EXEC_WITH_USER'] ?? 'docker exec --env XDEBUG_MODE=off --user www-data'; + $this->clientSecret = $_ENV['CLIENT_SECRET'] ?? ''; + $this->clientId = $_ENV['CLIENT_ID'] ?? ''; + + if (!self::$setupCompleted) { + $this->setupEnvironment(); + self::$setupCompleted = true; + } + } + + protected function setupEnvironment(): void { + // Create temporary folder + if (!is_dir($this->folderPrefix)) { + mkdir($this->folderPrefix, 0755, true); + } + + // Download PUP file + $pupFile = $this->folderPrefix . '/pup.exe'; + if (!file_exists($pupFile)) { + $pupContent = file_get_contents('http://amtso.eicar.org/PotentiallyUnwanted.exe'); + if ($pupContent === false) { + $this->markTestSkipped('Could not download PUP file from AMTSO'); + } + file_put_contents($pupFile, $pupContent); + } + + // Create test user + $this->executeDockerCommand("php occ user:add {$this->testUser} --password-from-env", [ + 'OC_PASS' => $this->testUserPassword + ]); + + // Create user directory + $this->executeDockerCommand("mkdir -p /var/www/html/data/{$this->testUser}/files"); + + // Set client secret if available + if (!empty($this->clientSecret)) { + $this->executeDockerCommand("php occ config:app:set gdatavaas clientSecret --value=\"{$this->clientSecret}\""); + } + + // Cache busting and enable app + $this->executeDockerCommand('php occ files:scan --all'); + sleep(2); + $this->executeDockerCommand('php occ app:enable gdatavaas'); + } + + protected function executeDockerCommand(string $command, array $env = []): array { + $envString = ''; + foreach ($env as $key => $value) { + $envString .= " --env {$key}=\"{$value}\""; + } + + $fullCommand = "{$this->dockerExecWithUser}{$envString} nextcloud-container {$command}"; + + exec($fullCommand . ' 2>&1', $output, $returnCode); + + return [ + 'output' => $output, + 'return_code' => $returnCode, + 'command' => $fullCommand + ]; + } + + protected function makeHttpRequest(string $method, string $url, array $options = []): array { + $client = HttpClientBuilder::buildDefault(); + $request = new Request($url, $method); + + $request->setHeader('OCS-APIRequest', 'true'); + + // Add authentication if provided + if (isset($options['auth'])) { + $credentials = base64_encode($options['auth']['username'] . ':' . $options['auth']['password']); + $request->removeHeader('Authorization'); + $request->setHeader('Authorization', 'Basic ' . $credentials); + } + + // Add request body if provided + if (isset($options['body'])) { + $request->setBody($options['body']); + } + + // Add headers if provided + if (isset($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && strpos($header, ':') !== false) { + [$name, $value] = explode(':', $header, 2); + $request->setHeader(trim($name), trim($value)); + } + } + } + + // Handle file upload for PUT requests + if (isset($options['file_handle'])) { + $request->setBody($options['file_handle']); + } + + var_dump($request); + + try { + $response = $client->request($request); + + return [ + 'body' => $response->getBody()->buffer(), + 'http_code' => $response->getStatus(), + 'error' => '' + ]; + } catch (\Throwable $e) { + return [ + 'body' => '', + 'http_code' => 0, + 'error' => $e->getMessage() + ]; + } + } + + protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array { + $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; + + return $this->makeHttpRequest('PUT', $url, [ + 'auth' => ['username' => $username, 'password' => $password], + 'body' => $content, + 'headers' => ['Content-Type: text/plain'] + ]); + } + + protected function deleteFileViaWebDAV(string $username, string $password, string $filename): array { + $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; + + return $this->makeHttpRequest('DELETE', $url, [ + 'auth' => ['username' => $username, 'password' => $password] + ]); + } + + protected function uploadFileFromDisk(string $username, string $password, string $filename, string $localPath): array { + if (!file_exists($localPath)) { + throw new RuntimeException("File not found: {$localPath}"); + } + + $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; + $fileContent = file_get_contents($localPath); + + if ($fileContent === false) { + throw new RuntimeException("Could not read file: {$localPath}"); + } + + return $this->makeHttpRequest('PUT', $url, [ + 'auth' => ['username' => $username, 'password' => $password], + 'body' => $fileContent, + ]); + } + + protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { + $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; + + $result = $this->makeHttpRequest('GET', $url, [ + 'auth' => ['username' => $username, 'password' => $password] + ]); + + echo "{$description} result: {$result['http_code']}\n"; + $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); + } + + protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { + $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; + + $result = $this->makeHttpRequest('POST', $url, [ + 'auth' => ['username' => $username, 'password' => $password], + 'body' => json_encode($data), + 'headers' => ['Content-Type: application/json'] + ]); + + echo "{$description} result: {$result['http_code']}\n"; + $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); + } + + protected function assertContainsVirusFound(array $response): void { + $this->assertStringContainsString('Virus found', $response['body'], 'Expected "Virus found" in response body'); + } + + protected function assertHttpCodeInRange(int $httpCode, int $min = 200, int $max = 299): void { + $this->assertGreaterThanOrEqual($min, $httpCode, "HTTP code {$httpCode} is below expected range {$min}-{$max}"); + $this->assertLessThan($max + 1, $httpCode, "HTTP code {$httpCode} is above expected range {$min}-{$max}"); + } + + protected function getTagsForFile(string $filePath): array { + $result = $this->executeDockerCommand("php occ gdatavaas:get-tags-for-file {$filePath}"); + return $result['output']; + } + + protected function assertHasTag(string $filePath, string $expectedTag): void { + $tags = $this->getTagsForFile($filePath); + $tagString = implode("\n", $tags); + $this->assertStringContainsString($expectedTag, $tagString, "Expected tag '{$expectedTag}' not found in file tags"); + } + + protected function assertTagCount(string $filePath, int $expectedCount): void { + $tags = $this->getTagsForFile($filePath); + // Filter out empty lines + $nonEmptyTags = array_filter($tags, function ($line) { + return trim($line) !== ''; + }); + $this->assertCount($expectedCount, $nonEmptyTags, "Expected {$expectedCount} tags, got " . count($nonEmptyTags)); + } + + public static function tearDownAfterClass(): void { + parent::tearDownAfterClass(); + + // Clean up temporary files + $folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; + if (is_dir($folderPrefix)) { + self::removeDirectory($folderPrefix); + } + } + + private static function removeDirectory(string $dir): void { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + + foreach ($files as $file) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (is_dir($path)) { + self::removeDirectory($path); + } else { + unlink($path); + } + } + + rmdir($dir); + } } From 7445edaff83db10a334d8347a32ef45555d438a4 Mon Sep 17 00:00:00 2001 From: Verdict-as-a-Service Team Date: Mon, 24 Nov 2025 10:22:19 +0100 Subject: [PATCH 15/38] More phpunit stuff --- tests/integration/BaseIntegrationTest.php | 55 ++++--- tests/integration/SettingsControllerTest.php | 162 ++++++++++++++----- tests/integration/phpunit.xml | 3 +- 3 files changed, 154 insertions(+), 66 deletions(-) diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index 80c9972..ea82e77 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -11,7 +11,8 @@ use PHPUnit\Framework\TestCase; use RuntimeException; -abstract class BaseIntegrationTest extends TestCase { +abstract class BaseIntegrationTest extends TestCase +{ protected string $hostname; protected string $folderPrefix; protected string $testUser; @@ -24,7 +25,8 @@ abstract class BaseIntegrationTest extends TestCase { protected static bool $setupCompleted = false; - protected function setUp(): void { + protected function setUp(): void + { parent::setUp(); // Load environment variables @@ -44,7 +46,8 @@ protected function setUp(): void { } } - protected function setupEnvironment(): void { + protected function setupEnvironment(): void + { // Create temporary folder if (!is_dir($this->folderPrefix)) { mkdir($this->folderPrefix, 0755, true); @@ -79,7 +82,8 @@ protected function setupEnvironment(): void { $this->executeDockerCommand('php occ app:enable gdatavaas'); } - protected function executeDockerCommand(string $command, array $env = []): array { + protected function executeDockerCommand(string $command, array $env = []): array + { $envString = ''; foreach ($env as $key => $value) { $envString .= " --env {$key}=\"{$value}\""; @@ -96,7 +100,8 @@ protected function executeDockerCommand(string $command, array $env = []): array ]; } - protected function makeHttpRequest(string $method, string $url, array $options = []): array { + protected function makeHttpRequest(string $method, string $url, array $options = []): array + { $client = HttpClientBuilder::buildDefault(); $request = new Request($url, $method); @@ -129,8 +134,6 @@ protected function makeHttpRequest(string $method, string $url, array $options = $request->setBody($options['file_handle']); } - var_dump($request); - try { $response = $client->request($request); @@ -148,7 +151,8 @@ protected function makeHttpRequest(string $method, string $url, array $options = } } - protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array { + protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array + { $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; return $this->makeHttpRequest('PUT', $url, [ @@ -158,7 +162,8 @@ protected function uploadFileViaWebDAV(string $username, string $password, strin ]); } - protected function deleteFileViaWebDAV(string $username, string $password, string $filename): array { + protected function deleteFileViaWebDAV(string $username, string $password, string $filename): array + { $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; return $this->makeHttpRequest('DELETE', $url, [ @@ -166,7 +171,8 @@ protected function deleteFileViaWebDAV(string $username, string $password, strin ]); } - protected function uploadFileFromDisk(string $username, string $password, string $filename, string $localPath): array { + protected function uploadFileFromDisk(string $username, string $password, string $filename, string $localPath): array + { if (!file_exists($localPath)) { throw new RuntimeException("File not found: {$localPath}"); } @@ -184,18 +190,19 @@ protected function uploadFileFromDisk(string $username, string $password, string ]); } - protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { + protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void + { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; $result = $this->makeHttpRequest('GET', $url, [ 'auth' => ['username' => $username, 'password' => $password] ]); - echo "{$description} result: {$result['http_code']}\n"; $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); } - protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { + protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void + { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; $result = $this->makeHttpRequest('POST', $url, [ @@ -204,31 +211,35 @@ protected function testPostEndpoint(string $endpoint, array $data, string $descr 'headers' => ['Content-Type: application/json'] ]); - echo "{$description} result: {$result['http_code']}\n"; $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); } - protected function assertContainsVirusFound(array $response): void { + protected function assertContainsVirusFound(array $response): void + { $this->assertStringContainsString('Virus found', $response['body'], 'Expected "Virus found" in response body'); } - protected function assertHttpCodeInRange(int $httpCode, int $min = 200, int $max = 299): void { + protected function assertHttpCodeInRange(int $httpCode, int $min = 200, int $max = 299): void + { $this->assertGreaterThanOrEqual($min, $httpCode, "HTTP code {$httpCode} is below expected range {$min}-{$max}"); $this->assertLessThan($max + 1, $httpCode, "HTTP code {$httpCode} is above expected range {$min}-{$max}"); } - protected function getTagsForFile(string $filePath): array { + protected function getTagsForFile(string $filePath): array + { $result = $this->executeDockerCommand("php occ gdatavaas:get-tags-for-file {$filePath}"); return $result['output']; } - protected function assertHasTag(string $filePath, string $expectedTag): void { + protected function assertHasTag(string $filePath, string $expectedTag): void + { $tags = $this->getTagsForFile($filePath); $tagString = implode("\n", $tags); $this->assertStringContainsString($expectedTag, $tagString, "Expected tag '{$expectedTag}' not found in file tags"); } - protected function assertTagCount(string $filePath, int $expectedCount): void { + protected function assertTagCount(string $filePath, int $expectedCount): void + { $tags = $this->getTagsForFile($filePath); // Filter out empty lines $nonEmptyTags = array_filter($tags, function ($line) { @@ -237,7 +248,8 @@ protected function assertTagCount(string $filePath, int $expectedCount): void { $this->assertCount($expectedCount, $nonEmptyTags, "Expected {$expectedCount} tags, got " . count($nonEmptyTags)); } - public static function tearDownAfterClass(): void { + public static function tearDownAfterClass(): void + { parent::tearDownAfterClass(); // Clean up temporary files @@ -247,7 +259,8 @@ public static function tearDownAfterClass(): void { } } - private static function removeDirectory(string $dir): void { + private static function removeDirectory(string $dir): void + { if (!is_dir($dir)) { return; } diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 953eec1..60046da 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -15,24 +15,55 @@ class SettingsControllerTest extends BaseIntegrationTest public static function adminGetRouteProvider(): array { return [ - ['getCounters'], ['getAuthMethod'], ['getCache'], ['getHashlookup'], + ]; + } + + public static function adminPostRouteProvider(): array + { + return [ + // TODO: use default settings + ['adminSettings', [ + 'username' => 'username', + 'password' => 'password', + 'clientId' => 'clientId', + 'clientSecret' => 'clientSecret', + 'authMethod' => 'authMethod', + 'maxScanSize' => 209715200, + 'timeout' => 900, + 'cache' => true, + 'hashlookup' => true + ]], + // ['setadvancedconfig'], + ]; + } + + public static function operatorGetRouteProvider(): array + { + return [ ['getSendMailOnVirusUpload'], ['getAutoScan'], ['getPrefixMalicious'], ['getDisableUnscannedTag'], - // ['setAutoScan'], - // ['setPrefixMalicious'], - // ['setSendMailOnVirusUpload'], - // ['setDisableUnscannedTag'], - // ['setadvancedconfig'], - ['operatorSettings'], - ['adminSettings'], - // ['resetalltags'], - // ['testsettings'], - // ['scan'] + ['getCounters'], + ]; + } + + public static function operatorPostRouteProvider(): array + { + return [ + ['operatorSettings', [ + 'quarantineFolder' => '', + 'scanOnlyThis' => '', + 'doNotScanThis' => '', + 'notifyMails' => '', + ]], + ['setAutoScan', ['autoScanFiles' => true]], + ['setPrefixMalicious', ['prefixMalicious' => '[VIRUS] ']], + ['setSendMailOnVirusUpload', ['sendMailOnVirusUpload' => 'false']], + ['setDisableUnscannedTag', ['disableUnscannedTag' => 'false']], ]; } @@ -42,20 +73,85 @@ public function testAdminCanAccessAdminGetRoutes(string $route): void $this->testGetEndpoint($route, "Admin access to {$route}", 200); } - // testAdminCanAccessAdminGetRoutes - // testAdminCanAccessAdminPostRoutes - // testAdminCanAccessOperatorGetRoutes - // testAdminCanAccessOperatorPostRoutes - // testOperatorCannotAccessAdminGetRoutes - // testOperatorCannotAccessAdminPostRoutes - // testOperatorCanAccessOperatorGetRoutes - // testOperatorCanAccessOperatorPostRoutes + #[DataProvider('adminPostRouteProvider')] + public function testAdminCanAccessAdminPostRoutes(string $route, array $data): void + { + $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); + } + + #[DataProvider('operatorGetRouteProvider')] + public function testAdminCanAccessOperatorGetRoutes(string $route): void + { + $this->testGetEndpoint($route, "Admin access to {$route}", 200); + } + + #[DataProvider('operatorPostRouteProvider')] + public function testAdminCanAccessOperatorPostRoutes(string $route, array $data): void + { + $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); + } + + + #[DataProvider('adminGetRouteProvider')] + public function testOperatorCannotAccessAdminGetRoutes(string $route): void + { + $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: "vaas-operator", password: "gdatavaas-operator"); + } + + + #[DataProvider('adminPostRouteProvider')] + public function testOperatorCannotAccessAdminPostRoutes(string $route, array $data): void + { + $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: "vaas-operator", password: "gdatavaas-operator"); + } + + #[DataProvider('operatorGetRouteProvider')] + public function testOperatorCanAccessOperatorGetRoutes(string $route): void + { + $this->testGetEndpoint($route, "Operator access to {$route}", 200, username: "vaas-operator", password: "gdatavaas-operator"); + } + + #[DataProvider('operatorPostRouteProvider')] + public function testOperatorCanAccessOperatorPostRoutes(string $route, array $data): void + { + $this->testPostEndpoint($route, $data, "Operator access to {$route}", 200, username: "vaas-operator", password: "gdatavaas-operator"); + } + + + #[DataProvider('adminGetRouteProvider')] + public function testUserCannotAccessAdminGetRoutes(string $route): void + { + $this->testGetEndpoint($route, "User access to {$route}", 403, username: "user", password: "gdatavaas-user"); + } + + + #[DataProvider('adminPostRouteProvider')] + public function testUserCannotAccessAdminPostRoutes(string $route, array $data): void + { + $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: "user", password: "gdatavaas-user"); + } + + #[DataProvider('operatorGetRouteProvider')] + public function testUserCannotAccessOperatorGetRoutes(string $route): void + { + $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: "user", password: "gdatavaas-user"); + } + + #[DataProvider('operatorPostRouteProvider')] + public function testUserCannotAccessOperatorPostRoutes(string $route, array $data): void + { + $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: "user", password: "gdatavaas-user"); + } + + + #[DataProvider('adminPostRouteProvider')] + #[DataProvider('operatorPostRouteProvider')] + public function testPostRoutesReturn400ForEmptyBody(string $route, array $data): void + { + $this->testPostEndpoint($route, [], "Admin access to {$route}", 400); + } - // testUserCannotAccessAdminGetRoutes - // testUserCannotAccessAdminPostRoutes - // testUserCannotAccessOperatorGetRoutes - // testUserCannotAccessOperatorPostRoutes // /** // * Test scan endpoint (POST /scan) @@ -95,16 +191,6 @@ public function testAdminCanAccessAdminGetRoutes(string $route): void // } // } - // /** - // * Test all GET endpoints - // * @dataProvider getEndpointsProvider - // */ - // public function testGetEndpoints(string $endpoint, string $description): void - // { - // echo "Testing {$endpoint}...\n"; - // $this->testGetEndpoint($endpoint, $description, 200); - // } - // /** // * Data provider for GET endpoints // */ @@ -122,16 +208,6 @@ public function testAdminCanAccessAdminGetRoutes(string $route): void // ]; // } - // /** - // * Test all POST settings endpoints - // * @dataProvider postEndpointsProvider - // */ - // public function testPostSettingsEndpoints(string $endpoint, array $data, string $description): void - // { - // echo "Testing {$endpoint}...\n"; - // $this->testPostEndpoint($endpoint, $data, $description, 200); - // } - // /** // * Data provider for POST settings endpoints // */ diff --git a/tests/integration/phpunit.xml b/tests/integration/phpunit.xml index 7e96c8e..13fa666 100644 --- a/tests/integration/phpunit.xml +++ b/tests/integration/phpunit.xml @@ -7,11 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later --> . From 4ed5cc3c7c0e22b90ae8bdd9e880fd356479be1f Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Mon, 24 Nov 2025 19:53:22 +0000 Subject: [PATCH 16/38] apply comments --- composer.json | 4 +- lib/Settings/VaasOperator.php | 2 + tests/bats/functionality-parallel.bats | 115 ---------- tests/integration/ApiEndpointTest.php | 224 ------------------- tests/integration/FileUploadTest.php | 140 ------------ tests/integration/README.md | 185 +-------------- tests/integration/SettingsControllerTest.php | 193 ---------------- tests/integration/TagManagementTest.php | 188 ---------------- 8 files changed, 9 insertions(+), 1042 deletions(-) delete mode 100644 tests/integration/ApiEndpointTest.php delete mode 100644 tests/integration/FileUploadTest.php delete mode 100644 tests/integration/TagManagementTest.php diff --git a/composer.json b/composer.json index 3ff7e64..111fe35 100644 --- a/composer.json +++ b/composer.json @@ -19,8 +19,8 @@ "colinodell/psr-testlogger": "1.3.0", "phpunit/phpunit": "10.5.52", "symfony/console": "v6.4.24", - "amphp/http-client": "^5.3", - "amphp/amp": "^3.1" + "amphp/http-client": "5.3", + "amphp/amp": "3.1" }, "autoload": { "psr-4": { diff --git a/lib/Settings/VaasOperator.php b/lib/Settings/VaasOperator.php index 4ae1b30..68bd218 100644 --- a/lib/Settings/VaasOperator.php +++ b/lib/Settings/VaasOperator.php @@ -51,10 +51,12 @@ public function getPriority(): int { return 20; } + #[\Override] public function getName(): ?string { return $this->l->t('Operator Settings'); } + #[\Override] public function getAuthorizedAppConfig(): array { return []; } diff --git a/tests/bats/functionality-parallel.bats b/tests/bats/functionality-parallel.bats index c2b3352..96cae7a 100755 --- a/tests/bats/functionality-parallel.bats +++ b/tests/bats/functionality-parallel.bats @@ -102,121 +102,6 @@ setup_file() { $DOCKER_EXEC_WITH_USER nextcloud-container rm /var/www/html/data/$TESTUSER/files/$TESTUSER.unscanned.pup.exe } - -# Controller endpoint tests using admin user - -# Helper function for testing GET endpoints -test_get_endpoint() { - local endpoint="$1" - local description="$2" - local expected_http_status="${3:-200}" # Default to 200 if not provided - local user_name="${4:-admin}" # Default to admin if not provided - local password="${5:-admin}" # Default to admin if not provided - - RESULT=$(curl --silent -w "%{http_code}" -u "$user_name:$password" -X GET \ - http://$HOSTNAME/apps/gdatavaas/$endpoint) - - echo "$description result: $RESULT" - [[ $RESULT -eq $expected_http_status ]] -} - -# Helper function for testing POST endpoints -test_post_endpoint() { - local endpoint="$1" - local data="$2" - local description="$3" - local expected_http_status="${4:-200}" # Default to 200 if not provided - local user_name="${5:-admin}" # Default to admin if not provided - local password="${6:-admin}" # Default to admin if not provided - - RESULT=$(curl --silent -w "%{http_code}" -u "$user_name:$password" -X POST \ - -H "Content-Type: application/json" \ - -d "$data" \ - http://$HOSTNAME/apps/gdatavaas/$endpoint) - - echo "$description result: $RESULT" - [[ $RESULT -eq $expected_http_status ]] -} - -@test "test scan endpoint (POST /scan)" { - # Create a test file first - echo $CLEAN_STRING > $FOLDER_PREFIX/test-scan.txt - docker cp $FOLDER_PREFIX/test-scan.txt nextcloud-container:/var/www/html/data/admin/files/test-scan.txt - $DOCKER_EXEC_WITH_USER nextcloud-container php occ files:scan admin - - # Test scan endpoint - test_post_endpoint "scan" '{"path":"test-scan.txt"}' "Scan endpoint" "200" - - $DOCKER_EXEC_WITH_USER nextcloud-container rm -f /var/www/html/data/admin/files/test-scan.txt -} - - - -# Parameterized tests for GET endpoints -@test "test GET endpoints" { - # Array of GET endpoints to test - declare -a get_endpoints=( - "getCounters:Get counters" - "getAuthMethod:Get auth method" - "getCache:Get cache" - "getHashlookup:Get hash lookup" - "getSendMailOnVirusUpload:Get send mail on virus upload" - "getAutoScan:Get auto scan" - "getPrefixMalicious:Get prefix malicious" - "getDisableUnscannedTag:Get disable unscanned tag" - ) - - for endpoint_info in "${get_endpoints[@]}"; do - IFS=':' read -r endpoint description <<< "$endpoint_info" - echo "Testing $endpoint..." - test_get_endpoint "$endpoint" "$description" "200" - done -} - -# Parameterized tests for POST endpoints with settings -@test "test POST settings endpoints" { - # Array of POST endpoints with their test data - declare -A post_endpoints=( - ["setAutoScan"]='{"autoScan":"true"}' - ["setPrefixMalicious"]='{"prefixMalicious":"[VIRUS] "}' - ["setSendMailOnVirusUpload"]='{"sendMailOnVirusUpload":"false"}' - ["setDisableUnscannedTag"]='{"disableUnscannedTag":"false"}' - ["setadvancedconfig"]='{"maxScanFileSize":"104857600","maxUploadFileSize":"104857600"}' - ) - - for endpoint in "${!post_endpoints[@]}"; do - echo "Testing $endpoint..." - test_post_endpoint "$endpoint" "${post_endpoints[$endpoint]}" "Set ${endpoint#set}" "200" - done -} - -@test "test operator settings endpoint" { - test_post_endpoint "operatorSettings" \ - '{"autoScan":"true","prefixMalicious":"[VIRUS]","sendMailOnVirusUpload":"false","disableUnscannedTag":"false"}' \ - "Set operator settings" "200" -} - -@test "test admin settings endpoint" { - test_post_endpoint "adminSettings" \ - '{"authMethod":"client-credentials","clientId":"test","clientSecret":"test","username":"","password":"","url":"https://gateway.production.vaas.gdatasecurity.de"}' \ - "Set admin settings" "200" -} - -@test "test reset all tags endpoint" { - test_post_endpoint "resetalltags" '{}' "Reset all tags" "200" -} - -@test "test settings validation endpoint" { - # Skip test if CLIENT_ID or CLIENT_SECRET are not set - if [[ -z "$CLIENT_ID" || -z "$CLIENT_SECRET" ]]; then - skip "CLIENT_ID and CLIENT_SECRET environment variables are required for this test" - fi - - test_post_endpoint "testsettings" \ - '{"authMethod":"client-credentials","clientId":"'$CLIENT_ID'","clientSecret":"'$CLIENT_SECRET'","username":"","password":"","url":"https://gateway.production.vaas.gdatasecurity.de"}' \ - "Test settings" "200" -} - teardown_file() { rm -rf $FOLDER_PREFIX/ } diff --git a/tests/integration/ApiEndpointTest.php b/tests/integration/ApiEndpointTest.php deleted file mode 100644 index 23df1aa..0000000 --- a/tests/integration/ApiEndpointTest.php +++ /dev/null @@ -1,224 +0,0 @@ - -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -namespace Integration; - -require_once __DIR__ . '/BaseIntegrationTest.php'; - -class ApiEndpointTest extends BaseIntegrationTest -{ - /** - * Test scan endpoint (POST /scan) - */ - public function testScanEndpoint(): void - { - // Create a test file first - $filename = 'test-scan.txt'; - $containerPath = "/var/www/html/data/admin/files/{$filename}"; - $testFile = $this->folderPrefix . '/test-scan.txt'; - - // Create local test file - file_put_contents($testFile, $this->cleanString); - - try { - // Copy to container - exec("docker cp {$testFile} nextcloud-container:{$containerPath}", $output, $returnCode); - if ($returnCode !== 0) { - $this->markTestSkipped('Could not copy test file to container'); - } - - // Set proper ownership - $this->executeDockerCommand("chown www-data:www-data {$containerPath}"); - - // Scan admin files - $this->executeDockerCommand("php occ files:scan admin"); - - // Test scan endpoint - $this->testPostEndpoint('scan', ['path' => $filename], 'Scan endpoint', 200); - - } finally { - // Clean up - $this->executeDockerCommand("rm -f {$containerPath}"); - if (file_exists($testFile)) { - unlink($testFile); - } - } - } - - /** - * Test all GET endpoints - * @dataProvider getEndpointsProvider - */ - public function testGetEndpoints(string $endpoint, string $description): void - { - echo "Testing {$endpoint}...\n"; - $this->testGetEndpoint($endpoint, $description, 200); - } - - /** - * Data provider for GET endpoints - */ - public static function getEndpointsProvider(): array - { - return [ - ['getCounters', 'Get counters'], - ['getAuthMethod', 'Get auth method'], - ['getCache', 'Get cache'], - ['getHashlookup', 'Get hash lookup'], - ['getSendMailOnVirusUpload', 'Get send mail on virus upload'], - ['getAutoScan', 'Get auto scan'], - ['getPrefixMalicious', 'Get prefix malicious'], - ['getDisableUnscannedTag', 'Get disable unscanned tag'], - ]; - } - - /** - * Test all POST settings endpoints - * @dataProvider postEndpointsProvider - */ - public function testPostSettingsEndpoints(string $endpoint, array $data, string $description): void - { - echo "Testing {$endpoint}...\n"; - $this->testPostEndpoint($endpoint, $data, $description, 200); - } - - /** - * Data provider for POST settings endpoints - */ - public static function postEndpointsProvider(): array - { - return [ - ['setAutoScan', ['autoScan' => 'true'], 'Set auto scan'], - ['setPrefixMalicious', ['prefixMalicious' => '[VIRUS] '], 'Set prefix malicious'], - ['setSendMailOnVirusUpload', ['sendMailOnVirusUpload' => 'false'], 'Set send mail on virus upload'], - ['setDisableUnscannedTag', ['disableUnscannedTag' => 'false'], 'Set disable unscanned tag'], - ['setadvancedconfig', ['maxScanFileSize' => '104857600', 'maxUploadFileSize' => '104857600'], 'Set advanced config'], - ]; - } - - /** - * Test operator settings endpoint - */ - public function testOperatorSettingsEndpoint(): void - { - $data = [ - 'autoScan' => 'true', - 'prefixMalicious' => '[VIRUS]', - 'sendMailOnVirusUpload' => 'false', - 'disableUnscannedTag' => 'false' - ]; - - $this->testPostEndpoint('operatorSettings', $data, 'Set operator settings', 200); - } - - /** - * Test admin settings endpoint - */ - public function testAdminSettingsEndpoint(): void - { - $data = [ - 'authMethod' => 'client-credentials', - 'clientId' => 'test', - 'clientSecret' => 'test', - 'username' => '', - 'password' => '', - 'url' => 'https://gateway.production.vaas.gdatasecurity.de' - ]; - - $this->testPostEndpoint('adminSettings', $data, 'Set admin settings', 200); - } - - /** - * Test reset all tags endpoint - */ - public function testResetAllTagsEndpoint(): void - { - $this->testPostEndpoint('resetalltags', [], 'Reset all tags', 200); - } - - /** - * Test settings validation endpoint - * This test requires valid CLIENT_ID and CLIENT_SECRET environment variables - */ - public function testSettingsValidationEndpoint(): void - { - if (empty($this->clientId) || empty($this->clientSecret)) { - $this->markTestSkipped('CLIENT_ID and CLIENT_SECRET environment variables are required for this test'); - } - - $data = [ - 'authMethod' => 'client-credentials', - 'clientId' => $this->clientId, - 'clientSecret' => $this->clientSecret, - 'username' => '', - 'password' => '', - 'url' => 'https://gateway.production.vaas.gdatasecurity.de' - ]; - - $this->testPostEndpoint('testsettings', $data, 'Test settings', 200); - } - - /** - * Test unauthorized access to admin endpoints - * Tests that non-admin users get appropriate error responses - */ - public function testUnauthorizedAccess(): void - { - // Test with regular user credentials (should fail for admin endpoints) - $this->testGetEndpoint('getCounters', 'Unauthorized get counters', 401, $this->testUser, $this->testUserPassword); - } - - /** - * Test invalid endpoint - * Tests that non-existent endpoints return 404 - */ - public function testInvalidEndpoint(): void - { - $url = "http://{$this->hostname}/apps/gdatavaas/nonexistent"; - - $result = $this->makeHttpRequest('GET', $url, [ - 'auth' => ['username' => 'admin', 'password' => 'admin'] - ]); - - echo "Invalid endpoint result: {$result['http_code']}\n"; - $this->assertEquals(404, $result['http_code'], 'Expected 404 for non-existent endpoint'); - } - - /** - * Test malformed JSON in POST requests - */ - public function testMalformedJsonPost(): void - { - $url = "http://{$this->hostname}/apps/gdatavaas/setAutoScan"; - - $result = $this->makeHttpRequest('POST', $url, [ - 'auth' => ['username' => 'admin', 'password' => 'admin'], - 'body' => '{"invalid": json}', // Malformed JSON - 'headers' => ['Content-Type: application/json'] - ]); - - echo "Malformed JSON result: {$result['http_code']}\n"; - // Depending on implementation, this might return 400 or 500 - $this->assertContains($result['http_code'], [400, 500], 'Expected error status for malformed JSON'); - } - - /** - * Test endpoint with missing required parameters - */ - public function testMissingParameters(): void - { - // Test scan endpoint without required 'path' parameter - $result = $this->makeHttpRequest('POST', "http://{$this->hostname}/apps/gdatavaas/scan", [ - 'auth' => ['username' => 'admin', 'password' => 'admin'], - 'body' => json_encode([]), // Empty data - 'headers' => ['Content-Type: application/json'] - ]); - - echo "Missing parameters result: {$result['http_code']}\n"; - // Should return an error status - $this->assertGreaterThanOrEqual(400, $result['http_code'], 'Expected error status for missing parameters'); - } -} \ No newline at end of file diff --git a/tests/integration/FileUploadTest.php b/tests/integration/FileUploadTest.php deleted file mode 100644 index 5d87c8f..0000000 --- a/tests/integration/FileUploadTest.php +++ /dev/null @@ -1,140 +0,0 @@ - -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -namespace Integration; - -class FileUploadTest extends BaseIntegrationTest -{ - /** - * Test admin EICAR upload - should be blocked with virus found message - */ - public function testAdminEicarUpload(): void - { - $filename = 'functionality-parallel.eicar.com.txt'; - - try { - $result = $this->uploadFileViaWebDAV('admin', 'admin', $filename, $this->eicarString); - - echo "EICAR upload result: {$result['http_code']}\n"; - echo "Response body: {$result['body']}\n"; - - $this->assertContainsVirusFound($result); - } finally { - // Clean up - attempt to delete file if it exists - $this->deleteFileViaWebDAV('admin', 'admin', $filename); - } - } - - /** - * Test admin clean file upload - should succeed - */ - public function testAdminCleanUpload(): void - { - $filename = 'functionality-parallel.clean.txt'; - - try { - $result = $this->uploadFileViaWebDAV('admin', 'admin', $filename, $this->cleanString); - - echo "Clean file upload result: {$result['http_code']}\n"; - - $this->assertHttpCodeInRange($result['http_code'], 200, 299); - } finally { - // Clean up - $this->deleteFileViaWebDAV('admin', 'admin', $filename); - } - } - - /** - * Test admin PUP (Potentially Unwanted Program) upload - should succeed - */ - public function testAdminPupUpload(): void - { - $filename = 'functionality-parallel.pup.exe'; - $pupFilePath = $this->folderPrefix . '/pup.exe'; - - if (!file_exists($pupFilePath)) { - $this->markTestSkipped('PUP file not available'); - } - - try { - $result = $this->uploadFileFromDisk('admin', 'admin', $filename, $pupFilePath); - - echo "PUP upload result: {$result['http_code']}\n"; - - $this->assertHttpCodeInRange($result['http_code'], 200, 299); - } finally { - // Clean up - $this->deleteFileViaWebDAV('admin', 'admin', $filename); - } - } - - /** - * Test testuser EICAR upload - should be blocked with virus found message - */ - public function testTestUserEicarUpload(): void - { - $filename = 'functionality-parallel.eicar.com.txt'; - - try { - $result = $this->uploadFileViaWebDAV($this->testUser, $this->testUserPassword, $filename, $this->eicarString); - - echo "Testuser EICAR upload result: {$result['http_code']}\n"; - echo "Response body: {$result['body']}\n"; - - // Log client secret for debugging - $configResult = $this->executeDockerCommand("php occ config:app:get gdatavaas clientSecret"); - echo "Client secret configured: " . (empty($configResult['output']) ? 'No' : 'Yes') . "\n"; - - $this->assertContainsVirusFound($result); - } finally { - // Clean up - $this->deleteFileViaWebDAV($this->testUser, $this->testUserPassword, $filename); - } - } - - /** - * Test testuser clean file upload - should succeed - */ - public function testTestUserCleanUpload(): void - { - $filename = 'functionality-parallel.clean.txt'; - - try { - $result = $this->uploadFileViaWebDAV($this->testUser, $this->testUserPassword, $filename, $this->cleanString); - - echo "Testuser clean file upload result: {$result['http_code']}\n"; - - $this->assertHttpCodeInRange($result['http_code'], 200, 299); - } finally { - // Clean up - $this->deleteFileViaWebDAV($this->testUser, $this->testUserPassword, $filename); - } - } - - /** - * Test testuser PUP upload - should succeed - */ - public function testTestUserPupUpload(): void - { - $filename = 'functionality-parallel.pup.exe'; - $pupFilePath = $this->folderPrefix . '/pup.exe'; - - if (!file_exists($pupFilePath)) { - $this->markTestSkipped('PUP file not available'); - } - - try { - $result = $this->uploadFileFromDisk($this->testUser, $this->testUserPassword, $filename, $pupFilePath); - - echo "Testuser PUP upload result: {$result['http_code']}\n"; - - $this->assertHttpCodeInRange($result['http_code'], 200, 299); - } finally { - // Clean up - $this->deleteFileViaWebDAV($this->testUser, $this->testUserPassword, $filename); - } - } -} \ No newline at end of file diff --git a/tests/integration/README.md b/tests/integration/README.md index 1e717e0..46a8504 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -1,185 +1,10 @@ # Integration Tests -This directory contains PHPUnit integration tests ported from the original BATS (Bash Automated Testing System) tests. These tests verify the functionality of the G DATA Antivirus app by testing real HTTP requests and Docker interactions. - -## Overview - -The integration tests are organized into the following test classes: - -- **BaseIntegrationTest.php**: Base class providing common functionality for HTTP requests, Docker interactions, and environment setup -- **FileUploadTest.php**: Tests for file uploads via WebDAV (EICAR, clean files, PUP files) -- **TagManagementTest.php**: Tests for file tagging functionality (won't scan, unscanned tags) -- **ApiEndpointTest.php**: Tests for REST API endpoints (GET/POST operations, settings validation) - -## Features Tested - -### File Upload Tests - -- EICAR virus file uploads (should be blocked) -- Clean file uploads (should succeed) -- PUP (Potentially Unwanted Program) file uploads (should succeed) -- Tests performed for both admin and test user accounts - -### Tag Management Tests - -- "Won't scan" tags for files exceeding size limits -- "Unscanned" tags for files that haven't been processed -- Tag verification and counting - -### API Endpoint Tests - -- GET endpoints for retrieving configuration and status -- POST endpoints for updating settings -- Authentication and authorization testing -- Error handling (invalid endpoints, malformed requests, missing parameters) +This directory contains PHPUnit integration tests. ## Prerequisites -1. **Docker Environment**: Tests require a running Nextcloud container named `nextcloud-container` -2. **Environment Variables**: Configure the following environment variables (see `.env-test` file): - - - `HOSTNAME`: Nextcloud server hostname (default: `127.0.0.1:8080`) - - `TESTUSER`: Test user name (default: `testuser`) - - `TESTUSER_PASSWORD`: Test user password - - `CLIENT_SECRET`: G DATA VaaS client secret (optional, required for some tests) - - `CLIENT_ID`: G DATA VaaS client ID (optional, required for validation tests) - -3. **Dependencies**: Ensure all Composer dependencies are installed: - ```bash - composer install - ``` - -## Running the Tests - -### Run All Integration Tests - -```bash -cd tests/integration -../vendor/bin/phpunit -``` - -### Run Specific Test Class - -```bash -cd tests/integration -../vendor/bin/phpunit FileUploadTest.php -../vendor/bin/phpunit TagManagementTest.php -../vendor/bin/phpunit ApiEndpointTest.php -``` - -### Run Individual Test Method - -```bash -cd tests/integration -../vendor/bin/phpunit --filter testAdminEicarUpload FileUploadTest.php -``` - -### Verbose Output - -```bash -cd tests/integration -../vendor/bin/phpunit --verbose -``` - -## Test Environment Setup - -The tests automatically perform the following setup operations: - -1. Create temporary directory for test files -2. Download PUP test file from AMTSO -3. Create test user in Nextcloud -4. Configure G DATA VaaS client secret (if available) -5. Enable the G DATA VaaS app -6. Perform initial file scan - -## Environment Configuration - -The tests load configuration from multiple sources in this order: - -1. `tests/bats/.env-test` (main test configuration) -2. `.env-local` (local overrides) -3. `.env` (fallback) -4. Built-in defaults - -### Key Environment Variables - -```bash -# Nextcloud server configuration -HOSTNAME=127.0.0.1:8080 -MAIL_HOSTNAME=127.0.0.1:8081 - -# Test data configuration -FOLDER_PREFIX=./tmp/functionality-parallel -TESTUSER=testuser -TESTUSER_PASSWORD=myfancysecurepassword234 - -# Test files -EICAR_STRING='X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' -CLEAN_STRING='nothingwronghere' - -# Docker configuration -DOCKER_EXEC_WITH_USER='docker exec --env XDEBUG_MODE=off --user www-data' - -# G DATA VaaS configuration (optional) -CLIENT_SECRET='your-client-secret' -CLIENT_ID='your-client-id' -``` - -## Test Data and Cleanup - -- **Temporary Files**: Tests create temporary files in `./tmp/functionality-parallel/` -- **Test Files**: Downloaded PUP file and generated large files are created as needed -- **Automatic Cleanup**: Tests automatically clean up created files after execution -- **Container Files**: Files uploaded to the Docker container are removed after each test - -## Troubleshooting - -### Common Issues - -1. **Docker Container Not Found**: - - - Ensure the Nextcloud container is running: `docker ps | grep nextcloud` - - Check container name matches `nextcloud-container` - -2. **Permission Errors**: - - - Verify Docker daemon is accessible - - Check file permissions in temporary directory - -3. **Network Connection Issues**: - - - Verify Nextcloud is accessible at configured hostname - - Check firewall and port configuration - -4. **Test File Download Failures**: - - Check internet connection for PUP file download - - Verify AMTSO test file availability - -### Debug Output - -Tests provide verbose output including: - -- HTTP response codes and bodies -- Docker command execution results -- File creation and cleanup status -- Tag verification results - -## Comparison with BATS Tests - -These PHPUnit tests provide equivalent functionality to the original BATS tests with additional benefits: - -- **Better Error Handling**: More detailed error messages and debugging information -- **Object-Oriented Structure**: Reusable components and cleaner test organization -- **IDE Integration**: Better support for debugging and code navigation -- **Type Safety**: PHP type hints and better parameter validation -- **Extensibility**: Easier to add new tests and modify existing ones - -## Contributing - -When adding new integration tests: - -1. Extend the appropriate test class or create a new one inheriting from `BaseIntegrationTest` -2. Follow the existing naming conventions -3. Include proper cleanup in `finally` blocks -4. Add appropriate assertions and error messages -5. Document any new environment variables or prerequisites +How-to run: + 1. Setup .env + 2. make test + \ No newline at end of file diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 60046da..67e6416 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -151,197 +151,4 @@ public function testPostRoutesReturn400ForEmptyBody(string $route, array $data): { $this->testPostEndpoint($route, [], "Admin access to {$route}", 400); } - - - // /** - // * Test scan endpoint (POST /scan) - // */ - // public function testScanEndpoint(): void - // { - // // Create a test file first - // $filename = 'test-scan.txt'; - // $containerPath = "/var/www/html/data/admin/files/{$filename}"; - // $testFile = $this->folderPrefix . '/test-scan.txt'; - - // // Create local test file - // file_put_contents($testFile, $this->cleanString); - - // try { - // // Copy to container - // exec("docker cp {$testFile} nextcloud-container:{$containerPath}", $output, $returnCode); - // if ($returnCode !== 0) { - // $this->markTestSkipped('Could not copy test file to container'); - // } - - // // Set proper ownership - // $this->executeDockerCommand("chown www-data:www-data {$containerPath}"); - - // // Scan admin files - // $this->executeDockerCommand("php occ files:scan admin"); - - // // Test scan endpoint - // $this->testPostEndpoint('scan', ['path' => $filename], 'Scan endpoint', 200); - - // } finally { - // // Clean up - // $this->executeDockerCommand("rm -f {$containerPath}"); - // if (file_exists($testFile)) { - // unlink($testFile); - // } - // } - // } - - // /** - // * Data provider for GET endpoints - // */ - // public static function getEndpointsProvider(): array - // { - // return [ - // ['getCounters', 'Get counters'], - // ['getAuthMethod', 'Get auth method'], - // ['getCache', 'Get cache'], - // ['getHashlookup', 'Get hash lookup'], - // ['getSendMailOnVirusUpload', 'Get send mail on virus upload'], - // ['getAutoScan', 'Get auto scan'], - // ['getPrefixMalicious', 'Get prefix malicious'], - // ['getDisableUnscannedTag', 'Get disable unscanned tag'], - // ]; - // } - - // /** - // * Data provider for POST settings endpoints - // */ - // public static function postEndpointsProvider(): array - // { - // return [ - // ['setAutoScan', ['autoScan' => 'true'], 'Set auto scan'], - // ['setPrefixMalicious', ['prefixMalicious' => '[VIRUS] '], 'Set prefix malicious'], - // ['setSendMailOnVirusUpload', ['sendMailOnVirusUpload' => 'false'], 'Set send mail on virus upload'], - // ['setDisableUnscannedTag', ['disableUnscannedTag' => 'false'], 'Set disable unscanned tag'], - // ['setadvancedconfig', ['maxScanFileSize' => '104857600', 'maxUploadFileSize' => '104857600'], 'Set advanced config'], - // ]; - // } - - // /** - // * Test operator settings endpoint - // */ - // public function testOperatorSettingsEndpoint(): void - // { - // $data = [ - // 'autoScan' => 'true', - // 'prefixMalicious' => '[VIRUS]', - // 'sendMailOnVirusUpload' => 'false', - // 'disableUnscannedTag' => 'false' - // ]; - - // $this->testPostEndpoint('operatorSettings', $data, 'Set operator settings', 200); - // } - - // /** - // * Test admin settings endpoint - // */ - // public function testAdminSettingsEndpoint(): void - // { - // $data = [ - // 'authMethod' => 'client-credentials', - // 'clientId' => 'test', - // 'clientSecret' => 'test', - // 'username' => '', - // 'password' => '', - // 'url' => 'https://gateway.production.vaas.gdatasecurity.de' - // ]; - - // $this->testPostEndpoint('adminSettings', $data, 'Set admin settings', 200); - // } - - // /** - // * Test reset all tags endpoint - // */ - // public function testResetAllTagsEndpoint(): void - // { - // $this->testPostEndpoint('resetalltags', [], 'Reset all tags', 200); - // } - - // /** - // * Test settings validation endpoint - // * This test requires valid CLIENT_ID and CLIENT_SECRET environment variables - // */ - // public function testSettingsValidationEndpoint(): void - // { - // if (empty($this->clientId) || empty($this->clientSecret)) { - // $this->markTestSkipped('CLIENT_ID and CLIENT_SECRET environment variables are required for this test'); - // } - - // $data = [ - // 'authMethod' => 'client-credentials', - // 'clientId' => $this->clientId, - // 'clientSecret' => $this->clientSecret, - // 'username' => '', - // 'password' => '', - // 'url' => 'https://gateway.production.vaas.gdatasecurity.de' - // ]; - - // $this->testPostEndpoint('testsettings', $data, 'Test settings', 200); - // } - - // /** - // * Test unauthorized access to admin endpoints - // * Tests that non-admin users get appropriate error responses - // */ - // public function testUnauthorizedAccess(): void - // { - // // Test with regular user credentials (should fail for admin endpoints) - // $this->testGetEndpoint('getCounters', 'Unauthorized get counters', 401, $this->testUser, $this->testUserPassword); - // } - - // /** - // * Test invalid endpoint - // * Tests that non-existent endpoints return 404 - // */ - // public function testInvalidEndpoint(): void - // { - // $url = "http://{$this->hostname}/apps/gdatavaas/nonexistent"; - - // $result = $this->makeHttpRequest('GET', $url, [ - // 'auth' => ['username' => 'admin', 'password' => 'admin'] - // ]); - - // echo "Invalid endpoint result: {$result['http_code']}\n"; - // $this->assertEquals(404, $result['http_code'], 'Expected 404 for non-existent endpoint'); - // } - - // /** - // * Test malformed JSON in POST requests - // */ - // public function testMalformedJsonPost(): void - // { - // $url = "http://{$this->hostname}/apps/gdatavaas/setAutoScan"; - - // $result = $this->makeHttpRequest('POST', $url, [ - // 'auth' => ['username' => 'admin', 'password' => 'admin'], - // 'body' => '{"invalid": json}', // Malformed JSON - // 'headers' => ['Content-Type: application/json'] - // ]); - - // echo "Malformed JSON result: {$result['http_code']}\n"; - // // Depending on implementation, this might return 400 or 500 - // $this->assertContains($result['http_code'], [400, 500], 'Expected error status for malformed JSON'); - // } - - // /** - // * Test endpoint with missing required parameters - // */ - // public function testMissingParameters(): void - // { - // // Test scan endpoint without required 'path' parameter - // $result = $this->makeHttpRequest('POST', "http://{$this->hostname}/apps/gdatavaas/scan", [ - // 'auth' => ['username' => 'admin', 'password' => 'admin'], - // 'body' => json_encode([]), // Empty data - // 'headers' => ['Content-Type: application/json'] - // ]); - - // echo "Missing parameters result: {$result['http_code']}\n"; - // // Should return an error status - // $this->assertGreaterThanOrEqual(400, $result['http_code'], 'Expected error status for missing parameters'); - // } } diff --git a/tests/integration/TagManagementTest.php b/tests/integration/TagManagementTest.php deleted file mode 100644 index 287a6c2..0000000 --- a/tests/integration/TagManagementTest.php +++ /dev/null @@ -1,188 +0,0 @@ - -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -namespace Integration; - -class TagManagementTest extends BaseIntegrationTest -{ - /** - * Test "Won't scan" tag for testuser with large file - * Files larger than the configured scan limit should get the "Won't scan" tag - */ - public function testWontScanTagForTestUser(): void - { - $largeFileName = "{$this->testUser}.too-large.dat"; - $localLargeFile = $this->folderPrefix . '/too-large.dat'; - $containerPath = "/var/www/html/data/{$this->testUser}/files/{$largeFileName}"; - - // Create large file (256MB + 1 byte = 268,435,457 bytes) - // This uses dd command equivalent in PHP - if (!file_exists($localLargeFile)) { - $this->createLargeFile($localLargeFile, 268435457); - } - - try { - // Copy large file to container - $copyResult = $this->executeDockerCommand("cp {$localLargeFile} nextcloud-container:{$containerPath}"); - if ($copyResult['return_code'] !== 0) { - // Alternative approach: use docker cp command directly - exec("docker cp {$localLargeFile} nextcloud-container:{$containerPath}", $output, $returnCode); - if ($returnCode !== 0) { - $this->markTestSkipped('Could not copy large file to container'); - } - } - - // Scan all files - $this->executeDockerCommand("php occ files:scan --all"); - - // Run GDATA VaaS scan - $this->executeDockerCommand("php occ gdatavaas:scan"); - - // Get tags for the file - $filePath = "{$this->testUser}/files/{$largeFileName}"; - - // Verify "Won't scan" tag is present - $this->assertHasTag($filePath, "Won't scan"); - - // Verify only one tag is present - $this->assertTagCount($filePath, 1); - - } finally { - // Clean up - remove file from container - $this->executeDockerCommand("rm -f {$containerPath}"); - - // Clean up local large file - if (file_exists($localLargeFile)) { - unlink($localLargeFile); - } - } - } - - /** - * Test unscanned job for admin user - * Files that haven't been scanned should get the "Unscanned" tag when running the tag-unscanned command - */ - public function testUnscannedJobForAdmin(): void - { - $filename = 'admin.unscanned.pup.exe'; - $pupFilePath = $this->folderPrefix . '/pup.exe'; - $containerPath = "/var/www/html/data/admin/files/{$filename}"; - - if (!file_exists($pupFilePath)) { - $this->markTestSkipped('PUP file not available'); - } - - try { - // Copy PUP file directly to container (bypassing normal upload process) - exec("docker cp {$pupFilePath} nextcloud-container:{$containerPath}", $output, $returnCode); - if ($returnCode !== 0) { - $this->markTestSkipped('Could not copy PUP file to container for admin'); - } - - // Set proper ownership - $this->executeDockerCommand("chown www-data:www-data {$containerPath}"); - - // Scan files for admin user only - $this->executeDockerCommand("php occ files:scan admin"); - - // Tag unscanned files - $this->executeDockerCommand("php occ gdatavaas:tag-unscanned"); - - // Verify "Unscanned" tag is present - $filePath = "admin/files/{$filename}"; - $this->assertHasTag($filePath, "Unscanned"); - - // Verify only one tag is present - $this->assertTagCount($filePath, 1); - - } finally { - // Clean up - $this->executeDockerCommand("rm -f {$containerPath}"); - } - } - - /** - * Test unscanned job for test user - */ - public function testUnscannedJobForTestUser(): void - { - $filename = "{$this->testUser}.unscanned.pup.exe"; - $pupFilePath = $this->folderPrefix . '/pup.exe'; - $containerPath = "/var/www/html/data/{$this->testUser}/files/{$filename}"; - - if (!file_exists($pupFilePath)) { - $this->markTestSkipped('PUP file not available'); - } - - try { - // Copy PUP file directly to container (bypassing normal upload process) - exec("docker cp {$pupFilePath} nextcloud-container:{$containerPath}", $output, $returnCode); - if ($returnCode !== 0) { - $this->markTestSkipped('Could not copy PUP file to container for testuser'); - } - - // Set proper ownership (this is done automatically in the BATS test by the docker exec user) - $this->executeDockerCommand("chown www-data:www-data {$containerPath}"); - - // Scan files for test user only - $this->executeDockerCommand("php occ files:scan {$this->testUser}"); - - // Tag unscanned files - $this->executeDockerCommand("php occ gdatavaas:tag-unscanned"); - - // Verify "Unscanned" tag is present - $filePath = "{$this->testUser}/files/{$filename}"; - $this->assertHasTag($filePath, "Unscanned"); - - // Verify only one tag is present - $this->assertTagCount($filePath, 1); - - } finally { - // Clean up - $this->executeDockerCommand("rm -f {$containerPath}"); - } - } - - /** - * Creates a large file with specified size - */ - private function createLargeFile(string $filePath, int $size): void - { - $chunkSize = 1024 * 1024; // 1MB chunks - $handle = fopen($filePath, 'wb'); - - if (!$handle) { - throw new \RuntimeException("Cannot create file: {$filePath}"); - } - - try { - $written = 0; - $zeroChunk = str_repeat("\0", $chunkSize); - - while ($written < $size) { - $remaining = $size - $written; - $writeSize = min($chunkSize, $remaining); - - if ($writeSize < $chunkSize) { - $chunk = str_repeat("\0", $writeSize); - } else { - $chunk = $zeroChunk; - } - - $bytesWritten = fwrite($handle, $chunk); - if ($bytesWritten === false) { - throw new \RuntimeException("Failed to write to file: {$filePath}"); - } - - $written += $bytesWritten; - } - } finally { - fclose($handle); - } - - echo "Created large file: {$filePath} ({$size} bytes)\n"; - } -} \ No newline at end of file From 0dceff7b2513705417862a3186fe46f22a27b677 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Tue, 25 Nov 2025 09:06:15 +0100 Subject: [PATCH 17/38] Review changes --- tests/bats/functionality-parallel.bats | 2 +- tests/integration/README.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/bats/functionality-parallel.bats b/tests/bats/functionality-parallel.bats index 96cae7a..a211353 100755 --- a/tests/bats/functionality-parallel.bats +++ b/tests/bats/functionality-parallel.bats @@ -102,6 +102,6 @@ setup_file() { $DOCKER_EXEC_WITH_USER nextcloud-container rm /var/www/html/data/$TESTUSER/files/$TESTUSER.unscanned.pup.exe } -teardown_file() { +@teardown_file() { rm -rf $FOLDER_PREFIX/ } diff --git a/tests/integration/README.md b/tests/integration/README.md index 46a8504..147fd50 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -1,10 +1,10 @@ # Integration Tests -This directory contains PHPUnit integration tests. +This directory contains PHPUnit integration tests. These tests verify the functionality of the G DATA Antivirus app by testing real HTTP requests +and `php occ` interactions. -## Prerequisites - -How-to run: +## How-to run + 1. In the project root 1. Setup .env - 2. make test - \ No newline at end of file + 1. make test + From c0bfe4bcf28c74824d00bc034ba02a63ff415dc9 Mon Sep 17 00:00:00 2001 From: Verdict-as-a-Service Team Date: Tue, 25 Nov 2025 09:19:01 +0100 Subject: [PATCH 18/38] php-cs --- lib/Controller/SettingsController.php | 10 +++++----- lib/Settings/VaasOperator.php | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 504b2d8..21240c8 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -10,8 +10,8 @@ use OCA\GDataVaas\Service\VerdictService; use OCA\GDataVaas\Settings\VaasOperator; use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\JSONResponse; use OCP\DB\Exception; use OCP\IAppConfig; use OCP\IRequest; @@ -77,7 +77,7 @@ public function setadvancedconfig($tokenEndpoint, $vaasUrl): JSONResponse { return new JSONResponse(['status' => 'success']); } - #[AuthorizedAdminSetting(settings: VaasOperator::class)] + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function setOperatorSettings( $quarantineFolder, $scanOnlyThis, @@ -99,18 +99,18 @@ public function setOperatorSettings( return new JSONResponse(['status' => 'success']); } - #[AuthorizedAdminSetting(settings: VaasOperator::class)] + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function setAutoScan(bool $autoScanFiles): JSONResponse { $this->config->setValueBool($this->appName, 'autoScanFiles', $autoScanFiles); return new JSONResponse(['status' => 'success']); } - #[AuthorizedAdminSetting(settings: VaasOperator::class)] + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function getAutoScan(): JSONResponse { return new JSONResponse(['status' => $this->config->getValueBool($this->appName, 'autoScanFiles')]); } - #[AuthorizedAdminSetting(settings: VaasOperator::class)] + #[AuthorizedAdminSetting(settings: VaasOperator::class)] public function setPrefixMalicious(bool $prefixMalicious): JSONResponse { $this->config->setValueBool($this->appName, 'prefixMalicious', $prefixMalicious); return new JSONResponse(['status' => 'success']); diff --git a/lib/Settings/VaasOperator.php b/lib/Settings/VaasOperator.php index 68bd218..ed2574e 100644 --- a/lib/Settings/VaasOperator.php +++ b/lib/Settings/VaasOperator.php @@ -53,11 +53,11 @@ public function getPriority(): int { #[\Override] public function getName(): ?string { - return $this->l->t('Operator Settings'); - } + return $this->l->t('Operator Settings'); + } #[\Override] - public function getAuthorizedAppConfig(): array { - return []; - } + public function getAuthorizedAppConfig(): array { + return []; + } } From d59aaf1fa52f4506c8d30a78cac9c4a2b9444468 Mon Sep 17 00:00:00 2001 From: Verdict-as-a-Service Team Date: Tue, 25 Nov 2025 09:20:26 +0100 Subject: [PATCH 19/38] php-cs --- tests/integration/BaseIntegrationTest.php | 51 ++-- tests/integration/SettingsControllerTest.php | 264 +++++++++---------- tests/integration/bootstrap.php | 108 ++++---- 3 files changed, 194 insertions(+), 229 deletions(-) diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index ea82e77..058c5fc 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -11,8 +11,7 @@ use PHPUnit\Framework\TestCase; use RuntimeException; -abstract class BaseIntegrationTest extends TestCase -{ +abstract class BaseIntegrationTest extends TestCase { protected string $hostname; protected string $folderPrefix; protected string $testUser; @@ -25,8 +24,7 @@ abstract class BaseIntegrationTest extends TestCase protected static bool $setupCompleted = false; - protected function setUp(): void - { + protected function setUp(): void { parent::setUp(); // Load environment variables @@ -46,8 +44,7 @@ protected function setUp(): void } } - protected function setupEnvironment(): void - { + protected function setupEnvironment(): void { // Create temporary folder if (!is_dir($this->folderPrefix)) { mkdir($this->folderPrefix, 0755, true); @@ -82,8 +79,7 @@ protected function setupEnvironment(): void $this->executeDockerCommand('php occ app:enable gdatavaas'); } - protected function executeDockerCommand(string $command, array $env = []): array - { + protected function executeDockerCommand(string $command, array $env = []): array { $envString = ''; foreach ($env as $key => $value) { $envString .= " --env {$key}=\"{$value}\""; @@ -100,8 +96,7 @@ protected function executeDockerCommand(string $command, array $env = []): array ]; } - protected function makeHttpRequest(string $method, string $url, array $options = []): array - { + protected function makeHttpRequest(string $method, string $url, array $options = []): array { $client = HttpClientBuilder::buildDefault(); $request = new Request($url, $method); @@ -151,8 +146,7 @@ protected function makeHttpRequest(string $method, string $url, array $options = } } - protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array - { + protected function uploadFileViaWebDAV(string $username, string $password, string $filename, string $content): array { $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; return $this->makeHttpRequest('PUT', $url, [ @@ -162,8 +156,7 @@ protected function uploadFileViaWebDAV(string $username, string $password, strin ]); } - protected function deleteFileViaWebDAV(string $username, string $password, string $filename): array - { + protected function deleteFileViaWebDAV(string $username, string $password, string $filename): array { $url = "http://{$this->hostname}/remote.php/dav/files/{$username}/{$filename}"; return $this->makeHttpRequest('DELETE', $url, [ @@ -171,8 +164,7 @@ protected function deleteFileViaWebDAV(string $username, string $password, strin ]); } - protected function uploadFileFromDisk(string $username, string $password, string $filename, string $localPath): array - { + protected function uploadFileFromDisk(string $username, string $password, string $filename, string $localPath): array { if (!file_exists($localPath)) { throw new RuntimeException("File not found: {$localPath}"); } @@ -190,8 +182,7 @@ protected function uploadFileFromDisk(string $username, string $password, string ]); } - protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void - { + protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; $result = $this->makeHttpRequest('GET', $url, [ @@ -201,8 +192,7 @@ protected function testGetEndpoint(string $endpoint, string $description, int $e $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); } - protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void - { + protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; $result = $this->makeHttpRequest('POST', $url, [ @@ -214,32 +204,27 @@ protected function testPostEndpoint(string $endpoint, array $data, string $descr $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); } - protected function assertContainsVirusFound(array $response): void - { + protected function assertContainsVirusFound(array $response): void { $this->assertStringContainsString('Virus found', $response['body'], 'Expected "Virus found" in response body'); } - protected function assertHttpCodeInRange(int $httpCode, int $min = 200, int $max = 299): void - { + protected function assertHttpCodeInRange(int $httpCode, int $min = 200, int $max = 299): void { $this->assertGreaterThanOrEqual($min, $httpCode, "HTTP code {$httpCode} is below expected range {$min}-{$max}"); $this->assertLessThan($max + 1, $httpCode, "HTTP code {$httpCode} is above expected range {$min}-{$max}"); } - protected function getTagsForFile(string $filePath): array - { + protected function getTagsForFile(string $filePath): array { $result = $this->executeDockerCommand("php occ gdatavaas:get-tags-for-file {$filePath}"); return $result['output']; } - protected function assertHasTag(string $filePath, string $expectedTag): void - { + protected function assertHasTag(string $filePath, string $expectedTag): void { $tags = $this->getTagsForFile($filePath); $tagString = implode("\n", $tags); $this->assertStringContainsString($expectedTag, $tagString, "Expected tag '{$expectedTag}' not found in file tags"); } - protected function assertTagCount(string $filePath, int $expectedCount): void - { + protected function assertTagCount(string $filePath, int $expectedCount): void { $tags = $this->getTagsForFile($filePath); // Filter out empty lines $nonEmptyTags = array_filter($tags, function ($line) { @@ -248,8 +233,7 @@ protected function assertTagCount(string $filePath, int $expectedCount): void $this->assertCount($expectedCount, $nonEmptyTags, "Expected {$expectedCount} tags, got " . count($nonEmptyTags)); } - public static function tearDownAfterClass(): void - { + public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); // Clean up temporary files @@ -259,8 +243,7 @@ public static function tearDownAfterClass(): void } } - private static function removeDirectory(string $dir): void - { + private static function removeDirectory(string $dir): void { if (!is_dir($dir)) { return; } diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 67e6416..5f7e4de 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -10,145 +10,127 @@ require_once __DIR__ . '/BaseIntegrationTest.php'; -class SettingsControllerTest extends BaseIntegrationTest -{ - public static function adminGetRouteProvider(): array - { - return [ - ['getAuthMethod'], - ['getCache'], - ['getHashlookup'], - ]; - } - - public static function adminPostRouteProvider(): array - { - return [ - // TODO: use default settings - ['adminSettings', [ - 'username' => 'username', - 'password' => 'password', - 'clientId' => 'clientId', - 'clientSecret' => 'clientSecret', - 'authMethod' => 'authMethod', - 'maxScanSize' => 209715200, - 'timeout' => 900, - 'cache' => true, - 'hashlookup' => true - ]], - // ['setadvancedconfig'], - ]; - } - - public static function operatorGetRouteProvider(): array - { - return [ - ['getSendMailOnVirusUpload'], - ['getAutoScan'], - ['getPrefixMalicious'], - ['getDisableUnscannedTag'], - ['getCounters'], - ]; - } - - public static function operatorPostRouteProvider(): array - { - return [ - ['operatorSettings', [ - 'quarantineFolder' => '', - 'scanOnlyThis' => '', - 'doNotScanThis' => '', - 'notifyMails' => '', - ]], - ['setAutoScan', ['autoScanFiles' => true]], - ['setPrefixMalicious', ['prefixMalicious' => '[VIRUS] ']], - ['setSendMailOnVirusUpload', ['sendMailOnVirusUpload' => 'false']], - ['setDisableUnscannedTag', ['disableUnscannedTag' => 'false']], - ]; - } - - #[DataProvider('adminGetRouteProvider')] - public function testAdminCanAccessAdminGetRoutes(string $route): void - { - $this->testGetEndpoint($route, "Admin access to {$route}", 200); - } - - - #[DataProvider('adminPostRouteProvider')] - public function testAdminCanAccessAdminPostRoutes(string $route, array $data): void - { - $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); - } - - #[DataProvider('operatorGetRouteProvider')] - public function testAdminCanAccessOperatorGetRoutes(string $route): void - { - $this->testGetEndpoint($route, "Admin access to {$route}", 200); - } - - #[DataProvider('operatorPostRouteProvider')] - public function testAdminCanAccessOperatorPostRoutes(string $route, array $data): void - { - $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); - } - - - #[DataProvider('adminGetRouteProvider')] - public function testOperatorCannotAccessAdminGetRoutes(string $route): void - { - $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: "vaas-operator", password: "gdatavaas-operator"); - } - - - #[DataProvider('adminPostRouteProvider')] - public function testOperatorCannotAccessAdminPostRoutes(string $route, array $data): void - { - $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: "vaas-operator", password: "gdatavaas-operator"); - } - - #[DataProvider('operatorGetRouteProvider')] - public function testOperatorCanAccessOperatorGetRoutes(string $route): void - { - $this->testGetEndpoint($route, "Operator access to {$route}", 200, username: "vaas-operator", password: "gdatavaas-operator"); - } - - #[DataProvider('operatorPostRouteProvider')] - public function testOperatorCanAccessOperatorPostRoutes(string $route, array $data): void - { - $this->testPostEndpoint($route, $data, "Operator access to {$route}", 200, username: "vaas-operator", password: "gdatavaas-operator"); - } - - - #[DataProvider('adminGetRouteProvider')] - public function testUserCannotAccessAdminGetRoutes(string $route): void - { - $this->testGetEndpoint($route, "User access to {$route}", 403, username: "user", password: "gdatavaas-user"); - } - - - #[DataProvider('adminPostRouteProvider')] - public function testUserCannotAccessAdminPostRoutes(string $route, array $data): void - { - $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: "user", password: "gdatavaas-user"); - } - - #[DataProvider('operatorGetRouteProvider')] - public function testUserCannotAccessOperatorGetRoutes(string $route): void - { - $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: "user", password: "gdatavaas-user"); - } - - #[DataProvider('operatorPostRouteProvider')] - public function testUserCannotAccessOperatorPostRoutes(string $route, array $data): void - { - $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: "user", password: "gdatavaas-user"); - } - - - #[DataProvider('adminPostRouteProvider')] - #[DataProvider('operatorPostRouteProvider')] - public function testPostRoutesReturn400ForEmptyBody(string $route, array $data): void - { - $this->testPostEndpoint($route, [], "Admin access to {$route}", 400); - } +class SettingsControllerTest extends BaseIntegrationTest { + public static function adminGetRouteProvider(): array { + return [ + ['getAuthMethod'], + ['getCache'], + ['getHashlookup'], + ]; + } + + public static function adminPostRouteProvider(): array { + return [ + // TODO: use default settings + ['adminSettings', [ + 'username' => 'username', + 'password' => 'password', + 'clientId' => 'clientId', + 'clientSecret' => 'clientSecret', + 'authMethod' => 'authMethod', + 'maxScanSize' => 209715200, + 'timeout' => 900, + 'cache' => true, + 'hashlookup' => true + ]], + // ['setadvancedconfig'], + ]; + } + + public static function operatorGetRouteProvider(): array { + return [ + ['getSendMailOnVirusUpload'], + ['getAutoScan'], + ['getPrefixMalicious'], + ['getDisableUnscannedTag'], + ['getCounters'], + ]; + } + + public static function operatorPostRouteProvider(): array { + return [ + ['operatorSettings', [ + 'quarantineFolder' => '', + 'scanOnlyThis' => '', + 'doNotScanThis' => '', + 'notifyMails' => '', + ]], + ['setAutoScan', ['autoScanFiles' => true]], + ['setPrefixMalicious', ['prefixMalicious' => '[VIRUS] ']], + ['setSendMailOnVirusUpload', ['sendMailOnVirusUpload' => 'false']], + ['setDisableUnscannedTag', ['disableUnscannedTag' => 'false']], + ]; + } + + #[DataProvider('adminGetRouteProvider')] + public function testAdminCanAccessAdminGetRoutes(string $route): void { + $this->testGetEndpoint($route, "Admin access to {$route}", 200); + } + + + #[DataProvider('adminPostRouteProvider')] + public function testAdminCanAccessAdminPostRoutes(string $route, array $data): void { + $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); + } + + #[DataProvider('operatorGetRouteProvider')] + public function testAdminCanAccessOperatorGetRoutes(string $route): void { + $this->testGetEndpoint($route, "Admin access to {$route}", 200); + } + + #[DataProvider('operatorPostRouteProvider')] + public function testAdminCanAccessOperatorPostRoutes(string $route, array $data): void { + $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); + } + + + #[DataProvider('adminGetRouteProvider')] + public function testOperatorCannotAccessAdminGetRoutes(string $route): void { + $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: 'vaas-operator', password: 'gdatavaas-operator'); + } + + + #[DataProvider('adminPostRouteProvider')] + public function testOperatorCannotAccessAdminPostRoutes(string $route, array $data): void { + $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: 'vaas-operator', password: 'gdatavaas-operator'); + } + + #[DataProvider('operatorGetRouteProvider')] + public function testOperatorCanAccessOperatorGetRoutes(string $route): void { + $this->testGetEndpoint($route, "Operator access to {$route}", 200, username: 'vaas-operator', password: 'gdatavaas-operator'); + } + + #[DataProvider('operatorPostRouteProvider')] + public function testOperatorCanAccessOperatorPostRoutes(string $route, array $data): void { + $this->testPostEndpoint($route, $data, "Operator access to {$route}", 200, username: 'vaas-operator', password: 'gdatavaas-operator'); + } + + + #[DataProvider('adminGetRouteProvider')] + public function testUserCannotAccessAdminGetRoutes(string $route): void { + $this->testGetEndpoint($route, "User access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); + } + + + #[DataProvider('adminPostRouteProvider')] + public function testUserCannotAccessAdminPostRoutes(string $route, array $data): void { + $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); + } + + #[DataProvider('operatorGetRouteProvider')] + public function testUserCannotAccessOperatorGetRoutes(string $route): void { + $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); + } + + #[DataProvider('operatorPostRouteProvider')] + public function testUserCannotAccessOperatorPostRoutes(string $route, array $data): void { + $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); + } + + + #[DataProvider('adminPostRouteProvider')] + #[DataProvider('operatorPostRouteProvider')] + public function testPostRoutesReturn400ForEmptyBody(string $route, array $data): void { + $this->testPostEndpoint($route, [], "Admin access to {$route}", 400); + } } diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 0f1c065..33c5777 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -13,84 +13,84 @@ // Load environment variables for integration testing $envFile = __DIR__ . '/../bats/.env-test'; if (file_exists($envFile)) { - $envContent = file_get_contents($envFile); - $envLines = explode("\n", $envContent); - - foreach ($envLines as $line) { - $line = trim($line); - if (empty($line) || strpos($line, '#') === 0 || strpos($line, 'export') !== 0) { - continue; - } - - // Remove 'export ' prefix and parse key=value - $line = substr($line, 7); // Remove 'export ' - if (strpos($line, '=') !== false) { - list($key, $value) = explode('=', $line, 2); - // Remove quotes if present - $value = trim($value, '"\''); - $_ENV[$key] = $value; - putenv("$key=$value"); - } - } + $envContent = file_get_contents($envFile); + $envLines = explode("\n", $envContent); + + foreach ($envLines as $line) { + $line = trim($line); + if (empty($line) || strpos($line, '#') === 0 || strpos($line, 'export') !== 0) { + continue; + } + + // Remove 'export ' prefix and parse key=value + $line = substr($line, 7); // Remove 'export ' + if (strpos($line, '=') !== false) { + [$key, $value] = explode('=', $line, 2); + // Remove quotes if present + $value = trim($value, '"\''); + $_ENV[$key] = $value; + putenv("$key=$value"); + } + } } // Load local environment files $localEnvFiles = [PROJECT_ROOT . '/.env-local', PROJECT_ROOT . '/.env']; foreach ($localEnvFiles as $envFile) { - if (file_exists($envFile)) { - $envContent = file_get_contents($envFile); - $envLines = explode("\n", $envContent); - - foreach ($envLines as $line) { - $line = trim($line); - if (empty($line) || strpos($line, '#') === 0) { - continue; - } - - if (strpos($line, '=') !== false) { - list($key, $value) = explode('=', $line, 2); - // Remove quotes if present - $value = trim($value, '"\''); - $_ENV[$key] = $value; - putenv("$key=$value"); - } - } - break; // Use first found env file - } + if (file_exists($envFile)) { + $envContent = file_get_contents($envFile); + $envLines = explode("\n", $envContent); + + foreach ($envLines as $line) { + $line = trim($line); + if (empty($line) || strpos($line, '#') === 0) { + continue; + } + + if (strpos($line, '=') !== false) { + [$key, $value] = explode('=', $line, 2); + // Remove quotes if present + $value = trim($value, '"\''); + $_ENV[$key] = $value; + putenv("$key=$value"); + } + } + break; // Use first found env file + } } // Set default values if not defined if (!isset($_ENV['HOSTNAME'])) { - $_ENV['HOSTNAME'] = '127.0.0.1:8080'; - putenv('HOSTNAME=127.0.0.1:8080'); + $_ENV['HOSTNAME'] = '127.0.0.1:8080'; + putenv('HOSTNAME=127.0.0.1:8080'); } if (!isset($_ENV['FOLDER_PREFIX'])) { - $_ENV['FOLDER_PREFIX'] = './tmp/functionality-parallel'; - putenv('FOLDER_PREFIX=./tmp/functionality-parallel'); + $_ENV['FOLDER_PREFIX'] = './tmp/functionality-parallel'; + putenv('FOLDER_PREFIX=./tmp/functionality-parallel'); } if (!isset($_ENV['TESTUSER'])) { - $_ENV['TESTUSER'] = 'testuser'; - putenv('TESTUSER=testuser'); + $_ENV['TESTUSER'] = 'testuser'; + putenv('TESTUSER=testuser'); } if (!isset($_ENV['TESTUSER_PASSWORD'])) { - $_ENV['TESTUSER_PASSWORD'] = 'myfancysecurepassword234'; - putenv('TESTUSER_PASSWORD=myfancysecurepassword234'); + $_ENV['TESTUSER_PASSWORD'] = 'myfancysecurepassword234'; + putenv('TESTUSER_PASSWORD=myfancysecurepassword234'); } if (!isset($_ENV['EICAR_STRING'])) { - $_ENV['EICAR_STRING'] = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; - putenv('EICAR_STRING=X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'); + $_ENV['EICAR_STRING'] = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; + putenv('EICAR_STRING=X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'); } if (!isset($_ENV['CLEAN_STRING'])) { - $_ENV['CLEAN_STRING'] = 'nothingwronghere'; - putenv('CLEAN_STRING=nothingwronghere'); + $_ENV['CLEAN_STRING'] = 'nothingwronghere'; + putenv('CLEAN_STRING=nothingwronghere'); } if (!isset($_ENV['DOCKER_EXEC_WITH_USER'])) { - $_ENV['DOCKER_EXEC_WITH_USER'] = 'docker exec --env XDEBUG_MODE=off --user www-data'; - putenv('DOCKER_EXEC_WITH_USER=docker exec --env XDEBUG_MODE=off --user www-data'); -} \ No newline at end of file + $_ENV['DOCKER_EXEC_WITH_USER'] = 'docker exec --env XDEBUG_MODE=off --user www-data'; + putenv('DOCKER_EXEC_WITH_USER=docker exec --env XDEBUG_MODE=off --user www-data'); +} From e18109a2ba795d4f1c9b2c5408a857910071e6ed Mon Sep 17 00:00:00 2001 From: Verdict-as-a-Service Team Date: Tue, 25 Nov 2025 09:27:53 +0100 Subject: [PATCH 20/38] SPDX --- .prettierrc | 2 -- .prettierrc.yaml | 5 +++++ tests/integration/README.md | 5 +++++ tests/integration/SettingsControllerTest.php | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 .prettierrc create mode 100644 .prettierrc.yaml diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 1d2127c..0000000 --- a/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ -semi: false -singleQuote: true diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..d700982 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 G DATA CyberDefense AG +# SPDX-License-Identifier: AGPL-3.0-or-later + +semi: false +singleQuote: true diff --git a/tests/integration/README.md b/tests/integration/README.md index 147fd50..eaa3bec 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -1,3 +1,8 @@ + + # Integration Tests This directory contains PHPUnit integration tests. These tests verify the functionality of the G DATA Antivirus app by testing real HTTP requests diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 5f7e4de..873aa73 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -1,6 +1,6 @@ +// SPDX-FileCopyrightText: 2025 G DATA CyberDefense AG // // SPDX-License-Identifier: AGPL-3.0-or-later From 329b7a241f0bd57bb6aa4aa27990e6db330b4767 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Tue, 25 Nov 2025 09:51:07 +0100 Subject: [PATCH 21/38] Lines < 120 chars --- templates/operator.php | 82 +++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 12 deletions(-) diff --git a/templates/operator.php b/templates/operator.php index 54711da..c3e0049 100644 --- a/templates/operator.php +++ b/templates/operator.php @@ -14,26 +14,66 @@

Operator Settings

- - + + - - + + - - + + - - + +
" class="visible">
+
" + class="visible"> + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
- Caution: The use of the "Scan only this" and "Do not scan this" settings should be approached with caution. Using these settings allows malicious users to upload and distribute malicious content via the Nextcloud instance. It is recommended that you carefully consider the implications of these settings and use them in a way that does not jeopardize the security of your system and data. + Caution: The use of the "Scan only this" and "Do not scan this" + settings should be approached with caution. Using these settings allows malicious users to upload + and distribute malicious content via the Nextcloud instance. It is recommended that you carefully + consider the implications of these settings and use them in a way that does not jeopardize the + security of your system and data.
@@ -51,21 +91,39 @@ -
" class="visible">
+ +
" + class="visible"> + +
+ -
" class="visible">
+ +
" + class="visible"> + +
+ -
+ +
+ +
+

From d37e005d760d6828bd1d52a33b48ff512dcc03c5 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Tue, 25 Nov 2025 11:34:15 +0100 Subject: [PATCH 22/38] Lines < 120 chars --- tests/integration/BaseIntegrationTest.php | 17 ++++++- tests/integration/SettingsControllerTest.php | 52 +++++++++++++++++--- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index 058c5fc..aeebc37 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -182,7 +182,13 @@ protected function uploadFileFromDisk(string $username, string $password, string ]); } - protected function testGetEndpoint(string $endpoint, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { + protected function testGetEndpoint( + string $endpoint, + string $description, + int $expectedHttpStatus = 200, + string $username = 'admin', + string $password = 'admin' + ): void { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; $result = $this->makeHttpRequest('GET', $url, [ @@ -192,7 +198,14 @@ protected function testGetEndpoint(string $endpoint, string $description, int $e $this->assertEquals($expectedHttpStatus, $result['http_code'], "Failed: {$description}"); } - protected function testPostEndpoint(string $endpoint, array $data, string $description, int $expectedHttpStatus = 200, string $username = 'admin', string $password = 'admin'): void { + protected function testPostEndpoint( + string $endpoint, + array $data, + string $description, + int $expectedHttpStatus = 200, + string $username = 'admin', + string $password = 'admin' + ): void { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; $result = $this->makeHttpRequest('POST', $url, [ diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 873aa73..8d43335 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -86,23 +86,49 @@ public function testAdminCanAccessOperatorPostRoutes(string $route, array $data) #[DataProvider('adminGetRouteProvider')] public function testOperatorCannotAccessAdminGetRoutes(string $route): void { - $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: 'vaas-operator', password: 'gdatavaas-operator'); + $this->testGetEndpoint( + $route, + "Operator access to {$route}", + 403, + username: 'vaas-operator', + password: 'gdatavaas-operator' + ); } #[DataProvider('adminPostRouteProvider')] public function testOperatorCannotAccessAdminPostRoutes(string $route, array $data): void { - $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: 'vaas-operator', password: 'gdatavaas-operator'); + $this->testPostEndpoint( + $route, + $data, + "Operator access to {$route}", + 403, + username: 'vaas-operator', + password: 'gdatavaas-operator' + ); } #[DataProvider('operatorGetRouteProvider')] public function testOperatorCanAccessOperatorGetRoutes(string $route): void { - $this->testGetEndpoint($route, "Operator access to {$route}", 200, username: 'vaas-operator', password: 'gdatavaas-operator'); + $this->testGetEndpoint( + $route, + "Operator access to {$route}", + 200, + username: 'vaas-operator', + password: 'gdatavaas-operator' + ); } #[DataProvider('operatorPostRouteProvider')] public function testOperatorCanAccessOperatorPostRoutes(string $route, array $data): void { - $this->testPostEndpoint($route, $data, "Operator access to {$route}", 200, username: 'vaas-operator', password: 'gdatavaas-operator'); + $this->testPostEndpoint( + $route, + $data, + "Operator access to {$route}", + 200, + username: 'vaas-operator', + password: 'gdatavaas-operator' + ); } @@ -114,7 +140,14 @@ public function testUserCannotAccessAdminGetRoutes(string $route): void { #[DataProvider('adminPostRouteProvider')] public function testUserCannotAccessAdminPostRoutes(string $route, array $data): void { - $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); + $this->testPostEndpoint( + $route, + $data, + "Operator access to {$route}", + 403, + username: 'user', + password: 'gdatavaas-user' + ); } #[DataProvider('operatorGetRouteProvider')] @@ -124,7 +157,14 @@ public function testUserCannotAccessOperatorGetRoutes(string $route): void { #[DataProvider('operatorPostRouteProvider')] public function testUserCannotAccessOperatorPostRoutes(string $route, array $data): void { - $this->testPostEndpoint($route, $data, "Operator access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); + $this->testPostEndpoint( + $route, + $data, + "Operator access to {$route}", + 403, + username: 'user', + password: 'gdatavaas-user' + ); } From 75103b55bef6cc04db22be67befb270849088016 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Tue, 25 Nov 2025 12:49:26 +0000 Subject: [PATCH 23/38] Remove testPostRoutesReturn400ForEmptyBody --- tests/integration/SettingsControllerTest.php | 114 ++++++++++--------- 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 8d43335..707342e 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -10,8 +10,10 @@ require_once __DIR__ . '/BaseIntegrationTest.php'; -class SettingsControllerTest extends BaseIntegrationTest { - public static function adminGetRouteProvider(): array { +class SettingsControllerTest extends BaseIntegrationTest +{ + public static function adminGetRouteProvider(): array + { return [ ['getAuthMethod'], ['getCache'], @@ -19,7 +21,8 @@ public static function adminGetRouteProvider(): array { ]; } - public static function adminPostRouteProvider(): array { + public static function adminPostRouteProvider(): array + { return [ // TODO: use default settings ['adminSettings', [ @@ -37,7 +40,8 @@ public static function adminPostRouteProvider(): array { ]; } - public static function operatorGetRouteProvider(): array { + public static function operatorGetRouteProvider(): array + { return [ ['getSendMailOnVirusUpload'], ['getAutoScan'], @@ -47,7 +51,8 @@ public static function operatorGetRouteProvider(): array { ]; } - public static function operatorPostRouteProvider(): array { + public static function operatorPostRouteProvider(): array + { return [ ['operatorSettings', [ 'quarantineFolder' => '', @@ -63,114 +68,119 @@ public static function operatorPostRouteProvider(): array { } #[DataProvider('adminGetRouteProvider')] - public function testAdminCanAccessAdminGetRoutes(string $route): void { + public function testAdminCanAccessAdminGetRoutes(string $route): void + { $this->testGetEndpoint($route, "Admin access to {$route}", 200); } #[DataProvider('adminPostRouteProvider')] - public function testAdminCanAccessAdminPostRoutes(string $route, array $data): void { + public function testAdminCanAccessAdminPostRoutes(string $route, array $data): void + { $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); } #[DataProvider('operatorGetRouteProvider')] - public function testAdminCanAccessOperatorGetRoutes(string $route): void { + public function testAdminCanAccessOperatorGetRoutes(string $route): void + { $this->testGetEndpoint($route, "Admin access to {$route}", 200); } #[DataProvider('operatorPostRouteProvider')] - public function testAdminCanAccessOperatorPostRoutes(string $route, array $data): void { + public function testAdminCanAccessOperatorPostRoutes(string $route, array $data): void + { $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); } #[DataProvider('adminGetRouteProvider')] - public function testOperatorCannotAccessAdminGetRoutes(string $route): void { + public function testOperatorCannotAccessAdminGetRoutes(string $route): void + { $this->testGetEndpoint( - $route, - "Operator access to {$route}", - 403, - username: 'vaas-operator', + $route, + "Operator access to {$route}", + 403, + username: 'vaas-operator', password: 'gdatavaas-operator' ); } #[DataProvider('adminPostRouteProvider')] - public function testOperatorCannotAccessAdminPostRoutes(string $route, array $data): void { + public function testOperatorCannotAccessAdminPostRoutes(string $route, array $data): void + { $this->testPostEndpoint( - $route, - $data, - "Operator access to {$route}", - 403, - username: 'vaas-operator', + $route, + $data, + "Operator access to {$route}", + 403, + username: 'vaas-operator', password: 'gdatavaas-operator' ); } #[DataProvider('operatorGetRouteProvider')] - public function testOperatorCanAccessOperatorGetRoutes(string $route): void { + public function testOperatorCanAccessOperatorGetRoutes(string $route): void + { $this->testGetEndpoint( - $route, - "Operator access to {$route}", - 200, - username: 'vaas-operator', + $route, + "Operator access to {$route}", + 200, + username: 'vaas-operator', password: 'gdatavaas-operator' ); } #[DataProvider('operatorPostRouteProvider')] - public function testOperatorCanAccessOperatorPostRoutes(string $route, array $data): void { + public function testOperatorCanAccessOperatorPostRoutes(string $route, array $data): void + { $this->testPostEndpoint( - $route, - $data, - "Operator access to {$route}", - 200, - username: 'vaas-operator', + $route, + $data, + "Operator access to {$route}", + 200, + username: 'vaas-operator', password: 'gdatavaas-operator' ); } #[DataProvider('adminGetRouteProvider')] - public function testUserCannotAccessAdminGetRoutes(string $route): void { + public function testUserCannotAccessAdminGetRoutes(string $route): void + { $this->testGetEndpoint($route, "User access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); } #[DataProvider('adminPostRouteProvider')] - public function testUserCannotAccessAdminPostRoutes(string $route, array $data): void { + public function testUserCannotAccessAdminPostRoutes(string $route, array $data): void + { $this->testPostEndpoint( - $route, - $data, - "Operator access to {$route}", - 403, - username: 'user', + $route, + $data, + "Operator access to {$route}", + 403, + username: 'user', password: 'gdatavaas-user' ); } #[DataProvider('operatorGetRouteProvider')] - public function testUserCannotAccessOperatorGetRoutes(string $route): void { + public function testUserCannotAccessOperatorGetRoutes(string $route): void + { $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); } #[DataProvider('operatorPostRouteProvider')] - public function testUserCannotAccessOperatorPostRoutes(string $route, array $data): void { + public function testUserCannotAccessOperatorPostRoutes(string $route, array $data): void + { $this->testPostEndpoint( - $route, - $data, - "Operator access to {$route}", - 403, - username: 'user', + $route, + $data, + "Operator access to {$route}", + 403, + username: 'user', password: 'gdatavaas-user' ); } - - - #[DataProvider('adminPostRouteProvider')] - #[DataProvider('operatorPostRouteProvider')] - public function testPostRoutesReturn400ForEmptyBody(string $route, array $data): void { - $this->testPostEndpoint($route, [], "Admin access to {$route}", 400); - } } From 3f8a165dd1e95d8628da4c829724236864c625cb Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Tue, 25 Nov 2025 13:51:27 +0100 Subject: [PATCH 24/38] Exclude BaseIntegrationTest from tests --- tests/integration/phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/phpunit.xml b/tests/integration/phpunit.xml index 13fa666..1889748 100644 --- a/tests/integration/phpunit.xml +++ b/tests/integration/phpunit.xml @@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later > . + BaseIntegrationTest.php From f2e07848604bd4a1d28920ad52c3e903529b28de Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Tue, 25 Nov 2025 15:31:16 +0100 Subject: [PATCH 25/38] Update copyright --- lib/Settings/VaasOperator.php | 2 +- src/operator-settings.js | 2 +- templates/admin.php | 2 +- templates/operator.php | 2 +- tests/integration/BaseIntegrationTest.php | 2 +- tests/integration/bootstrap.php | 2 +- tests/integration/phpunit.xml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/Settings/VaasOperator.php b/lib/Settings/VaasOperator.php index ed2574e..bbcc38e 100644 --- a/lib/Settings/VaasOperator.php +++ b/lib/Settings/VaasOperator.php @@ -1,6 +1,6 @@ +// SPDX-FileCopyrightText: 2025 G DATA CyberDefense AG // // SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/src/operator-settings.js b/src/operator-settings.js index 8a5349e..1a21875 100644 --- a/src/operator-settings.js +++ b/src/operator-settings.js @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Lennart Dohmann +// SPDX-FileCopyrightText: 2025 G DATA CyberDefense AG // // SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/templates/admin.php b/templates/admin.php index f160411..0e1af93 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -1,6 +1,6 @@ + * SPDX-FileCopyrightText: 2025 G DATA CyberDefense AG * SPDX-License-Identifier: AGPL-3.0-or-later */ diff --git a/templates/operator.php b/templates/operator.php index c3e0049..b6002c0 100644 --- a/templates/operator.php +++ b/templates/operator.php @@ -1,6 +1,6 @@ + * SPDX-FileCopyrightText: 2025 G DATA CyberDefense AG * SPDX-License-Identifier: AGPL-3.0-or-later */ diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index aeebc37..f051ba5 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -1,6 +1,6 @@ +// SPDX-FileCopyrightText: 2025 G DATA CyberDefense AG // // SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 33c5777..ed20611 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -1,6 +1,6 @@ +// SPDX-FileCopyrightText: 2025 G DATA CyberDefense AG // // SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/tests/integration/phpunit.xml b/tests/integration/phpunit.xml index 1889748..60bd213 100644 --- a/tests/integration/phpunit.xml +++ b/tests/integration/phpunit.xml @@ -1,7 +1,7 @@ From fa4b8dfd6a8a1c611610222d91ce97770fb8bbc4 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 00:54:49 +0000 Subject: [PATCH 26/38] apply more comments --- Makefile | 7 ++ appinfo/routes.php | 2 +- composer.json | 3 +- lib/Controller/SettingsController.php | 2 +- src/admin-settings.js | 2 +- tests/integration/SettingsControllerTest.php | 9 +- tests/integration/bootstrap.php | 99 ++++++++++---------- 7 files changed, 67 insertions(+), 57 deletions(-) diff --git a/Makefile b/Makefile index a1d0d1c..e167f99 100644 --- a/Makefile +++ b/Makefile @@ -77,6 +77,13 @@ unittests: composer install ./vendor/bin/phpunit --bootstrap tests/unittests/bootstrap.php tests/unittests/ --testdox +# Run integration tests +.PHONY: integrationtests +integrationtests: + ./scripts/run-app.sh "32.0.0" 1 + composer install + ./vendor/bin/phpunit --bootstrap tests/integration/bootstrap.php tests/integration/ --testdox + # Run bats tests .PHONY: bats bats: diff --git a/appinfo/routes.php b/appinfo/routes.php index 589f07a..cb4dfb8 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -22,7 +22,7 @@ ['name' => 'settings#getCounters', 'url' => '/getCounters', 'verb' => 'GET'], // admin ['name' => 'settings#setAdminSettings', 'url' => '/adminSettings', 'verb' => 'POST'], - ['name' => 'settings#setadvancedconfig', 'url' => '/setadvancedconfig', 'verb' => 'POST'], + ['name' => 'settings#setAdvancedConfig', 'url' => '/setAdvancedConfig', 'verb' => 'POST'], ['name' => 'settings#getAuthMethod', 'url' => '/getAuthMethod', 'verb' => 'GET'], ['name' => 'settings#resetAllTags', 'url' => '/resetalltags', 'verb' => 'POST'], ['name' => 'settings#testsettings', 'url' => '/testsettings', 'verb' => 'POST'], diff --git a/composer.json b/composer.json index 111fe35..b75c58e 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ ], "require": { "gdata/vaas": "11.0.1", - "coduo/php-humanizer": "5.0.0" + "coduo/php-humanizer": "5.0.0", + "vlucas/phpdotenv": "5.6" }, "require-dev": { "nextcloud/ocp": "v32.0.0", diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 21240c8..bc12bcb 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -71,7 +71,7 @@ public function setAdminSettings( return new JSONResponse(['status' => 'success']); } - public function setadvancedconfig($tokenEndpoint, $vaasUrl): JSONResponse { + public function setAdvancedConfig($tokenEndpoint, $vaasUrl): JSONResponse { $this->config->setValueString($this->appName, 'tokenEndpoint', $tokenEndpoint); $this->config->setValueString($this->appName, 'vaasUrl', $vaasUrl); return new JSONResponse(['status' => 'success']); diff --git a/src/admin-settings.js b/src/admin-settings.js index a4d3e92..3aeb20a 100644 --- a/src/admin-settings.js +++ b/src/admin-settings.js @@ -106,7 +106,7 @@ document.addEventListener('DOMContentLoaded', async () => { const tokenEndpoint = document.querySelector('#token_endpoint').value; const vaasUrl = document.querySelector('#vaas_url').value; - const response = await postData(OC.generateUrl('apps/gdatavaas/setadvancedconfig'), { + const response = await postData(OC.generateUrl('apps/gdatavaas/setAdvancedConfig'), { tokenEndpoint, vaasUrl }); diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 707342e..19a1a5e 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -24,19 +24,18 @@ public static function adminGetRouteProvider(): array public static function adminPostRouteProvider(): array { return [ - // TODO: use default settings ['adminSettings', [ - 'username' => 'username', - 'password' => 'password', + 'username' => 'admin', + 'password' => 'admin', 'clientId' => 'clientId', 'clientSecret' => 'clientSecret', - 'authMethod' => 'authMethod', + 'authMethod' => 'ResourceOwnerPassword', 'maxScanSize' => 209715200, 'timeout' => 900, 'cache' => true, 'hashlookup' => true ]], - // ['setadvancedconfig'], + ['setAdvancedConfig', ['tokenEndpoint' => 'tokenEndpoint', 'vaasUrl' => 'vaasUrl']], ]; } diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index ed20611..1ad3056 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -10,54 +10,57 @@ define('INTEGRATION_TEST_ROOT', __DIR__); define('PROJECT_ROOT', __DIR__ . '/../..'); -// Load environment variables for integration testing -$envFile = __DIR__ . '/../bats/.env-test'; -if (file_exists($envFile)) { - $envContent = file_get_contents($envFile); - $envLines = explode("\n", $envContent); - - foreach ($envLines as $line) { - $line = trim($line); - if (empty($line) || strpos($line, '#') === 0 || strpos($line, 'export') !== 0) { - continue; - } - - // Remove 'export ' prefix and parse key=value - $line = substr($line, 7); // Remove 'export ' - if (strpos($line, '=') !== false) { - [$key, $value] = explode('=', $line, 2); - // Remove quotes if present - $value = trim($value, '"\''); - $_ENV[$key] = $value; - putenv("$key=$value"); - } - } -} - -// Load local environment files -$localEnvFiles = [PROJECT_ROOT . '/.env-local', PROJECT_ROOT . '/.env']; -foreach ($localEnvFiles as $envFile) { - if (file_exists($envFile)) { - $envContent = file_get_contents($envFile); - $envLines = explode("\n", $envContent); - - foreach ($envLines as $line) { - $line = trim($line); - if (empty($line) || strpos($line, '#') === 0) { - continue; - } - - if (strpos($line, '=') !== false) { - [$key, $value] = explode('=', $line, 2); - // Remove quotes if present - $value = trim($value, '"\''); - $_ENV[$key] = $value; - putenv("$key=$value"); - } - } - break; // Use first found env file - } -} +$dotenv = Dotenv\Dotenv::createImmutable(PROJECT_ROOT); +$dotenv->load(); + +// // Load environment variables for integration testing +// $envFile = __DIR__ . '/../bats/.env-test'; +// if (file_exists($envFile)) { +// $envContent = file_get_contents($envFile); +// $envLines = explode("\n", $envContent); + +// foreach ($envLines as $line) { +// $line = trim($line); +// if (empty($line) || strpos($line, '#') === 0 || strpos($line, 'export') !== 0) { +// continue; +// } + +// // Remove 'export ' prefix and parse key=value +// $line = substr($line, 7); // Remove 'export ' +// if (strpos($line, '=') !== false) { +// [$key, $value] = explode('=', $line, 2); +// // Remove quotes if present +// $value = trim($value, '"\''); +// $_ENV[$key] = $value; +// putenv("$key=$value"); +// } +// } +// } + +// // Load local environment files +// $localEnvFiles = [PROJECT_ROOT . '/.env-local', PROJECT_ROOT . '/.env']; +// foreach ($localEnvFiles as $envFile) { +// if (file_exists($envFile)) { +// $envContent = file_get_contents($envFile); +// $envLines = explode("\n", $envContent); + +// foreach ($envLines as $line) { +// $line = trim($line); +// if (empty($line) || strpos($line, '#') === 0) { +// continue; +// } + +// if (strpos($line, '=') !== false) { +// [$key, $value] = explode('=', $line, 2); +// // Remove quotes if present +// $value = trim($value, '"\''); +// $_ENV[$key] = $value; +// putenv("$key=$value"); +// } +// } +// break; // Use first found env file +// } +// } // Set default values if not defined if (!isset($_ENV['HOSTNAME'])) { From 0bc9bffbe3aad68011d53799e0e9c19b37c98a90 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Wed, 26 Nov 2025 09:14:09 +0100 Subject: [PATCH 27/38] Remove dead/obsolete code --- tests/integration/bootstrap.php | 87 --------------------------------- 1 file changed, 87 deletions(-) diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 1ad3056..7254bc8 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -6,94 +6,7 @@ require_once __DIR__ . '/../../vendor/autoload.php'; -// Define constants for integration testing -define('INTEGRATION_TEST_ROOT', __DIR__); define('PROJECT_ROOT', __DIR__ . '/../..'); $dotenv = Dotenv\Dotenv::createImmutable(PROJECT_ROOT); $dotenv->load(); - -// // Load environment variables for integration testing -// $envFile = __DIR__ . '/../bats/.env-test'; -// if (file_exists($envFile)) { -// $envContent = file_get_contents($envFile); -// $envLines = explode("\n", $envContent); - -// foreach ($envLines as $line) { -// $line = trim($line); -// if (empty($line) || strpos($line, '#') === 0 || strpos($line, 'export') !== 0) { -// continue; -// } - -// // Remove 'export ' prefix and parse key=value -// $line = substr($line, 7); // Remove 'export ' -// if (strpos($line, '=') !== false) { -// [$key, $value] = explode('=', $line, 2); -// // Remove quotes if present -// $value = trim($value, '"\''); -// $_ENV[$key] = $value; -// putenv("$key=$value"); -// } -// } -// } - -// // Load local environment files -// $localEnvFiles = [PROJECT_ROOT . '/.env-local', PROJECT_ROOT . '/.env']; -// foreach ($localEnvFiles as $envFile) { -// if (file_exists($envFile)) { -// $envContent = file_get_contents($envFile); -// $envLines = explode("\n", $envContent); - -// foreach ($envLines as $line) { -// $line = trim($line); -// if (empty($line) || strpos($line, '#') === 0) { -// continue; -// } - -// if (strpos($line, '=') !== false) { -// [$key, $value] = explode('=', $line, 2); -// // Remove quotes if present -// $value = trim($value, '"\''); -// $_ENV[$key] = $value; -// putenv("$key=$value"); -// } -// } -// break; // Use first found env file -// } -// } - -// Set default values if not defined -if (!isset($_ENV['HOSTNAME'])) { - $_ENV['HOSTNAME'] = '127.0.0.1:8080'; - putenv('HOSTNAME=127.0.0.1:8080'); -} - -if (!isset($_ENV['FOLDER_PREFIX'])) { - $_ENV['FOLDER_PREFIX'] = './tmp/functionality-parallel'; - putenv('FOLDER_PREFIX=./tmp/functionality-parallel'); -} - -if (!isset($_ENV['TESTUSER'])) { - $_ENV['TESTUSER'] = 'testuser'; - putenv('TESTUSER=testuser'); -} - -if (!isset($_ENV['TESTUSER_PASSWORD'])) { - $_ENV['TESTUSER_PASSWORD'] = 'myfancysecurepassword234'; - putenv('TESTUSER_PASSWORD=myfancysecurepassword234'); -} - -if (!isset($_ENV['EICAR_STRING'])) { - $_ENV['EICAR_STRING'] = 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'; - putenv('EICAR_STRING=X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'); -} - -if (!isset($_ENV['CLEAN_STRING'])) { - $_ENV['CLEAN_STRING'] = 'nothingwronghere'; - putenv('CLEAN_STRING=nothingwronghere'); -} - -if (!isset($_ENV['DOCKER_EXEC_WITH_USER'])) { - $_ENV['DOCKER_EXEC_WITH_USER'] = 'docker exec --env XDEBUG_MODE=off --user www-data'; - putenv('DOCKER_EXEC_WITH_USER=docker exec --env XDEBUG_MODE=off --user www-data'); -} From aaee78dc6f02b51e3a13ac3fb4438f3111122cfa Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Wed, 26 Nov 2025 09:19:38 +0100 Subject: [PATCH 28/38] Run all integration tests --- .github/workflows/tests.yml | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2f04d4..37adedb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,8 +9,8 @@ defaults: shell: bash on: push: - branches: ["main"] - tags: ["*"] + branches: ['main'] + tags: ['*'] pull_request: workflow_dispatch: @@ -39,7 +39,7 @@ jobs: env: IS_CI: 1 options: --name nextcloud-antivirus-build-container - needs: + needs: - build-devcontainer steps: - uses: actions/checkout@v4 @@ -47,7 +47,7 @@ jobs: - name: postCreateCommands run: | source .devcontainer/postCreateCommands.sh - + - name: add composer bin to path run: | echo $(composer config home --global)/vendor/bin >> $GITHUB_PATH @@ -82,21 +82,13 @@ jobs: run: | docker network connect nextcloud-gdata-antivirus_nextcloud-network nextcloud-antivirus-build-container - - name: run tests - id: bats-tests + - name: run integration tests env: CLIENT_ID: ${{ secrets.VAAS_CLIENT_ID }} CLIENT_SECRET: ${{ secrets.VAAS_CLIENT_SECRET }} run: | - if bats --verbose-run --timing --trace ./tests/bats; then - echo "bats_run=success" | tee -a "$GITHUB_OUTPUT"; - else - echo "bats_run=fail" | tee -a "$GITHUB_OUTPUT"; - fi - - - name: fail if bats tests did fail - if: steps.bats-tests.outputs.bats_run == 'fail' - run: exit 1 + bats --verbose-run --timing --trace ./tests/bats + ./vendor/bin/phpunit --bootstrap tests/integration/bootstrap.php tests/integration/ - uses: actions/upload-artifact@master with: From 3055527c32346ebad1a8a5a3ac98f273ba6bead8 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 08:38:25 +0000 Subject: [PATCH 29/38] php-cs-fixer --- templates/operator.php | 28 +++++------ tests/integration/BaseIntegrationTest.php | 22 ++++----- tests/integration/SettingsControllerTest.php | 51 +++++++------------- 3 files changed, 42 insertions(+), 59 deletions(-) diff --git a/templates/operator.php b/templates/operator.php index b6002c0..d05c812 100644 --- a/templates/operator.php +++ b/templates/operator.php @@ -15,8 +15,8 @@
-
" +
" class="visible">
@@ -28,8 +28,8 @@ class="visible">
-
@@ -41,8 +41,8 @@ class="visible">
-
@@ -54,8 +54,8 @@ class="visible">
-
@@ -92,8 +92,8 @@ class="visible">
-
" +
" class="visible">
@@ -105,8 +105,8 @@ class="visible">
-
" +
" class="visible">
@@ -118,8 +118,8 @@ class="visible">
-
diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/BaseIntegrationTest.php index f051ba5..22f9ac3 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/BaseIntegrationTest.php @@ -183,11 +183,11 @@ protected function uploadFileFromDisk(string $username, string $password, string } protected function testGetEndpoint( - string $endpoint, - string $description, - int $expectedHttpStatus = 200, - string $username = 'admin', - string $password = 'admin' + string $endpoint, + string $description, + int $expectedHttpStatus = 200, + string $username = 'admin', + string $password = 'admin', ): void { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; @@ -199,12 +199,12 @@ protected function testGetEndpoint( } protected function testPostEndpoint( - string $endpoint, - array $data, - string $description, - int $expectedHttpStatus = 200, - string $username = 'admin', - string $password = 'admin' + string $endpoint, + array $data, + string $description, + int $expectedHttpStatus = 200, + string $username = 'admin', + string $password = 'admin', ): void { $url = "http://{$this->hostname}/apps/gdatavaas/{$endpoint}"; diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index 19a1a5e..d93e12d 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -10,10 +10,8 @@ require_once __DIR__ . '/BaseIntegrationTest.php'; -class SettingsControllerTest extends BaseIntegrationTest -{ - public static function adminGetRouteProvider(): array - { +class SettingsControllerTest extends BaseIntegrationTest { + public static function adminGetRouteProvider(): array { return [ ['getAuthMethod'], ['getCache'], @@ -21,8 +19,7 @@ public static function adminGetRouteProvider(): array ]; } - public static function adminPostRouteProvider(): array - { + public static function adminPostRouteProvider(): array { return [ ['adminSettings', [ 'username' => 'admin', @@ -39,8 +36,7 @@ public static function adminPostRouteProvider(): array ]; } - public static function operatorGetRouteProvider(): array - { + public static function operatorGetRouteProvider(): array { return [ ['getSendMailOnVirusUpload'], ['getAutoScan'], @@ -50,8 +46,7 @@ public static function operatorGetRouteProvider(): array ]; } - public static function operatorPostRouteProvider(): array - { + public static function operatorPostRouteProvider(): array { return [ ['operatorSettings', [ 'quarantineFolder' => '', @@ -67,34 +62,29 @@ public static function operatorPostRouteProvider(): array } #[DataProvider('adminGetRouteProvider')] - public function testAdminCanAccessAdminGetRoutes(string $route): void - { + public function testAdminCanAccessAdminGetRoutes(string $route): void { $this->testGetEndpoint($route, "Admin access to {$route}", 200); } #[DataProvider('adminPostRouteProvider')] - public function testAdminCanAccessAdminPostRoutes(string $route, array $data): void - { + public function testAdminCanAccessAdminPostRoutes(string $route, array $data): void { $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); } #[DataProvider('operatorGetRouteProvider')] - public function testAdminCanAccessOperatorGetRoutes(string $route): void - { + public function testAdminCanAccessOperatorGetRoutes(string $route): void { $this->testGetEndpoint($route, "Admin access to {$route}", 200); } #[DataProvider('operatorPostRouteProvider')] - public function testAdminCanAccessOperatorPostRoutes(string $route, array $data): void - { + public function testAdminCanAccessOperatorPostRoutes(string $route, array $data): void { $this->testPostEndpoint($route, $data, "Admin access to {$route}", 200); } #[DataProvider('adminGetRouteProvider')] - public function testOperatorCannotAccessAdminGetRoutes(string $route): void - { + public function testOperatorCannotAccessAdminGetRoutes(string $route): void { $this->testGetEndpoint( $route, "Operator access to {$route}", @@ -106,8 +96,7 @@ public function testOperatorCannotAccessAdminGetRoutes(string $route): void #[DataProvider('adminPostRouteProvider')] - public function testOperatorCannotAccessAdminPostRoutes(string $route, array $data): void - { + public function testOperatorCannotAccessAdminPostRoutes(string $route, array $data): void { $this->testPostEndpoint( $route, $data, @@ -119,8 +108,7 @@ public function testOperatorCannotAccessAdminPostRoutes(string $route, array $da } #[DataProvider('operatorGetRouteProvider')] - public function testOperatorCanAccessOperatorGetRoutes(string $route): void - { + public function testOperatorCanAccessOperatorGetRoutes(string $route): void { $this->testGetEndpoint( $route, "Operator access to {$route}", @@ -131,8 +119,7 @@ public function testOperatorCanAccessOperatorGetRoutes(string $route): void } #[DataProvider('operatorPostRouteProvider')] - public function testOperatorCanAccessOperatorPostRoutes(string $route, array $data): void - { + public function testOperatorCanAccessOperatorPostRoutes(string $route, array $data): void { $this->testPostEndpoint( $route, $data, @@ -145,15 +132,13 @@ public function testOperatorCanAccessOperatorPostRoutes(string $route, array $da #[DataProvider('adminGetRouteProvider')] - public function testUserCannotAccessAdminGetRoutes(string $route): void - { + public function testUserCannotAccessAdminGetRoutes(string $route): void { $this->testGetEndpoint($route, "User access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); } #[DataProvider('adminPostRouteProvider')] - public function testUserCannotAccessAdminPostRoutes(string $route, array $data): void - { + public function testUserCannotAccessAdminPostRoutes(string $route, array $data): void { $this->testPostEndpoint( $route, $data, @@ -165,14 +150,12 @@ public function testUserCannotAccessAdminPostRoutes(string $route, array $data): } #[DataProvider('operatorGetRouteProvider')] - public function testUserCannotAccessOperatorGetRoutes(string $route): void - { + public function testUserCannotAccessOperatorGetRoutes(string $route): void { $this->testGetEndpoint($route, "Operator access to {$route}", 403, username: 'user', password: 'gdatavaas-user'); } #[DataProvider('operatorPostRouteProvider')] - public function testUserCannotAccessOperatorPostRoutes(string $route, array $data): void - { + public function testUserCannotAccessOperatorPostRoutes(string $route, array $data): void { $this->testPostEndpoint( $route, $data, From ec86529caf03c045a725200675eb9327af6f09d0 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 08:49:39 +0000 Subject: [PATCH 30/38] Find .env file properly --- tests/integration/bootstrap.php | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 7254bc8..578f733 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -4,9 +4,36 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use Dotenv\Dotenv; + require_once __DIR__ . '/../../vendor/autoload.php'; -define('PROJECT_ROOT', __DIR__ . '/../..'); +/** + * Search upward from the given directory for a .env file. + */ +function findEnvDirectory(string $startDir): ?string +{ + $dir = $startDir; + + while (true) { + if (file_exists($dir . '/.env')) { + return $dir; + } + + $parent = dirname($dir); + + // Stop if we've reached the root directory + if ($parent === $dir) { + return null; + } + + $dir = $parent; + } +} + +$envDir = findEnvDirectory(__DIR__); -$dotenv = Dotenv\Dotenv::createImmutable(PROJECT_ROOT); -$dotenv->load(); +if ($envDir !== null) { + $dotenv = Dotenv::createImmutable($envDir); + $dotenv->safeLoad(); +} From bd43233cd03f79cb60c9d3ab1fc4f8f842de1317 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 09:05:17 +0000 Subject: [PATCH 31/38] php-cs-fixer --- tests/integration/bootstrap.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 578f733..8b69ea5 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -11,8 +11,7 @@ /** * Search upward from the given directory for a .env file. */ -function findEnvDirectory(string $startDir): ?string -{ +function findEnvDirectory(string $startDir): ?string { $dir = $startDir; while (true) { From 3d18f8a5de5139e721236cde00218ac2526fd5e1 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 09:10:21 +0000 Subject: [PATCH 32/38] npm install --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55e3473..880c679 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,8 @@ "@nextcloud/webpack-vue-config": "6.3.0" }, "engines": { - "node": "20.0.0", - "npm": "7.0.0 || 8.0.0 || 9.0.0 || 10.0.0 || 11.0.0" + "node": ">=20.0.0", + "npm": ">=7.0.0" } }, "node_modules/@ampproject/remapping": { From bcb7a131d4e12ae4e256af90c0c7243d71efc47d Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 09:33:26 +0000 Subject: [PATCH 33/38] Test timeout, indentation --- templates/operator.php | 77 ++++++++++++++++++----------------- tests/integration/phpunit.xml | 4 +- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/templates/operator.php b/templates/operator.php index d05c812..6f614ee 100644 --- a/templates/operator.php +++ b/templates/operator.php @@ -1,4 +1,5 @@ * SPDX-License-Identifier: AGPL-3.0-or-later @@ -16,63 +17,63 @@
" - class="visible"> - + . 'They can still be downloaded etc. there, but this helps to prevent accidental use.')); ?>" + class="visible"> +
- +
- + . 'file names or file types. Wildcards are not supported.')); ?>" + class="visible"> +
- +
- + . 'file names or file types. Wildcards are not supported.')); ?>" + class="visible"> +
- +
- + . 'or a user tries to upload them. Must be comma-separated.')); ?>" + class="visible"> +
- +
- +
- Caution: The use of the "Scan only this" and "Do not scan this" - settings should be approached with caution. Using these settings allows malicious users to upload - and distribute malicious content via the Nextcloud instance. It is recommended that you carefully - consider the implications of these settings and use them in a way that does not jeopardize the + Caution: The use of the "Scan only this" and "Do not scan this" + settings should be approached with caution. Using these settings allows malicious users to upload + and distribute malicious content via the Nextcloud instance. It is recommended that you carefully + consider the implications of these settings and use them in a way that does not jeopardize the security of your system and data.
@@ -81,10 +82,10 @@ class="visible"> - + @@ -106,9 +107,9 @@ class="visible"> @@ -119,15 +120,15 @@ class="visible">
- +
@@ -93,9 +94,9 @@ class="visible">
" - class="visible"> - + . 'of the file name. Increases the visibility of malicious content.')); ?>" + class="visible"> +
" - class="visible"> - + . 'but they will still be scanned if "Automatic file scanning" is switched on.')); ?>" + class="visible"> +
- + . 'to all \'Notify Mails\' receiver')); ?>" + class="visible"> +

- +

- + \ No newline at end of file diff --git a/tests/integration/phpunit.xml b/tests/integration/phpunit.xml index 60bd213..295e9b8 100644 --- a/tests/integration/phpunit.xml +++ b/tests/integration/phpunit.xml @@ -7,9 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later --> From 8402fd311205cf5feef14d6ee5b1bb04fac6aa78 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 09:55:44 +0000 Subject: [PATCH 34/38] Update packages --- composer.json | 18 +- package-lock.json | 3917 ++++++++++++++++++++++++++++++--------------- package.json | 12 +- 3 files changed, 2597 insertions(+), 1350 deletions(-) diff --git a/composer.json b/composer.json index b75c58e..4ea6f79 100644 --- a/composer.json +++ b/composer.json @@ -10,18 +10,18 @@ ], "require": { "gdata/vaas": "11.0.1", - "coduo/php-humanizer": "5.0.0", - "vlucas/phpdotenv": "5.6" + "coduo/php-humanizer": "5.0.0" }, "require-dev": { - "nextcloud/ocp": "v32.0.0", + "nextcloud/ocp": "v32.0.2", "psalm/phar": "6.8.2", "nextcloud/coding-standard": "v1.4.0", - "colinodell/psr-testlogger": "1.3.0", - "phpunit/phpunit": "10.5.52", - "symfony/console": "v6.4.24", - "amphp/http-client": "5.3", - "amphp/amp": "3.1" + "colinodell/psr-testlogger": "1.3.1", + "phpunit/phpunit": "10.5.58", + "symfony/console": "v6.4.27", + "amphp/http-client": "5.3.4", + "amphp/amp": "3.1.1", + "vlucas/phpdotenv": "5.6.2" }, "autoload": { "psr-4": { @@ -48,4 +48,4 @@ "php": "8.1" } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 880c679..05c4588 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,16 +10,16 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@mdi/svg": "7.4.47", - "@nextcloud/dialogs": "6.3.2", + "@nextcloud/dialogs": "7.1.0", "@nextcloud/files": "3.12.0", - "@nextcloud/router": "3.0.1" + "@nextcloud/router": "3.1.0" }, "devDependencies": { - "@nextcloud/babel-config": "1.2.0", - "@nextcloud/browserslist-config": "3.0.1", + "@nextcloud/babel-config": "1.3.0", + "@nextcloud/browserslist-config": "3.1.2", "@nextcloud/eslint-config": "8.4.2", - "@nextcloud/stylelint-config": "3.1.0", - "@nextcloud/webpack-vue-config": "6.3.0" + "@nextcloud/stylelint-config": "3.1.1", + "@nextcloud/webpack-vue-config": "7.0.2" }, "engines": { "node": ">=20.0.0", @@ -331,15 +331,15 @@ "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -381,11 +381,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.3", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", - "peer": true, "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -1447,9 +1448,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.3", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -1487,12 +1489,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1505,6 +1508,22 @@ "node-fetch": "^3.3.0" } }, + "node_modules/@ckpack/vue-color": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@ckpack/vue-color/-/vue-color-1.6.0.tgz", + "integrity": "sha512-b9kFTKhYbNArfgP1lmnaVm0VNsWdZjqIbyHUYry7mZ+E7JeTQclbjq1+2xWn0SE3wzqRYlXmAVjECPOgteWmMQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.6.0", + "material-colors": "^1.2.6" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.5", "dev": true, @@ -1569,6 +1588,15 @@ "@csstools/css-tokenizer": "^3.0.4" } }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "dev": true, @@ -1602,573 +1630,1016 @@ "node": ">=16" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "dev": true, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "peer": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "peer": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "dev": true, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "android" + ], "peer": true, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "dev": true, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "android" + ], "peer": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "dev": true, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">=18" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "dev": true, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "peer": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@file-type/xml": { - "version": "0.4.3", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "sax": "^1.4.1", - "strtok3": "^10.2.2" + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@floating-ui/utils": "^0.2.10" + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "engines": { + "node": ">=18" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "license": "MIT", - "peer": true - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "dev": true, - "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, "engines": { - "node": ">=10.10.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "dev": true, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">=18" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "dev": true, - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "dev": true, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "dev": true, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "dev": true, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "dev": true, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "license": "MIT", - "peer": true + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "dev": true, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "engines": { + "node": ">=18" } }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "peer": true, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": ">=18" } }, - "node_modules/@jsonjoy.com/buffers": { - "version": "1.0.0", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "peer": true, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": ">=18" } }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "dev": true, - "license": "Apache-2.0", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "peer": true, "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "node": ">=18" } }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.11.0", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "peer": true, "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.1", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=10.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" + "url": "https://opencollective.com/eslint" }, "peerDependencies": { - "tslib": "2" + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.1", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", "dev": true, "license": "Apache-2.0", "peer": true, - "dependencies": { - "@jsonjoy.com/util": "^1.3.0" - }, "engines": { - "node": ">=10.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=10.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@keyv/serialize": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/@linusborg/vue-simple-portal": { - "version": "0.1.5", - "license": "Apache-2.0", "peer": true, "dependencies": { - "nanoid": "^3.1.20" - }, - "peerDependencies": { - "vue": "^2.6.6" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@mapbox/hast-util-table-cell-style": { - "version": "0.2.1", - "license": "BSD-2-Clause", + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", "peer": true, "dependencies": { - "unist-util-visit": "^1.4.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=12" + "node": "*" } }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-is": { - "version": "3.0.0", + "node_modules/@eslint/js": { + "version": "8.57.1", + "dev": true, "license": "MIT", - "peer": true + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-visit": { - "version": "1.4.1", + "node_modules/@file-type/xml": { + "version": "0.4.3", "license": "MIT", - "peer": true, "dependencies": { - "unist-util-visit-parents": "^2.0.0" + "sax": "^1.4.1", + "strtok3": "^10.2.2" } }, - "node_modules/@mapbox/hast-util-table-cell-style/node_modules/unist-util-visit-parents": { - "version": "2.1.2", + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", - "peer": true, "dependencies": { - "unist-util-is": "^3.0.0" + "@floating-ui/utils": "^0.2.10" } }, - "node_modules/@mdi/js": { - "version": "7.4.47", - "license": "Apache-2.0" + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } }, - "node_modules/@mdi/svg": { - "version": "7.4.47", - "license": "Apache-2.0" + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" }, - "node_modules/@nextcloud/auth": { - "version": "2.5.2", - "license": "GPL-3.0-or-later", + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "dev": true, + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@nextcloud/browser-storage": "^0.4.0", - "@nextcloud/event-bus": "^3.3.2" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + "node": ">=10.10.0" } }, - "node_modules/@nextcloud/axios": { - "version": "2.5.1", - "license": "GPL-3.0-or-later", + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "@nextcloud/auth": "^2.3.0", - "@nextcloud/router": "^3.0.1", - "axios": "^1.6.8" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": "*" } }, - "node_modules/@nextcloud/babel-config": { - "version": "1.2.0", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", "dev": true, - "license": "AGPL-3.0-or-later", + "license": "Apache-2.0", + "peer": true, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": ">=12.22" }, - "peerDependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "^7.24.5" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@nextcloud/browser-storage": { - "version": "0.4.0", - "license": "GPL-3.0-or-later", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "devOptional": true, + "license": "MIT", + "peer": true, "dependencies": { - "core-js": "3.37.0" - }, - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@nextcloud/browserslist-config": { - "version": "3.0.1", - "dev": true, - "license": "GPL-3.0-or-later", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "devOptional": true, + "license": "MIT", + "peer": true, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": ">=6.0.0" } }, - "node_modules/@nextcloud/capabilities": { - "version": "1.2.0", - "license": "GPL-3.0-or-later", + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "devOptional": true, + "license": "MIT", + "peer": true, "dependencies": { - "@nextcloud/initial-state": "^2.1.0" - }, - "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/@nextcloud/dialogs": { - "version": "6.3.2", - "license": "AGPL-3.0-or-later", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "devOptional": true, + "license": "MIT", + "peer": true, "dependencies": { - "@mdi/js": "^7.4.47", - "@nextcloud/auth": "^2.5.1", - "@nextcloud/axios": "^2.5.1", - "@nextcloud/browser-storage": "^0.4.0", - "@nextcloud/event-bus": "^3.3.2", - "@nextcloud/files": "^3.10.2", - "@nextcloud/initial-state": "^2.2.0", - "@nextcloud/l10n": "^3.3.0", - "@nextcloud/router": "^3.0.1", - "@nextcloud/sharing": "^0.2.4", - "@nextcloud/typings": "^1.9.1", - "@types/toastify-js": "^1.12.4", - "@vueuse/core": "^11.3.0", - "cancelable-promise": "^4.3.1", - "p-queue": "^8.1.0", - "toastify-js": "^1.12.0", - "vue-frag": "^1.4.3", - "webdav": "^5.8.0" - }, + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "dev": true, + "license": "Apache-2.0", + "peer": true, "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0", - "npm": "^10.5.1" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { - "@nextcloud/vue": "^8.24.0", - "vue": "^2.7.16" + "tslib": "2" } }, - "node_modules/@nextcloud/eslint-config": { - "version": "8.4.2", + "node_modules/@jsonjoy.com/buffers": { + "version": "1.0.0", "dev": true, - "license": "AGPL-3.0-or-later", + "license": "Apache-2.0", + "peer": true, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { - "@babel/core": "^7.26.9", - "@babel/eslint-parser": "^7.16.5", - "@nextcloud/eslint-plugin": "^2.2.1", - "@vue/eslint-config-typescript": "^13.0.0", - "eslint": "^8.27.0", - "eslint-config-standard": "^17.1.0", - "eslint-import-resolver-exports": "^1.0.0-beta.5", - "eslint-import-resolver-typescript": "^3.8.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jsdoc": "^46.2.6", - "eslint-plugin-n": "^16.0.0", - "eslint-plugin-promise": "^6.6.0", - "eslint-plugin-vue": "^9.7.0", - "typescript": "^5.0.2" + "tslib": "2" } }, - "node_modules/@nextcloud/eslint-plugin": { - "version": "2.2.1", + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", "dev": true, - "license": "ISC", + "license": "Apache-2.0", "peer": true, - "dependencies": { - "fast-xml-parser": "^4.2.5", - "requireindex": "^1.2.0", - "semver": "^7.5.3" - }, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { - "eslint": ">=7.0.0" + "tslib": "2" } }, - "node_modules/@nextcloud/eslint-plugin/node_modules/semver": { - "version": "7.7.2", + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.11.0", "dev": true, - "license": "ISC", + "license": "Apache-2.0", "peer": true, - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.1", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0" }, "engines": { - "node": ">=10" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@nextcloud/event-bus": { - "version": "3.3.2", - "license": "GPL-3.0-or-later", + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "peer": true, "dependencies": { - "@types/semver": "^7.5.8", - "semver": "^7.6.3" + "@jsonjoy.com/util": "^1.3.0" }, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" - } - }, - "node_modules/@nextcloud/event-bus/node_modules/semver": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "license": "Apache-2.0" + }, + "node_modules/@mdi/svg": { + "version": "7.4.47", + "license": "Apache-2.0" + }, + "node_modules/@nextcloud/auth": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.5.3.tgz", + "integrity": "sha512-KIhWLk0BKcP4hvypE4o11YqKOPeFMfEFjRrhUUF+h7Fry+dhTBIEIxuQPVCKXMIpjTDd8791y8V6UdRZ2feKAQ==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/event-bus": "^3.3.2" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/axios": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@nextcloud/axios/-/axios-2.5.2.tgz", + "integrity": "sha512-8frJb77jNMbz00TjsSqs1PymY0nIEbNM4mVmwen2tXY7wNgRai6uXilIlXKOYB9jR/F/HKRj6B4vUwVwZbhdbw==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/auth": "^2.5.1", + "@nextcloud/router": "^3.0.1", + "axios": "^1.12.2" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/babel-config": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/babel-config/-/babel-config-1.3.0.tgz", + "integrity": "sha512-qk4mBJahzp2mkiizU9RbeABa6JhqSwR43SXptNQhM3kpxAuP2OAQQhomYnxog/XfFcYExZzOkgRBPlcLEoik0w==", + "dev": true, + "license": "AGPL-3.0-or-later", + "engines": { + "node": "^20 || ^22 || ^24" + }, + "peerDependencies": { + "@babel/core": "^7.27.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/preset-env": "^7.27.2" + } + }, + "node_modules/@nextcloud/browser-storage": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@nextcloud/browser-storage/-/browser-storage-0.5.0.tgz", + "integrity": "sha512-usYr4GlJQlK3hgZURvklqWb9ivi7sgsSuFqXrs7s4hl1LTS4enzPrnkQumm6nRsQruf0ITS+OBsK+oELEbvYPA==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^24 || ^22 || ^20" + } + }, + "node_modules/@nextcloud/browserslist-config": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@nextcloud/browserslist-config/-/browserslist-config-3.1.2.tgz", + "integrity": "sha512-2iXl1rqQOHvggFIl/V3J5OpbodVazOsO38Gz/2sUAmtWXuOpGZG+7i6zQcVqGVaT1VzyPJ1gPiMpyyZi/XRWNA==", + "dev": true, + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20 || ^22 || ^24", + "npm": ">=10.5.0" + }, + "peerDependencies": { + "browserslist": "^4.26.3" + } + }, + "node_modules/@nextcloud/capabilities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@nextcloud/capabilities/-/capabilities-1.2.1.tgz", + "integrity": "sha512-snZ0/910zzwN6PDsIlx2Uvktr1S5x0ClhDUnfPlCj7ntNvECzuVHNY5wzby22LIkc+9ZjaDKtCwuCt2ye+9p/Q==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/initial-state": "^3.0.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/capabilities/node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", + "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/dialogs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-7.1.0.tgz", + "integrity": "sha512-/q4vr4AqJkQhCf8r1i89c9g3A49HgrBkqUCLc9sDUZFLqpTHxC4eqP8zO5/G9iTdVttq8RHva/u8XV8EtGHf3A==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@mdi/js": "^7.4.47", + "@nextcloud/auth": "^2.5.3", + "@nextcloud/axios": "^2.5.2", + "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/event-bus": "^3.3.2", + "@nextcloud/files": "^3.12.0", + "@nextcloud/initial-state": "^3.0.0", + "@nextcloud/l10n": "^3.4.0", + "@nextcloud/paths": "^2.2.1", + "@nextcloud/router": "^3.0.1", + "@nextcloud/sharing": "^0.3.0", + "@nextcloud/typings": "^1.9.1", + "@nextcloud/vue": "^9.0.1", + "@types/toastify-js": "^1.12.4", + "@vueuse/core": "^13.7.0", + "cancelable-promise": "^4.3.1", + "p-queue": "^9.0.0", + "toastify-js": "^1.12.0", + "vite-plugin-node-polyfills": "^0.24.0", + "vue": "^3.5.22", + "webdav": "^5.8.0" + }, + "engines": { + "node": "^20 || ^22 || ^24" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", + "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/sharing": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", + "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@nextcloud/initial-state": "^3.0.0", + "is-svg": "^6.1.0" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + }, + "optionalDependencies": { + "@nextcloud/files": "^3.12.0" + } + }, + "node_modules/@nextcloud/eslint-config": { + "version": "8.4.2", + "dev": true, + "license": "AGPL-3.0-or-later", + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.26.9", + "@babel/eslint-parser": "^7.16.5", + "@nextcloud/eslint-plugin": "^2.2.1", + "@vue/eslint-config-typescript": "^13.0.0", + "eslint": "^8.27.0", + "eslint-config-standard": "^17.1.0", + "eslint-import-resolver-exports": "^1.0.0-beta.5", + "eslint-import-resolver-typescript": "^3.8.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-jsdoc": "^46.2.6", + "eslint-plugin-n": "^16.0.0", + "eslint-plugin-promise": "^6.6.0", + "eslint-plugin-vue": "^9.7.0", + "typescript": "^5.0.2" + } + }, + "node_modules/@nextcloud/eslint-plugin": { + "version": "2.2.1", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "fast-xml-parser": "^4.2.5", + "requireindex": "^1.2.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^20.0.0", + "npm": "^10.0.0" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/@nextcloud/eslint-plugin/node_modules/semver": { + "version": "7.7.2", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nextcloud/event-bus": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@nextcloud/event-bus/-/event-bus-3.3.3.tgz", + "integrity": "sha512-zIfvKmUGkXpVzRKoXrcO9hkoiKDm65fqNxy/XIbIxrQhZByPq3gDkjBpnu3V5Gs8JdYwa73R8DjzV9oH8HYhIg==", + "license": "GPL-3.0-or-later", + "dependencies": { + "@types/semver": "^7.7.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^20 || ^22 || ^24" + } + }, + "node_modules/@nextcloud/event-bus/node_modules/semver": { "version": "7.7.2", "license": "ISC", "bin": { @@ -2207,7 +2678,9 @@ } }, "node_modules/@nextcloud/l10n": { - "version": "3.4.0", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@nextcloud/l10n/-/l10n-3.4.1.tgz", + "integrity": "sha512-aTFinTcKiK2gEXwLgutXekpZZ8/v/4QiC8C3QCLH5m0o+WtxsBC+fqV142ebC/rfDnzCLhY4ZtswSu8bFbZocg==", "license": "GPL-3.0-or-later", "dependencies": { "@nextcloud/router": "^3.0.1", @@ -2240,14 +2713,15 @@ } }, "node_modules/@nextcloud/router": { - "version": "3.0.1", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.1.0.tgz", + "integrity": "sha512-e4dkIaxRSwdZJlZFpn9x03QgBn/Sa2hN1hp/BA7+AbzykmSAlKuWfdmX8j/8ewrLpQwYmZR23IZO9XwpJXq2Uw==", "license": "GPL-3.0-or-later", "dependencies": { - "@nextcloud/typings": "^1.7.0" + "@nextcloud/typings": "^1.10.0" }, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, "node_modules/@nextcloud/sharing": { @@ -2261,15 +2735,16 @@ } }, "node_modules/@nextcloud/stylelint-config": { - "version": "3.1.0", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@nextcloud/stylelint-config/-/stylelint-config-3.1.1.tgz", + "integrity": "sha512-nvkmeHkifV7MEmtNhkYVQXUgcqldm8pbq2TvKPSpdrXB247Xh6OpFhupDbTAgeEQDDRDneayEVfj6e6Kb9w3sQ==", "dev": true, "license": "AGPL-3.0-or-later", "dependencies": { "stylelint-use-logical": "^2.1.2" }, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": "^20 || ^22 || ^24" }, "peerDependencies": { "stylelint": "^16.13.2", @@ -2277,98 +2752,79 @@ "stylelint-config-recommended-vue": "^1.5.0" } }, - "node_modules/@nextcloud/timezones": { - "version": "0.2.0", - "license": "AGPL-3.0-or-later", - "peer": true, - "dependencies": { - "ical.js": "^2.1.0" - }, - "engines": { - "node": "^20 || ^22" - } - }, "node_modules/@nextcloud/typings": { - "version": "1.9.1", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.10.0.tgz", + "integrity": "sha512-SMC42rDjOH3SspPTLMZRv76ZliHpj2JJkF8pGLP8l1QrVTZxE47Qz5qeKmbj2VL+dRv2e/NgixlAFmzVnxkhqg==", "license": "GPL-3.0-or-later", "dependencies": { "@types/jquery": "3.5.16" }, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, "node_modules/@nextcloud/vue": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-8.31.0.tgz", - "integrity": "sha512-P5m3Odfw4m0siu7qs88WJutu2C8mEknqMS1ijlqYtQvc8qZwmpQshgCV5GhyyBTTK9Baicthm+ULglIf/Eq/sg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.3.0.tgz", + "integrity": "sha512-aeRZGqZMwtW62ASxmMsi7H5rnk9Sof3tmlBk7HfScawQzNlkQwquqLRH1h2miRsUGRTkrTgCLcQlQKUQXxQ3YQ==", "license": "AGPL-3.0-or-later", - "peer": true, "dependencies": { + "@ckpack/vue-color": "^1.6.0", "@floating-ui/dom": "^1.7.4", - "@linusborg/vue-simple-portal": "^0.1.5", - "@nextcloud/auth": "^2.5.2", - "@nextcloud/axios": "^2.5.0", - "@nextcloud/browser-storage": "^0.4.0", - "@nextcloud/capabilities": "^1.2.0", - "@nextcloud/event-bus": "^3.3.2", - "@nextcloud/initial-state": "^2.2.0", - "@nextcloud/l10n": "^3.4.0", + "@nextcloud/auth": "^2.5.3", + "@nextcloud/axios": "^2.5.2", + "@nextcloud/browser-storage": "^0.5.0", + "@nextcloud/capabilities": "^1.2.1", + "@nextcloud/event-bus": "^3.3.3", + "@nextcloud/initial-state": "^3.0.0", + "@nextcloud/l10n": "^3.4.1", "@nextcloud/logger": "^3.0.2", - "@nextcloud/router": "^3.0.1", + "@nextcloud/router": "^3.1.0", "@nextcloud/sharing": "^0.3.0", - "@nextcloud/timezones": "^0.2.0", - "@nextcloud/vue-select": "^3.26.0", - "@vueuse/components": "^11.0.0", - "@vueuse/core": "^11.0.0", + "@vuepic/vue-datepicker": "^11.0.3", + "@vueuse/components": "^14.0.0", + "@vueuse/core": "^14.0.0", "blurhash": "^2.0.5", "clone": "^2.1.2", - "debounce": "^2.2.0", - "dompurify": "^3.2.4", + "debounce": "^3.0.0", + "dompurify": "^3.3.0", "emoji-mart-vue-fast": "^15.0.5", "escape-html": "^1.0.3", - "floating-vue": "^1.0.0-beta.19", - "focus-trap": "^7.4.3", - "linkify-string": "^4.3.2", - "md5": "^2.3.0", - "p-queue": "^8.1.1", + "floating-vue": "^5.2.2", + "focus-trap": "^7.6.6", + "linkifyjs": "^4.3.2", + "p-queue": "^9.0.0", "rehype-external-links": "^3.0.0", "rehype-highlight": "^7.0.2", - "rehype-react": "^7.1.2", + "rehype-react": "^8.0.0", "remark-breaks": "^4.0.0", "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", + "remark-rehype": "^11.1.2", "remark-unlink-protocols": "^1.0.0", - "splitpanes": "^2.4.1", - "string-length": "^5.0.1", + "splitpanes": "^4.0.4", "striptags": "^3.2.0", - "tabbable": "^6.2.0", + "tabbable": "^6.3.0", "tributejs": "^5.1.3", - "unified": "^11.0.1", + "ts-md5": "^2.0.1", + "unified": "^11.0.5", "unist-builder": "^4.0.0", "unist-util-visit": "^5.0.0", - "vue": "^2.7.16", - "vue-color": "^2.8.1", - "vue-frag": "^1.4.3", - "vue-router": "^3.6.5", - "vue2-datepicker": "^3.11.0" + "vue": "^3.5.18", + "vue-router": "^4.6.3", + "vue-select": "^4.0.0-beta.6" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + "node": "^20.11.0 || ^22 || ^24" } }, - "node_modules/@nextcloud/vue-select": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue-select/-/vue-select-3.26.0.tgz", - "integrity": "sha512-UvJExrxzx5pP3lv7j6zrv2yj6B1dXph7sh3lLNPnbJPjPoH/yg58mHNFBcPJrRYMbpy2t3hlC6F7s33KCTr9FA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - }, - "peerDependencies": { - "vue": "2.x" + "node_modules/@nextcloud/vue/node_modules/@nextcloud/initial-state": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", + "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing": { @@ -2376,7 +2832,6 @@ "resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz", "integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==", "license": "GPL-3.0-or-later", - "peer": true, "dependencies": { "@nextcloud/initial-state": "^3.0.0", "is-svg": "^6.1.0" @@ -2388,24 +2843,41 @@ "@nextcloud/files": "^3.12.0" } }, - "node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing/node_modules/@nextcloud/initial-state": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-3.0.0.tgz", - "integrity": "sha512-cV+HBdkQJGm8FxkBI5rFT/FbMNWNBvpbj6OPrg4Ae4YOOsQ15CL8InPOAw1t4XkOkQK2NEdUGQLVUz/19wXbdQ==", - "license": "GPL-3.0-or-later", - "peer": true, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + "node_modules/@nextcloud/vue/node_modules/@vueuse/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.0.0.tgz", + "integrity": "sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.0.0", + "@vueuse/shared": "14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@nextcloud/vue/node_modules/@vueuse/metadata": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.0.0.tgz", + "integrity": "sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" } }, "node_modules/@nextcloud/webpack-vue-config": { - "version": "6.3.0", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@nextcloud/webpack-vue-config/-/webpack-vue-config-7.0.2.tgz", + "integrity": "sha512-7pzQptkQ4XEi9hyhLjdJLAyG4ZnnHuDNsOnMBc1RcRniMCTBp8xD5T1H3gCm4lKmB5/pEyuTK52/rj1w/xqmKw==", "dev": true, "hasInstallScript": true, "license": "AGPL-3.0-or-later", "engines": { - "node": "^20.0.0", - "npm": "^9.0.0 || ^10.0.0" + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" }, "peerDependencies": { "@babel/core": "^7.22.9", @@ -2467,90 +2939,454 @@ "node": ">= 8" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "dev": true, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.4.0" - } + "optional": true, + "os": [ + "win32" + ], + "peer": true }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "dev": true, - "hasInstallScript": true, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], "license": "MIT", "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } + "os": [ + "win32" + ], + "peer": true }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "peer": true }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "peer": true }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -2602,8 +3438,9 @@ }, "node_modules/@types/debug": { "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/ms": "*" } @@ -2634,9 +3471,16 @@ }, "node_modules/@types/estree": { "version": "1.0.8", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", "license": "MIT", - "peer": true + "dependencies": { + "@types/estree": "*" + } }, "node_modules/@types/express": { "version": "4.17.23", @@ -2664,8 +3508,9 @@ }, "node_modules/@types/hast": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "*" } @@ -2706,8 +3551,9 @@ }, "node_modules/@types/mdast": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "*" } @@ -2720,12 +3566,13 @@ }, "node_modules/@types/ms": { "version": "2.1.0", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" }, "node_modules/@types/node": { "version": "24.3.0", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -2753,14 +3600,6 @@ "license": "MIT", "peer": true }, - "node_modules/@types/react": { - "version": "19.1.10", - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.0.2" - } - }, "node_modules/@types/retry": { "version": "0.12.2", "dev": true, @@ -2825,11 +3664,14 @@ }, "node_modules/@types/unist": { "version": "3.0.3", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" }, "node_modules/@types/web-bluetooth": { - "version": "0.0.20", + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", "license": "MIT" }, "node_modules/@types/ws": { @@ -3052,8 +3894,7 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.11.1", @@ -3081,26 +3922,62 @@ ], "peer": true }, - "node_modules/@vue/compiler-sfc": { - "version": "2.7.16", - "peer": true, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", "dependencies": { - "@babel/parser": "^7.23.5", - "postcss": "^8.4.14", - "source-map": "^0.6.1" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" } }, - "node_modules/@vue/compiler-sfc/node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/@vue/eslint-config-typescript": { "version": "13.0.0", "dev": true, @@ -3125,117 +4002,158 @@ } } }, - "node_modules/@vueuse/components": { - "version": "11.3.0", + "node_modules/@vue/reactivity": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", "license": "MIT", - "peer": true, "dependencies": { - "@vueuse/core": "11.3.0", - "@vueuse/shared": "11.3.0", - "vue-demi": ">=0.14.10" + "@vue/shared": "3.5.25" } }, - "node_modules/@vueuse/components/node_modules/vue-demi": { - "version": "0.14.10", - "hasInstallScript": true, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", "license": "MIT", - "peer": true, - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" }, - "engines": { - "node": ">=12" + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" + }, + "node_modules/@vuepic/vue-datepicker": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-11.0.3.tgz", + "integrity": "sha512-sb2adwqwK2PizLQOpxCYps2SwhVT6/ic2HMIOqHJXuYa6iAJZWGL5YVlS7O4aW+sk6ZyxlDURLO7kDZPL4HB/w==", + "license": "MIT", + "dependencies": { + "date-fns": "^4.1.0" }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "engines": { + "node": ">=18.12.0" }, "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "vue": ">=3.3.0" } }, - "node_modules/@vueuse/core": { - "version": "11.3.0", + "node_modules/@vueuse/components": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-14.0.0.tgz", + "integrity": "sha512-0PFAbAzKo+Ipt45R0OVHvZwjTj9oDZJQ/lc77d020fKl9GrxEIRvVIzMW1CZVn1vwmGhXEZPIF3erjixW2yqpg==", "license": "MIT", "dependencies": { - "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "11.3.0", - "@vueuse/shared": "11.3.0", - "vue-demi": ">=0.14.10" + "@vueuse/core": "14.0.0", + "@vueuse/shared": "14.0.0" }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "peerDependencies": { + "vue": "^3.5.0" } }, - "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.10", - "hasInstallScript": true, + "node_modules/@vueuse/components/node_modules/@vueuse/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.0.0.tgz", + "integrity": "sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==", "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.0.0", + "@vueuse/shared": "14.0.0" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "vue": "^3.5.0" } }, - "node_modules/@vueuse/metadata": { - "version": "11.3.0", + "node_modules/@vueuse/components/node_modules/@vueuse/metadata": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.0.0.tgz", + "integrity": "sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@vueuse/shared": { - "version": "11.3.0", + "node_modules/@vueuse/core": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz", + "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==", "license": "MIT", "dependencies": { - "vue-demi": ">=0.14.10" + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "13.9.0", + "@vueuse/shared": "13.9.0" }, "funding": { "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" } }, - "node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.10", - "hasInstallScript": true, + "node_modules/@vueuse/core/node_modules/@vueuse/shared": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.9.0.tgz", + "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==", "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz", + "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.0.0.tgz", + "integrity": "sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "peerDependencies": { + "vue": "^3.5.0" } }, "node_modules/@webassemblyjs/ast": { @@ -3476,7 +4394,7 @@ }, "node_modules/acorn": { "version": "8.15.0", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "bin": { @@ -3759,9 +4677,7 @@ }, "node_modules/asn1.js": { "version": "4.10.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -3770,15 +4686,11 @@ }, "node_modules/asn1.js/node_modules/bn.js": { "version": "4.12.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/assert": { "version": "2.1.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", @@ -3807,13 +4719,13 @@ }, "node_modules/asynckit": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -3892,8 +4804,9 @@ }, "node_modules/bail": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3909,7 +4822,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "dev": true, "funding": [ { "type": "github", @@ -3924,8 +4836,18 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", + "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } }, "node_modules/batch": { "version": "0.6.1", @@ -3947,14 +4869,13 @@ }, "node_modules/blurhash": { "version": "2.0.5", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", + "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", + "license": "MIT" }, "node_modules/bn.js": { "version": "5.2.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/body-parser": { "version": "1.20.3", @@ -4047,15 +4968,20 @@ }, "node_modules/brorand": { "version": "1.1.0", - "dev": true, + "license": "MIT" + }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", "license": "MIT", - "peer": true + "dependencies": { + "resolve": "^1.17.0" + } }, "node_modules/browserify-aes": { "version": "1.2.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -4067,9 +4993,7 @@ }, "node_modules/browserify-cipher": { "version": "1.0.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "browserify-aes": "^1.0.4", "browserify-des": "^1.0.0", @@ -4078,9 +5002,7 @@ }, "node_modules/browserify-des": { "version": "1.0.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cipher-base": "^1.0.1", "des.js": "^1.0.0", @@ -4090,9 +5012,7 @@ }, "node_modules/browserify-rsa": { "version": "4.1.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bn.js": "^5.2.1", "randombytes": "^2.1.0", @@ -4104,9 +5024,7 @@ }, "node_modules/browserify-sign": { "version": "4.2.3", - "dev": true, "license": "ISC", - "peer": true, "dependencies": { "bn.js": "^5.2.1", "browserify-rsa": "^4.1.0", @@ -4125,15 +5043,11 @@ }, "node_modules/browserify-sign/node_modules/isarray": { "version": "1.0.0", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/browserify-sign/node_modules/readable-stream": { "version": "2.3.8", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4146,36 +5060,30 @@ }, "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/browserify-sign/node_modules/string_decoder": { "version": "1.1.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/browserify-zlib": { "version": "0.2.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pako": "~1.0.5" } }, "node_modules/browserslist": { - "version": "4.25.3", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, "funding": [ { @@ -4194,10 +5102,11 @@ "license": "MIT", "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001735", - "electron-to-chromium": "^1.5.204", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -4232,15 +5141,13 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true }, "node_modules/buffer-xor": { "version": "1.0.3", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/builtin-modules": { "version": "3.3.0", @@ -4256,9 +5163,7 @@ }, "node_modules/builtin-status-codes": { "version": "3.0.0", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/builtins": { "version": "5.1.0", @@ -4330,9 +5235,7 @@ }, "node_modules/call-bind": { "version": "1.0.8", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -4359,9 +5262,7 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -4387,7 +5288,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001736", + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", "dev": true, "funding": [ { @@ -4406,6 +5309,16 @@ "license": "CC-BY-4.0", "peer": true }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "dev": true, @@ -4422,18 +5335,41 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/char-regex": { + "node_modules/character-entities": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.20" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/character-entities": { - "version": "2.0.2", + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4448,7 +5384,7 @@ }, "node_modules/chokidar": { "version": "4.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -4472,9 +5408,7 @@ }, "node_modules/cipher-base": { "version": "1.0.6", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" @@ -4483,15 +5417,11 @@ "node": ">= 0.10" } }, - "node_modules/clamp": { - "version": "1.0.1", - "license": "MIT", - "peer": true - }, "node_modules/clone": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.8" } @@ -4554,6 +5484,8 @@ }, "node_modules/combined-stream": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4564,8 +5496,9 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4573,7 +5506,7 @@ }, "node_modules/commander": { "version": "2.20.3", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true }, @@ -4647,15 +5580,11 @@ } }, "node_modules/console-browserify": { - "version": "1.2.0", - "dev": true, - "peer": true + "version": "1.2.0" }, "node_modules/constants-browserify": { "version": "1.0.0", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -4700,7 +5629,9 @@ "peer": true }, "node_modules/core-js": { - "version": "3.37.0", + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -4723,9 +5654,7 @@ }, "node_modules/core-util-is": { "version": "1.0.3", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cosmiconfig": { "version": "9.0.0", @@ -4755,9 +5684,7 @@ }, "node_modules/create-ecdh": { "version": "4.0.4", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" @@ -4765,15 +5692,11 @@ }, "node_modules/create-ecdh/node_modules/bn.js": { "version": "4.12.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/create-hash": { "version": "1.2.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -4784,9 +5707,7 @@ }, "node_modules/create-hmac": { "version": "1.1.7", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -4796,6 +5717,12 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, @@ -4819,9 +5746,7 @@ }, "node_modules/crypto-browserify": { "version": "3.12.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "browserify-cipher": "^1.0.1", "browserify-sign": "^4.2.3", @@ -4925,9 +5850,10 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "license": "MIT", - "peer": true + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", @@ -4987,17 +5913,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-format-parse": { - "version": "0.2.7", + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", - "peer": true + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } }, "node_modules/debounce": { - "version": "2.2.0", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-3.0.0.tgz", + "integrity": "sha512-64byRbF0/AirwbuHqB3/ZpMG9/nckDa6ZA0yd6UnaQNwbbemCOwvz2sL5sjXLHhZHADyiwLm0M5qMhltUUx+TA==", "license": "MIT", - "peer": true, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5006,7 +5938,6 @@ "node_modules/debug": { "version": "4.4.1", "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -5021,8 +5952,9 @@ }, "node_modules/decode-named-character-reference": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", "license": "MIT", - "peer": true, "dependencies": { "character-entities": "^2.0.0" }, @@ -5067,9 +5999,7 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5096,9 +6026,7 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -5113,6 +6041,8 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -5129,17 +6059,16 @@ }, "node_modules/dequal": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } }, "node_modules/des.js": { "version": "1.1.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" @@ -5176,8 +6105,9 @@ }, "node_modules/devlop": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", - "peer": true, "dependencies": { "dequal": "^2.0.0" }, @@ -5188,9 +6118,7 @@ }, "node_modules/diffie-hellman": { "version": "5.0.3", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", @@ -5199,9 +6127,7 @@ }, "node_modules/diffie-hellman/node_modules/bn.js": { "version": "4.12.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dir-glob": { "version": "3.0.1", @@ -5293,7 +6219,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.6", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -5332,16 +6260,16 @@ "peer": true }, "node_modules/electron-to-chromium": { - "version": "1.5.207", + "version": "1.5.260", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz", + "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==", "dev": true, "license": "ISC", "peer": true }, "node_modules/elliptic": { "version": "6.6.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -5354,16 +6282,13 @@ }, "node_modules/elliptic/node_modules/bn.js": { "version": "4.12.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/emoji-mart-vue-fast": { "version": "15.0.5", "resolved": "https://registry.npmjs.org/emoji-mart-vue-fast/-/emoji-mart-vue-fast-15.0.5.tgz", "integrity": "sha512-wnxLor8ggpqshoOPwIc33MdOC3A1XFeDLgUwYLPtNPL8VeAtXJAVrnFq1CN5PeCYAFoLo4IufHQZ9CfHD4IZiw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "core-js": "^3.23.5" @@ -5402,9 +6327,7 @@ }, "node_modules/entities": { "version": "4.5.0", - "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -5582,8 +6505,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "peer": true, @@ -6215,6 +7182,22 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "dev": true, @@ -6244,22 +7227,20 @@ }, "node_modules/eventemitter3": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, "node_modules/events": { "version": "3.3.0", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.x" } }, "node_modules/evp_bytestokey": { "version": "1.0.3", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -6343,8 +7324,9 @@ }, "node_modules/extend": { "version": "3.0.2", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -6534,9 +7516,7 @@ }, "node_modules/find-up": { "version": "5.0.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -6578,36 +7558,40 @@ "peer": true }, "node_modules/floating-vue": { - "version": "1.0.0-beta.19", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.2.2.tgz", + "integrity": "sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==", "license": "MIT", - "peer": true, "dependencies": { - "@floating-ui/dom": "^0.1.10", - "vue-resize": "^1.0.0" + "@floating-ui/dom": "~1.1.1", + "vue-resize": "^2.0.0-alpha.1" }, "peerDependencies": { - "vue": "^2.6.10" + "@nuxt/kit": "^3.2.0", + "vue": "^3.2.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } } }, - "node_modules/floating-vue/node_modules/@floating-ui/core": { - "version": "0.3.1", - "license": "MIT", - "peer": true - }, "node_modules/floating-vue/node_modules/@floating-ui/dom": { - "version": "0.1.10", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz", + "integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==", "license": "MIT", - "peer": true, "dependencies": { - "@floating-ui/core": "^0.3.0" + "@floating-ui/core": "^1.1.0" } }, "node_modules/focus-trap": { - "version": "7.6.5", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", + "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "license": "MIT", - "peer": true, "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.3.0" } }, "node_modules/follow-redirects": { @@ -6630,9 +7614,7 @@ }, "node_modules/for-each": { "version": "0.3.5", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-callable": "^1.2.7" }, @@ -6644,7 +7626,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6691,6 +7675,21 @@ "license": "ISC", "peer": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -7004,9 +8003,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -7054,9 +8051,7 @@ }, "node_modules/hash-base": { "version": "3.0.5", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" @@ -7075,9 +8070,7 @@ }, "node_modules/hash.js": { "version": "1.1.7", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -7093,34 +8086,40 @@ "node": ">= 0.4" } }, - "node_modules/hast-to-hyperscript": { - "version": "10.0.3", + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "license": "MIT", - "peer": true, "dependencies": { - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.1", - "web-namespaces": "^2.0.0" + "@types/hast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hast-to-hyperscript/node_modules/@types/unist": { - "version": "2.0.11", - "license": "MIT", - "peer": true - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", - "peer": true, "dependencies": { - "@types/hast": "^3.0.0" + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", @@ -7129,8 +8128,9 @@ }, "node_modules/hast-util-to-text": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", "license": "MIT", - "peer": true, "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", @@ -7143,9 +8143,13 @@ } }, "node_modules/hast-util-whitespace": { - "version": "2.0.1", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", - "peer": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -7153,17 +8157,16 @@ }, "node_modules/highlight.js": { "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=12.0.0" } }, "node_modules/hmac-drbg": { "version": "1.0.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -7345,9 +8348,7 @@ }, "node_modules/https-browserify": { "version": "1.0.0", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/hyperdyperid": { "version": "1.2.0", @@ -7358,11 +8359,6 @@ "node": ">=10.18" } }, - "node_modules/ical.js": { - "version": "2.2.1", - "license": "MPL-2.0", - "peer": true - }, "node_modules/iconv-lite": { "version": "0.4.24", "dev": true, @@ -7389,7 +8385,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "dev": true, "funding": [ { "type": "github", @@ -7404,8 +8399,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", @@ -7418,7 +8412,7 @@ }, "node_modules/immutable": { "version": "5.1.3", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true }, @@ -7478,9 +8472,7 @@ }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", @@ -7489,9 +8481,10 @@ "peer": true }, "node_modules/inline-style-parser": { - "version": "0.1.1", - "license": "MIT", - "peer": true + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -7527,20 +8520,43 @@ }, "node_modules/is-absolute-url": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", "license": "MIT", - "peer": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-arguments": { "version": "1.2.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -7679,9 +8695,7 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -7691,9 +8705,7 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "hasown": "^2.0.2" }, @@ -7737,6 +8749,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "3.0.0", "dev": true, @@ -7787,9 +8809,7 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", @@ -7815,6 +8835,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "dev": true, @@ -7847,9 +8877,7 @@ }, "node_modules/is-nan": { "version": "1.3.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" @@ -7921,8 +8949,9 @@ }, "node_modules/is-plain-obj": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7941,9 +8970,7 @@ }, "node_modules/is-regex": { "version": "1.2.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -8032,9 +9059,7 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "which-typed-array": "^1.1.16" }, @@ -8105,9 +9130,7 @@ }, "node_modules/isarray": { "version": "2.0.5", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -8124,6 +9147,15 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "dev": true, @@ -8160,7 +9192,9 @@ "peer": true }, "node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "peer": true, @@ -8285,18 +9319,11 @@ "license": "MIT", "peer": true }, - "node_modules/linkify-string": { - "version": "4.3.2", - "license": "MIT", - "peer": true, - "peerDependencies": { - "linkifyjs": "^4.0.0" - } - }, "node_modules/linkifyjs": { "version": "4.3.2", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" }, "node_modules/loader-runner": { "version": "4.3.0", @@ -8309,9 +9336,7 @@ }, "node_modules/locate-path": { "version": "6.0.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -8340,21 +9365,27 @@ "license": "MIT", "peer": true }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "license": "MIT", - "peer": true - }, "node_modules/lodash.truncate": { "version": "4.4.2", "dev": true, "license": "MIT", "peer": true }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lowlight": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", @@ -8374,10 +9405,20 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/material-colors": { "version": "1.2.6", - "license": "ISC", - "peer": true + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", + "license": "ISC" }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -8407,9 +9448,7 @@ }, "node_modules/md5.js": { "version": "1.3.5", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -8418,8 +9457,9 @@ }, "node_modules/mdast-squeeze-paragraphs": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-6.0.0.tgz", + "integrity": "sha512-6NDbJPTg0M0Ye+TlYwX1KJ1LFbp515P2immRJyJQhc9Na9cetHzSoHNYIQcXpANEAP1sm9yd/CTZU2uHqR5A+w==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdast": "^4.0.0", "unist-util-visit": "^5.0.0" @@ -8431,8 +9471,9 @@ }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", @@ -8446,8 +9487,9 @@ }, "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8457,8 +9499,9 @@ }, "node_modules/mdast-util-from-markdown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", @@ -8478,10 +9521,71 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-newline-to-break": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" @@ -8491,10 +9595,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", - "peer": true, "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -8511,10 +9630,32 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdast": "^4.0.0" }, @@ -8605,6 +9746,8 @@ }, "node_modules/micromark": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -8616,7 +9759,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -8639,6 +9781,8 @@ }, "node_modules/micromark-core-commonmark": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -8650,7 +9794,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", @@ -8672,6 +9815,8 @@ }, "node_modules/micromark-factory-destination": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", @@ -8683,7 +9828,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -8692,6 +9836,8 @@ }, "node_modules/micromark-factory-label": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", @@ -8703,7 +9849,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", @@ -8713,6 +9858,8 @@ }, "node_modules/micromark-factory-space": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -8724,7 +9871,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -8732,6 +9878,8 @@ }, "node_modules/micromark-factory-title": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", @@ -8743,7 +9891,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -8753,6 +9900,8 @@ }, "node_modules/micromark-factory-whitespace": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8764,7 +9913,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -8774,6 +9922,8 @@ }, "node_modules/micromark-util-character": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -8785,7 +9935,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -8793,6 +9942,8 @@ }, "node_modules/micromark-util-chunked": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", @@ -8804,13 +9955,14 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-classify-character": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -8822,7 +9974,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -8831,6 +9982,8 @@ }, "node_modules/micromark-util-combine-extensions": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -8842,7 +9995,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -8850,6 +10002,8 @@ }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -8861,13 +10015,14 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-decode-string": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8879,7 +10034,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", @@ -8889,6 +10043,8 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -8899,11 +10055,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -8914,11 +10071,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -8930,13 +10088,14 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "node_modules/micromark-util-resolve-all": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -8948,13 +10107,14 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-types": "^2.0.0" } }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -8966,7 +10126,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", @@ -8975,6 +10134,8 @@ }, "node_modules/micromark-util-subtokenize": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -8986,7 +10147,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", @@ -8996,6 +10156,8 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -9006,11 +10168,12 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromark-util-types": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -9021,8 +10184,7 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", @@ -9039,9 +10201,7 @@ }, "node_modules/miller-rabin": { "version": "4.0.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bn.js": "^4.0.0", "brorand": "^1.0.1" @@ -9052,9 +10212,7 @@ }, "node_modules/miller-rabin/node_modules/bn.js": { "version": "4.12.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/mime": { "version": "1.6.0", @@ -9087,15 +10245,11 @@ }, "node_modules/minimalistic-assert": { "version": "1.0.1", - "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/minimatch": { "version": "9.0.5", @@ -9121,8 +10275,7 @@ }, "node_modules/ms": { "version": "2.1.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -9146,7 +10299,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -9289,15 +10441,123 @@ "engines": { "node": ">=16" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/node-stdlib-browser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz", + "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.12.1", + "domain-browser": "4.22.0", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-stdlib-browser/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/node-stdlib-browser/node_modules/domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/node-stdlib-browser/node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" } }, - "node_modules/node-releases": { - "version": "2.0.19", - "dev": true, + "node_modules/node-stdlib-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/node-stdlib-browser/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", - "peer": true + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -9322,9 +10582,7 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -9334,9 +10592,7 @@ }, "node_modules/object-is": { "version": "1.1.6", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" @@ -9350,18 +10606,14 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { "version": "4.1.7", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -9500,9 +10752,7 @@ }, "node_modules/os-browserify": { "version": "0.3.0", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/own-keys": { "version": "1.0.1", @@ -9523,9 +10773,7 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -9538,9 +10786,7 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -9552,16 +10798,16 @@ } }, "node_modules/p-queue": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", - "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.0.1.tgz", + "integrity": "sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==", "license": "MIT", "dependencies": { "eventemitter3": "^5.0.1", - "p-timeout": "^6.1.2" + "p-timeout": "^7.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9585,10 +10831,12 @@ } }, "node_modules/p-timeout": { - "version": "6.1.4", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", "license": "MIT", "engines": { - "node": ">=14.16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9605,9 +10853,7 @@ }, "node_modules/pako": { "version": "1.0.11", - "dev": true, - "license": "(MIT AND Zlib)", - "peer": true + "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { "version": "1.0.1", @@ -9623,9 +10869,7 @@ }, "node_modules/parse-asn1": { "version": "5.1.7", - "dev": true, "license": "ISC", - "peer": true, "dependencies": { "asn1.js": "^4.10.1", "browserify-aes": "^1.2.0", @@ -9638,6 +10882,31 @@ "node": ">= 0.10" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "dev": true, @@ -9667,15 +10936,11 @@ }, "node_modules/path-browserify": { "version": "1.0.1", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/path-exists": { "version": "4.0.0", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -9700,9 +10965,7 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/path-posix": { "version": "1.0.0", @@ -9725,9 +10988,7 @@ }, "node_modules/pbkdf2": { "version": "3.1.3", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "create-hash": "~1.1.3", "create-hmac": "^1.1.7", @@ -9742,9 +11003,7 @@ }, "node_modules/pbkdf2/node_modules/create-hash": { "version": "1.1.3", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -9754,18 +11013,14 @@ }, "node_modules/pbkdf2/node_modules/hash-base": { "version": "2.0.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.1" } }, "node_modules/pbkdf2/node_modules/ripemd160": { "version": "2.0.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "hash-base": "^2.0.0", "inherits": "^2.0.1" @@ -9773,8 +11028,7 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -9854,9 +11108,7 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -9878,7 +11130,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10076,40 +11327,22 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "2.8.8", - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/process": { "version": "0.11.10", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6.0" } }, "node_modules/process-nextick-args": { "version": "2.0.1", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/property-information": { - "version": "6.5.0", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -10139,13 +11372,13 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, "node_modules/public-encrypt": { "version": "4.0.3", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", @@ -10157,9 +11390,7 @@ }, "node_modules/public-encrypt/node_modules/bn.js": { "version": "4.12.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.1", @@ -10172,9 +11403,7 @@ }, "node_modules/qs": { "version": "6.14.0", - "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "side-channel": "^1.1.0" }, @@ -10187,8 +11416,6 @@ }, "node_modules/querystring-es3": { "version": "0.2.1", - "dev": true, - "peer": true, "engines": { "node": ">=0.4.x" } @@ -10219,18 +11446,14 @@ }, "node_modules/randombytes": { "version": "2.1.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } }, "node_modules/randomfill": { "version": "1.0.4", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" @@ -10278,7 +11501,7 @@ }, "node_modules/readdirp": { "version": "4.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "engines": { @@ -10410,8 +11633,9 @@ }, "node_modules/rehype-external-links": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", "license": "MIT", - "peer": true, "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", @@ -10427,8 +11651,9 @@ }, "node_modules/rehype-highlight": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", "license": "MIT", - "peer": true, "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", @@ -10442,111 +11667,14 @@ } }, "node_modules/rehype-react": { - "version": "7.2.0", - "license": "MIT", - "peer": true, - "dependencies": { - "@mapbox/hast-util-table-cell-style": "^0.2.0", - "@types/hast": "^2.0.0", - "hast-to-hyperscript": "^10.0.0", - "hast-util-whitespace": "^2.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=17" - } - }, - "node_modules/rehype-react/node_modules/@types/hast": { - "version": "2.3.10", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/rehype-react/node_modules/@types/unist": { - "version": "2.0.11", - "license": "MIT", - "peer": true - }, - "node_modules/rehype-react/node_modules/is-buffer": { - "version": "2.0.5", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/rehype-react/node_modules/unified": { - "version": "10.1.2", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/vfile": { - "version": "5.3.7", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-react/node_modules/vfile-message": { - "version": "3.1.4", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/rehype-react/-/rehype-react-8.0.0.tgz", + "integrity": "sha512-vzo0YxYbB2HE+36+9HWXVdxNoNDubx63r5LBzpxBGVWM8s9mdnMdbmuJBAX6TTyuGdZjZix6qU3GcSuKCIWivw==", "license": "MIT", - "peer": true, "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" + "@types/hast": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "unified": "^11.0.0" }, "funding": { "type": "opencollective", @@ -10555,8 +11683,9 @@ }, "node_modules/remark-breaks": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", @@ -10569,8 +11698,9 @@ }, "node_modules/remark-parse": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", @@ -10584,8 +11714,9 @@ }, "node_modules/remark-rehype": { "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", - "peer": true, "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", @@ -10600,8 +11731,9 @@ }, "node_modules/remark-unlink-protocols": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/remark-unlink-protocols/-/remark-unlink-protocols-1.0.0.tgz", + "integrity": "sha512-5j/F28jhFmxeyz8nuJYYIWdR4nNpKWZ8A+tVwnK/0pq7Rjue33CINEYSckSq2PZvedhKUwbn08qyiuGoPLBung==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdast": "^4.0.0", "mdast-squeeze-paragraphs": "^6.0.0", @@ -10632,9 +11764,7 @@ }, "node_modules/resolve": { "version": "1.22.10", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", @@ -10734,14 +11864,54 @@ }, "node_modules/ripemd160": { "version": "2.0.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, "node_modules/run-applescript": { "version": "7.0.0", "dev": true, @@ -10798,7 +11968,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "dev": true, "funding": [ { "type": "github", @@ -10813,8 +11982,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", @@ -10834,9 +12002,7 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -10857,7 +12023,7 @@ }, "node_modules/sass": { "version": "1.90.0", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -11152,9 +12318,7 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -11198,9 +12362,7 @@ }, "node_modules/setimmediate": { "version": "1.0.5", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -11210,9 +12372,7 @@ }, "node_modules/sha.js": { "version": "2.4.12", - "dev": true, "license": "(MIT AND BSD-3-Clause)", - "peer": true, "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", @@ -11275,9 +12435,7 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -11294,9 +12452,7 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -11310,9 +12466,7 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -11328,9 +12482,7 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -11406,14 +12558,13 @@ "node_modules/source-map-js": { "version": "1.2.1", "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-support": { "version": "0.5.21", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -11423,7 +12574,7 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "peer": true, "engines": { @@ -11432,8 +12583,9 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -11506,9 +12658,16 @@ } }, "node_modules/splitpanes": { - "version": "2.4.1", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-4.0.4.tgz", + "integrity": "sha512-RbysugZhjbCw5fgplvk3hOXr41stahQDtZhHVkhnnJI6H4wlGDhM2kIpbehy7v92duy9GnMa8zIhHigIV1TWtg==", "license": "MIT", - "peer": true + "funding": { + "url": "https://github.com/sponsors/antoniandre" + }, + "peerDependencies": { + "vue": "^3.2.0" + } }, "node_modules/stable-hash": { "version": "0.0.5", @@ -11540,9 +12699,7 @@ }, "node_modules/stream-browserify": { "version": "3.0.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" @@ -11550,9 +12707,7 @@ }, "node_modules/stream-browserify/node_modules/readable-stream": { "version": "3.6.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11564,9 +12719,7 @@ }, "node_modules/stream-http": { "version": "3.2.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "builtin-status-codes": "^3.0.0", "inherits": "^2.0.4", @@ -11576,9 +12729,7 @@ }, "node_modules/stream-http/node_modules/readable-stream": { "version": "3.6.2", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11590,53 +12741,11 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/string-length": { - "version": "5.0.1", - "license": "MIT", - "peer": true, - "dependencies": { - "char-regex": "^2.0.0", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "6.2.0", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "7.1.0", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/string-width": { "version": "4.2.3", "dev": true, @@ -11707,6 +12816,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "dev": true, @@ -11742,8 +12865,9 @@ }, "node_modules/striptags": { "version": "3.2.0", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz", + "integrity": "sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==", + "license": "MIT" }, "node_modules/strnum": { "version": "1.1.2", @@ -11785,12 +12909,22 @@ "webpack": "^5.27.0" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, "node_modules/style-to-object": { - "version": "0.4.4", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", - "peer": true, "dependencies": { - "inline-style-parser": "0.1.1" + "inline-style-parser": "0.2.7" } }, "node_modules/stylelint": { @@ -12142,9 +13276,7 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -12158,9 +13290,10 @@ "peer": true }, "node_modules/tabbable": { - "version": "6.2.0", - "license": "MIT", - "peer": true + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", + "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "license": "MIT" }, "node_modules/table": { "version": "6.9.0", @@ -12211,7 +13344,7 @@ }, "node_modules/terser": { "version": "5.43.1", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "peer": true, "dependencies": { @@ -12291,9 +13424,7 @@ }, "node_modules/timers-browserify": { "version": "2.0.12", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "setimmediate": "^1.0.4" }, @@ -12301,19 +13432,15 @@ "node": ">=0.6.0" } }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "license": "MIT", - "peer": true - }, "node_modules/tinyglobby": { - "version": "0.2.14", - "dev": true, + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "peer": true, "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -12324,7 +13451,6 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -12341,7 +13467,6 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -12353,9 +13478,7 @@ }, "node_modules/to-buffer": { "version": "1.2.1", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", @@ -12408,13 +13531,15 @@ }, "node_modules/tributejs": { "version": "5.1.3", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/tributejs/-/tributejs-5.1.3.tgz", + "integrity": "sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==", + "license": "MIT" }, "node_modules/trim-lines": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12422,8 +13547,9 @@ }, "node_modules/trough": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -12473,6 +13599,15 @@ "node": ">=10" } }, + "node_modules/ts-md5": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-2.0.1.tgz", + "integrity": "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "dev": true, @@ -12505,9 +13640,7 @@ }, "node_modules/tty-browserify": { "version": "0.0.1", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/type-check": { "version": "0.4.0", @@ -12548,9 +13681,7 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -12622,7 +13753,7 @@ }, "node_modules/typescript": { "version": "5.9.2", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -12657,7 +13788,7 @@ }, "node_modules/undici-types": { "version": "7.10.0", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true }, @@ -12703,8 +13834,9 @@ }, "node_modules/unified": { "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -12721,8 +13853,9 @@ }, "node_modules/unist-builder": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", + "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0" }, @@ -12733,8 +13866,9 @@ }, "node_modules/unist-util-find-after": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" @@ -12745,9 +13879,10 @@ } }, "node_modules/unist-util-is": { - "version": "6.0.0", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0" }, @@ -12758,8 +13893,9 @@ }, "node_modules/unist-util-position": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0" }, @@ -12770,8 +13906,9 @@ }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0" }, @@ -12782,8 +13919,9 @@ }, "node_modules/unist-util-visit": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", @@ -12795,9 +13933,10 @@ } }, "node_modules/unist-util-visit-parents": { - "version": "6.0.1", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" @@ -12851,7 +13990,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "dev": true, "funding": [ { @@ -12891,9 +14032,7 @@ }, "node_modules/url": { "version": "0.11.4", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "punycode": "^1.4.1", "qs": "^6.12.3" @@ -12919,15 +14058,11 @@ }, "node_modules/url/node_modules/punycode": { "version": "1.4.1", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/util": { "version": "0.12.5", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", @@ -12938,9 +14073,7 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -12971,8 +14104,9 @@ }, "node_modules/vfile": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" @@ -12984,8 +14118,9 @@ }, "node_modules/vfile-message": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" @@ -12995,30 +14130,151 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "dev": true, + "node_modules/vite": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", + "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "license": "MIT", - "peer": true + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } }, - "node_modules/vue": { - "version": "2.7.16", + "node_modules/vite-plugin-node-polyfills": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.24.0.tgz", + "integrity": "sha512-GA9QKLH+vIM8NPaGA+o2t8PDfFUl32J8rUp1zQfMKVJQiNkOX4unE51tR6ppl6iKw5yOrDAdSH7r/UIFLCVhLw==", "license": "MIT", - "peer": true, "dependencies": { - "@vue/compiler-sfc": "2.7.16", - "csstype": "^3.1.0" + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/davidmyersdev" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/vue-color": { - "version": "2.8.2", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.25", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "license": "MIT", "dependencies": { - "clamp": "^1.0.1", - "lodash.throttle": "^4.0.0", - "material-colors": "^1.0.0", - "tinycolor2": "^1.1.2" + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/vue-eslint-parser": { @@ -13094,16 +14350,6 @@ "node": ">=10" } }, - "node_modules/vue-frag": { - "version": "1.4.3", - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/vue-frag?sponsor=1" - }, - "peerDependencies": { - "vue": "^2.6.0" - } - }, "node_modules/vue-loader": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.4.2.tgz", @@ -13129,30 +14375,36 @@ } }, "node_modules/vue-resize": { - "version": "1.0.1", + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz", + "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==", "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.13.10" - }, "peerDependencies": { - "vue": "^2.6.0" + "vue": "^3.0.0" } }, "node_modules/vue-router": { - "version": "3.6.5", - "license": "MIT", - "peer": true - }, - "node_modules/vue2-datepicker": { - "version": "3.11.1", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", + "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==", "license": "MIT", - "peer": true, "dependencies": { - "date-format-parse": "^0.2.7" + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" }, "peerDependencies": { - "vue": "^2.5.0" + "vue": "^3.5.0" + } + }, + "node_modules/vue-select": { + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/vue-select/-/vue-select-4.0.0-beta.6.tgz", + "integrity": "sha512-K+zrNBSpwMPhAxYLTCl56gaMrWZGgayoWCLqe5rWwkB8aUbAUh7u6sXjIR7v4ckp2WKC7zEEUY27g6h1MRsIHw==", + "license": "MIT", + "peerDependencies": { + "vue": "3.x" } }, "node_modules/watchpack": { @@ -13177,15 +14429,6 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "license": "MIT", - "peer": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "license": "MIT", @@ -13586,9 +14829,7 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -13686,9 +14927,7 @@ }, "node_modules/xtend": { "version": "4.0.2", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.4" } @@ -13701,15 +14940,23 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index ca2dc98..0a4ab11 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ }, "dependencies": { "@mdi/svg": "7.4.47", - "@nextcloud/dialogs": "6.3.2", + "@nextcloud/dialogs": "7.1.0", "@nextcloud/files": "3.12.0", - "@nextcloud/router": "3.0.1" + "@nextcloud/router": "3.1.0" }, "browserslist": [ "extends @nextcloud/browserslist-config" @@ -31,11 +31,11 @@ "npm": ">=7.0.0" }, "devDependencies": { - "@nextcloud/babel-config": "1.2.0", - "@nextcloud/browserslist-config": "3.0.1", + "@nextcloud/babel-config": "1.3.0", + "@nextcloud/browserslist-config": "3.1.2", "@nextcloud/eslint-config": "8.4.2", - "@nextcloud/stylelint-config": "3.1.0", - "@nextcloud/webpack-vue-config": "6.3.0" + "@nextcloud/stylelint-config": "3.1.1", + "@nextcloud/webpack-vue-config": "7.0.2" }, "overrides": { "@nextcloud/axios": { From 808534d4d9ff60baa70a42b7cf07b22bf78bcdd4 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Wed, 26 Nov 2025 10:40:12 +0100 Subject: [PATCH 35/38] Simplify lint-eslint --- .github/workflows/lint-eslint.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/lint-eslint.yml b/.github/workflows/lint-eslint.yml index 1534c47..1eaab79 100644 --- a/.github/workflows/lint-eslint.yml +++ b/.github/workflows/lint-eslint.yml @@ -13,7 +13,6 @@ on: pull_request permissions: contents: read - jobs: changes: runs-on: ubuntu-latest @@ -43,14 +42,12 @@ jobs: - '**.ts' - '**.vue' - lint: + eslint: runs-on: ubuntu-latest needs: changes if: needs.changes.outputs.src != 'false' - name: NPM lint - steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -80,18 +77,3 @@ jobs: - name: Lint run: npm run lint - - summary: - permissions: - contents: none - runs-on: ubuntu-latest - needs: [changes, lint] - - if: always() - - # This is the summary, we just avoid to rename it so that branch protection rules still match - name: eslint - - steps: - - name: Summary status - run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi From cb1bd19c2655ada58146167f63ffcf387b08baf8 Mon Sep 17 00:00:00 2001 From: Philip Stadermann Date: Wed, 26 Nov 2025 10:57:42 +0100 Subject: [PATCH 36/38] Newline --- templates/operator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/operator.php b/templates/operator.php index 6f614ee..e9d0fa2 100644 --- a/templates/operator.php +++ b/templates/operator.php @@ -131,4 +131,5 @@ class="visible">

- \ No newline at end of file + + From 89df5432800460540f6150252ee93bed4a377c44 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 10:34:50 +0000 Subject: [PATCH 37/38] fix test --- Makefile | 2 +- .../{BaseIntegrationTest.php => IntegrationTestBase.php} | 4 ++-- tests/integration/SettingsControllerTest.php | 4 ++-- tests/integration/phpunit.xml | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) rename tests/integration/{BaseIntegrationTest.php => IntegrationTestBase.php} (98%) diff --git a/Makefile b/Makefile index e167f99..f5c3275 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ unittests: integrationtests: ./scripts/run-app.sh "32.0.0" 1 composer install - ./vendor/bin/phpunit --bootstrap tests/integration/bootstrap.php tests/integration/ --testdox + ./vendor/bin/phpunit -c tests/integration/phpunit.xml tests/integration/ --testdox # Run bats tests .PHONY: bats diff --git a/tests/integration/BaseIntegrationTest.php b/tests/integration/IntegrationTestBase.php similarity index 98% rename from tests/integration/BaseIntegrationTest.php rename to tests/integration/IntegrationTestBase.php index 22f9ac3..19ba99a 100644 --- a/tests/integration/BaseIntegrationTest.php +++ b/tests/integration/IntegrationTestBase.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\TestCase; use RuntimeException; -abstract class BaseIntegrationTest extends TestCase { +abstract class IntegrationTestBase extends TestCase { protected string $hostname; protected string $folderPrefix; protected string $testUser; @@ -28,7 +28,7 @@ protected function setUp(): void { parent::setUp(); // Load environment variables - $this->hostname = $_ENV['HOSTNAME'] ?? '127.0.0.1:8080'; + $this->hostname = $_ENV['NEXTCLOUD_HOSTNAME'] ?? '127.0.0.1:8080'; $this->folderPrefix = $_ENV['FOLDER_PREFIX'] ?? './tmp/functionality-parallel'; $this->testUser = $_ENV['TESTUSER'] ?? 'testuser'; $this->testUserPassword = $_ENV['TESTUSER_PASSWORD'] ?? 'myfancysecurepassword234'; diff --git a/tests/integration/SettingsControllerTest.php b/tests/integration/SettingsControllerTest.php index d93e12d..ce7117b 100644 --- a/tests/integration/SettingsControllerTest.php +++ b/tests/integration/SettingsControllerTest.php @@ -8,9 +8,9 @@ use PHPUnit\Framework\Attributes\DataProvider; -require_once __DIR__ . '/BaseIntegrationTest.php'; +require_once __DIR__ . '/IntegrationTestBase.php'; -class SettingsControllerTest extends BaseIntegrationTest { +class SettingsControllerTest extends IntegrationTestBase { public static function adminGetRouteProvider(): array { return [ ['getAuthMethod'], diff --git a/tests/integration/phpunit.xml b/tests/integration/phpunit.xml index 295e9b8..6dbf87e 100644 --- a/tests/integration/phpunit.xml +++ b/tests/integration/phpunit.xml @@ -12,7 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-or-later > . - BaseIntegrationTest.php From 051a4fedcf44c2f6351cf7f54c32b5440628ca95 Mon Sep 17 00:00:00 2001 From: PT-ATA No One Date: Wed, 26 Nov 2025 10:58:13 +0000 Subject: [PATCH 38/38] add github hostname env --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 37adedb..fc399de 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -86,6 +86,7 @@ jobs: env: CLIENT_ID: ${{ secrets.VAAS_CLIENT_ID }} CLIENT_SECRET: ${{ secrets.VAAS_CLIENT_SECRET }} + NEXTCLOUD_HOSTNAME: nextcloud-container run: | bats --verbose-run --timing --trace ./tests/bats ./vendor/bin/phpunit --bootstrap tests/integration/bootstrap.php tests/integration/