aboutsummaryrefslogtreecommitdiffstats
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/OC/admin.js22
-rw-r--r--core/src/OC/appconfig.js27
-rw-r--r--core/src/OC/apps.js23
-rw-r--r--core/src/OC/appswebroots.js21
-rw-r--r--core/src/OC/backbone-webdav.js23
-rw-r--r--core/src/OC/backbone.js22
-rw-r--r--core/src/OC/capabilities.js23
-rw-r--r--core/src/OC/config.js21
-rw-r--r--core/src/OC/constants.js22
-rw-r--r--core/src/OC/currentuser.js22
-rw-r--r--core/src/OC/debug.js21
-rw-r--r--core/src/OC/dialogs.js393
-rw-r--r--core/src/OC/eventsource.js32
-rw-r--r--core/src/OC/get_set.js22
-rw-r--r--core/src/OC/host.js22
-rw-r--r--core/src/OC/index.js27
-rw-r--r--core/src/OC/l10n.js32
-rw-r--r--core/src/OC/menu.js25
-rw-r--r--core/src/OC/msg.js23
-rw-r--r--core/src/OC/navigation.js22
-rw-r--r--core/src/OC/notification.js38
-rw-r--r--core/src/OC/password-confirmation.js23
-rw-r--r--core/src/OC/plugins.js22
-rw-r--r--core/src/OC/query-string.js27
-rw-r--r--core/src/OC/requesttoken.js57
-rw-r--r--core/src/OC/requesttoken.ts49
-rw-r--r--core/src/OC/routing.js23
-rw-r--r--core/src/OC/theme.js21
-rw-r--r--core/src/OC/util-history.js24
-rw-r--r--core/src/OC/util.js22
-rw-r--r--core/src/OC/webroot.js21
-rw-r--r--core/src/OC/xhr-error.js36
-rw-r--r--core/src/OCA/index.js21
-rw-r--r--core/src/OCP/accessibility.js21
-rw-r--r--core/src/OCP/appconfig.js23
-rw-r--r--core/src/OCP/collaboration.js24
-rw-r--r--core/src/OCP/comments.js24
-rw-r--r--core/src/OCP/index.js23
-rw-r--r--core/src/OCP/loader.js29
-rw-r--r--core/src/OCP/toast.js24
-rw-r--r--core/src/OCP/whatsnew.js22
-rw-r--r--core/src/Polyfill/index.js22
-rw-r--r--core/src/Polyfill/tooltip.js46
-rw-r--r--core/src/Util/a11y.js21
-rw-r--r--core/src/Util/get-url-parameter.js24
-rw-r--r--core/src/ajax-cron.ts18
-rw-r--r--core/src/components/AccountMenu/AccountMenuEntry.vue117
-rw-r--r--core/src/components/AccountMenu/AccountMenuProfileEntry.vue100
-rw-r--r--core/src/components/AppMenu.vue342
-rw-r--r--core/src/components/AppMenuEntry.vue189
-rw-r--r--core/src/components/AppMenuIcon.vue67
-rw-r--r--core/src/components/ContactsMenu.js24
-rw-r--r--core/src/components/ContactsMenu/Contact.vue71
-rw-r--r--core/src/components/LegacyDialogPrompt.vue111
-rw-r--r--core/src/components/MainMenu.js25
-rw-r--r--core/src/components/Profile/PrimaryActionButton.vue25
-rw-r--r--core/src/components/PublicPageMenu/PublicPageMenuCustomEntry.vue36
-rw-r--r--core/src/components/PublicPageMenu/PublicPageMenuEntry.vue51
-rw-r--r--core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue90
-rw-r--r--core/src/components/PublicPageMenu/PublicPageMenuExternalEntry.vue36
-rw-r--r--core/src/components/PublicPageMenu/PublicPageMenuLinkEntry.vue51
-rw-r--r--core/src/components/UnifiedSearch/CustomDateRangeModal.vue10
-rw-r--r--core/src/components/UnifiedSearch/LegacySearchResult.vue29
-rw-r--r--core/src/components/UnifiedSearch/SearchFilterChip.vue6
-rw-r--r--core/src/components/UnifiedSearch/SearchResult.vue126
-rw-r--r--core/src/components/UnifiedSearch/SearchResultPlaceholders.vue4
-rw-r--r--core/src/components/UnifiedSearch/SearchableList.vue51
-rw-r--r--core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue166
-rw-r--r--core/src/components/UnifiedSearch/UnifiedSearchModal.vue795
-rw-r--r--core/src/components/UserMenu.js27
-rw-r--r--core/src/components/UserMenu/ProfileUserMenuEntry.vue140
-rw-r--r--core/src/components/UserMenu/UserMenuEntry.vue106
-rw-r--r--core/src/components/login/LoginButton.vue27
-rw-r--r--core/src/components/login/LoginForm.cy.ts76
-rw-r--r--core/src/components/login/LoginForm.vue132
-rw-r--r--core/src/components/login/PasswordLessLoginForm.vue234
-rw-r--r--core/src/components/login/ResetPassword.vue170
-rw-r--r--core/src/components/login/UpdatePassword.vue23
-rw-r--r--core/src/components/setup/RecommendedApps.vue99
-rw-r--r--core/src/eventbus.d.ts14
-rw-r--r--core/src/files/client.js32
-rw-r--r--core/src/files/fileinfo.js27
-rw-r--r--core/src/globals.js31
-rw-r--r--core/src/icons.js20
-rw-r--r--core/src/init.js29
-rw-r--r--core/src/install.js175
-rw-r--r--core/src/install.ts43
-rw-r--r--core/src/jquery/avatar.js32
-rw-r--r--core/src/jquery/contactsmenu.js27
-rw-r--r--core/src/jquery/css/jquery-ui-fixes.scss33
-rw-r--r--core/src/jquery/css/jquery.ocdialog.scss18
-rw-r--r--core/src/jquery/exists.js22
-rw-r--r--core/src/jquery/filterattr.js22
-rw-r--r--core/src/jquery/index.js23
-rw-r--r--core/src/jquery/ocdialog.js26
-rw-r--r--core/src/jquery/octemplate.js25
-rw-r--r--core/src/jquery/placeholder.js26
-rw-r--r--core/src/jquery/requesttoken.js26
-rw-r--r--core/src/jquery/selectrange.js22
-rw-r--r--core/src/jquery/showpassword.js25
-rw-r--r--core/src/jquery/ui-fixes.js22
-rw-r--r--core/src/legacy-unified-search.js25
-rw-r--r--core/src/logger.js26
-rw-r--r--core/src/login.js24
-rw-r--r--core/src/main.js48
-rw-r--r--core/src/maintenance.js23
-rw-r--r--core/src/mixins/Nextcloud.js21
-rw-r--r--core/src/mixins/auth.js19
-rw-r--r--core/src/profile.ts45
-rw-r--r--core/src/profile/ProfileSections.js42
-rw-r--r--core/src/public-page-menu.ts15
-rw-r--r--core/src/public-page-user-menu.ts15
-rw-r--r--core/src/public.ts26
-rw-r--r--core/src/recommendedapps.js25
-rw-r--r--core/src/services/BrowserStorageService.js21
-rw-r--r--core/src/services/BrowsersListService.js21
-rw-r--r--core/src/services/LegacyUnifiedSearchService.js24
-rw-r--r--core/src/services/UnifiedSearchService.js27
-rw-r--r--core/src/services/WebAuthnAuthenticationService.js44
-rw-r--r--core/src/services/WebAuthnAuthenticationService.ts42
-rw-r--r--core/src/session-heartbeat.js185
-rw-r--r--core/src/session-heartbeat.ts158
-rw-r--r--core/src/store/unified-search-external-filters.js17
-rw-r--r--core/src/systemtags/merged-systemtags.js22
-rw-r--r--core/src/systemtags/systemtagmodel.js26
-rw-r--r--core/src/systemtags/systemtags.js26
-rw-r--r--core/src/systemtags/systemtagscollection.js23
-rw-r--r--core/src/systemtags/systemtagsinputfield.js27
-rw-r--r--core/src/systemtags/systemtagsmappingcollection.js24
-rw-r--r--core/src/tests/.eslintrc.js22
-rw-r--r--core/src/tests/OC/requesttoken.spec.js73
-rw-r--r--core/src/tests/OC/requesttoken.spec.ts147
-rw-r--r--core/src/tests/OC/session-heartbeat.spec.ts123
-rw-r--r--core/src/tests/components/ContactsMenu/Contact.spec.js27
-rw-r--r--core/src/tests/views/ContactsMenu.spec.js53
-rw-r--r--core/src/twofactor-request-token.ts25
-rw-r--r--core/src/types/navigation.d.ts30
-rw-r--r--core/src/unified-search.js55
-rw-r--r--core/src/unified-search.ts63
-rw-r--r--core/src/unsupported-browser-redirect.js24
-rw-r--r--core/src/unsupported-browser.js20
-rw-r--r--core/src/utils/ClipboardFallback.ts47
-rw-r--r--core/src/utils/RedirectUnsupportedBrowsers.js20
-rw-r--r--core/src/utils/xhr-request.js103
-rw-r--r--core/src/views/AccountMenu.vue247
-rw-r--r--core/src/views/ContactsMenu.vue74
-rw-r--r--core/src/views/LegacyUnifiedSearch.vue93
-rw-r--r--core/src/views/Login.vue138
-rw-r--r--core/src/views/Profile.vue491
-rw-r--r--core/src/views/PublicPageMenu.vue131
-rw-r--r--core/src/views/PublicPageUserMenu.vue138
-rw-r--r--core/src/views/Setup.cy.ts369
-rw-r--r--core/src/views/Setup.vue460
-rw-r--r--core/src/views/UnifiedSearch.vue216
-rw-r--r--core/src/views/UnifiedSearchModal.vue623
-rw-r--r--core/src/views/UnsupportedBrowser.vue68
-rw-r--r--core/src/views/UserMenu.vue281
157 files changed, 5515 insertions, 5513 deletions
diff --git a/core/src/OC/admin.js b/core/src/OC/admin.js
index 5c939415266..d29e4cf676b 100644
--- a/core/src/OC/admin.js
+++ b/core/src/OC/admin.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
const isAdmin = !!window._oc_isadmin
diff --git a/core/src/OC/appconfig.js b/core/src/OC/appconfig.js
index 551259bd46c..350ffc3f21c 100644
--- a/core/src/OC/appconfig.js
+++ b/core/src/OC/appconfig.js
@@ -1,28 +1,7 @@
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2014 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
diff --git a/core/src/OC/apps.js b/core/src/OC/apps.js
index 9f0ae217526..dec2b94bfbb 100644
--- a/core/src/OC/apps.js
+++ b/core/src/OC/apps.js
@@ -1,24 +1,7 @@
/**
- * @copyright Bernhard Posselt 2014
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2014 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
diff --git a/core/src/OC/appswebroots.js b/core/src/OC/appswebroots.js
index ec2420eeed5..debbd2084bf 100644
--- a/core/src/OC/appswebroots.js
+++ b/core/src/OC/appswebroots.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
const appswebroots = (window._oc_appswebroots !== undefined) ? window._oc_appswebroots : false
diff --git a/core/src/OC/backbone-webdav.js b/core/src/OC/backbone-webdav.js
index ab234e22005..318c50e8ee5 100644
--- a/core/src/OC/backbone-webdav.js
+++ b/core/src/OC/backbone-webdav.js
@@ -1,25 +1,6 @@
/**
- * Copyright (c) 2015
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
diff --git a/core/src/OC/backbone.js b/core/src/OC/backbone.js
index 5d93ce21428..08520e278f6 100644
--- a/core/src/OC/backbone.js
+++ b/core/src/OC/backbone.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import VendorBackbone from 'backbone'
diff --git a/core/src/OC/capabilities.js b/core/src/OC/capabilities.js
index 72a71d7c743..10623229625 100644
--- a/core/src/OC/capabilities.js
+++ b/core/src/OC/capabilities.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getCapabilities as realGetCapabilities } from '@nextcloud/capabilities'
diff --git a/core/src/OC/config.js b/core/src/OC/config.js
index 702105a4836..c47df61f6e6 100644
--- a/core/src/OC/config.js
+++ b/core/src/OC/config.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
const config = window._oc_config || {}
diff --git a/core/src/OC/constants.js b/core/src/OC/constants.js
index f2ba7bf7a97..5298107e94d 100644
--- a/core/src/OC/constants.js
+++ b/core/src/OC/constants.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
export const coreApps = ['', 'admin', 'log', 'core/search', 'core', '3rdparty']
diff --git a/core/src/OC/currentuser.js b/core/src/OC/currentuser.js
index c6e8a8ee62c..a022698eab0 100644
--- a/core/src/OC/currentuser.js
+++ b/core/src/OC/currentuser.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
const rawUid = document
diff --git a/core/src/OC/debug.js b/core/src/OC/debug.js
index 25a6eae9597..52a9ef28145 100644
--- a/core/src/OC/debug.js
+++ b/core/src/OC/debug.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
const base = window._oc_debug
diff --git a/core/src/OC/dialogs.js b/core/src/OC/dialogs.js
index 6ce960ee41c..5c6934e67a2 100644
--- a/core/src/OC/dialogs.js
+++ b/core/src/OC/dialogs.js
@@ -1,46 +1,7 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- * @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
- *
- * @author Bartek Przybylski <bart.p.pl@gmail.com>
- * @author Christopher Schäpers <kondou@ts.unde.re>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Florian Schunk <florian.schunk@rwth-aachen.de>
- * @author Gary Kim <gary@garykim.dev>
- * @author Hendrik Leppelsack <hendrik@leppelsack.de>
- * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Loïc Hermann <loic.hermann@sciam.fr>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Olivier Paroz <github@oparoz.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sujith Haridasan <Sujith_Haridasan@mentor.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Thomas Tanghus <thomas@tanghus.net>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
@@ -48,18 +9,22 @@ import _ from 'underscore'
import $ from 'jquery'
import IconMove from '@mdi/svg/svg/folder-move.svg?raw'
-import IconCopy from '@mdi/svg/svg/folder-multiple.svg?raw'
+import IconCopy from '@mdi/svg/svg/folder-multiple-outline.svg?raw'
import OC from './index.js'
-import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'
+import { DialogBuilder, FilePickerType, getFilePickerBuilder, spawnDialog } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
import { basename } from 'path'
+import { defineAsyncComponent } from 'vue'
/**
* this class to ease the usage of jquery dialogs
*/
const Dialogs = {
// dialog button types
+ /** @deprecated use `@nextcloud/dialogs` */
YES_NO_BUTTONS: 70,
+ /** @deprecated use `@nextcloud/dialogs` */
OK_BUTTONS: 71,
/** @deprecated use FilePickerType from `@nextcloud/dialogs` */
@@ -73,15 +38,14 @@ const Dialogs = {
/** @deprecated use FilePickerType from `@nextcloud/dialogs` */
FILEPICKER_TYPE_CUSTOM: 5,
- // used to name each dialog
- dialogsCounter: 0,
-
/**
* displays alert dialog
* @param {string} text content of dialog
* @param {string} title dialog title
* @param {function} callback which will be triggered when user presses OK
* @param {boolean} [modal] make the dialog modal
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
*/
alert: function(text, title, callback, modal) {
this.message(
@@ -93,12 +57,15 @@ const Dialogs = {
modal
)
},
+
/**
* displays info dialog
* @param {string} text content of dialog
* @param {string} title dialog title
* @param {function} callback which will be triggered when user presses OK
* @param {boolean} [modal] make the dialog modal
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
*/
info: function(text, title, callback, modal) {
this.message(text, title, 'info', Dialogs.OK_BUTTON, callback, modal)
@@ -111,6 +78,8 @@ const Dialogs = {
* @param {function} callback which will be triggered when user presses OK (true or false would be passed to callback respectively)
* @param {boolean} [modal] make the dialog modal
* @returns {Promise}
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
*/
confirm: function(text, title, callback, modal) {
return this.message(
@@ -130,16 +99,34 @@ const Dialogs = {
* @param {function} callback which will be triggered when user presses OK (true or false would be passed to callback respectively)
* @param {boolean} [modal] make the dialog modal
* @returns {Promise}
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
*/
- confirmDestructive: function(text, title, buttons, callback, modal) {
- return this.message(
- text,
- title,
- 'none',
- buttons,
- callback,
- modal === undefined ? true : modal
- )
+ confirmDestructive: function(text, title, buttons = Dialogs.OK_BUTTONS, callback = () => {}, modal) {
+ return (new DialogBuilder())
+ .setName(title)
+ .setText(text)
+ .setButtons(
+ buttons === Dialogs.OK_BUTTONS
+ ? [
+ {
+ label: t('core', 'Yes'),
+ type: 'error',
+ callback: () => {
+ callback.clicked = true
+ callback(true)
+ },
+ }
+ ]
+ : Dialogs._getLegacyButtons(buttons, callback)
+ )
+ .build()
+ .show()
+ .then(() => {
+ if (!callback.clicked) {
+ callback(false)
+ }
+ })
},
/**
* displays confirmation dialog
@@ -148,17 +135,35 @@ const Dialogs = {
* @param {function} callback which will be triggered when user presses OK (true or false would be passed to callback respectively)
* @param {boolean} [modal] make the dialog modal
* @returns {Promise}
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
*/
confirmHtml: function(text, title, callback, modal) {
- return this.message(
- text,
- title,
- 'notice',
- Dialogs.YES_NO_BUTTONS,
- callback,
- modal,
- true
- )
+ return (new DialogBuilder())
+ .setName(title)
+ .setText('')
+ .setButtons([
+ {
+ label: t('core', 'No'),
+ callback: () => {},
+ },
+ {
+ label: t('core', 'Yes'),
+ type: 'primary',
+ callback: () => {
+ callback.clicked = true
+ callback(true)
+ },
+ },
+ ])
+ .build()
+ .setHTML(text)
+ .show()
+ .then(() => {
+ if (!callback.clicked) {
+ callback(false)
+ }
+ })
},
/**
* displays prompt dialog
@@ -169,69 +174,25 @@ const Dialogs = {
* @param {string} name name of the input field
* @param {boolean} password whether the input should be a password input
* @returns {Promise}
+ *
+ * @deprecated Use NcDialog from `@nextcloud/vue` instead
*/
prompt: function(text, title, callback, modal, name, password) {
- return $.when(this._getMessageTemplate()).then(function($tmpl) {
- var dialogName = 'oc-dialog-' + Dialogs.dialogsCounter + '-content'
- var dialogId = '#' + dialogName
- var $dlg = $tmpl.octemplate({
- dialog_name: dialogName,
- title: title,
- message: text,
- type: 'notice'
- })
- var input = $('<input/>')
- input.attr('type', password ? 'password' : 'text').attr('id', dialogName + '-input').attr('placeholder', name)
- var label = $('<label/>').attr('for', dialogName + '-input').text(name + ': ')
- $dlg.append(label)
- $dlg.append(input)
- if (modal === undefined) {
- modal = false
- }
- $('body').append($dlg)
-
- // wrap callback in _.once():
- // only call callback once and not twice (button handler and close
- // event) but call it for the close event, if ESC or the x is hit
- if (callback !== undefined) {
- callback = _.once(callback)
- }
-
- var buttonlist = [{
- text: t('core', 'No'),
- click: function() {
- if (callback !== undefined) {
- // eslint-disable-next-line standard/no-callback-literal
- callback(false, input.val())
- }
- $(dialogId).ocdialog('close')
- }
- }, {
- text: t('core', 'Yes'),
- click: function() {
- if (callback !== undefined) {
- // eslint-disable-next-line standard/no-callback-literal
- callback(true, input.val())
- }
- $(dialogId).ocdialog('close')
+ return new Promise((resolve) => {
+ spawnDialog(
+ defineAsyncComponent(() => import('../components/LegacyDialogPrompt.vue')),
+ {
+ text,
+ name: title,
+ callback,
+ inputName: name,
+ isPassword: !!password
},
- defaultButton: true
- }]
-
- $(dialogId).ocdialog({
- closeOnEscape: true,
- modal: modal,
- buttons: buttonlist,
- close: function() {
- // callback is already fired if Yes/No is clicked directly
- if (callback !== undefined) {
- // eslint-disable-next-line standard/no-callback-literal
- callback(false, input.val())
- }
- }
- })
- input.focus()
- Dialogs.dialogsCounter++
+ (...args) => {
+ callback(...args)
+ resolve()
+ },
+ )
})
},
@@ -317,13 +278,13 @@ const Dialogs = {
} else {
builder.setButtonFactory((nodes, path) => {
const buttons = []
- const node = nodes?.[0]?.attributes?.displayName || nodes?.[0]?.basename
- const target = node || basename(path)
+ const [node] = nodes
+ const target = node?.displayname || node?.basename || basename(path)
if (type === FilePickerType.Choose) {
buttons.push({
callback: legacyCallback(callback, FilePickerType.Choose),
- label: node && !this.multiSelect ? t('core', 'Choose {file}', { file: node }) : t('core', 'Choose'),
+ label: node && !this.multiSelect ? t('core', 'Choose {file}', { file: target }) : t('core', 'Choose'),
type: 'primary',
})
}
@@ -363,105 +324,81 @@ const Dialogs = {
/**
* Displays raw dialog
* You better use a wrapper instead ...
+ *
+ * @deprecated 30.0.0 Use `@nextcloud/dialogs` instead or build your own with `@nextcloud/vue` NcDialog
*/
- message: function(content, title, dialogType, buttons, callback, modal, allowHtml) {
- return $.when(this._getMessageTemplate()).then(function($tmpl) {
- var dialogName = 'oc-dialog-' + Dialogs.dialogsCounter + '-content'
- var dialogId = '#' + dialogName
- var $dlg = $tmpl.octemplate({
- dialog_name: dialogName,
- title: title,
- message: content,
- type: dialogType
- }, allowHtml ? { escapeFunction: '' } : {})
- if (modal === undefined) {
- modal = false
+ message: function(content, title, dialogType, buttons, callback = () => {}, modal, allowHtml) {
+ const builder = (new DialogBuilder())
+ .setName(title)
+ .setText(allowHtml ? '' : content)
+ .setButtons(Dialogs._getLegacyButtons(buttons, callback))
+
+ switch (dialogType) {
+ case 'alert':
+ builder.setSeverity('warning')
+ break
+ case 'notice':
+ builder.setSeverity('info')
+ break
+ default:
+ break
+ }
+
+ const dialog = builder.build()
+
+ if (allowHtml) {
+ dialog.setHTML(content)
+ }
+
+ return dialog.show().then(() => {
+ if(!callback._clicked) {
+ callback(false)
}
- $('body').append($dlg)
- var buttonlist = []
- switch (buttons) {
+ })
+ },
+
+ /**
+ * Helper for legacy API
+ * @deprecated
+ */
+ _getLegacyButtons(buttons, callback) {
+ const buttonList = []
+
+ switch (typeof buttons === 'object' ? buttons.type : buttons) {
case Dialogs.YES_NO_BUTTONS:
- buttonlist = [{
- text: t('core', 'No'),
- click: function() {
- if (callback !== undefined) {
- callback(false)
- }
- $(dialogId).ocdialog('close')
- }
- },
- {
- text: t('core', 'Yes'),
- click: function() {
- if (callback !== undefined) {
- callback(true)
- }
- $(dialogId).ocdialog('close')
+ buttonList.push({
+ label: buttons?.cancel ?? t('core', 'No'),
+ callback: () => {
+ callback._clicked = true
+ callback(false)
},
- defaultButton: true
- }]
+ })
+ buttonList.push({
+ label: buttons?.confirm ?? t('core', 'Yes'),
+ type: 'primary',
+ callback: () => {
+ callback._clicked = true
+ callback(true)
+ },
+ })
break
- case Dialogs.OK_BUTTON:
- var functionToCall = function() {
- $(dialogId).ocdialog('close')
- if (callback !== undefined) {
- callback()
- }
- }
- buttonlist[0] = {
- text: t('core', 'OK'),
- click: functionToCall,
- defaultButton: true
- }
+ case Dialogs.OK_BUTTONS:
+ buttonList.push({
+ label: buttons?.confirm ?? t('core', 'OK'),
+ type: 'primary',
+ callback: () => {
+ callback._clicked = true
+ callback(true)
+ },
+ })
break
default:
- if (typeof(buttons) === 'object') {
- switch (buttons.type) {
- case Dialogs.YES_NO_BUTTONS:
- buttonlist = [{
- text: buttons.cancel || t('core', 'No'),
- click: function() {
- if (callback !== undefined) {
- callback(false)
- }
- $(dialogId).ocdialog('close')
- }
- },
- {
- text: buttons.confirm || t('core', 'Yes'),
- click: function() {
- if (callback !== undefined) {
- callback(true)
- }
- $(dialogId).ocdialog('close')
- },
- defaultButton: true,
- classes: buttons.confirmClasses
- }]
- break
- }
- }
+ console.error('Invalid call to OC.dialogs')
break
- }
-
- $(dialogId).ocdialog({
- closeOnEscape: true,
- closeCallback: () => { callback && callback(false) },
- modal: modal,
- buttons: buttonlist
- })
- Dialogs.dialogsCounter++
- })
- .fail(function(status, error) {
- // If the method is called while navigating away from
- // the page, we still want to deliver the message.
- if (status === 0) {
- alert(title + ': ' + content)
- } else {
- alert(t('core', 'Error loading message template: {error}', { error: error }))
- }
- })
+ }
+ return buttonList
},
+
_fileexistsshown: false,
/**
* Displays file exists dialog
@@ -470,6 +407,8 @@ const Dialogs = {
* @param {object} replacement file with name, size and mtime
* @param {object} controller with onCancel, onSkip, onReplace and onRename methods
* @returns {Promise} jquery promise that resolves after the dialog template was loaded
+ *
+ * @deprecated 29.0.0 Use openConflictPicker from the @nextcloud/upload package instead
*/
fileexists: function(data, original, replacement, controller) {
var self = this
@@ -829,27 +768,11 @@ const Dialogs = {
return dialogDeferred.promise()
},
- _getMessageTemplate: function() {
- var defer = $.Deferred()
- if (!this.$messageTemplate) {
- var self = this
- $.get(OC.filePath('core', 'templates', 'message.html'), function(tmpl) {
- self.$messageTemplate = $(tmpl)
- defer.resolve(self.$messageTemplate)
- })
- .fail(function(jqXHR, textStatus, errorThrown) {
- defer.reject(jqXHR.status, errorThrown)
- })
- } else {
- defer.resolve(this.$messageTemplate)
- }
- return defer.promise()
- },
_getFileExistsTemplate: function() {
var defer = $.Deferred()
if (!this.$fileexistsTemplate) {
var self = this
- $.get(OC.filePath('files', 'templates', 'fileexists.html'), function(tmpl) {
+ $.get(OC.filePath('core', 'templates/legacy', 'fileexists.html'), function(tmpl) {
self.$fileexistsTemplate = $(tmpl)
defer.resolve(self.$fileexistsTemplate)
})
diff --git a/core/src/OC/eventsource.js b/core/src/OC/eventsource.js
index 940b25655e4..090c351c057 100644
--- a/core/src/OC/eventsource.js
+++ b/core/src/OC/eventsource.js
@@ -1,35 +1,13 @@
/**
- * @copyright 2012 Robin Appelman icewind1991@gmail.com
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
import $ from 'jquery'
-import { getToken } from './requesttoken.js'
+import { getRequestToken } from './requesttoken.ts'
/**
* Create a new event source
@@ -50,7 +28,7 @@ const OCEventSource = function(src, data) {
dataStr += name + '=' + encodeURIComponent(data[name]) + '&'
}
}
- dataStr += 'requesttoken=' + encodeURIComponent(getToken())
+ dataStr += 'requesttoken=' + encodeURIComponent(getRequestToken())
if (!this.useFallBack && typeof EventSource !== 'undefined') {
joinChar = '&'
if (src.indexOf('?') === -1) {
diff --git a/core/src/OC/get_set.js b/core/src/OC/get_set.js
index 32f202ad35b..0c909ad04fd 100644
--- a/core/src/OC/get_set.js
+++ b/core/src/OC/get_set.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
export const get = context => name => {
diff --git a/core/src/OC/host.js b/core/src/OC/host.js
index 31f13d01a7f..75c7d63804b 100644
--- a/core/src/OC/host.js
+++ b/core/src/OC/host.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
export const getProtocol = () => window.location.protocol.split(':')[0]
diff --git a/core/src/OC/index.js b/core/src/OC/index.js
index 33dd45a17ee..5afc941b396 100644
--- a/core/src/OC/index.js
+++ b/core/src/OC/index.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { subscribe } from '@nextcloud/event-bus'
@@ -68,9 +49,7 @@ import {
getPort,
getProtocol,
} from './host.js'
-import {
- getToken as getRequestToken,
-} from './requesttoken.js'
+import { getRequestToken } from './requesttoken.ts'
import {
hideMenus,
registerMenu,
diff --git a/core/src/OC/l10n.js b/core/src/OC/l10n.js
index b04d4bf9fba..02f912d6a99 100644
--- a/core/src/OC/l10n.js
+++ b/core/src/OC/l10n.js
@@ -1,30 +1,8 @@
/**
- * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
- * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2014 ownCloud, Inc.
+ * SPDX-FileCopyrightText: 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import Handlebars from 'handlebars'
@@ -62,7 +40,7 @@ const L10n = {
* @deprecated 26.0.0 use `register` from https://www.npmjs.com/package/@nextcloud/l10
*
* @param {string} appName name of the app
- * @param {Object<string, string>} bundle bundle
+ * @param {Record<string, string>} bundle bundle
*/
register,
diff --git a/core/src/OC/menu.js b/core/src/OC/menu.js
index efa917decd2..4b4eb658592 100644
--- a/core/src/OC/menu.js
+++ b/core/src/OC/menu.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import _ from 'underscore'
@@ -123,7 +104,7 @@ export const hideMenus = function(complete) {
/**
* Shows a given element as menu
*
- * @param {object} [$toggle=null] menu toggle
+ * @param {object} [$toggle] menu toggle
* @param {object} $menuEl menu element
* @param {Function} complete callback when the showing animation is done
*/
diff --git a/core/src/OC/msg.js b/core/src/OC/msg.js
index ef6f9ec0f09..655631a03ff 100644
--- a/core/src/OC/msg.js
+++ b/core/src/OC/msg.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author rakekniven <mark.ziegler@rakekniven.de>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
diff --git a/core/src/OC/navigation.js b/core/src/OC/navigation.js
index 2102c37b3f5..b279b9a60f3 100644
--- a/core/src/OC/navigation.js
+++ b/core/src/OC/navigation.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
export const redirect = targetURL => { window.location = targetURL }
diff --git a/core/src/OC/notification.js b/core/src/OC/notification.js
index 949df6a519c..b658f4163bb 100644
--- a/core/src/OC/notification.js
+++ b/core/src/OC/notification.js
@@ -1,28 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author npmbuildbot[bot] "npmbuildbot[bot]@users.noreply.github.com"
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import _ from 'underscore'
@@ -98,7 +76,7 @@ export default {
* @param {string} html Message to display
* @param {object} [options] options
* @param {string} [options.type] notification type
- * @param {number} [options.timeout=0] timeout value, defaults to 0 (permanent)
+ * @param {number} [options.timeout] timeout value, defaults to 0 (permanent)
* @return {jQuery} jQuery element for notification row
* @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
@@ -117,7 +95,7 @@ export default {
* @param {string} text Message to display
* @param {object} [options] options
* @param {string} [options.type] notification type
- * @param {number} [options.timeout=0] timeout value, defaults to 0 (permanent)
+ * @param {number} [options.timeout] timeout value, defaults to 0 (permanent)
* @return {jQuery} jQuery element for notification row
* @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
@@ -142,7 +120,7 @@ export default {
* Updates (replaces) a sanitized notification.
*
* @param {string} text Message to display
- * @return {jQuery} JQuery element for notificaiton row
+ * @return {jQuery} JQuery element for notification row
* @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
showUpdate(text) {
@@ -160,10 +138,10 @@ export default {
*
* @param {string} text Message to show
* @param {Array} [options] options array
- * @param {number} [options.timeout=7] timeout in seconds, if this is 0 it will show the message permanently
- * @param {boolean} [options.isHTML=false] an indicator for HTML notifications (true) or text (false)
+ * @param {number} [options.timeout] timeout in seconds, if this is 0 it will show the message permanently
+ * @param {boolean} [options.isHTML] an indicator for HTML notifications (true) or text (false)
* @param {string} [options.type] notification type
- * @return {JQuery} the toast element
+ * @return {jQuery} the toast element
* @deprecated 17.0.0 use the `@nextcloud/dialogs` package
*/
showTemporary(text, options) {
diff --git a/core/src/OC/password-confirmation.js b/core/src/OC/password-confirmation.js
index 210d6b1e5c1..621f7a0695f 100644
--- a/core/src/OC/password-confirmation.js
+++ b/core/src/OC/password-confirmation.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { confirmPassword, isPasswordConfirmationRequired } from '@nextcloud/password-confirmation'
diff --git a/core/src/OC/plugins.js b/core/src/OC/plugins.js
index 4425c118589..8212fc0b4ee 100644
--- a/core/src/OC/plugins.js
+++ b/core/src/OC/plugins.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
export default {
diff --git a/core/src/OC/query-string.js b/core/src/OC/query-string.js
index 56bf85186fb..df0f366133a 100644
--- a/core/src/OC/query-string.js
+++ b/core/src/OC/query-string.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
@@ -28,7 +9,7 @@ import $ from 'jquery'
* Parses a URL query string into a JS map
*
* @param {string} queryString query string in the format param1=1234&param2=abcde&param3=xyz
- * @return {Object<string, string>} map containing key/values matching the URL parameters
+ * @return {Record<string, string>} map containing key/values matching the URL parameters
*/
export const parse = queryString => {
let pos
@@ -77,7 +58,7 @@ export const parse = queryString => {
/**
* Builds a URL query from a JS map.
*
- * @param {Object<string, string>} params map containing key/values matching the URL parameters
+ * @param {Record<string, string>} params map containing key/values matching the URL parameters
* @return {string} String containing a URL query (without question) mark
*/
export const build = params => {
diff --git a/core/src/OC/requesttoken.js b/core/src/OC/requesttoken.js
deleted file mode 100644
index eba15e88e08..00000000000
--- a/core/src/OC/requesttoken.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import { emit } from '@nextcloud/event-bus'
-
-/**
- * @private
- * @param {Document} global the document to read the initial value from
- * @param {Function} emit the function to invoke for every new token
- * @return {object}
- */
-export const manageToken = (global, emit) => {
- let token = global.getElementsByTagName('head')[0].getAttribute('data-requesttoken')
-
- return {
- getToken: () => token,
- setToken: newToken => {
- token = newToken
-
- emit('csrf-token-update', {
- token,
- })
- },
- }
-}
-
-const manageFromDocument = manageToken(document, emit)
-
-/**
- * @return {string}
- */
-export const getToken = manageFromDocument.getToken
-
-/**
- * @param {string} newToken new token
- */
-export const setToken = manageFromDocument.setToken
diff --git a/core/src/OC/requesttoken.ts b/core/src/OC/requesttoken.ts
new file mode 100644
index 00000000000..8ecf0b3de7e
--- /dev/null
+++ b/core/src/OC/requesttoken.ts
@@ -0,0 +1,49 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { emit } from '@nextcloud/event-bus'
+import { generateUrl } from '@nextcloud/router'
+
+/**
+ * Get the current CSRF token.
+ */
+export function getRequestToken(): string {
+ return document.head.dataset.requesttoken!
+}
+
+/**
+ * Set a new CSRF token (e.g. because of session refresh).
+ * This also emits an event bus event for the updated token.
+ *
+ * @param token - The new token
+ * @fires Error - If the passed token is not a potential valid token
+ */
+export function setRequestToken(token: string): void {
+ if (!token || typeof token !== 'string') {
+ throw new Error('Invalid CSRF token given', { cause: { token } })
+ }
+
+ document.head.dataset.requesttoken = token
+ emit('csrf-token-update', { token })
+}
+
+/**
+ * Fetch the request token from the API.
+ * This does also set it on the current context, see `setRequestToken`.
+ *
+ * @fires Error - If the request failed
+ */
+export async function fetchRequestToken(): Promise<string> {
+ const url = generateUrl('/csrftoken')
+
+ const response = await fetch(url)
+ if (!response.ok) {
+ throw new Error('Could not fetch CSRF token from API', { cause: response })
+ }
+
+ const { token } = await response.json()
+ setRequestToken(token)
+ return token
+}
diff --git a/core/src/OC/routing.js b/core/src/OC/routing.js
index 8752aa3883e..4b81714d6f0 100644
--- a/core/src/OC/routing.js
+++ b/core/src/OC/routing.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import {
diff --git a/core/src/OC/theme.js b/core/src/OC/theme.js
index b7fcfd8ce4d..af45c37de7e 100644
--- a/core/src/OC/theme.js
+++ b/core/src/OC/theme.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
export const theme = window._theme || {}
diff --git a/core/src/OC/util-history.js b/core/src/OC/util-history.js
index d18b8743936..7ecd0e098c6 100644
--- a/core/src/OC/util-history.js
+++ b/core/src/OC/util-history.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import _ from 'underscore'
@@ -45,7 +27,7 @@ export default {
* or a map
* @param {string} [url] URL to be used, otherwise the current URL will be used,
* using the params as query string
- * @param {boolean} [replace=false] whether to replace instead of pushing
+ * @param {boolean} [replace] whether to replace instead of pushing
*/
_pushState(params, url, replace) {
let strParams
diff --git a/core/src/OC/util.js b/core/src/OC/util.js
index f0dd7e0ac14..c46d9a141b1 100644
--- a/core/src/OC/util.js
+++ b/core/src/OC/util.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import moment from 'moment'
diff --git a/core/src/OC/webroot.js b/core/src/OC/webroot.js
index f5d063d6b50..cbe5a6190e1 100644
--- a/core/src/OC/webroot.js
+++ b/core/src/OC/webroot.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
let webroot = window._oc_webroot
diff --git a/core/src/OC/xhr-error.js b/core/src/OC/xhr-error.js
index a346daa6984..233aaf60350 100644
--- a/core/src/OC/xhr-error.js
+++ b/core/src/OC/xhr-error.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import _ from 'underscore'
@@ -26,16 +8,18 @@ import $ from 'jquery'
import OC from './index.js'
import Notification from './notification.js'
+import { getCurrentUser } from '@nextcloud/auth'
+import { showWarning } from '@nextcloud/dialogs'
/**
* Warn users that the connection to the server was lost temporarily
*
- * This function is throttled to prevent stacked notfications.
+ * This function is throttled to prevent stacked notifications.
* After 7sec the first notification is gone, then we can show another one
* if necessary.
*/
export const ajaxConnectionLostHandler = _.throttle(() => {
- Notification.showTemporary(t('core', 'Connection to server lost'))
+ showWarning(t('core', 'Connection to server lost'))
}, 7 * 1000, { trailing: false })
/**
@@ -46,13 +30,13 @@ export const ajaxConnectionLostHandler = _.throttle(() => {
*/
export const processAjaxError = xhr => {
// purposefully aborted request ?
- // OC._userIsNavigatingAway needed to distinguish ajax calls cancelled by navigating away
- // from calls cancelled by failed cross-domain ajax due to SSO redirect
+ // OC._userIsNavigatingAway needed to distinguish Ajax calls cancelled by navigating away
+ // from calls cancelled by failed cross-domain Ajax due to SSO redirect
if (xhr.status === 0 && (xhr.statusText === 'abort' || xhr.statusText === 'timeout' || OC._reloadCalled)) {
return
}
- if (_.contains([302, 303, 307, 401], xhr.status) && OC.currentUser) {
+ if ([302, 303, 307, 401].includes(xhr.status) && getCurrentUser()) {
// sometimes "beforeunload" happens later, so need to defer the reload a bit
setTimeout(function() {
if (!OC._userIsNavigatingAway && !OC._reloadCalled) {
@@ -65,7 +49,7 @@ export const processAjaxError = xhr => {
OC.reload()
}
timer++
- }, 1000 // 1 second interval
+ }, 1000, // 1 second interval
)
// only call reload once
diff --git a/core/src/OCA/index.js b/core/src/OCA/index.js
index f49f0319a9c..cf5c29ce60a 100644
--- a/core/src/OCA/index.js
+++ b/core/src/OCA/index.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
diff --git a/core/src/OCP/accessibility.js b/core/src/OCP/accessibility.js
index d318d3497c4..4a1399f3f96 100644
--- a/core/src/OCP/accessibility.js
+++ b/core/src/OCP/accessibility.js
@@ -1,23 +1,6 @@
/**
- * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { loadState } from '@nextcloud/initial-state'
diff --git a/core/src/OCP/appconfig.js b/core/src/OCP/appconfig.js
index 5bd8ca9efae..78f94922d53 100644
--- a/core/src/OCP/appconfig.js
+++ b/core/src/OCP/appconfig.js
@@ -1,24 +1,7 @@
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
diff --git a/core/src/OCP/collaboration.js b/core/src/OCP/collaboration.js
index 2e49cebe9f1..82ff34392cf 100644
--- a/core/src/OCP/collaboration.js
+++ b/core/src/OCP/collaboration.js
@@ -1,31 +1,13 @@
/**
- * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import escapeHTML from 'escape-html'
/**
* @typedef TypeDefinition
- * @function {Function} action This action is executed to let the user select a resource
+ * @function action This action is executed to let the user select a resource
* @param {string} icon Contains the icon css class for the type
* @function Object() { [native code] }
*/
diff --git a/core/src/OCP/comments.js b/core/src/OCP/comments.js
index cd1e8a8fa77..34699a477d1 100644
--- a/core/src/OCP/comments.js
+++ b/core/src/OCP/comments.js
@@ -1,24 +1,6 @@
/**
- * @copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
@@ -34,7 +16,7 @@ import $ from 'jquery'
*
* This is a copy of the backend regex in IURLGenerator, make sure to adjust both when changing
*/
-const urlRegex = /(\s|^)(https?:\/\/)((?:[-A-Z0-9+_]+\.)+[-A-Z]+(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*)(\s|$)/ig
+const urlRegex = /(\s|^)(https?:\/\/)([-A-Z0-9+_.]+(?::[0-9]+)?(?:\/[-A-Z0-9+&@#%?=~_|!:,.;()]*)*)(\s|$)/ig
/**
* @param {any} content -
diff --git a/core/src/OCP/index.js b/core/src/OCP/index.js
index aee37d17030..94f4e8e5eb3 100644
--- a/core/src/OCP/index.js
+++ b/core/src/OCP/index.js
@@ -1,25 +1,6 @@
/**
- * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { loadState } from '@nextcloud/initial-state'
diff --git a/core/src/OCP/loader.js b/core/src/OCP/loader.js
index 6e58208a849..d307eb27996 100644
--- a/core/src/OCP/loader.js
+++ b/core/src/OCP/loader.js
@@ -1,27 +1,10 @@
/**
- * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import { generateFilePath } from '@nextcloud/router'
+
const loadedScripts = {}
const loadedStylesheets = {}
/**
@@ -44,7 +27,7 @@ export default {
}
loadedScripts[key] = true
return new Promise(function(resolve, reject) {
- const scriptPath = OC.filePath(app, 'js', file)
+ const scriptPath = generateFilePath(app, 'js', file)
const script = document.createElement('script')
script.src = scriptPath
script.setAttribute('nonce', btoa(OC.requestToken))
@@ -68,7 +51,7 @@ export default {
}
loadedStylesheets[key] = true
return new Promise(function(resolve, reject) {
- const stylePath = OC.filePath(app, 'css', file)
+ const stylePath = generateFilePath(app, 'css', file)
const link = document.createElement('link')
link.href = stylePath
link.type = 'text/css'
diff --git a/core/src/OCP/toast.js b/core/src/OCP/toast.js
index 40c46aa8f51..f93344bbc8e 100644
--- a/core/src/OCP/toast.js
+++ b/core/src/OCP/toast.js
@@ -1,24 +1,6 @@
/**
- * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import {
@@ -28,6 +10,8 @@ import {
showWarning,
} from '@nextcloud/dialogs'
+/** @typedef {import('toastify-js')} Toast */
+
export default {
/**
* @deprecated 19.0.0 use `showSuccess` from the `@nextcloud/dialogs` package instead
diff --git a/core/src/OCP/whatsnew.js b/core/src/OCP/whatsnew.js
index aafe1740e0d..acada6a8383 100644
--- a/core/src/OCP/whatsnew.js
+++ b/core/src/OCP/whatsnew.js
@@ -1,24 +1,6 @@
/**
- * @copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import _ from 'underscore'
diff --git a/core/src/Polyfill/index.js b/core/src/Polyfill/index.js
deleted file mode 100644
index 273d608870c..00000000000
--- a/core/src/Polyfill/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
diff --git a/core/src/Polyfill/tooltip.js b/core/src/Polyfill/tooltip.js
deleted file mode 100644
index 925b67c3e8b..00000000000
--- a/core/src/Polyfill/tooltip.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * @copyright 2019 Julius Härtl <jus@bitgrid.net>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import $ from 'jquery'
-
-$.prototype.tooltip = (function(tooltip) {
- return function(config) {
- try {
- return tooltip.call(this, config)
- } catch (ex) {
- if (ex instanceof TypeError && config === 'destroy') {
- if (window.TESTING === undefined) {
- OC.debug && console.warn('Deprecated call $.tooltip(\'destroy\') has been deprecated and should be removed')
- }
- return tooltip.call(this, 'dispose')
- }
- if (ex instanceof TypeError && config === 'fixTitle') {
- if (window.TESTING === undefined) {
- OC.debug && console.warn('Deprecated call $.tooltip(\'fixTitle\') has been deprecated and should be removed')
- }
- return tooltip.call(this, '_fixTitle')
- }
- }
- }
-})($.prototype.tooltip)
diff --git a/core/src/Util/a11y.js b/core/src/Util/a11y.js
index 550ae375e02..2eb753b3faf 100644
--- a/core/src/Util/a11y.js
+++ b/core/src/Util/a11y.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2022 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
diff --git a/core/src/Util/get-url-parameter.js b/core/src/Util/get-url-parameter.js
index 00fa66a9eb3..6df264f009f 100644
--- a/core/src/Util/get-url-parameter.js
+++ b/core/src/Util/get-url-parameter.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
@@ -27,6 +9,6 @@
export default function getURLParameter(name) {
return decodeURIComponent(
// eslint-disable-next-line no-sparse-arrays
- (new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [, ''])[1].replace(/\+/g, '%20')
+ (new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [, ''])[1].replace(/\+/g, '%20'),
) || ''
}
diff --git a/core/src/ajax-cron.ts b/core/src/ajax-cron.ts
new file mode 100644
index 00000000000..d903a3596ea
--- /dev/null
+++ b/core/src/ajax-cron.ts
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getRootUrl } from '@nextcloud/router'
+import logger from './logger'
+
+window.addEventListener('DOMContentLoaded', async () => {
+ // When the page is loaded send GET to the cron endpoint to trigger background jobs
+ try {
+ logger.debug('Running web cron')
+ await window.fetch(`${getRootUrl()}/cron.php`)
+ logger.debug('Web cron successfull')
+ } catch (e) {
+ logger.debug('Running web cron failed', { error: e })
+ }
+})
diff --git a/core/src/components/AccountMenu/AccountMenuEntry.vue b/core/src/components/AccountMenu/AccountMenuEntry.vue
new file mode 100644
index 00000000000..d983226d273
--- /dev/null
+++ b/core/src/components/AccountMenu/AccountMenuEntry.vue
@@ -0,0 +1,117 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcListItem :id="href ? undefined : id"
+ :anchor-id="id"
+ :active="active"
+ class="account-menu-entry"
+ compact
+ :href="href"
+ :name="name"
+ target="_self"
+ @click="onClick">
+ <template #icon>
+ <NcLoadingIcon v-if="loading" :size="20" class="account-menu-entry__loading" />
+ <slot v-else-if="$scopedSlots.icon" name="icon" />
+ <img v-else
+ class="account-menu-entry__icon"
+ :class="{ 'account-menu-entry__icon--active': active }"
+ :src="iconSource"
+ alt="">
+ </template>
+ </NcListItem>
+</template>
+
+<script lang="ts">
+import { loadState } from '@nextcloud/initial-state'
+import { defineComponent } from 'vue'
+
+import NcListItem from '@nextcloud/vue/components/NcListItem'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+
+const versionHash = loadState('core', 'versionHash', '')
+
+export default defineComponent({
+ name: 'AccountMenuEntry',
+
+ components: {
+ NcListItem,
+ NcLoadingIcon,
+ },
+
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ href: {
+ type: String,
+ required: true,
+ },
+ active: {
+ type: Boolean,
+ default: false,
+ },
+ icon: {
+ type: String,
+ default: '',
+ },
+ },
+
+ data() {
+ return {
+ loading: false,
+ }
+ },
+
+ computed: {
+ iconSource() {
+ return `${this.icon}?v=${versionHash}`
+ },
+ },
+
+ methods: {
+ onClick(e: MouseEvent) {
+ this.$emit('click', e)
+
+ // Allow to not show the loading indicator
+ // in case the click event was already handled
+ if (!e.defaultPrevented) {
+ this.loading = true
+ }
+ },
+ },
+})
+</script>
+
+<style lang="scss" scoped>
+.account-menu-entry {
+ &__icon {
+ height: 16px;
+ width: 16px;
+ margin: calc((var(--default-clickable-area) - 16px) / 2); // 16px icon size
+ filter: var(--background-invert-if-dark);
+
+ &--active {
+ filter: var(--primary-invert-if-dark);
+ }
+ }
+
+ &__loading {
+ height: 20px;
+ width: 20px;
+ margin: calc((var(--default-clickable-area) - 20px) / 2); // 20px icon size
+ }
+
+ :deep(.list-item-content__main) {
+ width: fit-content;
+ }
+}
+</style>
diff --git a/core/src/components/AccountMenu/AccountMenuProfileEntry.vue b/core/src/components/AccountMenu/AccountMenuProfileEntry.vue
new file mode 100644
index 00000000000..8b895b8ca31
--- /dev/null
+++ b/core/src/components/AccountMenu/AccountMenuProfileEntry.vue
@@ -0,0 +1,100 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcListItem :id="profileEnabled ? undefined : id"
+ :anchor-id="id"
+ :active="active"
+ compact
+ :href="profileEnabled ? href : undefined"
+ :name="displayName"
+ target="_self">
+ <template v-if="profileEnabled" #subname>
+ {{ name }}
+ </template>
+ <template v-if="loading" #indicator>
+ <NcLoadingIcon />
+ </template>
+ </NcListItem>
+</template>
+
+<script lang="ts">
+import { loadState } from '@nextcloud/initial-state'
+import { getCurrentUser } from '@nextcloud/auth'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { defineComponent } from 'vue'
+
+import NcListItem from '@nextcloud/vue/components/NcListItem'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+
+const { profileEnabled } = loadState('user_status', 'profileEnabled', { profileEnabled: false })
+
+export default defineComponent({
+ name: 'AccountMenuProfileEntry',
+
+ components: {
+ NcListItem,
+ NcLoadingIcon,
+ },
+
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ href: {
+ type: String,
+ required: true,
+ },
+ active: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ setup() {
+ return {
+ profileEnabled,
+ displayName: getCurrentUser()!.displayName,
+ }
+ },
+
+ data() {
+ return {
+ loading: false,
+ }
+ },
+
+ mounted() {
+ subscribe('settings:profile-enabled:updated', this.handleProfileEnabledUpdate)
+ subscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
+ },
+
+ beforeDestroy() {
+ unsubscribe('settings:profile-enabled:updated', this.handleProfileEnabledUpdate)
+ unsubscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
+ },
+
+ methods: {
+ handleClick() {
+ if (this.profileEnabled) {
+ this.loading = true
+ }
+ },
+
+ handleProfileEnabledUpdate(profileEnabled: boolean) {
+ this.profileEnabled = profileEnabled
+ },
+
+ handleDisplayNameUpdate(displayName: string) {
+ this.displayName = displayName
+ },
+ },
+})
+</script>
diff --git a/core/src/components/AppMenu.vue b/core/src/components/AppMenu.vue
index 2213840a7c0..88f626ff569 100644
--- a/core/src/components/AppMenu.vue
+++ b/core/src/components/AppMenu.vue
@@ -1,309 +1,161 @@
<!--
- - @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <nav class="app-menu"
+ <nav ref="appMenu"
+ class="app-menu"
:aria-label="t('core', 'Applications menu')">
- <ul class="app-menu-main">
- <li v-for="app in mainAppList"
+ <ul :aria-label="t('core', 'Apps')"
+ class="app-menu__list">
+ <AppMenuEntry v-for="app in mainAppList"
:key="app.id"
- :data-app-id="app.id"
- class="app-menu-entry"
- :class="{ 'app-menu-entry__active': app.active }">
- <a :href="app.href"
- :class="{ 'has-unread': app.unread > 0 }"
- :aria-label="appLabel(app)"
- :title="app.name"
- :aria-current="app.active ? 'page' : false"
- :target="app.target ? '_blank' : undefined"
- :rel="app.target ? 'noopener noreferrer' : undefined">
- <img :src="app.icon" alt="">
- <div class="app-menu-entry--label">
- {{ app.name }}
- <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
- </div>
- </a>
- </li>
+ :app="app" />
</ul>
- <NcActions class="app-menu-more" :aria-label="t('core', 'More apps')">
+ <NcActions class="app-menu__overflow" :aria-label="t('core', 'More apps')">
<NcActionLink v-for="app in popoverAppList"
:key="app.id"
- :aria-label="appLabel(app)"
:aria-current="app.active ? 'page' : false"
:href="app.href"
- class="app-menu-popover-entry">
- <template #icon>
- <div class="app-icon" :class="{ 'has-unread': app.unread > 0 }">
- <img :src="app.icon" alt="">
- </div>
- </template>
+ :icon="app.icon"
+ class="app-menu__overflow-entry">
{{ app.name }}
- <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
</NcActionLink>
</NcActions>
</nav>
</template>
-<script>
-import { loadState } from '@nextcloud/initial-state'
+<script lang="ts">
+import type { INavigationEntry } from '../types/navigation'
+
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
+import { loadState } from '@nextcloud/initial-state'
+import { n, t } from '@nextcloud/l10n'
+import { useElementSize } from '@vueuse/core'
+import { defineComponent, ref } from 'vue'
-export default {
+import AppMenuEntry from './AppMenuEntry.vue'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcActionLink from '@nextcloud/vue/components/NcActionLink'
+import logger from '../logger'
+
+export default defineComponent({
name: 'AppMenu',
+
components: {
- NcActions, NcActionLink,
+ AppMenuEntry,
+ NcActions,
+ NcActionLink,
+ },
+
+ setup() {
+ const appMenu = ref()
+ const { width: appMenuWidth } = useElementSize(appMenu)
+ return {
+ t,
+ n,
+ appMenu,
+ appMenuWidth,
+ }
},
+
data() {
+ const appList = loadState<INavigationEntry[]>('core', 'apps', [])
return {
- apps: loadState('core', 'apps', {}),
- appLimit: 0,
- observer: null,
+ appList,
}
},
+
computed: {
- appList() {
- return Object.values(this.apps)
+ appLimit() {
+ const maxApps = Math.floor(this.appMenuWidth / 50)
+ if (maxApps < this.appList.length) {
+ // Ensure there is space for the overflow menu
+ return Math.max(maxApps - 1, 0)
+ }
+ return maxApps
},
+
mainAppList() {
return this.appList.slice(0, this.appLimit)
},
+
popoverAppList() {
return this.appList.slice(this.appLimit)
},
- appLabel() {
- return (app) => app.name
- + (app.active ? ' (' + t('core', 'Currently open') + ')' : '')
- + (app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '')
- },
},
+
mounted() {
- this.observer = new ResizeObserver(this.resize)
- this.observer.observe(this.$el)
- this.resize()
subscribe('nextcloud:app-menu.refresh', this.setApps)
},
+
beforeDestroy() {
- this.observer.disconnect()
unsubscribe('nextcloud:app-menu.refresh', this.setApps)
},
+
methods: {
- setNavigationCounter(id, counter) {
- this.$set(this.apps[id], 'unread', counter)
- },
- setApps({ apps }) {
- this.apps = apps
- },
- resize() {
- const availableWidth = this.$el.offsetWidth
- let appCount = Math.floor(availableWidth / 50) - 1
- const popoverAppCount = this.appList.length - appCount
- if (popoverAppCount === 1) {
- appCount--
+ setNavigationCounter(id: string, counter: number) {
+ const app = this.appList.find(({ app }) => app === id)
+ if (app) {
+ this.$set(app, 'unread', counter)
+ } else {
+ logger.warn(`Could not find app "${id}" for setting navigation count`)
}
- if (appCount < 1) {
- appCount = 0
- }
- this.appLimit = appCount
+ },
+
+ setApps({ apps }: { apps: INavigationEntry[]}) {
+ this.appList = apps
},
},
-}
+})
</script>
-<style lang="scss" scoped>
-$header-icon-size: 20px;
-
+<style scoped lang="scss">
.app-menu {
- width: 100%;
+ // The size the currently focussed entry will grow to show the full name
+ --app-menu-entry-growth: calc(var(--default-grid-baseline) * 4);
display: flex;
- flex-shrink: 1;
- flex-wrap: wrap;
-}
-.app-menu-main {
- display: flex;
- flex-wrap: nowrap;
+ flex: 1 1;
+ width: 0;
- .app-menu-entry {
- width: 50px;
- height: 50px;
- position: relative;
+ &__list {
display: flex;
+ flex-wrap: nowrap;
+ margin-inline: calc(var(--app-menu-entry-growth) / 2);
+ }
- &.app-menu-entry__active {
- opacity: 1;
-
- &::before {
- content: " ";
- position: absolute;
- pointer-events: none;
- border-bottom-color: var(--color-main-background);
- transform: translateX(-50%);
- width: 12px;
- height: 5px;
- border-radius: 3px;
- background-color: var(--color-primary-text);
- left: 50%;
- bottom: 6px;
- display: block;
- transition: all 0.1s ease-in-out;
- opacity: 1;
- }
-
- .app-menu-entry--label {
- font-weight: bold;
- }
- }
-
- a {
- width: calc(100% - 4px);
- height: calc(100% - 4px);
- margin: 2px;
- // this is shown directly on the background which has `color-primary`, so we need `color-primary-text`
- color: var(--color-primary-text);
- position: relative;
- }
+ &__overflow {
+ margin-block: auto;
- img {
- transition: margin 0.1s ease-in-out;
- width: $header-icon-size;
- height: $header-icon-size;
- padding: calc((100% - $header-icon-size) / 2);
- box-sizing: content-box;
+ // Adjust the overflow NcActions styles as they are directly rendered on the background
+ :deep(.button-vue--vue-tertiary) {
+ opacity: .7;
+ margin: 3px;
filter: var(--background-image-invert-if-bright);
- }
- .app-menu-entry--label {
- opacity: 0;
- position: absolute;
- font-size: 12px;
- // this is shown directly on the background which has `color-primary`, so we need `color-primary-text`
- color: var(--color-primary-text);
- text-align: center;
- left: 50%;
- top: 45%;
- display: block;
- min-width: 100%;
- transform: translateX(-50%);
- transition: all 0.1s ease-in-out;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
- letter-spacing: -0.5px;
- filter: var(--background-image-invert-if-bright);
- }
+ /* Remove all background and align text color if not expanded */
+ &:not([aria-expanded="true"]) {
+ color: var(--color-background-plain-text);
- &:hover,
- &:focus-within {
- opacity: 1;
- .app-menu-entry--label {
- opacity: 1;
- font-weight: bolder;
- bottom: 0;
- width: 100%;
- text-overflow: ellipsis;
- overflow: hidden;
+ &:hover {
+ opacity: 1;
+ background-color: transparent !important;
+ }
}
- }
-
- }
- // Show labels
- &:hover,
- &:focus-within,
- .app-menu-entry:hover,
- .app-menu-entry:focus {
- opacity: 1;
-
- img {
- margin-top: -8px;
- }
-
- .app-menu-entry--label {
- opacity: 1;
- bottom: 0;
- }
-
- &::before, .app-menu-entry::before {
- opacity: 0;
- }
- }
-}
-
-::v-deep .app-menu-more .button-vue--vue-tertiary {
- opacity: .7;
- margin: 3px;
- filter: var(--background-image-invert-if-bright);
-
- /* Remove all background and align text color if not expanded */
- &:not([aria-expanded="true"]) {
- color: var(--color-primary-element-text);
-
- &:hover {
- opacity: 1;
- background-color: transparent !important;
+ &:focus-visible {
+ opacity: 1;
+ outline: none !important;
+ }
}
}
- &:focus-visible {
- opacity: 1;
- outline: none !important;
- }
-}
-
-.app-menu-popover-entry {
- .app-icon {
- position: relative;
- height: 44px;
- width: 48px;
- display: flex;
- align-items: center;
- justify-content: center;
- /* Icons are bright so invert them if bright color theme == bright background is used */
- filter: var(--background-invert-if-bright);
-
- &.has-unread::after {
- background-color: var(--color-main-text);
- }
-
- img {
- width: $header-icon-size;
- height: $header-icon-size;
+ &__overflow-entry {
+ :deep(.action-link__icon) {
+ // Icons are bright so invert them if bright color theme == bright background is used
+ filter: var(--background-invert-if-bright) !important;
}
}
}
-
-.has-unread::after {
- content: "";
- width: 8px;
- height: 8px;
- background-color: var(--color-primary-element-text);
- border-radius: 50%;
- position: absolute;
- display: block;
- top: 10px;
- right: 10px;
-}
-
-.unread-counter {
- display: none;
-}
</style>
diff --git a/core/src/components/AppMenuEntry.vue b/core/src/components/AppMenuEntry.vue
new file mode 100644
index 00000000000..4c5acb7e9c8
--- /dev/null
+++ b/core/src/components/AppMenuEntry.vue
@@ -0,0 +1,189 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+
+<template>
+ <li ref="containerElement"
+ class="app-menu-entry"
+ :class="{
+ 'app-menu-entry--active': app.active,
+ 'app-menu-entry--truncated': needsSpace,
+ }">
+ <a class="app-menu-entry__link"
+ :href="app.href"
+ :title="app.name"
+ :aria-current="app.active ? 'page' : false"
+ :target="app.target ? '_blank' : undefined"
+ :rel="app.target ? 'noopener noreferrer' : undefined">
+ <AppMenuIcon class="app-menu-entry__icon" :app="app" />
+ <span ref="labelElement" class="app-menu-entry__label">
+ {{ app.name }}
+ </span>
+ </a>
+ </li>
+</template>
+
+<script setup lang="ts">
+import type { INavigationEntry } from '../types/navigation'
+import { onMounted, ref, watch } from 'vue'
+import AppMenuIcon from './AppMenuIcon.vue'
+
+const props = defineProps<{
+ app: INavigationEntry
+}>()
+
+const containerElement = ref<HTMLLIElement>()
+const labelElement = ref<HTMLSpanElement>()
+const needsSpace = ref(false)
+
+/** Update the space requirements of the app label */
+function calculateSize() {
+ const maxWidth = containerElement.value!.clientWidth
+ // Also keep the 0.5px letter spacing in mind
+ needsSpace.value = (maxWidth - props.app.name.length * 0.5) < (labelElement.value!.scrollWidth)
+}
+// Update size on mounted and when the app name changes
+onMounted(calculateSize)
+watch(() => props.app.name, calculateSize)
+</script>
+
+<style scoped lang="scss">
+.app-menu-entry {
+ --app-menu-entry-font-size: 12px;
+ width: var(--header-height);
+ height: var(--header-height);
+ position: relative;
+
+ &__link {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ // Set color as this is shown directly on the background
+ color: var(--color-background-plain-text);
+ // Make space for focus-visible outline
+ width: calc(100% - 4px);
+ height: calc(100% - 4px);
+ margin: 2px;
+ }
+
+ &__label {
+ opacity: 0;
+ position: absolute;
+ font-size: var(--app-menu-entry-font-size);
+ // this is shown directly on the background
+ color: var(--color-background-plain-text);
+ text-align: center;
+ bottom: 0;
+ inset-inline-start: 50%;
+ top: 50%;
+ display: block;
+ transform: translateX(-50%);
+ max-width: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ letter-spacing: -0.5px;
+ }
+ body[dir=rtl] &__label {
+ transform: translateX(50%) !important;
+ }
+
+ &__icon {
+ font-size: var(--app-menu-entry-font-size);
+ }
+
+ &--active {
+ // When hover or focus, show the label and make it bolder than the other entries
+ .app-menu-entry__label {
+ font-weight: bolder;
+ }
+
+ // When active show a line below the entry as an "active" indicator
+ &::before {
+ content: " ";
+ position: absolute;
+ pointer-events: none;
+ border-bottom-color: var(--color-main-background);
+ transform: translateX(-50%);
+ width: 10px;
+ height: 5px;
+ border-radius: 3px;
+ background-color: var(--color-background-plain-text);
+ inset-inline-start: 50%;
+ bottom: 8px;
+ display: block;
+ transition: all var(--animation-quick) ease-in-out;
+ opacity: 1;
+ }
+ body[dir=rtl] &::before {
+ transform: translateX(50%) !important;
+ }
+ }
+
+ &__icon,
+ &__label {
+ transition: all var(--animation-quick) ease-in-out;
+ }
+
+ // Make the hovered entry bold to see that it is hovered
+ &:hover .app-menu-entry__label,
+ &:focus-within .app-menu-entry__label {
+ font-weight: bold;
+ }
+
+ // Adjust the width when an entry is focussed
+ // The focussed / hovered entry should grow, while both neighbors need to shrink
+ &--truncated:hover,
+ &--truncated:focus-within {
+ .app-menu-entry__label {
+ max-width: calc(var(--header-height) + var(--app-menu-entry-growth));
+ }
+
+ // The next entry needs to shrink half the growth
+ + .app-menu-entry {
+ .app-menu-entry__label {
+ font-weight: normal;
+ max-width: calc(var(--header-height) - var(--app-menu-entry-growth));
+ }
+ }
+ }
+
+ // The previous entry needs to shrink half the growth
+ &:has(+ .app-menu-entry--truncated:hover),
+ &:has(+ .app-menu-entry--truncated:focus-within) {
+ .app-menu-entry__label {
+ font-weight: normal;
+ max-width: calc(var(--header-height) - var(--app-menu-entry-growth));
+ }
+ }
+}
+</style>
+
+<style lang="scss">
+// Showing the label
+.app-menu-entry:hover,
+.app-menu-entry:focus-within,
+.app-menu__list:hover,
+.app-menu__list:focus-within {
+ // Move icon up so that the name does not overflow the icon
+ .app-menu-entry__icon {
+ margin-block-end: 1lh;
+ }
+
+ // Make the label visible
+ .app-menu-entry__label {
+ opacity: 1;
+ }
+
+ // Hide indicator when the text is shown
+ .app-menu-entry--active::before {
+ opacity: 0;
+ }
+
+ .app-menu-icon__unread {
+ opacity: 0;
+ }
+}
+</style>
diff --git a/core/src/components/AppMenuIcon.vue b/core/src/components/AppMenuIcon.vue
new file mode 100644
index 00000000000..1b0d48daf8c
--- /dev/null
+++ b/core/src/components/AppMenuIcon.vue
@@ -0,0 +1,67 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+
+<template>
+ <span class="app-menu-icon"
+ role="img"
+ :aria-hidden="ariaHidden"
+ :aria-label="ariaLabel">
+ <img class="app-menu-icon__icon" :src="app.icon" alt="">
+ <IconDot v-if="app.unread" class="app-menu-icon__unread" :size="10" />
+ </span>
+</template>
+
+<script setup lang="ts">
+import type { INavigationEntry } from '../types/navigation.ts'
+
+import { n } from '@nextcloud/l10n'
+import { computed } from 'vue'
+import IconDot from 'vue-material-design-icons/CircleOutline.vue'
+
+const props = defineProps<{
+ app: INavigationEntry
+}>()
+
+// only hide if there are no unread notifications
+const ariaHidden = computed(() => !props.app.unread ? 'true' : undefined)
+
+const ariaLabel = computed(() => {
+ if (!props.app.unread) {
+ return undefined
+ }
+
+ return `${props.app.name} (${n('core', '{count} notification', '{count} notifications', props.app.unread, { count: props.app.unread })})`
+})
+</script>
+
+<style scoped lang="scss">
+$icon-size: 20px;
+$unread-indicator-size: 10px;
+
+.app-menu-icon {
+ box-sizing: border-box;
+ position: relative;
+
+ height: $icon-size;
+ width: $icon-size;
+
+ &__icon {
+ transition: margin 0.1s ease-in-out;
+ height: $icon-size;
+ width: $icon-size;
+ filter: var(--background-image-invert-if-bright);
+ mask: var(--header-menu-icon-mask);
+ }
+
+ &__unread {
+ color: var(--color-error);
+ position: absolute;
+ // Align the dot to the top right corner of the icon
+ inset-block-end: calc($icon-size + ($unread-indicator-size / -2));
+ inset-inline-end: calc($unread-indicator-size / -2);
+ transition: all 0.1s ease-in-out;
+ }
+}
+</style>
diff --git a/core/src/components/ContactsMenu.js b/core/src/components/ContactsMenu.js
index 1b7b25873d0..e07a699ab9f 100644
--- a/core/src/components/ContactsMenu.js
+++ b/core/src/components/ContactsMenu.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Christopher Ng <chrng8@gmail.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import Vue from 'vue'
@@ -34,6 +15,7 @@ export const setUp = () => {
if (mountPoint) {
// eslint-disable-next-line no-new
new Vue({
+ name: 'ContactsMenuRoot',
el: mountPoint,
render: h => h(ContactsMenu),
})
diff --git a/core/src/components/ContactsMenu/Contact.vue b/core/src/components/ContactsMenu/Contact.vue
index a450127b937..322f53647b1 100644
--- a/core/src/components/ContactsMenu/Contact.vue
+++ b/core/src/components/ContactsMenu/Contact.vue
@@ -1,23 +1,7 @@
<!--
- - @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<li class="contact">
@@ -39,7 +23,7 @@
:inline="contact.topAction ? 1 : 0">
<template v-for="(action, idx) in actions">
<NcActionLink v-if="action.hyperlink !== '#'"
- :key="idx"
+ :key="`${idx}-link`"
:href="action.hyperlink"
class="other-actions">
<template #icon>
@@ -47,30 +31,46 @@
</template>
{{ action.title }}
</NcActionLink>
- <NcActionText v-else :key="idx" class="other-actions">
+ <NcActionText v-else :key="`${idx}-text`" class="other-actions">
<template #icon>
<img aria-hidden="true" class="contact__action__icon" :src="action.icon">
</template>
{{ action.title }}
</NcActionText>
</template>
+ <NcActionButton v-for="action in jsActions"
+ :key="action.id"
+ :close-after-click="true"
+ class="other-actions"
+ @click="action.callback(contact)">
+ <template #icon>
+ <NcIconSvgWrapper class="contact__action__icon-svg"
+ :svg="action.iconSvg(contact)" />
+ </template>
+ {{ action.displayName(contact) }}
+ </NcActionButton>
</NcActions>
</li>
</template>
<script>
-import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
-import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
+import NcActionLink from '@nextcloud/vue/components/NcActionLink'
+import NcActionText from '@nextcloud/vue/components/NcActionText'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import { getEnabledContactsMenuActions } from '@nextcloud/vue/functions/contactsMenu'
export default {
name: 'Contact',
components: {
NcActionLink,
NcActionText,
+ NcActionButton,
NcActions,
NcAvatar,
+ NcIconSvgWrapper,
},
props: {
contact: {
@@ -85,6 +85,9 @@ export default {
}
return this.contact.actions
},
+ jsActions() {
+ return getEnabledContactsMenuActions(this.contact)
+ },
preloadedUserStatus() {
if (this.contact.status) {
return {
@@ -94,7 +97,7 @@ export default {
}
}
return undefined
- }
+ },
},
}
</script>
@@ -104,7 +107,8 @@ export default {
display: flex;
position: relative;
align-items: center;
- padding: 3px 3px 3px 10px;
+ padding: 3px;
+ padding-inline-start: 10px;
&__action {
&__icon {
@@ -113,9 +117,10 @@ export default {
padding: 12px;
filter: var(--background-invert-if-dark);
}
- }
- &__avatar-wrapper {
+ &__icon-svg {
+ padding: 5px;
+ }
}
&__avatar {
@@ -124,8 +129,8 @@ export default {
&__body {
flex-grow: 1;
- padding-left: 10px;
- margin-left: 10px;
+ padding-inline-start: 10px;
+ margin-inline-start: 10px;
min-width: 0;
div {
@@ -178,11 +183,11 @@ export default {
/* actions menu */
.menu {
top: 47px;
- margin-right: 13px;
+ margin-inline-end: 13px;
}
.popovermenu::after {
- right: 2px;
+ inset-inline-end: 2px;
}
}
</style>
diff --git a/core/src/components/LegacyDialogPrompt.vue b/core/src/components/LegacyDialogPrompt.vue
new file mode 100644
index 00000000000..f2ee4be9151
--- /dev/null
+++ b/core/src/components/LegacyDialogPrompt.vue
@@ -0,0 +1,111 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcDialog dialog-classes="legacy-prompt__dialog"
+ :buttons="buttons"
+ :name="name"
+ @update:open="$emit('close', false, inputValue)">
+ <p class="legacy-prompt__text" v-text="text" />
+ <NcPasswordField v-if="isPassword"
+ ref="input"
+ autocomplete="new-password"
+ class="legacy-prompt__input"
+ :label="name"
+ :name="inputName"
+ :value.sync="inputValue" />
+ <NcTextField v-else
+ ref="input"
+ class="legacy-prompt__input"
+ :label="name"
+ :name="inputName"
+ :value.sync="inputValue" />
+ </NcDialog>
+</template>
+
+<script lang="ts">
+import { translate as t } from '@nextcloud/l10n'
+import { defineComponent } from 'vue'
+
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
+
+export default defineComponent({
+ name: 'LegacyDialogPrompt',
+
+ components: {
+ NcDialog,
+ NcTextField,
+ NcPasswordField,
+ },
+
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+
+ text: {
+ type: String,
+ required: true,
+ },
+
+ isPassword: {
+ type: Boolean,
+ required: true,
+ },
+
+ inputName: {
+ type: String,
+ default: 'prompt-input',
+ },
+ },
+
+ emits: ['close'],
+
+ data() {
+ return {
+ inputValue: '',
+ }
+ },
+
+ computed: {
+ buttons() {
+ return [
+ {
+ label: t('core', 'No'),
+ callback: () => this.$emit('close', false, this.inputValue),
+ },
+ {
+ label: t('core', 'Yes'),
+ type: 'primary',
+ callback: () => this.$emit('close', true, this.inputValue),
+ },
+ ]
+ },
+ },
+
+ mounted() {
+ this.$nextTick(() => this.$refs.input?.focus?.())
+ },
+})
+</script>
+
+<style scoped lang="scss">
+.legacy-prompt {
+ &__text {
+ margin-block: 0 .75em;
+ }
+
+ &__input {
+ margin-block: 0 1em;
+ }
+}
+
+:deep(.legacy-prompt__dialog .dialog__actions) {
+ min-width: calc(100% - 12px);
+ justify-content: space-between;
+}
+</style>
diff --git a/core/src/components/MainMenu.js b/core/src/components/MainMenu.js
index 46e0e5c510b..21a0b6a772f 100644
--- a/core/src/components/MainMenu.js
+++ b/core/src/components/MainMenu.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
@@ -36,7 +17,7 @@ export const setUp = () => {
},
})
- const container = document.getElementById('header-left__appmenu')
+ const container = document.getElementById('header-start__appmenu')
if (!container) {
// no container, possibly we're on a public page
return
diff --git a/core/src/components/Profile/PrimaryActionButton.vue b/core/src/components/Profile/PrimaryActionButton.vue
index d09b348c62b..dbc446b3d90 100644
--- a/core/src/components/Profile/PrimaryActionButton.vue
+++ b/core/src/components/Profile/PrimaryActionButton.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -38,8 +21,8 @@
<script>
import { defineComponent } from 'vue'
-import { NcButton } from '@nextcloud/vue'
-import { translate as t } from '@nextcloud/l10n'
+import { t } from '@nextcloud/l10n'
+import NcButton from '@nextcloud/vue/components/NcButton'
export default defineComponent({
name: 'PrimaryActionButton',
diff --git a/core/src/components/PublicPageMenu/PublicPageMenuCustomEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuCustomEntry.vue
new file mode 100644
index 00000000000..f3c57a12042
--- /dev/null
+++ b/core/src/components/PublicPageMenu/PublicPageMenuCustomEntry.vue
@@ -0,0 +1,36 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<template>
+ <!-- eslint-disable-next-line vue/no-v-html -->
+ <li ref="listItem" :role="itemRole" v-html="html" />
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+
+defineProps<{
+ id: string
+ html: string
+}>()
+
+const listItem = ref<HTMLLIElement>()
+const itemRole = ref('presentation')
+
+onMounted(() => {
+ // check for proper roles
+ const menuitem = listItem.value?.querySelector('[role="menuitem"]')
+ if (menuitem) {
+ return
+ }
+ // check if a button is available
+ const button = listItem.value?.querySelector('button') ?? listItem.value?.querySelector('a')
+ if (button) {
+ button.role = 'menuitem'
+ } else {
+ // if nothing is available set role on `<li>`
+ itemRole.value = 'menuitem'
+ }
+})
+</script>
diff --git a/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue
new file mode 100644
index 00000000000..413806c7089
--- /dev/null
+++ b/core/src/components/PublicPageMenu/PublicPageMenuEntry.vue
@@ -0,0 +1,51 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<template>
+ <NcListItem :anchor-id="`${id}--link`"
+ compact
+ :details="details"
+ :href="href"
+ :name="label"
+ role="presentation"
+ @click="$emit('click')">
+ <template #icon>
+ <slot v-if="$scopedSlots.icon" name="icon" />
+ <div v-else role="presentation" :class="['icon', icon, 'public-page-menu-entry__icon']" />
+ </template>
+ </NcListItem>
+</template>
+
+<script setup lang="ts">
+import { onMounted } from 'vue'
+
+import NcListItem from '@nextcloud/vue/components/NcListItem'
+
+const props = defineProps<{
+ /** Only emit click event but do not open href */
+ clickOnly?: boolean
+ // menu entry props
+ id: string
+ label: string
+ icon?: string
+ href: string
+ details?: string
+}>()
+
+onMounted(() => {
+ const anchor = document.getElementById(`${props.id}--link`) as HTMLAnchorElement
+ // Make the `<a>` a menuitem
+ anchor.role = 'menuitem'
+ // Prevent native click handling if required
+ if (props.clickOnly) {
+ anchor.onclick = (event) => event.preventDefault()
+ }
+})
+</script>
+
+<style scoped>
+.public-page-menu-entry__icon {
+ padding-inline-start: var(--default-grid-baseline);
+}
+</style>
diff --git a/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue b/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue
new file mode 100644
index 00000000000..0f02bdf7524
--- /dev/null
+++ b/core/src/components/PublicPageMenu/PublicPageMenuExternalDialog.vue
@@ -0,0 +1,90 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<template>
+ <NcDialog is-form
+ :name="label"
+ :open.sync="open"
+ @submit="createFederatedShare">
+ <NcTextField ref="input"
+ :label="t('core', 'Federated user')"
+ :placeholder="t('core', 'user@your-nextcloud.org')"
+ required
+ :value.sync="remoteUrl" />
+ <template #actions>
+ <NcButton :disabled="loading" type="primary" native-type="submit">
+ <template v-if="loading" #icon>
+ <NcLoadingIcon />
+ </template>
+ {{ t('core', 'Create share') }}
+ </NcButton>
+ </template>
+ </NcDialog>
+</template>
+
+<script setup lang="ts">
+import type Vue from 'vue'
+
+import { t } from '@nextcloud/l10n'
+import { showError } from '@nextcloud/dialogs'
+import { generateUrl } from '@nextcloud/router'
+import { getSharingToken } from '@nextcloud/sharing/public'
+import { nextTick, onMounted, ref, watch } from 'vue'
+import axios from '@nextcloud/axios'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+import logger from '../../logger'
+
+defineProps<{
+ label: string
+}>()
+
+const loading = ref(false)
+const remoteUrl = ref('')
+// Todo: @nextcloud/vue should expose the types correctly
+const input = ref<Vue & { focus: () => void }>()
+const open = ref(true)
+
+// Focus when mounted
+onMounted(() => nextTick(() => input.value!.focus()))
+
+// Check validity
+watch(remoteUrl, () => {
+ let validity = ''
+ if (!remoteUrl.value.includes('@')) {
+ validity = t('core', 'The remote URL must include the user.')
+ } else if (!remoteUrl.value.match(/@(.+\..{2,}|localhost)(:\d\d+)?$/)) {
+ validity = t('core', 'Invalid remote URL.')
+ }
+ input.value!.$el.querySelector('input')!.setCustomValidity(validity)
+ input.value!.$el.querySelector('input')!.reportValidity()
+})
+
+/**
+ * Create a federated share for the current share
+ */
+async function createFederatedShare() {
+ loading.value = true
+
+ try {
+ const url = generateUrl('/apps/federatedfilesharing/createFederatedShare')
+ const { data } = await axios.post<{ remoteUrl: string }>(url, {
+ shareWith: remoteUrl.value,
+ token: getSharingToken(),
+ })
+ if (data.remoteUrl.includes('://')) {
+ window.location.href = data.remoteUrl
+ } else {
+ window.location.href = `${window.location.protocol}//${data.remoteUrl}`
+ }
+ } catch (error) {
+ logger.error('Failed to create federated share', { error })
+ showError(t('files_sharing', 'Failed to add the public link to your Nextcloud'))
+ } finally {
+ loading.value = false
+ }
+}
+</script>
diff --git a/core/src/components/PublicPageMenu/PublicPageMenuExternalEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuExternalEntry.vue
new file mode 100644
index 00000000000..a4451a38bbe
--- /dev/null
+++ b/core/src/components/PublicPageMenu/PublicPageMenuExternalEntry.vue
@@ -0,0 +1,36 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<template>
+ <PublicPageMenuEntry :id="id"
+ :icon="icon"
+ href="#"
+ :label="label"
+ @click="openDialog" />
+</template>
+
+<script setup lang="ts">
+import { spawnDialog } from '@nextcloud/dialogs'
+import PublicPageMenuEntry from './PublicPageMenuEntry.vue'
+import PublicPageMenuExternalDialog from './PublicPageMenuExternalDialog.vue'
+
+const props = defineProps<{
+ id: string
+ label: string
+ icon: string
+ href: string
+}>()
+
+const emit = defineEmits<{
+ (e: 'click'): void
+}>()
+
+/**
+ * Open the "create federated share" dialog
+ */
+function openDialog() {
+ spawnDialog(PublicPageMenuExternalDialog, { label: props.label })
+ emit('click')
+}
+</script>
diff --git a/core/src/components/PublicPageMenu/PublicPageMenuLinkEntry.vue b/core/src/components/PublicPageMenu/PublicPageMenuLinkEntry.vue
new file mode 100644
index 00000000000..5f3a4883d6d
--- /dev/null
+++ b/core/src/components/PublicPageMenu/PublicPageMenuLinkEntry.vue
@@ -0,0 +1,51 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<template>
+ <PublicPageMenuEntry :id="id"
+ click-only
+ :icon="icon"
+ :href="href"
+ :label="label"
+ @click="onClick" />
+</template>
+
+<script setup lang="ts">
+import { showSuccess } from '@nextcloud/dialogs'
+import { t } from '@nextcloud/l10n'
+import PublicPageMenuEntry from './PublicPageMenuEntry.vue'
+
+const props = defineProps<{
+ id: string
+ label: string
+ icon: string
+ href: string
+}>()
+
+const emit = defineEmits<{
+ (e: 'click'): void
+}>()
+
+/**
+ * Copy the href to the clipboard
+ */
+async function copyLink() {
+ try {
+ await window.navigator.clipboard.writeText(props.href)
+ showSuccess(t('core', 'Direct link copied'))
+ } catch {
+ // No secure context -> fallback to dialog
+ window.prompt(t('core', 'Please copy the link manually:'), props.href)
+ }
+}
+
+/**
+ * onclick handler to trigger the "copy link" action
+ * and emit the event so the menu can be closed
+ */
+function onClick() {
+ copyLink()
+ emit('click')
+}
+</script>
diff --git a/core/src/components/UnifiedSearch/CustomDateRangeModal.vue b/core/src/components/UnifiedSearch/CustomDateRangeModal.vue
index ec592732a8d..d86192d156e 100644
--- a/core/src/components/UnifiedSearch/CustomDateRangeModal.vue
+++ b/core/src/components/UnifiedSearch/CustomDateRangeModal.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcModal v-if="isModalOpen"
id="unified-search"
@@ -33,9 +37,9 @@
</template>
<script>
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcDateTimePicker from '@nextcloud/vue/dist/Components/NcDateTimePickerNative.js'
-import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePickerNative'
+import NcModal from '@nextcloud/vue/components/NcModal'
import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue'
export default {
diff --git a/core/src/components/UnifiedSearch/LegacySearchResult.vue b/core/src/components/UnifiedSearch/LegacySearchResult.vue
index 01f48a36709..4592adf08c9 100644
--- a/core/src/components/UnifiedSearch/LegacySearchResult.vue
+++ b/core/src/components/UnifiedSearch/LegacySearchResult.vue
@@ -1,24 +1,7 @@
- <!--
- - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+<!--
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<a :href="resourceUrl || '#'"
class="unified-search__result"
@@ -59,7 +42,7 @@
</template>
<script>
-import NcHighlight from '@nextcloud/vue/dist/Components/NcHighlight.js'
+import NcHighlight from '@nextcloud/vue/components/NcHighlight'
export default {
name: 'LegacySearchResult',
@@ -236,7 +219,7 @@ $margin: 10px;
flex-wrap: wrap;
// Set to minimum and gro from it
min-width: 0;
- padding-left: $margin;
+ padding-inline-start: $margin;
}
&-line-one,
diff --git a/core/src/components/UnifiedSearch/SearchFilterChip.vue b/core/src/components/UnifiedSearch/SearchFilterChip.vue
index 8342e9e256d..e08ddd58a4b 100644
--- a/core/src/components/UnifiedSearch/SearchFilterChip.vue
+++ b/core/src/components/UnifiedSearch/SearchFilterChip.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div class="chip">
<span class="icon">
@@ -50,7 +54,7 @@ export default {
.icon {
display: flex;
align-items: center;
- padding-right: 5px;
+ padding-inline-end: 5px;
img {
width: 20px;
diff --git a/core/src/components/UnifiedSearch/SearchResult.vue b/core/src/components/UnifiedSearch/SearchResult.vue
index a746a5751b7..4f33fbd54cc 100644
--- a/core/src/components/UnifiedSearch/SearchResult.vue
+++ b/core/src/components/UnifiedSearch/SearchResult.vue
@@ -1,16 +1,20 @@
+<!--
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <NcListItem class="result-items__item"
+ <NcListItem class="result-item"
:name="title"
:bold="false"
:href="resourceUrl"
target="_self">
<template #icon>
<div aria-hidden="true"
- class="result-items__item-icon"
+ class="result-item__icon"
:class="{
- 'result-items__item-icon--rounded': rounded,
- 'result-items__item-icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl),
- 'result-items__item-icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl),
+ 'result-item__icon--rounded': rounded,
+ 'result-item__icon--no-preview': !isValidIconOrPreviewUrl(thumbnailUrl),
+ 'result-item__icon--with-thumbnail': isValidIconOrPreviewUrl(thumbnailUrl),
[icon]: !isValidIconOrPreviewUrl(icon),
}"
:style="{
@@ -28,7 +32,7 @@
</template>
<script>
-import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
+import NcListItem from '@nextcloud/vue/components/NcListItem'
export default {
name: 'SearchResult',
@@ -97,73 +101,59 @@ export default {
</script>
<style lang="scss" scoped>
-@use "sass:math";
-$clickable-area: 44px;
-$margin: 10px;
-
-.result-items {
- &__item {
-
- ::v-deep a {
- border-radius: 12px;
- border: 2px solid transparent;
- border-radius: var(--border-radius-large) !important;
-
- &--focused {
- background-color: var(--color-background-hover);
- }
-
- &:active,
- &:hover,
- &:focus {
- background-color: var(--color-background-hover);
- border: 2px solid var(--color-border-maxcontrast);
- }
-
- * {
- cursor: pointer;
- }
-
- }
-
- &-icon {
- overflow: hidden;
- width: $clickable-area;
- height: $clickable-area;
- border-radius: var(--border-radius);
- background-repeat: no-repeat;
- background-position: center center;
- background-size: 32px;
-
- &--rounded {
- border-radius: math.div($clickable-area, 2);
- }
+.result-item {
+ :deep(a) {
+ border: 2px solid transparent;
+ border-radius: var(--border-radius-large) !important;
+
+ &:active,
+ &:hover,
+ &:focus {
+ background-color: var(--color-background-hover);
+ border: 2px solid var(--color-border-maxcontrast);
+ }
- &--no-preview {
- background-size: 32px;
- }
+ * {
+ cursor: pointer;
+ }
+ }
+
+ &__icon {
+ overflow: hidden;
+ width: var(--default-clickable-area);
+ height: var(--default-clickable-area);
+ border-radius: var(--border-radius);
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 32px;
+
+ &--rounded {
+ border-radius: calc(var(--default-clickable-area) / 2);
+ }
- &--with-thumbnail {
- background-size: cover;
- }
+ &--no-preview {
+ background-size: 32px;
+ }
- &--with-thumbnail:not(&--rounded) {
- // compensate for border
- max-width: $clickable-area - 2px;
- max-height: $clickable-area - 2px;
- border: 1px solid var(--color-border);
- }
+ &--with-thumbnail {
+ background-size: cover;
+ }
- img {
- // Make sure to keep ratio
- width: 100%;
- height: 100%;
+ &--with-thumbnail:not(#{&}--rounded) {
+ border: 1px solid var(--color-border);
+ // compensate for border
+ max-height: calc(var(--default-clickable-area) - 2px);
+ max-width: calc(var(--default-clickable-area) - 2px);
+ }
- object-fit: cover;
- object-position: center;
- }
- }
+ img {
+ // Make sure to keep ratio
+ width: 100%;
+ height: 100%;
- }
+ object-fit: cover;
+ object-position: center;
+ }
+ }
}
</style>
diff --git a/core/src/components/UnifiedSearch/SearchResultPlaceholders.vue b/core/src/components/UnifiedSearch/SearchResultPlaceholders.vue
index d2a297a0a37..aec2791d8e4 100644
--- a/core/src/components/UnifiedSearch/SearchResultPlaceholders.vue
+++ b/core/src/components/UnifiedSearch/SearchResultPlaceholders.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<ul>
<!-- Placeholder animation -->
diff --git a/core/src/components/UnifiedSearch/SearchableList.vue b/core/src/components/UnifiedSearch/SearchableList.vue
index 33f45d06266..d7abb6ffdbb 100644
--- a/core/src/components/UnifiedSearch/SearchableList.vue
+++ b/core/src/components/UnifiedSearch/SearchableList.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Marco Ambrosini <marcoambrosini@proton.me>
- -
- - @author Marco Ambrosini <marcoambrosini@proton.me>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -34,7 +17,7 @@
:show-trailing-button="searchTerm !== ''"
@update:value="searchTermChanged"
@trailing-button-click="clearSearch">
- <Magnify :size="20" />
+ <IconMagnify :size="20" />
</NcTextField>
<ul v-if="filteredList.length > 0" class="searchable-list__list">
<li v-for="element in filteredList"
@@ -46,7 +29,11 @@
:wide="true"
@click="itemSelected(element)">
<template #icon>
- <NcAvatar :user="element.user" :show-user-status="false" :hide-favorite="false" />
+ <NcAvatar v-if="element.isUser" :user="element.user" :show-user-status="false" />
+ <NcAvatar v-else
+ :is-no-user="true"
+ :display-name="element.displayName"
+ :show-user-status="false" />
</template>
{{ element.displayName }}
</NcButton>
@@ -55,7 +42,7 @@
<div v-else class="searchable-list__empty-content">
<NcEmptyContent :name="emptyContentText">
<template #icon>
- <AlertCircleOutline />
+ <IconAlertCircleOutline />
</template>
</NcEmptyContent>
</div>
@@ -64,22 +51,26 @@
</template>
<script>
-import { NcPopover, NcTextField, NcAvatar, NcEmptyContent, NcButton } from '@nextcloud/vue'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcPopover from '@nextcloud/vue/components/NcPopover'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
-import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
-import Magnify from 'vue-material-design-icons/Magnify.vue'
+import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
+import IconMagnify from 'vue-material-design-icons/Magnify.vue'
export default {
name: 'SearchableList',
components: {
- NcPopover,
- NcTextField,
- Magnify,
- AlertCircleOutline,
+ IconMagnify,
+ IconAlertCircleOutline,
NcAvatar,
- NcEmptyContent,
NcButton,
+ NcEmptyContent,
+ NcPopover,
+ NcTextField,
},
props: {
diff --git a/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue b/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue
new file mode 100644
index 00000000000..171eada8a06
--- /dev/null
+++ b/core/src/components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue
@@ -0,0 +1,166 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <Transition>
+ <div v-if="open"
+ class="local-unified-search animated-width"
+ :class="{ 'local-unified-search--open': open }">
+ <!-- We can not use labels as it breaks the header layout so only aria-label and placeholder -->
+ <NcInputField ref="searchInput"
+ class="local-unified-search__input animated-width"
+ :aria-label="t('core', 'Search in current app')"
+ :placeholder="t('core', 'Search in current app')"
+ show-trailing-button
+ :trailing-button-label="t('core', 'Clear search')"
+ :value="query"
+ @update:value="$emit('update:query', $event)"
+ @trailing-button-click="clearAndCloseSearch">
+ <template #trailing-button-icon>
+ <NcIconSvgWrapper :path="mdiClose" />
+ </template>
+ </NcInputField>
+
+ <NcButton ref="searchGlobalButton"
+ class="local-unified-search__global-search"
+ :aria-label="t('core', 'Search everywhere')"
+ :title="t('core', 'Search everywhere')"
+ type="tertiary-no-background"
+ @click="$emit('global-search')">
+ <template v-if="!isMobile" #default>
+ {{ t('core', 'Search everywhere') }}
+ </template>
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiCloudSearchOutline" />
+ </template>
+ </NcButton>
+ </div>
+ </Transition>
+</template>
+
+<script lang="ts" setup>
+import type { ComponentPublicInstance } from 'vue'
+import { mdiCloudSearchOutline, mdiClose } from '@mdi/js'
+import { translate as t } from '@nextcloud/l10n'
+import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
+import { useElementSize } from '@vueuse/core'
+import { computed, ref, watchEffect } from 'vue'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcInputField from '@nextcloud/vue/components/NcInputField'
+
+const props = defineProps<{
+ query: string,
+ open: boolean
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:open', open: boolean): void
+ (e: 'update:query', query: string): void
+ (e: 'global-search'): void
+}>()
+
+// Hacky type until the library provides real Types
+type FocusableComponent = ComponentPublicInstance<object, object, object, Record<string, never>, { focus: () => void }>
+/** The input field component */
+const searchInput = ref<FocusableComponent>()
+/** When the search bar is opened we focus the input */
+watchEffect(() => {
+ if (props.open && searchInput.value) {
+ searchInput.value.focus()
+ }
+})
+
+/** Current window size is below the "mobile" breakpoint (currently 1024px) */
+const isMobile = useIsMobile()
+
+const searchGlobalButton = ref<ComponentPublicInstance>()
+/** Width of the search global button, used to resize the input field */
+const { width: searchGlobalButtonWidth } = useElementSize(searchGlobalButton)
+const searchGlobalButtonCSSWidth = computed(() => searchGlobalButtonWidth.value ? `${searchGlobalButtonWidth.value}px` : 'var(--default-clickable-area)')
+
+/**
+ * Clear the search query and close the search bar
+ */
+function clearAndCloseSearch() {
+ emit('update:query', '')
+ emit('update:open', false)
+}
+</script>
+
+<style scoped lang="scss">
+.local-unified-search {
+ --local-search-width: min(calc(250px + v-bind('searchGlobalButtonCSSWidth')), 95vw);
+ box-sizing: border-box;
+ position: relative;
+ height: var(--header-height);
+ width: var(--local-search-width);
+ display: flex;
+ align-items: center;
+ // Ensure it overlays the other entries
+ z-index: 10;
+ // add some padding for the focus visible outline
+ padding-inline: var(--border-width-input-focused);
+ // hide the overflow - needed for the transition
+ overflow: hidden;
+ // Ensure the position is fixed also during "position: absolut" (transition)
+ inset-inline-end: 0;
+
+ #{&} &__global-search {
+ position: absolute;
+ inset-inline-end: var(--default-clickable-area);
+ }
+
+ #{&} &__input {
+ box-sizing: border-box;
+ // override some nextcloud-vue styles
+ margin: 0;
+ width: var(--local-search-width);
+
+ // Fixup the spacing so we can fit in the "search globally" button
+ // this can break at any time the component library changes
+ :deep(input) {
+ // search global width + close button width
+ padding-inline-end: calc(v-bind('searchGlobalButtonCSSWidth') + var(--default-clickable-area));
+ }
+ }
+}
+
+.animated-width {
+ transition: width var(--animation-quick) linear;
+}
+
+// Make the position absolute during the transition
+// this is needed to "hide" the button behind it
+.v-leave-active {
+ position: absolute !important;
+}
+
+.v-enter,
+.v-leave-to {
+ &.local-unified-search {
+ // Start with only the overlay button
+ --local-search-width: var(--clickable-area-large);
+ }
+}
+
+@media screen and (max-width: 500px) {
+ .local-unified-search.local-unified-search--open {
+ // 100% but still show the menu toggle on the very right
+ --local-search-width: 100vw;
+ padding-inline: var(--default-grid-baseline);
+ }
+
+ // when open we need to position it absolute to allow overlay the full bar
+ :global(.unified-search-menu:has(.local-unified-search--open)) {
+ position: absolute !important;
+ inset-inline: 0;
+ }
+ // Hide all other entries, especially the user menu as it might leak pixels
+ :global(.header-end:has(.local-unified-search--open) > :not(.unified-search-menu)) {
+ display: none;
+ }
+}
+</style>
diff --git a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue
new file mode 100644
index 00000000000..b21c65301c4
--- /dev/null
+++ b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue
@@ -0,0 +1,795 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcDialog id="unified-search"
+ ref="unifiedSearchModal"
+ content-classes="unified-search-modal__content"
+ dialog-classes="unified-search-modal"
+ :name="t('core', 'Unified search')"
+ :open="open"
+ size="normal"
+ @update:open="onUpdateOpen">
+ <!-- Modal for picking custom time range -->
+ <CustomDateRangeModal :is-open="showDateRangeModal"
+ class="unified-search__date-range"
+ @set:custom-date-range="setCustomDateRange"
+ @update:is-open="showDateRangeModal = $event" />
+
+ <!-- Unified search form -->
+ <div class="unified-search-modal__header">
+ <NcInputField ref="searchInput"
+ data-cy-unified-search-input
+ :value.sync="searchQuery"
+ type="text"
+ :label="t('core', 'Search apps, files, tags, messages') + '...'"
+ @update:value="debouncedFind" />
+ <div class="unified-search-modal__filters" data-cy-unified-search-filters>
+ <NcActions :menu-name="t('core', 'Places')" :open.sync="providerActionMenuIsOpen" data-cy-unified-search-filter="places">
+ <template #icon>
+ <IconListBox :size="20" />
+ </template>
+ <!-- Provider id's may be duplicated since, plugin filters could depend on a provider that is already in the defaults.
+ provider.id concatenated to provider.name is used to create the item id, if same then, there should be an issue. -->
+ <NcActionButton v-for="provider in providers"
+ :key="`${provider.id}-${provider.name.replace(/\s/g, '')}`"
+ :disabled="provider.disabled"
+ @click="addProviderFilter(provider)">
+ <template #icon>
+ <img :src="provider.icon" class="filter-button__icon" alt="">
+ </template>
+ {{ provider.name }}
+ </NcActionButton>
+ </NcActions>
+ <NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen" data-cy-unified-search-filter="date">
+ <template #icon>
+ <IconCalendarRange :size="20" />
+ </template>
+ <NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
+ {{ t('core', 'Today') }}
+ </NcActionButton>
+ <NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
+ {{ t('core', 'Last 7 days') }}
+ </NcActionButton>
+ <NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
+ {{ t('core', 'Last 30 days') }}
+ </NcActionButton>
+ <NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
+ {{ t('core', 'This year') }}
+ </NcActionButton>
+ <NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
+ {{ t('core', 'Last year') }}
+ </NcActionButton>
+ <NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
+ {{ t('core', 'Custom date range') }}
+ </NcActionButton>
+ </NcActions>
+ <SearchableList :label-text="t('core', 'Search people')"
+ :search-list="userContacts"
+ :empty-content-text="t('core', 'Not found')"
+ data-cy-unified-search-filter="people"
+ @search-term-change="debouncedFilterContacts"
+ @item-selected="applyPersonFilter">
+ <template #trigger>
+ <NcButton>
+ <template #icon>
+ <IconAccountGroup :size="20" />
+ </template>
+ {{ t('core', 'People') }}
+ </NcButton>
+ </template>
+ </SearchableList>
+ <NcButton v-if="localSearch" data-cy-unified-search-filter="current-view" @click="searchLocally">
+ {{ t('core', 'Filter in current view') }}
+ <template #icon>
+ <IconFilter :size="20" />
+ </template>
+ </NcButton>
+ </div>
+ <div class="unified-search-modal__filters-applied">
+ <FilterChip v-for="filter in filters"
+ :key="filter.id"
+ :text="filter.name ?? filter.text"
+ :pretext="''"
+ @delete="removeFilter(filter)">
+ <template #icon>
+ <NcAvatar v-if="filter.type === 'person'"
+ :user="filter.user"
+ :size="24"
+ :disable-menu="true"
+ :show-user-status="false"
+ :hide-favorite="false" />
+ <IconCalendarRange v-else-if="filter.type === 'date'" />
+ <img v-else :src="filter.icon" alt="">
+ </template>
+ </FilterChip>
+ </div>
+ </div>
+
+ <div v-if="showEmptyContentInfo" class="unified-search-modal__no-content">
+ <NcEmptyContent :name="emptyContentMessage">
+ <template #icon>
+ <IconMagnify :size="64" />
+ </template>
+ </NcEmptyContent>
+ </div>
+
+ <div v-else class="unified-search-modal__results">
+ <h3 class="hidden-visually">
+ {{ t('core', 'Results') }}
+ </h3>
+ <div v-for="providerResult in results" :key="providerResult.id" class="result">
+ <h4 :id="`unified-search-result-${providerResult.id}`" class="result-title">
+ {{ providerResult.name }}
+ </h4>
+ <ul class="result-items" :aria-labelledby="`unified-search-result-${providerResult.id}`">
+ <SearchResult v-for="(result, index) in providerResult.results"
+ :key="index"
+ v-bind="result" />
+ </ul>
+ <div class="result-footer">
+ <NcButton v-if="providerResult.results.length === providerResult.limit" type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult)">
+ {{ t('core', 'Load more results') }}
+ <template #icon>
+ <IconDotsHorizontal :size="20" />
+ </template>
+ </NcButton>
+ <NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" type="tertiary-no-background">
+ {{ t('core', 'Search in') }} {{ providerResult.name }}
+ <template #icon>
+ <IconArrowRight :size="20" />
+ </template>
+ </NcButton>
+ </div>
+ </div>
+ </div>
+ </NcDialog>
+</template>
+
+<script lang="ts">
+import { subscribe } from '@nextcloud/event-bus'
+import { translate as t } from '@nextcloud/l10n'
+import { useBrowserLocation } from '@vueuse/core'
+import { defineComponent } from 'vue'
+import { getProviders, search as unifiedSearch, getContacts } from '../../services/UnifiedSearchService.js'
+import { useSearchStore } from '../../store/unified-search-external-filters.js'
+
+import debounce from 'debounce'
+import { unifiedSearchLogger } from '../../logger'
+
+import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
+import IconAccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue'
+import IconCalendarRange from 'vue-material-design-icons/CalendarRangeOutline.vue'
+import IconDotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
+import IconFilter from 'vue-material-design-icons/Filter.vue'
+import IconListBox from 'vue-material-design-icons/ListBox.vue'
+import IconMagnify from 'vue-material-design-icons/Magnify.vue'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcInputField from '@nextcloud/vue/components/NcInputField'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+
+import CustomDateRangeModal from './CustomDateRangeModal.vue'
+import FilterChip from './SearchFilterChip.vue'
+import SearchableList from './SearchableList.vue'
+import SearchResult from './SearchResult.vue'
+
+export default defineComponent({
+ name: 'UnifiedSearchModal',
+ components: {
+ IconArrowRight,
+ IconAccountGroup,
+ IconCalendarRange,
+ IconDotsHorizontal,
+ IconFilter,
+ IconListBox,
+ IconMagnify,
+
+ CustomDateRangeModal,
+ FilterChip,
+ NcActions,
+ NcActionButton,
+ NcAvatar,
+ NcButton,
+ NcEmptyContent,
+ NcDialog,
+ NcInputField,
+ SearchableList,
+ SearchResult,
+ },
+
+ props: {
+ /**
+ * Open state of the modal
+ */
+ open: {
+ type: Boolean,
+ required: true,
+ },
+
+ /**
+ * The current query string
+ */
+ query: {
+ type: String,
+ default: '',
+ },
+
+ /**
+ * If the current page / app supports local search
+ */
+ localSearch: {
+ type: Boolean,
+ default: false,
+ },
+ },
+
+ emits: ['update:open', 'update:query'],
+
+ setup() {
+ /**
+ * Reactive version of window.location
+ */
+ const currentLocation = useBrowserLocation()
+ const searchStore = useSearchStore()
+ return {
+ t,
+
+ currentLocation,
+ externalFilters: searchStore.externalFilters,
+ }
+ },
+
+ data() {
+ return {
+ providers: [],
+ providerActionMenuIsOpen: false,
+ dateActionMenuIsOpen: false,
+ providerResultLimit: 5,
+ dateFilter: { id: 'date', type: 'date', text: '', startFrom: null, endAt: null },
+ personFilter: { id: 'person', type: 'person', name: '' },
+ filteredProviders: [],
+ searching: false,
+ searchQuery: '',
+ lastSearchQuery: '',
+ placessearchTerm: '',
+ dateTimeFilter: null,
+ filters: [],
+ results: [],
+ contacts: [],
+ showDateRangeModal: false,
+ internalIsVisible: this.open,
+ initialized: false,
+ }
+ },
+
+ computed: {
+ isEmptySearch() {
+ return this.searchQuery.length === 0
+ },
+
+ hasNoResults() {
+ return !this.isEmptySearch && this.results.length === 0
+ },
+
+ showEmptyContentInfo() {
+ return this.isEmptySearch || this.hasNoResults
+ },
+
+ emptyContentMessage() {
+ if (this.searching && this.hasNoResults) {
+ return t('core', 'Searching …')
+ }
+ if (this.isEmptySearch) {
+ return t('core', 'Start typing to search')
+ }
+ return t('core', 'No matching results')
+ },
+
+ userContacts() {
+ return this.contacts
+ },
+
+ debouncedFind() {
+ return debounce(this.find, 300)
+ },
+
+ debouncedFilterContacts() {
+ return debounce(this.filterContacts, 300)
+ },
+ },
+
+ watch: {
+ open() {
+ // Load results when opened with already filled query
+ if (this.open) {
+ this.focusInput()
+ if (!this.initialized) {
+ Promise.all([getProviders(), getContacts({ searchTerm: '' })])
+ .then(([providers, contacts]) => {
+ this.providers = this.groupProvidersByApp([...providers, ...this.externalFilters])
+ this.contacts = this.mapContacts(contacts)
+ unifiedSearchLogger.debug('Search providers and contacts initialized:', { providers: this.providers, contacts: this.contacts })
+ this.initialized = true
+ })
+ .catch((error) => {
+ unifiedSearchLogger.error(error)
+ })
+ }
+ if (this.searchQuery) {
+ this.find(this.searchQuery)
+ }
+ }
+ },
+
+ query: {
+ immediate: true,
+ handler() {
+ this.searchQuery = this.query
+ },
+ },
+
+ searchQuery: {
+ handler() {
+ this.$emit('update:query', this.searchQuery)
+ },
+ },
+ },
+
+ mounted() {
+ subscribe('nextcloud:unified-search:add-filter', this.handlePluginFilter)
+ },
+ methods: {
+ /**
+ * On close the modal is closed and the query is reset
+ * @param open The new open state
+ */
+ onUpdateOpen(open: boolean) {
+ if (!open) {
+ this.$emit('update:open', false)
+ this.$emit('update:query', '')
+ }
+ },
+
+ /**
+ * Only close the modal but keep the query for in-app search
+ */
+ searchLocally() {
+ this.$emit('update:query', this.searchQuery)
+ this.$emit('update:open', false)
+ },
+ focusInput() {
+ this.$nextTick(() => {
+ this.$refs.searchInput?.focus()
+ })
+ },
+ find(query: string, providersToSearchOverride = null) {
+ if (query.length === 0) {
+ this.results = []
+ this.searching = false
+ return
+ }
+
+ // Reset the provider result limit when performing a new search
+ if (query !== this.lastSearchQuery) {
+ this.providerResultLimit = 5
+ }
+ this.lastSearchQuery = query
+
+ this.searching = true
+ const newResults = []
+ const providersToSearch = providersToSearchOverride || (this.filteredProviders.length > 0 ? this.filteredProviders : this.providers)
+ const searchProvider = (provider) => {
+ const params = {
+ type: provider.searchFrom ?? provider.id,
+ query,
+ cursor: null,
+ extraQueries: provider.extraParams,
+ }
+
+ // This block of filter checks should be dynamic somehow and should be handled in
+ // nextcloud/search lib
+ const activeFilters = this.filters.filter(filter => {
+ return filter.type !== 'provider' && this.providerIsCompatibleWithFilters(provider, [filter.type])
+ })
+
+ activeFilters.forEach(filter => {
+ switch (filter.type) {
+ case 'date':
+ if (provider.filters?.since && provider.filters?.until) {
+ params.since = this.dateFilter.startFrom
+ params.until = this.dateFilter.endAt
+ }
+ break
+ case 'person':
+ if (provider.filters?.person) {
+ params.person = this.personFilter.user
+ }
+ break
+ }
+ })
+
+ if (this.providerResultLimit > 5) {
+ params.limit = this.providerResultLimit
+ unifiedSearchLogger.debug('Limiting search to', params.limit)
+ }
+
+ const request = unifiedSearch(params).request
+
+ request().then((response) => {
+ newResults.push({
+ ...provider,
+ results: response.data.ocs.data.entries,
+ limit: params.limit ?? 5,
+ })
+
+ unifiedSearchLogger.debug('Unified search results:', { results: this.results, newResults })
+
+ this.updateResults(newResults)
+ this.searching = false
+ })
+ }
+
+ providersToSearch.forEach(searchProvider)
+ },
+ updateResults(newResults) {
+ let updatedResults = [...this.results]
+ // If filters are applied, remove any previous results for providers that are not in current filters
+ if (this.filters.length > 0) {
+ updatedResults = updatedResults.filter(result => {
+ return this.filters.some(filter => filter.id === result.id)
+ })
+ }
+ // Process the new results
+ newResults.forEach(newResult => {
+ const existingResultIndex = updatedResults.findIndex(result => result.id === newResult.id)
+ if (existingResultIndex !== -1) {
+ if (newResult.results.length === 0) {
+ // If the new results data has no matches for and existing result, remove the existing result
+ updatedResults.splice(existingResultIndex, 1)
+ } else {
+ // If input triggered a change in existing results, update existing result
+ updatedResults.splice(existingResultIndex, 1, newResult)
+ }
+ } else if (newResult.results.length > 0) {
+ // Push the new result to the array only if its results array is not empty
+ updatedResults.push(newResult)
+ }
+ })
+ const sortedResults = updatedResults.slice(0)
+ // Order results according to provider preference
+ sortedResults.sort((a, b) => {
+ const aProvider = this.providers.find(provider => provider.id === a.id)
+ const bProvider = this.providers.find(provider => provider.id === b.id)
+ const aOrder = aProvider ? aProvider.order : 0
+ const bOrder = bProvider ? bProvider.order : 0
+ return aOrder - bOrder
+ })
+ this.results = sortedResults
+ },
+ mapContacts(contacts) {
+ return contacts.map(contact => {
+ return {
+ // id: contact.id,
+ // name: '',
+ displayName: contact.fullName,
+ isNoUser: false,
+ subname: contact.emailAddresses[0] ? contact.emailAddresses[0] : '',
+ icon: '',
+ user: contact.id,
+ isUser: contact.isUser,
+ }
+ })
+ },
+ filterContacts(query) {
+ getContacts({ searchTerm: query }).then((contacts) => {
+ this.contacts = this.mapContacts(contacts)
+ unifiedSearchLogger.debug(`Contacts filtered by ${query}`, { contacts: this.contacts })
+ })
+ },
+ applyPersonFilter(person) {
+
+ const existingPersonFilter = this.filters.findIndex(filter => filter.id === person.id)
+ if (existingPersonFilter === -1) {
+ this.personFilter.id = person.id
+ this.personFilter.user = person.user
+ this.personFilter.name = person.displayName
+ this.filters.push(this.personFilter)
+ } else {
+ this.filters[existingPersonFilter].id = person.id
+ this.filters[existingPersonFilter].user = person.user
+ this.filters[existingPersonFilter].name = person.displayName
+ }
+
+ this.providers.forEach(async (provider, index) => {
+ this.providers[index].disabled = !(await this.providerIsCompatibleWithFilters(provider, ['person']))
+ })
+
+ this.debouncedFind(this.searchQuery)
+ unifiedSearchLogger.debug('Person filter applied', { person })
+ },
+ async loadMoreResultsForProvider(provider) {
+ this.providerResultLimit += 5
+ this.find(this.searchQuery, [provider])
+ },
+ addProviderFilter(providerFilter, loadMoreResultsForProvider = false) {
+ unifiedSearchLogger.debug('Applying provider filter', { providerFilter, loadMoreResultsForProvider })
+ if (!providerFilter.id) return
+ if (providerFilter.isPluginFilter) {
+ // There is no way to know what should go into the callback currently
+ // Here we are passing isProviderFilterApplied (boolean) which is a flag sent to the plugin
+ // This is sent to the plugin so that depending on whether the filter is applied or not, the plugin can decide what to do
+ // TODO : In nextcloud/search, this should be a proper interface that the plugin can implement
+ const isProviderFilterApplied = this.filteredProviders.some(provider => provider.id === providerFilter.id)
+ providerFilter.callback(!isProviderFilterApplied)
+ }
+ this.providerResultLimit = loadMoreResultsForProvider ? this.providerResultLimit : 5
+ this.providerActionMenuIsOpen = false
+ // With the possibility for other apps to add new filters
+ // Resulting in a possible id/provider collision
+ // If a user tries to apply a filter that seems to already exist, we remove the current one and add the new one.
+ const existingFilterIndex = this.filteredProviders.findIndex(existing => existing.id === providerFilter.id)
+ if (existingFilterIndex > -1) {
+ this.filteredProviders.splice(existingFilterIndex, 1)
+ this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
+ }
+ this.filteredProviders.push({
+ ...providerFilter,
+ type: providerFilter.type || 'provider',
+ isPluginFilter: providerFilter.isPluginFilter || false,
+ })
+ this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
+ unifiedSearchLogger.debug('Search filters (newly added)', { filters: this.filters })
+ this.debouncedFind(this.searchQuery)
+ },
+ removeFilter(filter) {
+ if (filter.type === 'provider') {
+ for (let i = 0; i < this.filteredProviders.length; i++) {
+ if (this.filteredProviders[i].id === filter.id) {
+ this.filteredProviders.splice(i, 1)
+ break
+ }
+ }
+ this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
+ unifiedSearchLogger.debug('Search filters (recently removed)', { filters: this.filters })
+
+ } else {
+ // Remove non provider filters such as date and person filters
+ for (let i = 0; i < this.filters.length; i++) {
+ if (this.filters[i].id === filter.id) {
+ this.filters.splice(i, 1)
+ this.enableAllProviders()
+ break
+ }
+ }
+ }
+ this.debouncedFind(this.searchQuery)
+ },
+ syncProviderFilters(firstArray, secondArray) {
+ // Create a copy of the first array to avoid modifying it directly.
+ const synchronizedArray = firstArray.slice()
+ // Remove items from the synchronizedArray that are not in the secondArray.
+ synchronizedArray.forEach((item, index) => {
+ const itemId = item.id
+ if (item.type === 'provider') {
+ if (!secondArray.some(secondItem => secondItem.id === itemId)) {
+ synchronizedArray.splice(index, 1)
+ }
+ }
+ })
+ // Add items to the synchronizedArray that are in the secondArray but not in the firstArray.
+ secondArray.forEach(secondItem => {
+ const itemId = secondItem.id
+ if (secondItem.type === 'provider') {
+ if (!synchronizedArray.some(item => item.id === itemId)) {
+ synchronizedArray.push(secondItem)
+ }
+ }
+ })
+
+ return synchronizedArray
+ },
+ updateDateFilter() {
+ const currFilterIndex = this.filters.findIndex(filter => filter.id === 'date')
+ if (currFilterIndex !== -1) {
+ this.filters[currFilterIndex] = this.dateFilter
+ } else {
+ this.filters.push(this.dateFilter)
+ }
+
+ this.providers.forEach(async (provider, index) => {
+ this.providers[index].disabled = !(await this.providerIsCompatibleWithFilters(provider, ['since', 'until']))
+ })
+ this.debouncedFind(this.searchQuery)
+ },
+ applyQuickDateRange(range) {
+ this.dateActionMenuIsOpen = false
+ const today = new Date()
+ let startDate
+ let endDate
+
+ switch (range) {
+ case 'today':
+ // For 'Today', both start and end are set to today
+ startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0)
+ endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999)
+ this.dateFilter.text = t('core', 'Today')
+ break
+ case '7days':
+ // For 'Last 7 days', start date is 7 days ago, end is today
+ startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6, 0, 0, 0, 0)
+ this.dateFilter.text = t('core', 'Last 7 days')
+ break
+ case '30days':
+ // For 'Last 30 days', start date is 30 days ago, end is today
+ startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29, 0, 0, 0, 0)
+ this.dateFilter.text = t('core', 'Last 30 days')
+ break
+ case 'thisyear':
+ // For 'This year', start date is the first day of the year, end is the last day of the year
+ startDate = new Date(today.getFullYear(), 0, 1, 0, 0, 0, 0)
+ endDate = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999)
+ this.dateFilter.text = t('core', 'This year')
+ break
+ case 'lastyear':
+ // For 'Last year', start date is the first day of the previous year, end is the last day of the previous year
+ startDate = new Date(today.getFullYear() - 1, 0, 1, 0, 0, 0, 0)
+ endDate = new Date(today.getFullYear() - 1, 11, 31, 23, 59, 59, 999)
+ this.dateFilter.text = t('core', 'Last year')
+ break
+ case 'custom':
+ this.showDateRangeModal = true
+ return
+ default:
+ return
+ }
+ this.dateFilter.startFrom = startDate
+ this.dateFilter.endAt = endDate
+ this.updateDateFilter()
+
+ },
+ setCustomDateRange(event) {
+ unifiedSearchLogger.debug('Custom date range', { range: event })
+ this.dateFilter.startFrom = event.startFrom
+ this.dateFilter.endAt = event.endAt
+ this.dateFilter.text = t('core', `Between ${this.dateFilter.startFrom.toLocaleDateString()} and ${this.dateFilter.endAt.toLocaleDateString()}`)
+ this.updateDateFilter()
+ },
+ handlePluginFilter(addFilterEvent) {
+ unifiedSearchLogger.debug('Handling plugin filter', { addFilterEvent })
+ for (let i = 0; i < this.filteredProviders.length; i++) {
+ const provider = this.filteredProviders[i]
+ if (provider.id === addFilterEvent.id) {
+ provider.name = addFilterEvent.filterUpdateText
+ // Filters attached may only make sense with certain providers,
+ // So, find the provider attached, add apply the extra parameters to those providers only
+ const compatibleProviderIndex = this.providers.findIndex(provider => provider.id === addFilterEvent.id)
+ if (compatibleProviderIndex > -1) {
+ provider.extraParams = addFilterEvent.filterParams
+ this.filteredProviders[i] = provider
+ }
+ break
+ }
+ }
+ this.debouncedFind(this.searchQuery)
+ },
+ groupProvidersByApp(filters) {
+ const groupedByProviderApp = {}
+
+ filters.forEach(filter => {
+ const provider = filter.appId ? filter.appId : 'general'
+ if (!groupedByProviderApp[provider]) {
+ groupedByProviderApp[provider] = []
+ }
+ groupedByProviderApp[provider].push(filter)
+ })
+
+ const flattenedArray = []
+ Object.values(groupedByProviderApp).forEach(group => {
+ flattenedArray.push(...group)
+ })
+
+ return flattenedArray
+ },
+ async providerIsCompatibleWithFilters(provider, filterIds) {
+ return filterIds.every(filterId => provider.filters?.[filterId] !== undefined)
+ },
+ async enableAllProviders() {
+ this.providers.forEach(async (_, index) => {
+ this.providers[index].disabled = false
+ })
+ },
+ },
+})
+</script>
+
+<style lang="scss" scoped>
+:deep(.unified-search-modal .unified-search-modal__content) {
+ --dialog-height: min(80vh, 800px);
+ box-sizing: border-box;
+ height: var(--dialog-height);
+ max-height: var(--dialog-height);
+ min-height: var(--dialog-height);
+
+ display: flex;
+ flex-direction: column;
+ // No padding to prevent scrollbar misplacement
+ padding-inline: 0;
+}
+
+.unified-search-modal {
+ &__header {
+ // Add background to prevent leaking scrolled content (because of sticky position)
+ background-color: var(--color-main-background);
+ // Fix padding to have the input centered
+ padding-inline-end: 12px;
+ // Some padding to make elements scrolled under sticky position look nicer
+ padding-block-end: 12px;
+ // Make it sticky with the input margin for the label
+ position: sticky;
+ top: 6px;
+ }
+
+ &__filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ justify-content: start;
+ padding-top: 4px;
+ }
+
+ &__filters-applied {
+ padding-top: 4px;
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__no-content {
+ display: flex;
+ align-items: center;
+ margin-top: 0.5em;
+ height: 70%;
+ }
+
+ &__results {
+ overflow: hidden scroll;
+ // Adjust padding to match container but keep the scrollbar on the very end
+ padding-inline: 0 12px;
+ padding-block: 0 12px;
+
+ .result {
+ &-title {
+ color: var(--color-primary-element);
+ font-size: 16px;
+ margin-block: 8px 4px;
+ }
+
+ &-footer {
+ justify-content: space-between;
+ align-items: center;
+ display: flex;
+ }
+ }
+
+ }
+}
+
+.filter-button__icon {
+ height: 20px;
+ width: 20px;
+ object-fit: contain;
+ filter: var(--background-invert-if-bright);
+ padding: 11px; // align with text to fit at least 44px
+}
+
+// Ensure modal is accessible on small devices
+@media only screen and (max-height: 400px) {
+ .unified-search-modal__results {
+ overflow: unset;
+ }
+}
+</style>
diff --git a/core/src/components/UserMenu.js b/core/src/components/UserMenu.js
index e165e784422..5c488f2341e 100644
--- a/core/src/components/UserMenu.js
+++ b/core/src/components/UserMenu.js
@@ -1,37 +1,20 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import Vue from 'vue'
-import UserMenu from '../views/UserMenu.vue'
+import AccountMenu from '../views/AccountMenu.vue'
export const setUp = () => {
const mountPoint = document.getElementById('user-menu')
if (mountPoint) {
// eslint-disable-next-line no-new
new Vue({
+ name: 'AccountMenuRoot',
el: mountPoint,
- render: h => h(UserMenu),
+ render: h => h(AccountMenu),
})
}
}
diff --git a/core/src/components/UserMenu/ProfileUserMenuEntry.vue b/core/src/components/UserMenu/ProfileUserMenuEntry.vue
deleted file mode 100644
index 61357f09ac6..00000000000
--- a/core/src/components/UserMenu/ProfileUserMenuEntry.vue
+++ /dev/null
@@ -1,140 +0,0 @@
-<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
--->
-
-<template>
- <li :id="id"
- class="menu-entry">
- <component :is="profileEnabled ? 'a' : 'span'"
- class="menu-entry__wrapper"
- :class="{
- active,
- 'menu-entry__wrapper--link': profileEnabled,
- }"
- :href="profileEnabled ? href : undefined"
- @click.exact="handleClick">
- <span class="menu-entry__content">
- <span class="menu-entry__displayname">{{ displayName }}</span>
- <NcLoadingIcon v-if="loading" :size="18" />
- </span>
- <span v-if="profileEnabled">{{ name }}</span>
- </component>
- </li>
-</template>
-
-<script>
-import { loadState } from '@nextcloud/initial-state'
-import { getCurrentUser } from '@nextcloud/auth'
-import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-
-const { profileEnabled } = loadState('user_status', 'profileEnabled', false)
-
-export default {
- name: 'ProfileUserMenuEntry',
-
- components: {
- NcLoadingIcon,
- },
-
- props: {
- id: {
- type: String,
- required: true,
- },
- name: {
- type: String,
- required: true,
- },
- href: {
- type: String,
- required: true,
- },
- active: {
- type: Boolean,
- required: true,
- },
- },
-
- data() {
- return {
- profileEnabled,
- displayName: getCurrentUser().displayName,
- loading: false,
- }
- },
-
- mounted() {
- subscribe('settings:profile-enabled:updated', this.handleProfileEnabledUpdate)
- subscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
- },
-
- beforeDestroy() {
- unsubscribe('settings:profile-enabled:updated', this.handleProfileEnabledUpdate)
- unsubscribe('settings:display-name:updated', this.handleDisplayNameUpdate)
- },
-
- methods: {
- handleClick() {
- if (this.profileEnabled) {
- this.loading = true
- }
- },
-
- handleProfileEnabledUpdate(profileEnabled) {
- this.profileEnabled = profileEnabled
- },
-
- handleDisplayNameUpdate(displayName) {
- this.displayName = displayName
- },
- },
-}
-</script>
-
-<style lang="scss" scoped>
-.menu-entry {
- &__wrapper {
- box-sizing: border-box;
- display: inline-flex;
- flex-direction: column;
- align-items: flex-start !important;
- padding: 10px 12px 5px 12px !important;
- height: var(--header-menu-item-height);
- color: var(--color-text-maxcontrast);
-
- &--link {
- height: calc(var(--header-menu-item-height) * 1.5) !important;
- color: var(--color-main-text);
- }
- }
-
- &__content {
- display: inline-flex;
- gap: 0 10px;
- }
-
- &__displayname {
- font-weight: bold;
- }
-}
-</style>
diff --git a/core/src/components/UserMenu/UserMenuEntry.vue b/core/src/components/UserMenu/UserMenuEntry.vue
deleted file mode 100644
index 8f09137256c..00000000000
--- a/core/src/components/UserMenu/UserMenuEntry.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
--->
-
-<template>
- <li :id="id"
- class="menu-entry">
- <a v-if="href"
- :href="href"
- :class="{ active }"
- @click.exact="handleClick">
- <NcLoadingIcon v-if="loading"
- class="menu-entry__loading-icon"
- :size="18" />
- <img v-else :src="cachedIcon" alt="">
- {{ name }}
- </a>
- <button v-else>
- <img :src="cachedIcon" alt="">
- {{ name }}
- </button>
- </li>
-</template>
-
-<script>
-import { loadState } from '@nextcloud/initial-state'
-
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-
-const versionHash = loadState('core', 'versionHash', '')
-
-export default {
- name: 'UserMenuEntry',
-
- components: {
- NcLoadingIcon,
- },
-
- props: {
- id: {
- type: String,
- required: true,
- },
- name: {
- type: String,
- required: true,
- },
- href: {
- type: String,
- required: true,
- },
- active: {
- type: Boolean,
- required: true,
- },
- icon: {
- type: String,
- required: true,
- },
- },
-
- data() {
- return {
- loading: false,
- }
- },
-
- computed: {
- cachedIcon() {
- return `${this.icon}?v=${versionHash}`
- },
- },
-
- methods: {
- handleClick() {
- this.loading = true
- },
- },
-}
-</script>
-
-<style lang="scss" scoped>
-.menu-entry {
- &__loading-icon {
- margin-right: 8px;
- }
-}
-</style>
diff --git a/core/src/components/login/LoginButton.vue b/core/src/components/login/LoginButton.vue
index 54c29245469..da387df0ff6 100644
--- a/core/src/components/login/LoginButton.vue
+++ b/core/src/components/login/LoginButton.vue
@@ -1,28 +1,13 @@
<!--
- - @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcButton type="primary"
native-type="submit"
:wide="true"
+ :disabled="loading"
@click="$emit('click')">
{{ !loading ? value : valueLoading }}
<template #icon>
@@ -33,7 +18,9 @@
</template>
<script>
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import { translate as t } from '@nextcloud/l10n'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
export default {
diff --git a/core/src/components/login/LoginForm.cy.ts b/core/src/components/login/LoginForm.cy.ts
new file mode 100644
index 00000000000..1b1aeda6306
--- /dev/null
+++ b/core/src/components/login/LoginForm.cy.ts
@@ -0,0 +1,76 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import LoginForm from './LoginForm.vue'
+
+describe('core: LoginForm', { testIsolation: true }, () => {
+ beforeEach(() => {
+ // Mock the required global state
+ cy.window().then(($window) => {
+ $window.OC = {
+ theme: {
+ name: 'J\'s cloud',
+ },
+ requestToken: 'request-token',
+ }
+ })
+ })
+
+ /**
+ * Ensure that characters like ' are not double HTML escaped.
+ * This was a bug in https://github.com/nextcloud/server/issues/34990
+ */
+ it('does not double escape special characters in product name', () => {
+ cy.mount(LoginForm, {
+ propsData: {
+ username: 'test-user',
+ },
+ })
+
+ cy.get('h2').contains('J\'s cloud')
+ })
+
+ it('fills username from props into form', () => {
+ cy.mount(LoginForm, {
+ propsData: {
+ username: 'test-user',
+ },
+ })
+
+ cy.get('input[name="user"]')
+ .should('exist')
+ .and('have.attr', 'id', 'user')
+
+ cy.get('input[name="user"]')
+ .should('have.value', 'test-user')
+ })
+
+ it('clears password after timeout', () => {
+ // mock timeout of 5 seconds
+ cy.window().then(($window) => {
+ const state = $window.document.createElement('input')
+ state.type = 'hidden'
+ state.id = 'initial-state-core-loginTimeout'
+ state.value = btoa(JSON.stringify(5))
+ $window.document.body.appendChild(state)
+ })
+
+ // mount forms
+ cy.mount(LoginForm)
+
+ cy.get('input[name="password"]')
+ .should('exist')
+ .type('MyPassword')
+
+ cy.get('input[name="password"]')
+ .should('have.value', 'MyPassword')
+
+ // Wait for timeout
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(5100)
+
+ cy.get('input[name="password"]')
+ .should('have.value', '')
+ })
+})
diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue
index 9a8689dc9cc..8cbe55f1f68 100644
--- a/core/src/components/login/LoginForm.vue
+++ b/core/src/components/login/LoginForm.vue
@@ -1,23 +1,7 @@
<!--
- - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<form ref="loginForm"
@@ -33,9 +17,9 @@
{{ t('core', 'Please contact your administrator.') }}
</NcNoteCard>
<NcNoteCard v-if="csrfCheckFailed"
- :heading="t('core', 'Temporary error')"
+ :heading="t('core', 'Session error')"
type="error">
- {{ t('core', 'Please try again.') }}
+ {{ t('core', 'It appears your session token has expired, please refresh the page and try again.') }}
</NcNoteCard>
<NcNoteCard v-if="messages.length > 0">
<div v-for="(message, index) in messages"
@@ -57,17 +41,22 @@
<!-- the following div ensures that the spinner is always inside the #message div -->
<div style="clear: both;" />
</div>
- <h2 class="login-form__headline" data-login-form-headline v-html="headline" />
+ <h2 class="login-form__headline" data-login-form-headline>
+ {{ headlineText }}
+ </h2>
<NcTextField id="user"
ref="user"
- :label="t('core', 'Account name or email')"
+ :label="loginText"
name="user"
+ :maxlength="255"
:value.sync="user"
:class="{shake: invalidPassword}"
autocapitalize="none"
:spellchecking="false"
:autocomplete="autoCompleteAllowed ? 'username' : 'off'"
required
+ :error="userNameInputLengthIs255"
+ :helper-text="userInputHelperText"
data-login-form-input-user
@change="updateUsername" />
@@ -99,7 +88,7 @@
:value="timezoneOffset">
<input type="hidden"
name="requesttoken"
- :value="OC.requestToken">
+ :value="requestToken">
<input v-if="directLogin"
type="hidden"
name="direct"
@@ -109,12 +98,16 @@
</template>
<script>
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t } from '@nextcloud/l10n'
import { generateUrl, imagePath } from '@nextcloud/router'
+import debounce from 'debounce'
-import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import AuthMixin from '../../mixins/auth.js'
import LoginButton from './LoginButton.vue'
export default {
@@ -127,6 +120,8 @@ export default {
NcNoteCard,
},
+ mixins: [AuthMixin],
+
props: {
username: {
type: String,
@@ -156,30 +151,61 @@ export default {
type: Boolean,
default: false,
},
+ emailStates: {
+ type: Array,
+ default() {
+ return []
+ },
+ },
},
- data() {
+ setup() {
+ // non reactive props
return {
- loading: false,
+ t,
+
+ // Disable escape and sanitize to prevent special characters to be html escaped
+ // For example "J's cloud" would be escaped to "J&#39; cloud". But we do not need escaping as Vue does this in `v-text` automatically
+ headlineText: t('core', 'Log in to {productName}', { productName: OC.theme.name }, undefined, { sanitize: false, escape: false }),
+
+ loginTimeout: loadState('core', 'loginTimeout', 300),
+ requestToken: window.OC.requestToken,
timezone: (new Intl.DateTimeFormat())?.resolvedOptions()?.timeZone,
timezoneOffset: (-new Date().getTimezoneOffset() / 60),
- headline: t('core', 'Log in to {productName}', { productName: OC.theme.name }),
+ }
+ },
+
+ data() {
+ return {
+ loading: false,
user: '',
password: '',
}
},
computed: {
+ /**
+ * Reset the login form after a long idle time (debounced)
+ */
+ resetFormTimeout() {
+ // Infinite timeout, do nothing
+ if (this.loginTimeout <= 0) {
+ return () => {}
+ }
+ // Debounce for given timeout (in seconds so convert to milli seconds)
+ return debounce(this.handleResetForm, this.loginTimeout * 1000)
+ },
+
isError() {
return this.invalidPassword || this.userDisabled
|| this.throttleDelay > 5000
},
errorLabel() {
if (this.invalidPassword) {
- return t('core', 'Wrong username or password.')
+ return t('core', 'Wrong login or password.')
}
if (this.userDisabled) {
- return t('core', 'User disabled')
+ return t('core', 'This account is disabled')
}
if (this.throttleDelay > 5000) {
return t('core', 'We have detected multiple invalid login attempts from your IP. Therefore your next login is throttled up to 30 seconds.')
@@ -207,6 +233,24 @@ export default {
loginActionUrl() {
return generateUrl('login')
},
+ emailEnabled() {
+ return this.emailStates ? this.emailStates.every((state) => state === '1') : 1
+ },
+ loginText() {
+ if (this.emailEnabled) {
+ return t('core', 'Account name or email')
+ }
+ return t('core', 'Account name')
+ },
+ },
+
+ watch: {
+ /**
+ * Reset form reset after the password was changed
+ */
+ password() {
+ this.resetFormTimeout()
+ },
},
mounted() {
@@ -219,10 +263,24 @@ export default {
},
methods: {
+ /**
+ * Handle reset of the login form after a long IDLE time
+ * This is recommended security behavior to prevent password leak on public devices
+ */
+ handleResetForm() {
+ this.password = ''
+ },
+
updateUsername() {
this.$emit('update:username', this.user)
},
- submit() {
+ submit(event) {
+ if (this.loading) {
+ // Prevent the form from being submitted twice
+ event.preventDefault()
+ return
+ }
+
this.loading = true
this.$emit('submit')
},
@@ -232,8 +290,9 @@ export default {
<style lang="scss" scoped>
.login-form {
- text-align: left;
+ text-align: start;
font-size: 1rem;
+ margin: 0;
&__fieldset {
width: 100%;
@@ -246,5 +305,10 @@ export default {
text-align: center;
overflow-wrap: anywhere;
}
+
+ // Only show the error state if the user interacted with the login box
+ :deep(input:invalid:not(:user-invalid)) {
+ border-color: var(--color-border-maxcontrast) !important;
+ }
}
</style>
diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue
index dbc4fdae695..bc4d25bf70f 100644
--- a/core/src/components/login/PasswordLessLoginForm.vue
+++ b/core/src/components/login/PasswordLessLoginForm.vue
@@ -1,61 +1,74 @@
+<!--
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <form v-if="(isHttps || isLocalhost) && hasPublicKeyCredential"
+ <form v-if="(isHttps || isLocalhost) && supportsWebauthn"
ref="loginForm"
+ aria-labelledby="password-less-login-form-title"
+ class="password-less-login-form"
method="post"
name="login"
@submit.prevent="submit">
- <h2>{{ t('core', 'Log in with a device') }}</h2>
- <fieldset>
- <NcTextField required
- :value="user"
- :autocomplete="autoCompleteAllowed ? 'on' : 'off'"
- :error="!validCredentials"
- :label="t('core', 'Username or email')"
- :placeholder="t('core', 'Username or email')"
- :helper-text="!validCredentials ? t('core', 'Your account is not setup for passwordless login.') : ''"
- @update:value="changeUsername" />
-
- <LoginButton v-if="validCredentials"
- :loading="loading"
- @click="authenticate" />
- </fieldset>
+ <h2 id="password-less-login-form-title">
+ {{ t('core', 'Log in with a device') }}
+ </h2>
+
+ <NcTextField required
+ :value="user"
+ :autocomplete="autoCompleteAllowed ? 'on' : 'off'"
+ :error="!validCredentials"
+ :label="t('core', 'Login or email')"
+ :placeholder="t('core', 'Login or email')"
+ :helper-text="!validCredentials ? t('core', 'Your account is not setup for passwordless login.') : ''"
+ @update:value="changeUsername" />
+
+ <LoginButton v-if="validCredentials"
+ :loading="loading"
+ @click="authenticate" />
</form>
- <div v-else-if="!hasPublicKeyCredential" class="update">
- <InformationIcon size="70" />
- <h2>{{ t('core', 'Browser not supported') }}</h2>
- <p class="infogroup">
- {{ t('core', 'Passwordless authentication is not supported in your browser.') }}
- </p>
- </div>
- <div v-else-if="!isHttps && !isLocalhost" class="update">
- <LockOpenIcon size="70" />
- <h2>{{ t('core', 'Your connection is not secure') }}</h2>
- <p class="infogroup">
- {{ t('core', 'Passwordless authentication is only available over a secure connection.') }}
- </p>
- </div>
+
+ <NcEmptyContent v-else-if="!isHttps && !isLocalhost"
+ :name="t('core', 'Your connection is not secure')"
+ :description="t('core', 'Passwordless authentication is only available over a secure connection.')">
+ <template #icon>
+ <LockOpenIcon />
+ </template>
+ </NcEmptyContent>
+
+ <NcEmptyContent v-else
+ :name="t('core', 'Browser not supported')"
+ :description="t('core', 'Passwordless authentication is not supported in your browser.')">
+ <template #icon>
+ <InformationIcon />
+ </template>
+ </NcEmptyContent>
</template>
-<script>
+<script type="ts">
+import { browserSupportsWebAuthn } from '@simplewebauthn/browser'
+import { defineComponent } from 'vue'
import {
+ NoValidCredentials,
startAuthentication,
finishAuthentication,
-} from '../../services/WebAuthnAuthenticationService.js'
-import LoginButton from './LoginButton.vue'
-import InformationIcon from 'vue-material-design-icons/Information.vue'
-import LockOpenIcon from 'vue-material-design-icons/LockOpen.vue'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+} from '../../services/WebAuthnAuthenticationService.ts'
-class NoValidCredentials extends Error {
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
-}
+import InformationIcon from 'vue-material-design-icons/InformationOutline.vue'
+import LoginButton from './LoginButton.vue'
+import LockOpenIcon from 'vue-material-design-icons/LockOpen.vue'
+import logger from '../../logger'
-export default {
+export default defineComponent({
name: 'PasswordLessLoginForm',
components: {
LoginButton,
InformationIcon,
LockOpenIcon,
+ NcEmptyContent,
NcTextField,
},
props: {
@@ -79,11 +92,14 @@ export default {
type: Boolean,
default: false,
},
- hasPublicKeyCredential: {
- type: Boolean,
- default: false,
- },
},
+
+ setup() {
+ return {
+ supportsWebauthn: browserSupportsWebAuthn(),
+ }
+ },
+
data() {
return {
user: this.username,
@@ -92,7 +108,7 @@ export default {
}
},
methods: {
- authenticate() {
+ async authenticate() {
// check required fields
if (!this.$refs.loginForm.checkValidity()) {
return
@@ -100,112 +116,25 @@ export default {
console.debug('passwordless login initiated')
- this.getAuthenticationData(this.user)
- .then(publicKey => {
- console.debug(publicKey)
- return publicKey
- })
- .then(this.sign)
- .then(this.completeAuthentication)
- .catch(error => {
- if (error instanceof NoValidCredentials) {
- this.validCredentials = false
- return
- }
- console.debug(error)
- })
+ try {
+ const params = await startAuthentication(this.user)
+ await this.completeAuthentication(params)
+ } catch (error) {
+ if (error instanceof NoValidCredentials) {
+ this.validCredentials = false
+ return
+ }
+ logger.debug(error)
+ }
},
changeUsername(username) {
this.user = username
this.$emit('update:username', this.user)
},
- getAuthenticationData(uid) {
- const base64urlDecode = function(input) {
- // Replace non-url compatible chars with base64 standard chars
- input = input
- .replace(/-/g, '+')
- .replace(/_/g, '/')
-
- // Pad out with standard base64 required padding characters
- const pad = input.length % 4
- if (pad) {
- if (pad === 1) {
- throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding')
- }
- input += new Array(5 - pad).join('=')
- }
-
- return window.atob(input)
- }
-
- return startAuthentication(uid)
- .then(publicKey => {
- console.debug('Obtained PublicKeyCredentialRequestOptions')
- console.debug(publicKey)
-
- if (!Object.prototype.hasOwnProperty.call(publicKey, 'allowCredentials')) {
- console.debug('No credentials found.')
- throw new NoValidCredentials()
- }
-
- publicKey.challenge = Uint8Array.from(base64urlDecode(publicKey.challenge), c => c.charCodeAt(0))
- publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) {
- return {
- ...data,
- id: Uint8Array.from(base64urlDecode(data.id), c => c.charCodeAt(0)),
- }
- })
-
- console.debug('Converted PublicKeyCredentialRequestOptions')
- console.debug(publicKey)
- return publicKey
- })
- .catch(error => {
- console.debug('Error while obtaining data')
- throw error
- })
- },
- sign(publicKey) {
- const arrayToBase64String = function(a) {
- return window.btoa(String.fromCharCode(...a))
- }
-
- const arrayToString = function(a) {
- return String.fromCharCode(...a)
- }
-
- return navigator.credentials.get({ publicKey })
- .then(data => {
- console.debug(data)
- console.debug(new Uint8Array(data.rawId))
- console.debug(arrayToBase64String(new Uint8Array(data.rawId)))
- return {
- id: data.id,
- type: data.type,
- rawId: arrayToBase64String(new Uint8Array(data.rawId)),
- response: {
- authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
- clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
- signature: arrayToBase64String(new Uint8Array(data.response.signature)),
- userHandle: data.response.userHandle ? arrayToString(new Uint8Array(data.response.userHandle)) : null,
- },
- }
- })
- .then(challenge => {
- console.debug(challenge)
- return challenge
- })
- .catch(error => {
- console.debug('GOT AN ERROR!')
- console.debug(error) // Example: timeout, interaction refused...
- })
- },
completeAuthentication(challenge) {
- console.debug('TIME TO COMPLETE')
-
const redirectUrl = this.redirectUrl
- return finishAuthentication(JSON.stringify(challenge))
+ return finishAuthentication(challenge)
.then(({ defaultRedirectUrl }) => {
console.debug('Logged in redirecting')
// Redirect url might be false so || should be used instead of ??.
@@ -220,21 +149,14 @@ export default {
// noop
},
},
-}
+})
</script>
<style lang="scss" scoped>
- fieldset {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-
- :deep(label) {
- text-align: initial;
- }
- }
-
- .update {
- margin: 0 auto;
- }
+.password-less-login-form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ margin: 0;
+}
</style>
diff --git a/core/src/components/login/ResetPassword.vue b/core/src/components/login/ResetPassword.vue
index e1d66daa4aa..fee1deacc36 100644
--- a/core/src/components/login/ResetPassword.vue
+++ b/core/src/components/login/ResetPassword.vue
@@ -1,72 +1,68 @@
<!--
- - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <form class="login-form" @submit.prevent="submit">
- <fieldset class="login-form__fieldset">
- <NcTextField id="user"
- :value.sync="user"
- name="user"
- autocapitalize="off"
- :label="t('core', 'Account name or email')"
- required
- @change="updateUsername" />
- <LoginButton :value="t('core', 'Reset password')" />
-
- <NcNoteCard v-if="message === 'send-success'"
- type="success">
- {{ t('core', 'If this account exists, a password reset message has been sent to its email address. If you do not receive it, verify your email address and/or account name, check your spam/junk folders or ask your local administration for help.') }}
- </NcNoteCard>
- <NcNoteCard v-else-if="message === 'send-error'"
- type="error">
- {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }}
- </NcNoteCard>
- <NcNoteCard v-else-if="message === 'reset-error'"
- type="error">
- {{ t('core', 'Password cannot be changed. Please contact your administrator.') }}
- </NcNoteCard>
-
- <a class="login-form__link"
- href="#"
- @click.prevent="$emit('abort')">
- {{ t('core', 'Back to login') }}
- </a>
- </fieldset>
+ <form class="reset-password-form" @submit.prevent="submit">
+ <h2>{{ t('core', 'Reset password') }}</h2>
+
+ <NcTextField id="user"
+ :value.sync="user"
+ name="user"
+ :maxlength="255"
+ autocapitalize="off"
+ :label="t('core', 'Login or email')"
+ :error="userNameInputLengthIs255"
+ :helper-text="userInputHelperText"
+ required
+ @change="updateUsername" />
+
+ <LoginButton :loading="loading" :value="t('core', 'Reset password')" />
+
+ <NcButton type="tertiary" wide @click="$emit('abort')">
+ {{ t('core', 'Back to login') }}
+ </NcButton>
+
+ <NcNoteCard v-if="message === 'send-success'"
+ type="success">
+ {{ t('core', 'If this account exists, a password reset message has been sent to its email address. If you do not receive it, verify your email address and/or Login, check your spam/junk folders or ask your local administration for help.') }}
+ </NcNoteCard>
+ <NcNoteCard v-else-if="message === 'send-error'"
+ type="error">
+ {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }}
+ </NcNoteCard>
+ <NcNoteCard v-else-if="message === 'reset-error'"
+ type="error">
+ {{ t('core', 'Password cannot be changed. Please contact your administrator.') }}
+ </NcNoteCard>
</form>
</template>
-<script>
-import axios from '@nextcloud/axios'
+<script lang="ts">
import { generateUrl } from '@nextcloud/router'
+import { defineComponent } from 'vue'
+
+import axios from '@nextcloud/axios'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+
+import AuthMixin from '../../mixins/auth.js'
import LoginButton from './LoginButton.vue'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import logger from '../../logger.js'
-export default {
+export default defineComponent({
name: 'ResetPassword',
components: {
LoginButton,
+ NcButton,
NcNoteCard,
NcTextField,
},
+
+ mixins: [AuthMixin],
+
props: {
username: {
type: String,
@@ -77,11 +73,12 @@ export default {
required: true,
},
},
+
data() {
return {
error: false,
loading: false,
- message: undefined,
+ message: '',
user: this.username,
}
},
@@ -94,57 +91,38 @@ export default {
updateUsername() {
this.$emit('update:username', this.user)
},
- submit() {
+
+ async submit() {
this.loading = true
this.error = false
this.message = ''
const url = generateUrl('/lostpassword/email')
- const data = {
- user: this.user,
- }
+ try {
+ const { data } = await axios.post(url, { user: this.user })
+ if (data.status !== 'success') {
+ throw new Error(`got status ${data.status}`)
+ }
+
+ this.message = 'send-success'
+ } catch (error) {
+ logger.error('could not send reset email request', { error })
- return axios.post(url, data)
- .then(resp => resp.data)
- .then(data => {
- if (data.status !== 'success') {
- throw new Error(`got status ${data.status}`)
- }
-
- this.message = 'send-success'
- })
- .catch(e => {
- console.error('could not send reset email request', e)
-
- this.error = true
- this.message = 'send-error'
- })
- .then(() => { this.loading = false })
+ this.error = true
+ this.message = 'send-error'
+ } finally {
+ this.loading = false
+ }
},
},
-}
+})
</script>
<style lang="scss" scoped>
-.login-form {
- text-align: left;
- font-size: 1rem;
-
- &__fieldset {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: .5rem;
- }
-
- &__link {
- display: block;
- font-weight: normal !important;
- padding-bottom: 1rem;
- cursor: pointer;
- font-size: var(--default-font-size);
- text-align: center;
- padding: .5rem 1rem 1rem 1rem;
- }
+.reset-password-form {
+ display: flex;
+ flex-direction: column;
+ gap: .5rem;
+ width: 100%;
}
</style>
diff --git a/core/src/components/login/UpdatePassword.vue b/core/src/components/login/UpdatePassword.vue
index e281ad7ffc7..b7b9ecccd0a 100644
--- a/core/src/components/login/UpdatePassword.vue
+++ b/core/src/components/login/UpdatePassword.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<form @submit.prevent="submit">
diff --git a/core/src/components/setup/RecommendedApps.vue b/core/src/components/setup/RecommendedApps.vue
index 472c011c762..f2120c28402 100644
--- a/core/src/components/setup/RecommendedApps.vue
+++ b/core/src/components/setup/RecommendedApps.vue
@@ -1,26 +1,10 @@
<!--
- - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <div class="guest-box">
+ <div class="guest-box" data-cy-setup-recommended-apps>
<h2>{{ t('core', 'Recommended apps') }}</h2>
<p v-if="loadingApps" class="loading text-center">
{{ t('core', 'Loading apps …') }}
@@ -28,20 +12,13 @@
<p v-else-if="loadingAppsError" class="loading-error text-center">
{{ t('core', 'Could not fetch list of apps from the App Store.') }}
</p>
- <p v-else-if="installingApps" class="text-center">
- {{ t('core', 'Installing apps …') }}
- </p>
<div v-for="app in recommendedApps" :key="app.id" class="app">
<template v-if="!isHidden(app.id)">
<img :src="customIcon(app.id)" alt="">
<div class="info">
- <h3>
- {{ customName(app) }}
- <span v-if="app.loading" class="icon icon-loading-small-dark" />
- <span v-else-if="app.active" class="icon icon-checkmark-white" />
- </h3>
- <p v-html="customDescription(app.id)" />
+ <h3>{{ customName(app) }}</h3>
+ <p v-text="customDescription(app.id)" />
<p v-if="app.installationError">
<strong>{{ t('core', 'App download or installation failed') }}</strong>
</p>
@@ -52,37 +29,43 @@
<strong>{{ t('core', 'Cannot install this app') }}</strong>
</p>
</div>
+ <NcCheckboxRadioSwitch :checked="app.isSelected || app.active"
+ :disabled="!app.isCompatible || app.active"
+ :loading="app.loading"
+ @update:checked="toggleSelect(app.id)" />
</template>
</div>
<div class="dialog-row">
- <NcButton v-if="showInstallButton"
- type="tertiary"
- role="link"
- :href="defaultPageUrl">
+ <NcButton v-if="showInstallButton && !installingApps"
+ data-cy-setup-recommended-apps-skip
+ :href="defaultPageUrl"
+ variant="tertiary">
{{ t('core', 'Skip') }}
</NcButton>
<NcButton v-if="showInstallButton"
- type="primary"
+ data-cy-setup-recommended-apps-install
+ :disabled="installingApps || !isAnyAppSelected"
+ variant="primary"
@click.stop.prevent="installApps">
- {{ t('core', 'Install recommended apps') }}
+ {{ installingApps ? t('core', 'Installing apps …') : t('core', 'Install recommended apps') }}
</NcButton>
</div>
</div>
</template>
<script>
-import axios from '@nextcloud/axios'
-import { generateUrl, imagePath } from '@nextcloud/router'
+import { t } from '@nextcloud/l10n'
import { loadState } from '@nextcloud/initial-state'
+import { generateUrl, imagePath } from '@nextcloud/router'
+import axios from '@nextcloud/axios'
import pLimit from 'p-limit'
-import { translate as t } from '@nextcloud/l10n'
-
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-
import logger from '../../logger.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+
const recommended = {
calendar: {
description: t('core', 'Schedule work & meetings, synced with all your devices.'),
@@ -97,7 +80,7 @@ const recommended = {
icon: imagePath('core', 'actions/mail.svg'),
},
spreed: {
- description: t('core', 'Chatting, video calls, screensharing, online meetings and web conferencing – in your browser and with mobile apps.'),
+ description: t('core', 'Chatting, video calls, screen sharing, online meetings and web conferencing – in your browser and with mobile apps.'),
icon: imagePath('core', 'apps/spreed.svg'),
},
richdocuments: {
@@ -118,6 +101,7 @@ const recommendedIds = Object.keys(recommended)
export default {
name: 'RecommendedApps',
components: {
+ NcCheckboxRadioSwitch,
NcButton,
},
data() {
@@ -127,20 +111,23 @@ export default {
loadingApps: true,
loadingAppsError: false,
apps: [],
- defaultPageUrl: loadState('core', 'defaultPageUrl')
+ defaultPageUrl: loadState('core', 'defaultPageUrl'),
}
},
computed: {
recommendedApps() {
return this.apps.filter(app => recommendedIds.includes(app.id))
},
+ isAnyAppSelected() {
+ return this.recommendedApps.some(app => app.isSelected)
+ },
},
async mounted() {
try {
const { data } = await axios.get(generateUrl('settings/apps/list'))
logger.info(`${data.apps.length} apps fetched`)
- this.apps = data.apps.map(app => Object.assign(app, { loading: false, installationError: false }))
+ this.apps = data.apps.map(app => Object.assign(app, { loading: false, installationError: false, isSelected: app.isCompatible }))
logger.debug(`${this.recommendedApps.length} recommended apps found`, { apps: this.recommendedApps })
this.showInstallButton = true
@@ -154,23 +141,24 @@ export default {
},
methods: {
installApps() {
- this.showInstallButton = false
this.installingApps = true
const limit = pLimit(1)
const installing = this.recommendedApps
- .filter(app => !app.active && app.isCompatible && app.canInstall)
- .map(app => limit(() => {
+ .filter(app => !app.active && app.isCompatible && app.canInstall && app.isSelected)
+ .map(app => limit(async () => {
logger.info(`installing ${app.id}`)
app.loading = true
return axios.post(generateUrl('settings/apps/enable'), { appIds: [app.id], groups: [] })
.catch(error => {
logger.error(`could not install ${app.id}`, { error })
+ app.isSelected = false
app.installationError = true
})
.then(() => {
logger.info(`installed ${app.id}`)
app.loading = false
+ app.active = true
})
}))
logger.debug(`installing ${installing.length} recommended apps`)
@@ -208,6 +196,14 @@ export default {
}
return !!recommended[appId].hidden
},
+ toggleSelect(appId) {
+ // disable toggle when installButton is disabled
+ if (!(appId in recommended) || !this.showInstallButton) {
+ return
+ }
+ const index = this.apps.findIndex(app => app.id === appId)
+ this.$set(this.apps[index], 'isSelected', !this.apps[index].isSelected)
+ },
},
}
</script>
@@ -250,16 +246,17 @@ p {
.info {
h3, p {
- text-align: left;
+ text-align: start;
}
h3 {
margin-top: 0;
}
+ }
- h3 > span.icon {
- display: inline-block;
- }
+ .checkbox-radio-switch {
+ margin-inline-start: auto;
+ padding: 0 2px;
}
}
</style>
diff --git a/core/src/eventbus.d.ts b/core/src/eventbus.d.ts
new file mode 100644
index 00000000000..4fac9bc7841
--- /dev/null
+++ b/core/src/eventbus.d.ts
@@ -0,0 +1,14 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+declare module '@nextcloud/event-bus' {
+ export interface NextcloudEvents {
+ // mapping of 'event name' => 'event type'
+ 'nextcloud:unified-search:reset': undefined
+ 'nextcloud:unified-search:search': { query: string }
+ }
+}
+
+export {}
diff --git a/core/src/files/client.js b/core/src/files/client.js
index 9d32fefdfc4..7c69a65161b 100644
--- a/core/src/files/client.js
+++ b/core/src/files/client.js
@@ -1,33 +1,7 @@
/**
- * Copyright (c) 2015
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Michael Jobst <mjobst+github@tecratech.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Tomasz Grobelny <tomasz@grobelny.net>
- * @author Vincent Petry <vincent@nextcloud.com>
- * @author Vinicius Cubas Brand <vinicius@eita.org.br>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
diff --git a/core/src/files/fileinfo.js b/core/src/files/fileinfo.js
index 3fe90f82ac9..7ebe06a8349 100644
--- a/core/src/files/fileinfo.js
+++ b/core/src/files/fileinfo.js
@@ -1,26 +1,7 @@
/**
- * Copyright (c) 2015
- *
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
@@ -166,7 +147,7 @@
for (const i in this.shareAttributes) {
const attr = this.shareAttributes[i]
if (attr.scope === 'permissions' && attr.key === 'download') {
- return attr.enabled
+ return attr.value === true
}
}
diff --git a/core/src/globals.js b/core/src/globals.js
index 1a4c0582768..4b07cc17c3e 100644
--- a/core/src/globals.js
+++ b/core/src/globals.js
@@ -1,26 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable @nextcloud/no-deprecations */
@@ -28,16 +8,13 @@ import { initCore } from './init.js'
import _ from 'underscore'
import $ from 'jquery'
-import 'jquery-migrate/dist/jquery-migrate.min.js'
// TODO: switch to `jquery-ui` package and import widgets and effects individually
// `jquery-ui-dist` is used as a workaround for the issue of missing effects
import 'jquery-ui-dist/jquery-ui.js'
import 'jquery-ui-dist/jquery-ui.css'
import 'jquery-ui-dist/jquery-ui.theme.css'
// END TODO
-import autosize from 'autosize'
import Backbone from 'backbone'
-import './Polyfill/tooltip.js'
import ClipboardJS from 'clipboard'
import { dav } from 'davclient.js'
import Handlebars from 'handlebars'
@@ -52,7 +29,7 @@ import 'strengthify/strengthify.css'
import OC from './OC/index.js'
import OCP from './OCP/index.js'
import OCA from './OCA/index.js'
-import { getToken as getRequestToken } from './OC/requesttoken.js'
+import { getRequestToken } from './OC/requesttoken.ts'
const warnIfNotTesting = function() {
if (window.TESTING === undefined) {
@@ -100,11 +77,11 @@ const setDeprecatedProp = (global, cb, msg) => {
window._ = _
setDeprecatedProp(['$', 'jQuery'], () => $, 'The global jQuery is deprecated. It will be removed in a later versions without another warning. Please ship your own.')
-setDeprecatedProp('autosize', () => autosize, 'please ship your own, this will be removed in Nextcloud 20')
setDeprecatedProp('Backbone', () => Backbone, 'please ship your own, this will be removed in Nextcloud 20')
setDeprecatedProp(['Clipboard', 'ClipboardJS'], () => ClipboardJS, 'please ship your own, this will be removed in Nextcloud 20')
window.dav = dav
setDeprecatedProp('Handlebars', () => Handlebars, 'please ship your own, this will be removed in Nextcloud 20')
+// Global md5 only required for: apps/files/js/file-upload.js
setDeprecatedProp('md5', () => md5, 'please ship your own, this will be removed in Nextcloud 20')
setDeprecatedProp('moment', () => moment, 'please ship your own, this will be removed in Nextcloud 20')
diff --git a/core/src/icons.js b/core/src/icons.js
index 624ba54be88..5845b01fea1 100644
--- a/core/src/icons.js
+++ b/core/src/icons.js
@@ -1,3 +1,7 @@
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
/* eslint-disable quote-props */
/* eslint-disable n/no-unpublished-import */
import path from 'path'
@@ -99,6 +103,7 @@ const icons = {
'tablet': path.join(__dirname, '../img', 'clients', 'tablet.svg'),
'tag': path.join(__dirname, '../img', 'actions', 'tag.svg'),
'talk': path.join(__dirname, '../img', 'apps', 'spreed.svg'),
+ 'teams': path.join(__dirname, '../img', 'apps', 'circles.svg'),
'template-add': path.join(__dirname, '../img', 'actions', 'template-add.svg'),
'timezone': path.join(__dirname, '../img', 'actions', 'timezone.svg'),
'toggle-background': path.join(__dirname, '../img', 'actions', 'toggle-background.svg'),
@@ -124,6 +129,10 @@ const icons = {
}
const iconsColor = {
+ 'add-folder-description': {
+ path: path.join(__dirname, '../img', 'actions', 'add-folder-description.svg'),
+ color: 'grey',
+ },
'settings': {
path: path.join(__dirname, '../img', 'actions', 'settings.svg'),
color: 'black',
@@ -166,6 +175,14 @@ const iconsColor = {
// TODO: replace primary ?
color: 'primary',
},
+ 'filetype-text': {
+ path: path.join(__dirname, '../img', 'filetypes', 'text.svg'),
+ color: 'grey',
+ },
+ 'file-text': {
+ path: path.join(__dirname, '../img', 'filetypes', 'text.svg'),
+ color: 'black',
+ },
}
// use this to define aliases to existing icons
@@ -209,7 +226,6 @@ const iconsAliases = {
'icon-category-security': 'icon-password-dark',
'icon-category-search': 'icon-search-dark',
'icon-category-tools': 'icon-settings-dark',
- 'icon-filetype-text': 'icon-file-grey',
'nav-icon-systemtagsfilter': 'icon-tag-dark',
}
@@ -221,7 +237,7 @@ const colorSvg = function(svg = '', color = '000') {
}
// add fill (fill is not present on black elements)
- const fillRe = /<((circle|rect|path)((?!fill)[a-z0-9 =".\-#():;,])+)\/>/gmi
+ const fillRe = /<((circle|rect|path)((?!fill=)[a-z0-9 =".\-#():;,])+)\/>/gmi
svg = svg.replace(fillRe, '<$1 fill="#' + color + '"/>')
// replace any fill or stroke colors
diff --git a/core/src/init.js b/core/src/init.js
index 6114c612705..1bcd8218702 100644
--- a/core/src/init.js
+++ b/core/src/init.js
@@ -1,27 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author nacho <nacho@ownyourbits.com>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* globals Snap */
@@ -29,12 +8,13 @@ import _ from 'underscore'
import $ from 'jquery'
import moment from 'moment'
-import { initSessionHeartBeat } from './session-heartbeat.js'
import OC from './OC/index.js'
+import { initSessionHeartBeat } from './session-heartbeat.ts'
import { setUp as setUpContactsMenu } from './components/ContactsMenu.js'
import { setUp as setUpMainMenu } from './components/MainMenu.js'
import { setUp as setUpUserMenu } from './components/UserMenu.js'
import { interceptRequests } from './utils/xhr-request.js'
+import { initFallbackClipboardAPI } from './utils/ClipboardFallback.ts'
// keep in sync with core/css/variables.scss
const breakpointMobileWidth = 1024
@@ -79,6 +59,7 @@ moment.locale(locale)
*/
export const initCore = () => {
interceptRequests()
+ initFallbackClipboardAPI()
$(window).on('unload.main', () => { OC._unloadCalled = true })
$(window).on('beforeunload.main', () => {
diff --git a/core/src/install.js b/core/src/install.js
deleted file mode 100644
index e8432de42b0..00000000000
--- a/core/src/install.js
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import $ from 'jquery'
-import { translate as t } from '@nextcloud/l10n'
-import { getToken } from './OC/requesttoken.js'
-import getURLParameter from './Util/get-url-parameter.js'
-
-import './jquery/showpassword.js'
-
-import 'jquery-ui/ui/widgets/button.js'
-import 'jquery-ui/themes/base/theme.css'
-import 'jquery-ui/themes/base/button.css'
-
-import './Polyfill/tooltip.js'
-
-import 'strengthify'
-import 'strengthify/strengthify.css'
-
-window.addEventListener('DOMContentLoaded', function() {
- const dbtypes = {
- sqlite: !!$('#hasSQLite').val(),
- mysql: !!$('#hasMySQL').val(),
- postgresql: !!$('#hasPostgreSQL').val(),
- oracle: !!$('#hasOracle').val(),
- }
-
- $('#selectDbType').buttonset()
- // change links inside an info box back to their default appearance
- $('#selectDbType p.info a').button('destroy')
-
- if ($('#hasSQLite').val()) {
- $('#use_other_db').hide()
- $('#use_oracle_db').hide()
- } else {
- $('#sqliteInformation').hide()
- }
- $('#adminlogin').change(function() {
- $('#adminlogin').val($.trim($('#adminlogin').val()))
- })
- $('#sqlite').click(function() {
- $('#use_other_db').slideUp(250)
- $('#use_oracle_db').slideUp(250)
- $('#sqliteInformation').show()
- $('#dbname').attr('pattern', '[0-9a-zA-Z$_-]+')
- })
-
- $('#mysql,#pgsql').click(function() {
- $('#use_other_db').slideDown(250)
- $('#use_oracle_db').slideUp(250)
- $('#sqliteInformation').hide()
- $('#dbname').attr('pattern', '[0-9a-zA-Z$_-]+')
- })
-
- $('#oci').click(function() {
- $('#use_other_db').slideDown(250)
- $('#use_oracle_db').show(250)
- $('#sqliteInformation').hide()
- $('#dbname').attr('pattern', '[0-9a-zA-Z$_-.]+')
- })
-
- $('#showAdvanced').click(function(e) {
- e.preventDefault()
- $('#datadirContent').slideToggle(250)
- $('#databaseBackend').slideToggle(250)
- $('#databaseField').slideToggle(250)
- })
- $('form').submit(function() {
- // Save form parameters
- const post = $(this).serializeArray()
-
- // Show spinner while finishing setup
- $('.float-spinner').show(250)
-
- // Disable inputs
- $('input[type="submit"]').attr('disabled', 'disabled').val($('input[type="submit"]').data('finishing'))
- $('input', this).addClass('ui-state-disabled').attr('disabled', 'disabled')
- // only disable buttons if they are present
- if ($('#selectDbType').find('.ui-button').length > 0) {
- $('#selectDbType').buttonset('disable')
- }
- $('.strengthify-wrapper, .tipsy')
- .css('filter', 'alpha(opacity=30)')
- .css('opacity', 0.3)
-
- // Create the form
- const form = $('<form>')
- form.attr('action', $(this).attr('action'))
- form.attr('method', 'POST')
-
- for (let i = 0; i < post.length; i++) {
- const input = $('<input type="hidden">')
- input.attr(post[i])
- form.append(input)
- }
-
- // Add redirect_url
- const redirectURL = getURLParameter('redirect_url')
- if (redirectURL) {
- const redirectURLInput = $('<input type="hidden">')
- redirectURLInput.attr({
- name: 'redirect_url',
- value: redirectURL,
- })
- form.append(redirectURLInput)
- }
-
- // Submit the form
- form.appendTo(document.body)
- form.submit()
- return false
- })
-
- // Expand latest db settings if page was reloaded on error
- const currentDbType = $('input[type="radio"]:checked').val()
-
- if (currentDbType === undefined) {
- $('input[type="radio"]').first().click()
- }
-
- if (
- currentDbType === 'sqlite'
- || (dbtypes.sqlite && currentDbType === undefined)
- ) {
- $('#datadirContent').hide(250)
- $('#databaseBackend').hide(250)
- $('#databaseField').hide(250)
- $('.float-spinner').hide(250)
- }
-
- $('#adminpass').strengthify({
- zxcvbn: OC.linkTo('core', 'vendor/zxcvbn/dist/zxcvbn.js'),
- titles: [
- t('core', 'Very weak password'),
- t('core', 'Weak password'),
- t('core', 'So-so password'),
- t('core', 'Good password'),
- t('core', 'Strong password'),
- ],
- drawTitles: true,
- nonce: btoa(getToken()),
- })
-
- $('#dbpass').showPassword().keyup()
- $('.toggle-password').click(function(event) {
- event.preventDefault()
- const currentValue = $(this).parent().children('input').attr('type')
- if (currentValue === 'password') {
- $(this).parent().children('input').attr('type', 'text')
- } else {
- $(this).parent().children('input').attr('type', 'password')
- }
- })
-})
diff --git a/core/src/install.ts b/core/src/install.ts
new file mode 100644
index 00000000000..4ef608ec2bd
--- /dev/null
+++ b/core/src/install.ts
@@ -0,0 +1,43 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import Vue from 'vue'
+import Setup from './views/Setup.vue'
+
+type Error = {
+ error: string
+ hint: string
+}
+
+export type DbType = 'sqlite' | 'mysql' | 'pgsql' | 'oci'
+
+export type SetupConfig = {
+ adminlogin: string
+ adminpass: string
+ directory: string
+ dbuser: string
+ dbpass: string
+ dbname: string
+ dbtablespace: string
+ dbhost: string
+ dbtype: DbType | ''
+
+ databases: Partial<Record<DbType, string>>
+
+ hasAutoconfig: boolean
+ htaccessWorking: boolean
+ serverRoot: string
+
+ errors: string[]|Error[]
+}
+
+export type SetupLinks = {
+ adminInstall: string
+ adminSourceInstall: string
+ adminDBConfiguration: string
+}
+
+const SetupVue = Vue.extend(Setup)
+new SetupVue().$mount('#content')
diff --git a/core/src/jquery/avatar.js b/core/src/jquery/avatar.js
index 1d86e54e9d3..3851a26ce31 100644
--- a/core/src/jquery/avatar.js
+++ b/core/src/jquery/avatar.js
@@ -1,30 +1,12 @@
/**
- * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import { getCurrentUser } from '@nextcloud/auth'
+import { generateUrl } from '@nextcloud/router'
import $ from 'jquery'
-import OC from '../OC/index.js'
-
/**
* This plugin inserts the right avatar for the user, depending on, whether a
* custom avatar is uploaded - which it uses then - or not, and display a
@@ -108,8 +90,8 @@ $.fn.avatar = function(user, size, ie8fix, hidedefault, callback, displayname) {
let url
// If this is our own avatar we have to use the version attribute
- if (user === OC.getCurrentUser().uid) {
- url = OC.generateUrl(
+ if (user === getCurrentUser()?.uid) {
+ url = generateUrl(
'/avatar/{user}/{size}?v={version}',
{
user,
@@ -117,7 +99,7 @@ $.fn.avatar = function(user, size, ie8fix, hidedefault, callback, displayname) {
version: oc_userconfig.avatar.version,
})
} else {
- url = OC.generateUrl(
+ url = generateUrl(
'/avatar/{user}/{size}',
{
user,
diff --git a/core/src/jquery/contactsmenu.js b/core/src/jquery/contactsmenu.js
index 4026f48068d..fba014c364e 100644
--- a/core/src/jquery/contactsmenu.js
+++ b/core/src/jquery/contactsmenu.js
@@ -1,30 +1,11 @@
/**
- * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
-import OC from '../OC/index.js'
+import { generateUrl } from '@nextcloud/router'
import { isA11yActivation } from '../Util/a11y.js'
const LIST = ''
@@ -70,7 +51,7 @@ $.fn.contactsMenu = function(shareWith, shareType, appendTo) {
}
$list.addClass('loaded')
- $.ajax(OC.generateUrl('/contactsmenu/findOne'), {
+ $.ajax(generateUrl('/contactsmenu/findOne'), {
method: 'POST',
data: {
shareType,
diff --git a/core/src/jquery/css/jquery-ui-fixes.scss b/core/src/jquery/css/jquery-ui-fixes.scss
index 4c2241d8d5d..637f4bfe14b 100644
--- a/core/src/jquery/css/jquery-ui-fixes.scss
+++ b/core/src/jquery/css/jquery-ui-fixes.scss
@@ -1,3 +1,7 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
/* Component containers
----------------------------------*/
.ui-widget-content {
@@ -5,14 +9,17 @@
background: var(--color-main-background) none;
color: var(--color-main-text);
}
+
.ui-widget-content a {
color: var(--color-main-text);
}
+
.ui-widget-header {
border: none;
color: var(--color-main-text);
background-image: none;
}
+
.ui-widget-header a {
color: var(--color-main-text);
}
@@ -27,11 +34,13 @@
font-weight: bold;
color: #555;
}
+
.ui-state-default a,
.ui-state-default a:link,
.ui-state-default a:visited {
color: #555;
}
+
.ui-state-hover,
.ui-widget-content .ui-state-hover,
.ui-widget-header .ui-state-hover,
@@ -43,12 +52,14 @@
font-weight: bold;
color: var(--color-main-text);
}
+
.ui-state-hover a,
.ui-state-hover a:hover,
.ui-state-hover a:link,
.ui-state-hover a:visited {
color: var(--color-main-text);
}
+
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active {
@@ -57,6 +68,7 @@
font-weight: bold;
color: var(--color-main-text);
}
+
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
@@ -73,11 +85,13 @@
color: var(--color-text-light);
font-weight: 600;
}
+
.ui-state-highlight a,
.ui-widget-content .ui-state-highlight a,
.ui-widget-header .ui-state-highlight a {
color: var(--color-text-lighter);
}
+
.ui-state-error,
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
@@ -85,11 +99,13 @@
background: var(--color-error) none;
color: #ffffff;
}
+
.ui-state-error a,
.ui-widget-content .ui-state-error a,
.ui-widget-header .ui-state-error a {
color: #ffffff;
}
+
.ui-state-error-text,
.ui-widget-content .ui-state-error-text,
.ui-widget-header .ui-state-error-text {
@@ -101,20 +117,25 @@
.ui-state-default .ui-icon {
background-image: url('images/ui-icons_1d2d44_256x240.png');
}
+
.ui-state-hover .ui-icon,
.ui-state-focus .ui-icon {
background-image: url('images/ui-icons_1d2d44_256x240.png');
}
+
.ui-state-active .ui-icon {
background-image: url('images/ui-icons_1d2d44_256x240.png');
}
+
.ui-state-highlight .ui-icon {
background-image: url('images/ui-icons_ffffff_256x240.png');
}
+
.ui-state-error .ui-icon,
.ui-state-error-text .ui-icon {
background-image: url('images/ui-icons_ffd27a_256x240.png');
}
+
.ui-icon.ui-icon-none {
display: none;
}
@@ -126,6 +147,7 @@
background: #666666 url('images/ui-bg_diagonals-thick_20_666666_40x40.png') 50% 50% repeat;
opacity: .5;
}
+
.ui-widget-shadow {
margin: -5px 0 0 -5px;
padding: 5px;
@@ -139,8 +161,8 @@
border: none;
.ui-tabs-nav.ui-corner-all {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
+ border-end-start-radius: 0;
+ border-end-end-radius: 0;
}
.ui-tabs-nav {
@@ -185,7 +207,8 @@
.ui-menu-item a {
color: var(--color-text-lighter);
display: block;
- padding: 4px 4px 4px 14px;
+ padding: 4px;
+ padding-inline-start: 14px;
&.ui-state-focus, &.ui-state-active {
box-shadow: inset 4px 0 var(--color-primary-element);
@@ -201,8 +224,8 @@
&.ui-corner-all {
border-radius: 0;
- border-bottom-left-radius: var(--border-radius);
- border-bottom-right-radius: var(--border-radius);
+ border-end-start-radius: var(--border-radius);
+ border-end-end-radius: var(--border-radius);
}
.ui-state-hover, .ui-widget-content .ui-state-hover,
diff --git a/core/src/jquery/css/jquery.ocdialog.scss b/core/src/jquery/css/jquery.ocdialog.scss
index 1911dd29dc4..b950d98c381 100644
--- a/core/src/jquery/css/jquery.ocdialog.scss
+++ b/core/src/jquery/css/jquery.ocdialog.scss
@@ -1,28 +1,34 @@
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
.oc-dialog {
background: var(--color-main-background);
color: var(--color-text-light);
border-radius: var(--border-radius-large);
box-shadow: 0 0 30px var(--color-box-shadow);
padding: 24px;
- z-index: 10000;
+ z-index: 100001;
font-size: 100%;
box-sizing: border-box;
min-width: 200px;
top: 50%;
- left: 50%;
+ inset-inline-start: 50%;
transform: translate(-50%, -50%);
max-height: calc(100% - 20px);
max-width: calc(100% - 20px);
overflow: auto;
}
+
.oc-dialog-title {
background: var(--color-main-background);
}
+
.oc-dialog-buttonrow {
position: relative;
display: flex;
background: transparent;
- right: 0;
+ inset-inline-end: 0;
bottom: 0;
padding: 0;
padding-top: 10px;
@@ -53,7 +59,7 @@
width: 44px !important;
height: 44px !important;
top: 4px;
- right: 4px;
+ inset-inline-end: 4px;
padding: 25px;
background: var(--icon-close-dark) no-repeat center;
opacity: .5;
@@ -69,10 +75,10 @@
.oc-dialog-dim {
background-color: #000;
opacity: .2;
- z-index: 9999;
+ z-index: 100001;
position: fixed;
top: 0;
- left: 0;
+ inset-inline-start: 0;
width: 100%;
height: 100%;
}
diff --git a/core/src/jquery/exists.js b/core/src/jquery/exists.js
index 0f545cc8107..8a8efdb5a63 100644
--- a/core/src/jquery/exists.js
+++ b/core/src/jquery/exists.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
diff --git a/core/src/jquery/filterattr.js b/core/src/jquery/filterattr.js
index 44aad5b8ea2..f577e55e4e0 100644
--- a/core/src/jquery/filterattr.js
+++ b/core/src/jquery/filterattr.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
diff --git a/core/src/jquery/index.js b/core/src/jquery/index.js
index c0716890cb3..f285ba19449 100644
--- a/core/src/jquery/index.js
+++ b/core/src/jquery/index.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
diff --git a/core/src/jquery/ocdialog.js b/core/src/jquery/ocdialog.js
index 2d547443be9..a5f588ec659 100644
--- a/core/src/jquery/ocdialog.js
+++ b/core/src/jquery/ocdialog.js
@@ -1,26 +1,6 @@
/**
- * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Gary Kim <gary@garykim.dev>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
@@ -234,7 +214,7 @@ $.widget('oc.ocdialog', {
}
this.overlay = $('<div>')
.addClass('oc-dialog-dim')
- .appendTo(contentDiv)
+ .insertBefore(this.$dialog)
this.overlay.on('click keydown keyup', function(event) {
if (event.target !== self.$dialog.get(0) && self.$dialog.find($(event.target)).length === 0) {
event.preventDefault()
diff --git a/core/src/jquery/octemplate.js b/core/src/jquery/octemplate.js
index 7bf9f8dcb19..cecbe880aa6 100644
--- a/core/src/jquery/octemplate.js
+++ b/core/src/jquery/octemplate.js
@@ -1,25 +1,6 @@
/**
- * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
@@ -108,7 +89,7 @@ const Template = {
function(a, b) {
const r = o[b]
return typeof r === 'string' || typeof r === 'number' ? r : a
- }
+ },
)
} catch (e) {
console.error(e, 'data:', data)
diff --git a/core/src/jquery/placeholder.js b/core/src/jquery/placeholder.js
index 9e24216a1b5..e57951af5e4 100644
--- a/core/src/jquery/placeholder.js
+++ b/core/src/jquery/placeholder.js
@@ -1,27 +1,7 @@
/**
- * @copyright 2016-2018 John Molakvoæ <skjnldsv@protonmail.com>
- * @copyright 2013 Morris Jobke <morris.jobke@gmail.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Sergey Shliakhov <husband.sergey@gmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2013-2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
diff --git a/core/src/jquery/requesttoken.js b/core/src/jquery/requesttoken.js
index 68858aa1534..1e9e06515a6 100644
--- a/core/src/jquery/requesttoken.js
+++ b/core/src/jquery/requesttoken.js
@@ -1,33 +1,15 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
-import { getToken } from '../OC/requesttoken.js'
+import { getRequestToken } from '../OC/requesttoken.ts'
$(document).on('ajaxSend', function(elm, xhr, settings) {
if (settings.crossDomain === false) {
- xhr.setRequestHeader('requesttoken', getToken())
+ xhr.setRequestHeader('requesttoken', getRequestToken())
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
}
})
diff --git a/core/src/jquery/selectrange.js b/core/src/jquery/selectrange.js
index 713fd1edf8d..a4d8f49ce43 100644
--- a/core/src/jquery/selectrange.js
+++ b/core/src/jquery/selectrange.js
@@ -1,24 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
diff --git a/core/src/jquery/showpassword.js b/core/src/jquery/showpassword.js
index a00a57cc867..8d938d7853b 100644
--- a/core/src/jquery/showpassword.js
+++ b/core/src/jquery/showpassword.js
@@ -1,33 +1,16 @@
/**
- * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
+/** @typedef {import('jquery')} jQuery */
import $ from 'jquery'
/**
* @name Show Password
* @description
* @version 1.3.0
- * @requires Jquery 1.5
+ * @requires jQuery 1.5
*
* @author Jan Jarfalk <jan.jarfalk@unwrongest.com>
* author-website http://www.unwrongest.com
diff --git a/core/src/jquery/ui-fixes.js b/core/src/jquery/ui-fixes.js
index ab4235d9b53..e23464b2f9d 100644
--- a/core/src/jquery/ui-fixes.js
+++ b/core/src/jquery/ui-fixes.js
@@ -1,24 +1,6 @@
/**
- * @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import $ from 'jquery'
diff --git a/core/src/legacy-unified-search.js b/core/src/legacy-unified-search.js
index 943081f3d23..59ee462fbf5 100644
--- a/core/src/legacy-unified-search.js
+++ b/core/src/legacy-unified-search.js
@@ -1,34 +1,17 @@
/**
- * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getLoggerBuilder } from '@nextcloud/logger'
-import { getRequestToken } from '@nextcloud/auth'
+import { getCSPNonce } from '@nextcloud/auth'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import Vue from 'vue'
import UnifiedSearch from './views/LegacyUnifiedSearch.vue'
// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(getRequestToken())
+__webpack_nonce__ = getCSPNonce()
const logger = getLoggerBuilder()
.setApp('unified-search')
diff --git a/core/src/logger.js b/core/src/logger.js
index 593cc071850..78d51a798e4 100644
--- a/core/src/logger.js
+++ b/core/src/logger.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getCurrentUser } from '@nextcloud/auth'
@@ -36,3 +19,8 @@ const getLogger = user => {
}
export default getLogger(getCurrentUser())
+
+export const unifiedSearchLogger = getLoggerBuilder()
+ .setApp('unified-search')
+ .detectUser()
+ .build()
diff --git a/core/src/login.js b/core/src/login.js
index 964b26520ec..29affcda762 100644
--- a/core/src/login.js
+++ b/core/src/login.js
@@ -1,26 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import Vue from 'vue'
diff --git a/core/src/main.js b/core/src/main.js
index 46fe5fa1718..2d88f15562b 100644
--- a/core/src/main.js
+++ b/core/src/main.js
@@ -1,31 +1,10 @@
/**
- * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import 'core-js/stable/index.js'
import 'regenerator-runtime/runtime.js'
-import './Polyfill/index.js'
// If you remove the line below, tests won't pass
// eslint-disable-next-line no-unused-vars
@@ -35,10 +14,12 @@ import './globals.js'
import './jquery/index.js'
import { initCore } from './init.js'
import { registerAppsSlideToggle } from './OC/apps.js'
-import { getRequestToken } from '@nextcloud/auth'
+import { getCSPNonce } from '@nextcloud/auth'
+import { generateUrl } from '@nextcloud/router'
+import Axios from '@nextcloud/axios'
// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(getRequestToken())
+__webpack_nonce__ = getCSPNonce()
window.addEventListener('DOMContentLoaded', function() {
initCore()
@@ -51,3 +32,20 @@ window.addEventListener('DOMContentLoaded', function() {
window.onhashchange = _.bind(OC.Util.History._onPopState, OC.Util.History)
}
})
+
+// Fix error "CSRF check failed"
+document.addEventListener('DOMContentLoaded', function() {
+ const form = document.getElementById('password-input-form')
+ if (form) {
+ form.addEventListener('submit', async function(event) {
+ event.preventDefault()
+ const requestToken = document.getElementById('requesttoken')
+ if (requestToken) {
+ const url = generateUrl('/csrftoken')
+ const resp = await Axios.get(url)
+ requestToken.value = resp.data.token
+ }
+ form.submit()
+ })
+ }
+})
diff --git a/core/src/maintenance.js b/core/src/maintenance.js
index 3c13a45894c..e66b14a88f5 100644
--- a/core/src/maintenance.js
+++ b/core/src/maintenance.js
@@ -1,25 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import Axios from '@nextcloud/axios'
diff --git a/core/src/mixins/Nextcloud.js b/core/src/mixins/Nextcloud.js
index 4cef2b9b09e..3a94f85d2c6 100644
--- a/core/src/mixins/Nextcloud.js
+++ b/core/src/mixins/Nextcloud.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import L10n from '../OC/l10n.js'
diff --git a/core/src/mixins/auth.js b/core/src/mixins/auth.js
new file mode 100644
index 00000000000..f5b9365516e
--- /dev/null
+++ b/core/src/mixins/auth.js
@@ -0,0 +1,19 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export default {
+
+ computed: {
+ userNameInputLengthIs255() {
+ return this.user.length >= 255
+ },
+ userInputHelperText() {
+ if (this.userNameInputLengthIs255) {
+ return t('core', 'Email length is at max (255)')
+ }
+ return undefined
+ },
+ },
+}
diff --git a/core/src/profile.ts b/core/src/profile.ts
deleted file mode 100644
index ee1593e8705..00000000000
--- a/core/src/profile.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * @copyright 2021, Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import { getRequestToken } from '@nextcloud/auth'
-import Vue from 'vue'
-
-import Profile from './views/Profile.vue'
-import ProfileSections from './profile/ProfileSections.js'
-
-// @ts-expect-error Script nonce required for webpack loading additional scripts
-__webpack_nonce__ = btoa(getRequestToken() ?? '')
-
-if (!window.OCA) {
- window.OCA = {}
-}
-
-if (!window.OCA.Core) {
- window.OCA.Core = {}
-}
-Object.assign(window.OCA.Core, { ProfileSections: new ProfileSections() })
-
-const View = Vue.extend(Profile)
-
-window.addEventListener('DOMContentLoaded', () => {
- new View().$mount('#content')
-})
diff --git a/core/src/profile/ProfileSections.js b/core/src/profile/ProfileSections.js
deleted file mode 100644
index ac8e8644eeb..00000000000
--- a/core/src/profile/ProfileSections.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-export default class ProfileSections {
-
- _sections
-
- constructor() {
- this._sections = []
- }
-
- /**
- * @param {registerSectionCallback} section To be called to mount the section to the profile page
- */
- registerSection(section) {
- this._sections.push(section)
- }
-
- getSections() {
- return this._sections
- }
-
-}
diff --git a/core/src/public-page-menu.ts b/core/src/public-page-menu.ts
new file mode 100644
index 00000000000..b290d1d03e9
--- /dev/null
+++ b/core/src/public-page-menu.ts
@@ -0,0 +1,15 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getCSPNonce } from '@nextcloud/auth'
+import Vue from 'vue'
+
+import PublicPageMenu from './views/PublicPageMenu.vue'
+
+__webpack_nonce__ = getCSPNonce()
+
+const View = Vue.extend(PublicPageMenu)
+const instance = new View()
+instance.$mount('#public-page-menu')
diff --git a/core/src/public-page-user-menu.ts b/core/src/public-page-user-menu.ts
new file mode 100644
index 00000000000..25024271fb5
--- /dev/null
+++ b/core/src/public-page-user-menu.ts
@@ -0,0 +1,15 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getCSPNonce } from '@nextcloud/auth'
+import Vue from 'vue'
+
+import PublicPageUserMenu from './views/PublicPageUserMenu.vue'
+
+__webpack_nonce__ = getCSPNonce()
+
+const View = Vue.extend(PublicPageUserMenu)
+const instance = new View()
+instance.$mount('#public-page-user-menu')
diff --git a/core/src/public.ts b/core/src/public.ts
new file mode 100644
index 00000000000..ce4af8aa2ac
--- /dev/null
+++ b/core/src/public.ts
@@ -0,0 +1,26 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+const body = document.body
+const footer = document.querySelector('footer')
+let prevHeight = footer?.offsetHeight
+
+const onResize: ResizeObserverCallback = (entries) => {
+ for (const entry of entries) {
+ const height = entry.contentRect.height
+ if (height === prevHeight) {
+ return
+ }
+ prevHeight = height
+ body.style.setProperty('--footer-height', `${height}px`)
+ }
+}
+
+if (footer) {
+ new ResizeObserver(onResize)
+ .observe(footer, {
+ box: 'border-box', // <footer> is border-box
+ })
+}
diff --git a/core/src/recommendedapps.js b/core/src/recommendedapps.js
index 31179a5a4ad..13f16436ed3 100644
--- a/core/src/recommendedapps.js
+++ b/core/src/recommendedapps.js
@@ -1,26 +1,9 @@
/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { getRequestToken } from '@nextcloud/auth'
+import { getCSPNonce } from '@nextcloud/auth'
import { translate as t } from '@nextcloud/l10n'
import Vue from 'vue'
@@ -28,7 +11,7 @@ import logger from './logger.js'
import RecommendedApps from './components/setup/RecommendedApps.vue'
// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(getRequestToken())
+__webpack_nonce__ = getCSPNonce()
Vue.mixin({
methods: {
diff --git a/core/src/services/BrowserStorageService.js b/core/src/services/BrowserStorageService.js
index 3ecf52f423d..b7d34bf1716 100644
--- a/core/src/services/BrowserStorageService.js
+++ b/core/src/services/BrowserStorageService.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getBuilder } from '@nextcloud/browser-storage'
diff --git a/core/src/services/BrowsersListService.js b/core/src/services/BrowsersListService.js
index daf4e947dbb..77f217a86ac 100644
--- a/core/src/services/BrowsersListService.js
+++ b/core/src/services/BrowsersListService.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getUserAgentRegex } from 'browserslist-useragent-regexp'
diff --git a/core/src/services/LegacyUnifiedSearchService.js b/core/src/services/LegacyUnifiedSearchService.js
index 3c673479771..5b79c09b8b2 100644
--- a/core/src/services/LegacyUnifiedSearchService.js
+++ b/core/src/services/LegacyUnifiedSearchService.js
@@ -1,26 +1,6 @@
/**
- * @copyright 2020, John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateOcsUrl } from '@nextcloud/router'
diff --git a/core/src/services/UnifiedSearchService.js b/core/src/services/UnifiedSearchService.js
index 9e16fe1880c..7067c994c90 100644
--- a/core/src/services/UnifiedSearchService.js
+++ b/core/src/services/UnifiedSearchService.js
@@ -1,23 +1,6 @@
/**
- * @copyright 2023, Fon E. Noel NFEBE <fenn25.fn@gmail.com>
- *
- * @author Fon E. Noel NFEBE <fenn25.fn@gmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
@@ -65,9 +48,10 @@ export async function getProviders() {
* @param {string} options.until the search
* @param {string} options.limit the search
* @param {string} options.person the search
+ * @param {object} options.extraQueries additional queries to filter search results
* @return {object} {request: Promise, cancel: Promise}
*/
-export function search({ type, query, cursor, since, until, limit, person }) {
+export function search({ type, query, cursor, since, until, limit, person, extraQueries = {} }) {
/**
* Generate an axios cancel token
*/
@@ -84,6 +68,7 @@ export function search({ type, query, cursor, since, until, limit, person }) {
person,
// Sending which location we're currently at
from: window.location.pathname.replace('/index.php', '') + window.location.search,
+ ...extraQueries,
},
})
@@ -97,7 +82,7 @@ export function search({ type, query, cursor, since, until, limit, person }) {
* Get the list of active contacts
*
* @param {object} filter filter contacts by string
- * @param filter.searchTerm
+ * @param {string} filter.searchTerm the query
* @return {object} {request: Promise}
*/
export async function getContacts({ searchTerm }) {
diff --git a/core/src/services/WebAuthnAuthenticationService.js b/core/src/services/WebAuthnAuthenticationService.js
deleted file mode 100644
index 3eabceef5e4..00000000000
--- a/core/src/services/WebAuthnAuthenticationService.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import Axios from '@nextcloud/axios'
-import { generateUrl } from '@nextcloud/router'
-
-/**
- * @param {any} loginName -
- */
-export function startAuthentication(loginName) {
- const url = generateUrl('/login/webauthn/start')
-
- return Axios.post(url, { loginName })
- .then(resp => resp.data)
-}
-
-/**
- * @param {any} data -
- */
-export function finishAuthentication(data) {
- const url = generateUrl('/login/webauthn/finish')
-
- return Axios.post(url, { data })
- .then(resp => resp.data)
-}
diff --git a/core/src/services/WebAuthnAuthenticationService.ts b/core/src/services/WebAuthnAuthenticationService.ts
new file mode 100644
index 00000000000..df1837254ad
--- /dev/null
+++ b/core/src/services/WebAuthnAuthenticationService.ts
@@ -0,0 +1,42 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/browser'
+
+import { startAuthentication as startWebauthnAuthentication } from '@simplewebauthn/browser'
+import { generateUrl } from '@nextcloud/router'
+
+import Axios from '@nextcloud/axios'
+import logger from '../logger'
+
+export class NoValidCredentials extends Error {}
+
+/**
+ * Start webautn authentication
+ * This loads the challenge, connects to the authenticator and returns the repose that needs to be sent to the server.
+ *
+ * @param loginName Name to login
+ */
+export async function startAuthentication(loginName: string) {
+ const url = generateUrl('/login/webauthn/start')
+
+ const { data } = await Axios.post<PublicKeyCredentialRequestOptionsJSON>(url, { loginName })
+ if (!data.allowCredentials || data.allowCredentials.length === 0) {
+ logger.error('No valid credentials returned for webauthn')
+ throw new NoValidCredentials()
+ }
+ return await startWebauthnAuthentication({ optionsJSON: data })
+}
+
+/**
+ * Verify webauthn authentication
+ * @param authData The authentication data to sent to the server
+ */
+export async function finishAuthentication(authData: AuthenticationResponseJSON) {
+ const url = generateUrl('/login/webauthn/finish')
+
+ const { data } = await Axios.post(url, { data: JSON.stringify(authData) })
+ return data
+}
diff --git a/core/src/session-heartbeat.js b/core/src/session-heartbeat.js
deleted file mode 100644
index 153408b767b..00000000000
--- a/core/src/session-heartbeat.js
+++ /dev/null
@@ -1,185 +0,0 @@
-/**
- * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import $ from 'jquery'
-import { emit } from '@nextcloud/event-bus'
-import { loadState } from '@nextcloud/initial-state'
-import { getCurrentUser } from '@nextcloud/auth'
-import { generateUrl } from '@nextcloud/router'
-
-import OC from './OC/index.js'
-import { setToken as setRequestToken, getToken as getRequestToken } from './OC/requesttoken.js'
-
-let config = null
-/**
- * The legacy jsunit tests overwrite OC.config before calling initCore
- * therefore we need to wait with assigning the config fallback until initCore calls initSessionHeartBeat
- */
-const loadConfig = () => {
- try {
- config = loadState('core', 'config')
- } catch (e) {
- // This fallback is just for our legacy jsunit tests since we have no way to mock loadState calls
- config = OC.config
- }
-}
-
-/**
- * session heartbeat (defaults to enabled)
- *
- * @return {boolean}
- */
-const keepSessionAlive = () => {
- return config.session_keepalive === undefined
- || !!config.session_keepalive
-}
-
-/**
- * get interval in seconds
- *
- * @return {number}
- */
-const getInterval = () => {
- let interval = NaN
- if (config.session_lifetime) {
- interval = Math.floor(config.session_lifetime / 2)
- }
-
- // minimum one minute, max 24 hours, default 15 minutes
- return Math.min(
- 24 * 3600,
- Math.max(
- 60,
- isNaN(interval) ? 900 : interval
- )
- )
-}
-
-const getToken = async () => {
- const url = generateUrl('/csrftoken')
-
- // Not using Axios here as Axios is not stubbable with the sinon fake server
- // see https://stackoverflow.com/questions/41516044/sinon-mocha-test-with-async-ajax-calls-didnt-return-promises
- // see js/tests/specs/coreSpec.js for the tests
- const resp = await $.get(url)
-
- return resp.token
-}
-
-const poll = async () => {
- try {
- const token = await getToken()
- setRequestToken(token)
- } catch (e) {
- console.error('session heartbeat failed', e)
- }
-}
-
-const startPolling = () => {
- const interval = setInterval(poll, getInterval() * 1000)
-
- console.info('session heartbeat polling started')
-
- return interval
-}
-
-const registerAutoLogout = () => {
- if (!config.auto_logout || !getCurrentUser()) {
- return
- }
-
- let lastActive = Date.now()
- window.addEventListener('mousemove', e => {
- lastActive = Date.now()
- localStorage.setItem('lastActive', lastActive)
- })
-
- window.addEventListener('touchstart', e => {
- lastActive = Date.now()
- localStorage.setItem('lastActive', lastActive)
- })
-
- window.addEventListener('storage', e => {
- if (e.key !== 'lastActive') {
- return
- }
- lastActive = e.newValue
- })
-
- setInterval(function() {
- const timeout = Date.now() - config.session_lifetime * 1000
- if (lastActive < timeout) {
- console.info('Inactivity timout reached, logging out')
- const logoutUrl = generateUrl('/logout') + '?requesttoken=' + encodeURIComponent(getRequestToken())
- window.location = logoutUrl
- }
- }, 1000)
-}
-
-/**
- * Calls the server periodically to ensure that session and CSRF
- * token doesn't expire
- */
-export const initSessionHeartBeat = () => {
- loadConfig()
-
- registerAutoLogout()
-
- if (!keepSessionAlive()) {
- console.info('session heartbeat disabled')
- return
- }
- let interval = startPolling()
-
- window.addEventListener('online', async () => {
- console.info('browser is online again, resuming heartbeat')
- interval = startPolling()
- try {
- await poll()
- console.info('session token successfully updated after resuming network')
-
- // Let apps know we're online and requests will have the new token
- emit('networkOnline', {
- success: true,
- })
- } catch (e) {
- console.error('could not update session token after resuming network', e)
-
- // Let apps know we're online but requests might have an outdated token
- emit('networkOnline', {
- success: false,
- })
- }
- })
- window.addEventListener('offline', () => {
- console.info('browser is offline, stopping heartbeat')
-
- // Let apps know we're offline
- emit('networkOffline', {})
-
- clearInterval(interval)
- console.info('session heartbeat polling stopped')
- })
-}
diff --git a/core/src/session-heartbeat.ts b/core/src/session-heartbeat.ts
new file mode 100644
index 00000000000..42a9bfccef7
--- /dev/null
+++ b/core/src/session-heartbeat.ts
@@ -0,0 +1,158 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { emit } from '@nextcloud/event-bus'
+import { loadState } from '@nextcloud/initial-state'
+import { getCurrentUser } from '@nextcloud/auth'
+import { generateUrl } from '@nextcloud/router'
+import {
+ fetchRequestToken,
+ getRequestToken,
+} from './OC/requesttoken.ts'
+import logger from './logger.js'
+
+interface OcJsConfig {
+ auto_logout: boolean
+ session_keepalive: boolean
+ session_lifetime: number
+}
+
+// This is always set, exception would be e.g. error pages where this is undefined
+const {
+ auto_logout: autoLogout,
+ session_keepalive: keepSessionAlive,
+ session_lifetime: sessionLifetime,
+} = loadState<Partial<OcJsConfig>>('core', 'config', {})
+
+/**
+ * Calls the server periodically to ensure that session and CSRF
+ * token doesn't expire
+ */
+export function initSessionHeartBeat() {
+ registerAutoLogout()
+
+ if (!keepSessionAlive) {
+ logger.info('Session heartbeat disabled')
+ return
+ }
+
+ let interval = startPolling()
+ window.addEventListener('online', async () => {
+ logger.info('Browser is online again, resuming heartbeat')
+
+ interval = startPolling()
+ try {
+ await poll()
+ logger.info('Session token successfully updated after resuming network')
+
+ // Let apps know we're online and requests will have the new token
+ emit('networkOnline', {
+ success: true,
+ })
+ } catch (error) {
+ logger.error('could not update session token after resuming network', { error })
+
+ // Let apps know we're online but requests might have an outdated token
+ emit('networkOnline', {
+ success: false,
+ })
+ }
+ })
+
+ window.addEventListener('offline', () => {
+ logger.info('Browser is offline, stopping heartbeat')
+
+ // Let apps know we're offline
+ emit('networkOffline', {})
+
+ clearInterval(interval)
+ logger.info('Session heartbeat polling stopped')
+ })
+}
+
+/**
+ * Get interval in seconds
+ */
+function getInterval(): number {
+ const interval = sessionLifetime
+ ? Math.floor(sessionLifetime / 2)
+ : 900
+
+ // minimum one minute, max 24 hours, default 15 minutes
+ return Math.min(
+ 24 * 3600,
+ Math.max(
+ 60,
+ interval,
+ ),
+ )
+}
+
+/**
+ * Poll the CSRF token for changes.
+ * This will also extend the current session if needed.
+ */
+async function poll() {
+ try {
+ await fetchRequestToken()
+ } catch (error) {
+ logger.error('session heartbeat failed', { error })
+ }
+}
+
+/**
+ * Start an window interval with the polling as the callback.
+ *
+ * @return The interval id
+ */
+function startPolling(): number {
+ const interval = window.setInterval(poll, getInterval() * 1000)
+
+ logger.info('session heartbeat polling started')
+ return interval
+}
+
+/**
+ * If enabled this will register event listeners to track if a user is active.
+ * If not the user will be automatically logged out after the configured IDLE time.
+ */
+function registerAutoLogout() {
+ if (!autoLogout || !getCurrentUser()) {
+ return
+ }
+
+ let lastActive = Date.now()
+ window.addEventListener('mousemove', () => {
+ lastActive = Date.now()
+ localStorage.setItem('lastActive', JSON.stringify(lastActive))
+ })
+
+ window.addEventListener('touchstart', () => {
+ lastActive = Date.now()
+ localStorage.setItem('lastActive', JSON.stringify(lastActive))
+ })
+
+ window.addEventListener('storage', (event) => {
+ if (event.key !== 'lastActive') {
+ return
+ }
+ if (event.newValue === null) {
+ return
+ }
+ lastActive = JSON.parse(event.newValue)
+ })
+
+ let intervalId = 0
+ const logoutCheck = () => {
+ const timeout = Date.now() - (sessionLifetime ?? 86400) * 1000
+ if (lastActive < timeout) {
+ clearTimeout(intervalId)
+ logger.info('Inactivity timout reached, logging out')
+ const logoutUrl = generateUrl('/logout') + '?requesttoken=' + encodeURIComponent(getRequestToken())
+ window.location.href = logoutUrl
+ }
+ }
+ intervalId = window.setInterval(logoutCheck, 1000)
+}
diff --git a/core/src/store/unified-search-external-filters.js b/core/src/store/unified-search-external-filters.js
new file mode 100644
index 00000000000..55de34b8b2a
--- /dev/null
+++ b/core/src/store/unified-search-external-filters.js
@@ -0,0 +1,17 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { defineStore } from 'pinia'
+
+export const useSearchStore = defineStore('search', {
+ state: () => ({
+ externalFilters: [],
+ }),
+
+ actions: {
+ registerExternalFilter({ id, appId, searchFrom, label, callback, icon }) {
+ this.externalFilters.push({ id, appId, searchFrom, name: label, callback, icon, isPluginFilter: true })
+ },
+ },
+})
diff --git a/core/src/systemtags/merged-systemtags.js b/core/src/systemtags/merged-systemtags.js
index 87b0a7da378..e4ccb1d3802 100644
--- a/core/src/systemtags/merged-systemtags.js
+++ b/core/src/systemtags/merged-systemtags.js
@@ -1,23 +1,7 @@
/**
- * @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import './systemtags.js'
diff --git a/core/src/systemtags/systemtagmodel.js b/core/src/systemtags/systemtagmodel.js
index 261ba02c905..1d2cd3ae57d 100644
--- a/core/src/systemtags/systemtagmodel.js
+++ b/core/src/systemtags/systemtagmodel.js
@@ -1,26 +1,8 @@
/**
- * Copyright (c) 2015
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Michael Jobst <mjobst+github@tecratech.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ * @param {object} OC The OC namespace
*/
(function(OC) {
diff --git a/core/src/systemtags/systemtags.js b/core/src/systemtags/systemtags.js
index 90b415d1557..ceb4652fe1c 100644
--- a/core/src/systemtags/systemtags.js
+++ b/core/src/systemtags/systemtags.js
@@ -1,27 +1,7 @@
/**
- * Copyright (c) 2016
- *
- * @author Gary Kim <gary@garykim.dev>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
diff --git a/core/src/systemtags/systemtagscollection.js b/core/src/systemtags/systemtagscollection.js
index b123ef30fe4..960d26ed36e 100644
--- a/core/src/systemtags/systemtagscollection.js
+++ b/core/src/systemtags/systemtagscollection.js
@@ -1,24 +1,7 @@
/**
- * Copyright (c) 2015
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
diff --git a/core/src/systemtags/systemtagsinputfield.js b/core/src/systemtags/systemtagsinputfield.js
index 5f298577386..b31d24dd0b5 100644
--- a/core/src/systemtags/systemtagsinputfield.js
+++ b/core/src/systemtags/systemtagsinputfield.js
@@ -1,26 +1,7 @@
/**
- * Copyright (c) 2015
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
/* eslint-disable */
@@ -177,7 +158,7 @@ import templateSelection from './templates/selection.handlebars'
var $item = $(ev.target).closest('.systemtags-item')
var tagId = $item.attr('data-id')
this.collection.get(tagId).destroy()
- $(ev.target).tooltip('hide')
+ $(ev.target).tooltip('option', 'hide')
$item.closest('.select2-result').remove()
// TODO: spinner
return false
diff --git a/core/src/systemtags/systemtagsmappingcollection.js b/core/src/systemtags/systemtagsmappingcollection.js
index f7e9e9cfe40..78c23ff67f0 100644
--- a/core/src/systemtags/systemtagsmappingcollection.js
+++ b/core/src/systemtags/systemtagsmappingcollection.js
@@ -1,25 +1,7 @@
/**
- * Copyright (c) 2015
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Vincent Petry <vincent@nextcloud.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateRemoteUrl } from '@nextcloud/router'
diff --git a/core/src/tests/.eslintrc.js b/core/src/tests/.eslintrc.js
index b44ea2c697d..598fc5c28b4 100644
--- a/core/src/tests/.eslintrc.js
+++ b/core/src/tests/.eslintrc.js
@@ -1,25 +1,7 @@
/**
- * @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
module.exports = {
globals: {
jsdom: true,
diff --git a/core/src/tests/OC/requesttoken.spec.js b/core/src/tests/OC/requesttoken.spec.js
deleted file mode 100644
index 57a3d4de3c1..00000000000
--- a/core/src/tests/OC/requesttoken.spec.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author François Freitag <mail@franek.fr>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-
-import { manageToken, setToken } from '../../OC/requesttoken.js'
-
-describe('request token', () => {
-
- let emit
- let manager
- const token = 'abc123'
-
- beforeEach(() => {
- emit = jest.fn()
- const head = window.document.getElementsByTagName('head')[0]
- head.setAttribute('data-requesttoken', token)
-
- manager = manageToken(window.document, emit)
- })
-
- test('reads the token from the document', () => {
- expect(manager.getToken()).toBe('abc123')
- })
-
- test('remembers the updated token', () => {
- manager.setToken('bca321')
-
- expect(manager.getToken()).toBe('bca321')
- })
-
- describe('@nextcloud/auth integration', () => {
- let listener
-
- beforeEach(() => {
- listener = jest.fn()
-
- subscribe('csrf-token-update', listener)
- })
-
- afterEach(() => {
- unsubscribe('csrf-token-update', listener)
- })
-
- test('fires off an event for @nextcloud/auth', () => {
- setToken('123')
-
- expect(listener).toHaveBeenCalledWith({ token: '123' })
- })
- })
-
-})
diff --git a/core/src/tests/OC/requesttoken.spec.ts b/core/src/tests/OC/requesttoken.spec.ts
new file mode 100644
index 00000000000..8f92dbed153
--- /dev/null
+++ b/core/src/tests/OC/requesttoken.spec.ts
@@ -0,0 +1,147 @@
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { setupServer } from 'msw/node'
+import { http, HttpResponse } from 'msw'
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
+import { fetchRequestToken, getRequestToken, setRequestToken } from '../../OC/requesttoken.ts'
+
+const eventbus = vi.hoisted(() => ({ emit: vi.fn() }))
+vi.mock('@nextcloud/event-bus', () => eventbus)
+
+const server = setupServer()
+
+describe('getRequestToken', () => {
+ it('can read the token from DOM', () => {
+ mockToken('tokenmock-123')
+ expect(getRequestToken()).toBe('tokenmock-123')
+ })
+
+ it('can handle missing token', () => {
+ mockToken(undefined)
+ expect(getRequestToken()).toBeUndefined()
+ })
+})
+
+describe('setRequestToken', () => {
+ beforeEach(() => {
+ vi.resetAllMocks()
+ })
+
+ it('does emit an event on change', () => {
+ setRequestToken('new-token')
+ expect(eventbus.emit).toBeCalledTimes(1)
+ expect(eventbus.emit).toBeCalledWith('csrf-token-update', { token: 'new-token' })
+ })
+
+ it('does set the new token to the DOM', () => {
+ setRequestToken('new-token')
+ expect(document.head.dataset.requesttoken).toBe('new-token')
+ })
+
+ it('does remember the new token', () => {
+ mockToken('old-token')
+ setRequestToken('new-token')
+ expect(getRequestToken()).toBe('new-token')
+ })
+
+ it('throws if the token is not a string', () => {
+ // @ts-expect-error mocking
+ expect(() => setRequestToken(123)).toThrowError('Invalid CSRF token given')
+ })
+
+ it('throws if the token is not valid', () => {
+ expect(() => setRequestToken('')).toThrowError('Invalid CSRF token given')
+ })
+
+ it('does not emit an event if the token is not valid', () => {
+ expect(() => setRequestToken('')).toThrowError('Invalid CSRF token given')
+ expect(eventbus.emit).not.toBeCalled()
+ })
+})
+
+describe('fetchRequestToken', () => {
+ const successfullCsrf = http.get('/index.php/csrftoken', () => {
+ return HttpResponse.json({ token: 'new-token' })
+ })
+ const forbiddenCsrf = http.get('/index.php/csrftoken', () => {
+ return HttpResponse.json([], { status: 403 })
+ })
+ const serverErrorCsrf = http.get('/index.php/csrftoken', () => {
+ return HttpResponse.json([], { status: 500 })
+ })
+ const networkErrorCsrf = http.get('/index.php/csrftoken', () => {
+ return new HttpResponse(null, { type: 'error' })
+ })
+
+ beforeAll(() => {
+ server.listen()
+ })
+
+ beforeEach(() => {
+ vi.resetAllMocks()
+ })
+
+ it('correctly parses response', async () => {
+ server.use(successfullCsrf)
+
+ mockToken('oldToken')
+ const token = await fetchRequestToken()
+ expect(token).toBe('new-token')
+ })
+
+ it('sets the token', async () => {
+ server.use(successfullCsrf)
+
+ mockToken('oldToken')
+ await fetchRequestToken()
+ expect(getRequestToken()).toBe('new-token')
+ })
+
+ it('does emit an event', async () => {
+ server.use(successfullCsrf)
+
+ await fetchRequestToken()
+ expect(eventbus.emit).toHaveBeenCalledOnce()
+ expect(eventbus.emit).toBeCalledWith('csrf-token-update', { token: 'new-token' })
+ })
+
+ it('handles 403 error due to invalid cookies', async () => {
+ server.use(forbiddenCsrf)
+
+ mockToken('oldToken')
+ await expect(() => fetchRequestToken()).rejects.toThrowError('Could not fetch CSRF token from API')
+ expect(getRequestToken()).toBe('oldToken')
+ })
+
+ it('handles server error', async () => {
+ server.use(serverErrorCsrf)
+
+ mockToken('oldToken')
+ await expect(() => fetchRequestToken()).rejects.toThrowError('Could not fetch CSRF token from API')
+ expect(getRequestToken()).toBe('oldToken')
+ })
+
+ it('handles network error', async () => {
+ server.use(networkErrorCsrf)
+
+ mockToken('oldToken')
+ await expect(() => fetchRequestToken()).rejects.toThrow()
+ expect(getRequestToken()).toBe('oldToken')
+ })
+})
+
+/**
+ * Mock the request token directly so we can test reading it.
+ *
+ * @param token - The CSRF token to mock
+ */
+function mockToken(token?: string) {
+ if (token === undefined) {
+ delete document.head.dataset.requesttoken
+ } else {
+ document.head.dataset.requesttoken = token
+ }
+}
diff --git a/core/src/tests/OC/session-heartbeat.spec.ts b/core/src/tests/OC/session-heartbeat.spec.ts
new file mode 100644
index 00000000000..61b82d92887
--- /dev/null
+++ b/core/src/tests/OC/session-heartbeat.spec.ts
@@ -0,0 +1,123 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const requestToken = vi.hoisted(() => ({
+ fetchRequestToken: vi.fn<() => Promise<string>>(),
+ setRequestToken: vi.fn<(token: string) => void>(),
+}))
+vi.mock('../../OC/requesttoken.ts', () => requestToken)
+
+const initialState = vi.hoisted(() => ({ loadState: vi.fn() }))
+vi.mock('@nextcloud/initial-state', () => initialState)
+
+describe('Session heartbeat', () => {
+ beforeAll(() => {
+ vi.useFakeTimers()
+ })
+
+ beforeEach(() => {
+ vi.clearAllTimers()
+ vi.resetModules()
+ vi.resetAllMocks()
+ })
+
+ it('sends heartbeat half the session lifetime when heartbeat enabled', async () => {
+ initialState.loadState.mockImplementationOnce(() => ({
+ session_keepalive: true,
+ session_lifetime: 300,
+ }))
+
+ const { initSessionHeartBeat } = await import('../../session-heartbeat.ts')
+ initSessionHeartBeat()
+
+ // initial state loaded
+ expect(initialState.loadState).toBeCalledWith('core', 'config', {})
+
+ // less than half, still nothing
+ await vi.advanceTimersByTimeAsync(100 * 1000)
+ expect(requestToken.fetchRequestToken).not.toBeCalled()
+
+ // reach past half, one call
+ await vi.advanceTimersByTimeAsync(60 * 1000)
+ expect(requestToken.fetchRequestToken).toBeCalledTimes(1)
+
+ // almost there to the next, still one
+ await vi.advanceTimersByTimeAsync(135 * 1000)
+ expect(requestToken.fetchRequestToken).toBeCalledTimes(1)
+
+ // past it, second call
+ await vi.advanceTimersByTimeAsync(5 * 1000)
+ expect(requestToken.fetchRequestToken).toBeCalledTimes(2)
+ })
+
+ it('does not send heartbeat when heartbeat disabled', async () => {
+ initialState.loadState.mockImplementationOnce(() => ({
+ session_keepalive: false,
+ session_lifetime: 300,
+ }))
+
+ const { initSessionHeartBeat } = await import('../../session-heartbeat.ts')
+ initSessionHeartBeat()
+
+ // initial state loaded
+ expect(initialState.loadState).toBeCalledWith('core', 'config', {})
+
+ // less than half, still nothing
+ await vi.advanceTimersByTimeAsync(100 * 1000)
+ expect(requestToken.fetchRequestToken).not.toBeCalled()
+
+ // more than one, still nothing
+ await vi.advanceTimersByTimeAsync(300 * 1000)
+ expect(requestToken.fetchRequestToken).not.toBeCalled()
+ })
+
+ it('limit heartbeat to at least one minute', async () => {
+ initialState.loadState.mockImplementationOnce(() => ({
+ session_keepalive: true,
+ session_lifetime: 55,
+ }))
+
+ const { initSessionHeartBeat } = await import('../../session-heartbeat.ts')
+ initSessionHeartBeat()
+
+ // initial state loaded
+ expect(initialState.loadState).toBeCalledWith('core', 'config', {})
+
+ // 30 / 55 seconds
+ await vi.advanceTimersByTimeAsync(30 * 1000)
+ expect(requestToken.fetchRequestToken).not.toBeCalled()
+
+ // 59 / 55 seconds should not be called except it does not limit
+ await vi.advanceTimersByTimeAsync(29 * 1000)
+ expect(requestToken.fetchRequestToken).not.toBeCalled()
+
+ // now one minute has passed
+ await vi.advanceTimersByTimeAsync(1000)
+ expect(requestToken.fetchRequestToken).toHaveBeenCalledOnce()
+ })
+
+ it('limit heartbeat to at least one minute', async () => {
+ initialState.loadState.mockImplementationOnce(() => ({
+ session_keepalive: true,
+ session_lifetime: 50 * 60 * 60,
+ }))
+
+ const { initSessionHeartBeat } = await import('../../session-heartbeat.ts')
+ initSessionHeartBeat()
+
+ // initial state loaded
+ expect(initialState.loadState).toBeCalledWith('core', 'config', {})
+
+ // 23 hours
+ await vi.advanceTimersByTimeAsync(23 * 60 * 60 * 1000)
+ expect(requestToken.fetchRequestToken).not.toBeCalled()
+
+ // one day - it should be called now
+ await vi.advanceTimersByTimeAsync(60 * 60 * 1000)
+ expect(requestToken.fetchRequestToken).toHaveBeenCalledOnce()
+ })
+})
diff --git a/core/src/tests/components/ContactsMenu/Contact.spec.js b/core/src/tests/components/ContactsMenu/Contact.spec.js
index bdf0238e5f9..e83f75bfd15 100644
--- a/core/src/tests/components/ContactsMenu/Contact.spec.js
+++ b/core/src/tests/components/ContactsMenu/Contact.spec.js
@@ -1,24 +1,9 @@
/**
- * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import { describe, expect, it } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Contact from '../../../components/ContactsMenu/Contact.vue'
@@ -33,19 +18,19 @@ describe('Contact', function() {
topAction: {
title: 'Mail',
icon: 'icon-mail',
- hyperlink: 'mailto:deboraoliver%40centrexin.com'
+ hyperlink: 'mailto:deboraoliver%40centrexin.com',
},
emailAddresses: [],
actions: [
{
title: 'Mail',
icon: 'icon-mail',
- hyperlink: 'mailto:mathisholland%40virxo.com'
+ hyperlink: 'mailto:mathisholland%40virxo.com',
},
{
title: 'Details',
icon: 'icon-info',
- hyperlink: 'https://localhost/index.php/apps/contacts'
+ hyperlink: 'https://localhost/index.php/apps/contacts',
},
],
lastMessage: '',
diff --git a/core/src/tests/views/ContactsMenu.spec.js b/core/src/tests/views/ContactsMenu.spec.js
index 6b438a4998e..084c3215e47 100644
--- a/core/src/tests/views/ContactsMenu.spec.js
+++ b/core/src/tests/views/ContactsMenu.spec.js
@@ -1,31 +1,20 @@
/**
- * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import axios from '@nextcloud/axios'
import { mount, shallowMount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
import ContactsMenu from '../../views/ContactsMenu.vue'
-jest.mock('@nextcloud/axios', () => ({
- post: jest.fn(),
+const axios = vi.hoisted(() => ({
+ post: vi.fn(),
+}))
+vi.mock('@nextcloud/axios', () => ({ default: axios }))
+
+vi.mock('@nextcloud/auth', () => ({
+ getCurrentUser: () => ({ uid: 'user', isAdmin: false, displayName: 'User' }),
}))
describe('ContactsMenu', function() {
@@ -55,7 +44,7 @@ describe('ContactsMenu', function() {
it('shows error view when contacts can not be loaded', async () => {
const view = mount(ContactsMenu)
axios.post.mockResolvedValue({})
- jest.spyOn(console, 'error').mockImplementation(() => {})
+ vi.spyOn(console, 'error').mockImplementation(() => {})
try {
await view.vm.handleOpen()
@@ -72,7 +61,7 @@ describe('ContactsMenu', function() {
it('shows text when there are no contacts', async () => {
const view = mount(ContactsMenu)
- axios.post.mockResolvedValue({
+ axios.post.mockResolvedValueOnce({
data: {
contacts: [],
contactsAppEnabled: false,
@@ -98,19 +87,19 @@ describe('ContactsMenu', function() {
topAction: {
title: 'Mail',
icon: 'icon-mail',
- hyperlink: 'mailto:deboraoliver%40centrexin.com'
+ hyperlink: 'mailto:deboraoliver%40centrexin.com',
},
actions: [
{
title: 'Mail',
icon: 'icon-mail',
- hyperlink: 'mailto:mathisholland%40virxo.com'
+ hyperlink: 'mailto:mathisholland%40virxo.com',
},
{
title: 'Details',
icon: 'icon-info',
- hyperlink: 'https://localhost/index.php/apps/contacts'
- }
+ hyperlink: 'https://localhost/index.php/apps/contacts',
+ },
],
lastMessage: '',
emailAddresses: [],
@@ -121,23 +110,23 @@ describe('ContactsMenu', function() {
topAction: {
title: 'Mail',
icon: 'icon-mail',
- hyperlink: 'mailto:ceciliasoto%40essensia.com'
+ hyperlink: 'mailto:ceciliasoto%40essensia.com',
},
actions: [
{
title: 'Mail',
icon: 'icon-mail',
- hyperlink: 'mailto:pearliesellers%40inventure.com'
+ hyperlink: 'mailto:pearliesellers%40inventure.com',
},
{
title: 'Details',
icon: 'icon-info',
- hyperlink: 'https://localhost/index.php/apps/contacts'
- }
+ hyperlink: 'https://localhost/index.php/apps/contacts',
+ },
],
lastMessage: 'cu',
emailAddresses: [],
- }
+ },
],
contactsAppEnabled: true,
},
diff --git a/core/src/twofactor-request-token.ts b/core/src/twofactor-request-token.ts
new file mode 100644
index 00000000000..868ceec01e9
--- /dev/null
+++ b/core/src/twofactor-request-token.ts
@@ -0,0 +1,25 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { onRequestTokenUpdate } from '@nextcloud/auth'
+import { getBaseUrl } from '@nextcloud/router'
+
+document.addEventListener('DOMContentLoaded', () => {
+ onRequestTokenUpdate((token) => {
+ const cancelLink = window.document.getElementById('cancel-login')
+ if (!cancelLink) {
+ return
+ }
+
+ const href = cancelLink.getAttribute('href')
+ if (!href) {
+ return
+ }
+
+ const parsedHref = new URL(href, getBaseUrl())
+ parsedHref.searchParams.set('requesttoken', token)
+ cancelLink.setAttribute('href', parsedHref.pathname + parsedHref.search)
+ })
+})
diff --git a/core/src/types/navigation.d.ts b/core/src/types/navigation.d.ts
new file mode 100644
index 00000000000..5698aab205e
--- /dev/null
+++ b/core/src/types/navigation.d.ts
@@ -0,0 +1,30 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/** See NavigationManager */
+export interface INavigationEntry {
+ /** Navigation id */
+ id: string
+ /** If this is the currently active app */
+ active: boolean
+ /** Order where this entry should be shown */
+ order: number
+ /** Target of the navigation entry */
+ href: string
+ /** The icon used for the naviation entry */
+ icon: string
+ /** Type of the navigation entry ('link' vs 'settings') */
+ type: 'link' | 'settings'
+ /** Localized name of the navigation entry */
+ name: string
+ /** Whether this is the default app */
+ default?: boolean
+ /** App that registered this navigation entry (not necessarly the same as the id) */
+ app?: string
+ /** If this app has unread notification */
+ unread: number
+ /** True when the link should be opened in a new tab */
+ target?: boolean
+}
diff --git a/core/src/unified-search.js b/core/src/unified-search.js
deleted file mode 100644
index f9bddff4c68..00000000000
--- a/core/src/unified-search.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com>
- *
- * @author Fon E. Noel NFEBE <fenn25.fn@gmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-import { getLoggerBuilder } from '@nextcloud/logger'
-import { getRequestToken } from '@nextcloud/auth'
-import { translate as t, translatePlural as n } from '@nextcloud/l10n'
-import Vue from 'vue'
-
-import UnifiedSearch from './views/UnifiedSearch.vue'
-
-// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(getRequestToken())
-
-const logger = getLoggerBuilder()
- .setApp('unified-search')
- .detectUser()
- .build()
-
-Vue.mixin({
- data() {
- return {
- logger,
- }
- },
- methods: {
- t,
- n,
- },
-})
-
-export default new Vue({
- el: '#unified-search',
- // eslint-disable-next-line vue/match-component-file-name
- name: 'UnifiedSearchRoot',
- render: h => h(UnifiedSearch),
-})
diff --git a/core/src/unified-search.ts b/core/src/unified-search.ts
new file mode 100644
index 00000000000..a13b1036da1
--- /dev/null
+++ b/core/src/unified-search.ts
@@ -0,0 +1,63 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getLoggerBuilder } from '@nextcloud/logger'
+import { getCSPNonce } from '@nextcloud/auth'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import { createPinia, PiniaVuePlugin } from 'pinia'
+import Vue from 'vue'
+
+import UnifiedSearch from './views/UnifiedSearch.vue'
+import { useSearchStore } from '../src/store/unified-search-external-filters.js'
+
+// eslint-disable-next-line camelcase
+__webpack_nonce__ = getCSPNonce()
+
+const logger = getLoggerBuilder()
+ .setApp('unified-search')
+ .detectUser()
+ .build()
+
+Vue.mixin({
+ data() {
+ return {
+ logger,
+ }
+ },
+ methods: {
+ t,
+ n,
+ },
+})
+
+// Define type structure for unified searc action
+interface UnifiedSearchAction {
+ id: string;
+ appId: string;
+ searchFrom: string;
+ label: string;
+ icon: string;
+ callback: () => void;
+}
+
+// Register the add/register filter action API globally
+window.OCA = window.OCA || {}
+window.OCA.UnifiedSearch = {
+ registerFilterAction: ({ id, appId, searchFrom, label, callback, icon }: UnifiedSearchAction) => {
+ const searchStore = useSearchStore()
+ searchStore.registerExternalFilter({ id, appId, searchFrom, label, callback, icon })
+ },
+}
+
+Vue.use(PiniaVuePlugin)
+const pinia = createPinia()
+
+export default new Vue({
+ el: '#unified-search',
+ pinia,
+ // eslint-disable-next-line vue/match-component-file-name
+ name: 'UnifiedSearchRoot',
+ render: h => h(UnifiedSearch),
+})
diff --git a/core/src/unsupported-browser-redirect.js b/core/src/unsupported-browser-redirect.js
index dd116b3a6ee..64620afa085 100644
--- a/core/src/unsupported-browser-redirect.js
+++ b/core/src/unsupported-browser-redirect.js
@@ -1,28 +1,12 @@
/**
- * @copyright 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { getRequestToken } from '@nextcloud/auth'
+import { getCSPNonce } from '@nextcloud/auth'
// eslint-disable-next-line camelcase
-__webpack_nonce__ = btoa(getRequestToken())
+__webpack_nonce__ = getCSPNonce()
if (!window.TESTING && !OC?.config?.no_unsupported_browser_warning) {
window.addEventListener('DOMContentLoaded', async function() {
diff --git a/core/src/unsupported-browser.js b/core/src/unsupported-browser.js
index a9b44b666fc..d54b1c8fb24 100644
--- a/core/src/unsupported-browser.js
+++ b/core/src/unsupported-browser.js
@@ -1,22 +1,6 @@
/**
- * @copyright 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateUrl } from '@nextcloud/router'
diff --git a/core/src/utils/ClipboardFallback.ts b/core/src/utils/ClipboardFallback.ts
new file mode 100644
index 00000000000..b374f9d0a44
--- /dev/null
+++ b/core/src/utils/ClipboardFallback.ts
@@ -0,0 +1,47 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { t } from '@nextcloud/l10n'
+
+/**
+ *
+ * @param text
+ */
+function unsecuredCopyToClipboard(text) {
+ const textArea = document.createElement('textarea')
+ const textAreaContent = document.createTextNode(text)
+ textArea.appendChild(textAreaContent)
+ document.body.appendChild(textArea)
+
+ textArea.focus({ preventScroll: true })
+ textArea.select()
+
+ try {
+ // This is a fallback for browsers that do not support the Clipboard API
+ // execCommand is deprecated, but it is the only way to copy text to the clipboard in some browsers
+ document.execCommand('copy')
+ } catch (err) {
+ window.prompt(t('core', 'Clipboard not available, please copy manually'), text)
+ console.error('[ERROR] core: files Unable to copy to clipboard', err)
+ }
+
+ document.body.removeChild(textArea)
+}
+
+/**
+ *
+ */
+function initFallbackClipboardAPI() {
+ if (!window.navigator?.clipboard?.writeText) {
+ console.info('[INFO] core: Clipboard API not available, using fallback')
+ Object.defineProperty(window.navigator, 'clipboard', {
+ value: {
+ writeText: unsecuredCopyToClipboard,
+ },
+ writable: false,
+ })
+ }
+}
+
+export { initFallbackClipboardAPI }
diff --git a/core/src/utils/RedirectUnsupportedBrowsers.js b/core/src/utils/RedirectUnsupportedBrowsers.js
index 16076a9afd3..2880d051ca2 100644
--- a/core/src/utils/RedirectUnsupportedBrowsers.js
+++ b/core/src/utils/RedirectUnsupportedBrowsers.js
@@ -1,22 +1,6 @@
/**
- * @copyright 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateUrl } from '@nextcloud/router'
diff --git a/core/src/utils/xhr-request.js b/core/src/utils/xhr-request.js
index ff8b7641b07..7f074a857a6 100644
--- a/core/src/utils/xhr-request.js
+++ b/core/src/utils/xhr-request.js
@@ -1,30 +1,16 @@
-/*
- * @copyright Copyright (c) 2023 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { getRootUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import { generateUrl, getRootUrl } from '@nextcloud/router'
+import logger from '../logger.js'
/**
*
* @param {string} url the URL to check
- * @returns {boolean}
+ * @return {boolean}
*/
const isRelativeUrl = (url) => {
return !url.startsWith('https://') && !url.startsWith('http://')
@@ -43,6 +29,60 @@ const isNextcloudUrl = (url) => {
}
/**
+ * Check if a user was logged in but is now logged-out.
+ * If this is the case then the user will be forwarded to the login page.
+ * @return {Promise<void>}
+ */
+async function checkLoginStatus() {
+ // skip if no logged in user
+ if (getCurrentUser() === null) {
+ return
+ }
+
+ // skip if already running
+ if (checkLoginStatus.running === true) {
+ return
+ }
+
+ // only run one request in parallel
+ checkLoginStatus.running = true
+
+ try {
+ // We need to check this as a 401 in the first place could also come from other reasons
+ const { status } = await window.fetch(generateUrl('/apps/files'))
+ if (status === 401) {
+ console.warn('User session was terminated, forwarding to login page.')
+ await wipeBrowserStorages()
+ window.location = generateUrl('/login?redirect_url={url}', {
+ url: window.location.pathname + window.location.search + window.location.hash,
+ })
+ }
+ } catch (error) {
+ console.warn('Could not check login-state')
+ } finally {
+ delete checkLoginStatus.running
+ }
+}
+
+/**
+ * Clear all Browser storages connected to current origin.
+ * @return {Promise<void>}
+ */
+export async function wipeBrowserStorages() {
+ try {
+ window.localStorage.clear()
+ window.sessionStorage.clear()
+ const indexedDBList = await window.indexedDB.databases()
+ for (const indexedDB of indexedDBList) {
+ await window.indexedDB.deleteDatabase(indexedDB.name)
+ }
+ logger.debug('Browser storages cleared')
+ } catch (error) {
+ logger.error('Could not clear browser storages', { error })
+ }
+}
+
+/**
* Intercept XMLHttpRequest and fetch API calls to add X-Requested-With header
*
* This is also done in @nextcloud/axios but not all requests pass through that
@@ -51,17 +91,24 @@ export const interceptRequests = () => {
XMLHttpRequest.prototype.open = (function(open) {
return function(method, url, async) {
open.apply(this, arguments)
- if (isNextcloudUrl(url) && !this.getResponseHeader('X-Requested-With')) {
- this.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
+ if (isNextcloudUrl(url)) {
+ if (!this.getResponseHeader('X-Requested-With')) {
+ this.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
+ }
+ this.addEventListener('loadend', function() {
+ if (this.status === 401) {
+ checkLoginStatus()
+ }
+ })
}
}
})(XMLHttpRequest.prototype.open)
window.fetch = (function(fetch) {
- return (resource, options) => {
+ return async (resource, options) => {
// fetch allows the `input` to be either a Request object or any stringifyable value
if (!isNextcloudUrl(resource.url ?? resource.toString())) {
- return fetch(resource, options)
+ return await fetch(resource, options)
}
if (!options) {
options = {}
@@ -76,7 +123,11 @@ export const interceptRequests = () => {
options.headers['X-Requested-With'] = 'XMLHttpRequest'
}
- return fetch(resource, options)
+ const response = await fetch(resource, options)
+ if (response.status === 401) {
+ checkLoginStatus()
+ }
+ return response
}
})(window.fetch)
}
diff --git a/core/src/views/AccountMenu.vue b/core/src/views/AccountMenu.vue
new file mode 100644
index 00000000000..5b7ead636bd
--- /dev/null
+++ b/core/src/views/AccountMenu.vue
@@ -0,0 +1,247 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcHeaderMenu id="user-menu"
+ class="account-menu"
+ is-nav
+ :aria-label="t('core', 'Settings menu')"
+ :description="avatarDescription">
+ <template #trigger>
+ <!-- The `key` is a hack as NcAvatar does not handle updating the preloaded status on show status change -->
+ <NcAvatar :key="String(showUserStatus)"
+ class="account-menu__avatar"
+ disable-menu
+ disable-tooltip
+ :show-user-status="showUserStatus"
+ :user="currentUserId"
+ :preloaded-user-status="userStatus" />
+ </template>
+ <ul class="account-menu__list">
+ <AccountMenuProfileEntry :id="profileEntry.id"
+ :name="profileEntry.name"
+ :href="profileEntry.href"
+ :active="profileEntry.active" />
+ <AccountMenuEntry v-for="entry in otherEntries"
+ :id="entry.id"
+ :key="entry.id"
+ :name="entry.name"
+ :href="entry.href"
+ :active="entry.active"
+ :icon="entry.icon" />
+ </ul>
+ </NcHeaderMenu>
+</template>
+
+<script lang="ts">
+import { getCurrentUser } from '@nextcloud/auth'
+import { emit, subscribe } from '@nextcloud/event-bus'
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import { generateOcsUrl } from '@nextcloud/router'
+import { getCapabilities } from '@nextcloud/capabilities'
+import { defineComponent } from 'vue'
+import { getAllStatusOptions } from '../../../apps/user_status/src/services/statusOptionsService.js'
+
+import axios from '@nextcloud/axios'
+import logger from '../logger.js'
+
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
+import AccountMenuProfileEntry from '../components/AccountMenu/AccountMenuProfileEntry.vue'
+import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue'
+
+interface ISettingsNavigationEntry {
+ /**
+ * id of the entry, used as HTML ID, for example, "settings"
+ */
+ id: string
+ /**
+ * Label of the entry, for example, "Personal Settings"
+ */
+ name: string
+ /**
+ * Icon of the entry, for example, "/apps/settings/img/personal.svg"
+ */
+ icon: string
+ /**
+ * Type of the entry
+ */
+ type: 'settings'|'link'|'guest'
+ /**
+ * Link of the entry, for example, "/settings/user"
+ */
+ href: string
+ /**
+ * Whether the entry is active
+ */
+ active: boolean
+ /**
+ * Order of the entry
+ */
+ order: number
+ /**
+ * Number of unread pf this items
+ */
+ unread: number
+ /**
+ * Classes for custom styling
+ */
+ classes: string
+}
+
+const USER_DEFINABLE_STATUSES = getAllStatusOptions()
+
+export default defineComponent({
+ name: 'AccountMenu',
+
+ components: {
+ AccountMenuEntry,
+ AccountMenuProfileEntry,
+ NcAvatar,
+ NcHeaderMenu,
+ },
+
+ setup() {
+ const settingsNavEntries = loadState<Record<string, ISettingsNavigationEntry>>('core', 'settingsNavEntries', {})
+ const { profile: profileEntry, ...otherEntries } = settingsNavEntries
+
+ return {
+ currentDisplayName: getCurrentUser()?.displayName ?? getCurrentUser()!.uid,
+ currentUserId: getCurrentUser()!.uid,
+
+ profileEntry,
+ otherEntries,
+
+ t,
+ }
+ },
+
+ data() {
+ return {
+ showUserStatus: false,
+ userStatus: {
+ status: null,
+ icon: null,
+ message: null,
+ },
+ }
+ },
+
+ computed: {
+ translatedUserStatus() {
+ return {
+ ...this.userStatus,
+ status: this.translateStatus(this.userStatus.status),
+ }
+ },
+
+ avatarDescription() {
+ const description = [
+ t('core', 'Avatar of {displayName}', { displayName: this.currentDisplayName }),
+ ...Object.values(this.translatedUserStatus).filter(Boolean),
+ ].join(' — ')
+ return description
+ },
+ },
+
+ async created() {
+ if (!getCapabilities()?.user_status?.enabled) {
+ return
+ }
+
+ const url = generateOcsUrl('/apps/user_status/api/v1/user_status')
+ try {
+ const response = await axios.get(url)
+ const { status, icon, message } = response.data.ocs.data
+ this.userStatus = { status, icon, message }
+ } catch (e) {
+ logger.error('Failed to load user status')
+ }
+ this.showUserStatus = true
+ },
+
+ mounted() {
+ subscribe('user_status:status.updated', this.handleUserStatusUpdated)
+ emit('core:user-menu:mounted')
+ },
+
+ methods: {
+ handleUserStatusUpdated(state) {
+ if (this.currentUserId === state.userId) {
+ this.userStatus = {
+ status: state.status,
+ icon: state.icon,
+ message: state.message,
+ }
+ }
+ },
+
+ translateStatus(status) {
+ const statusMap = Object.fromEntries(
+ USER_DEFINABLE_STATUSES.map(({ type, label }) => [type, label]),
+ )
+ if (statusMap[status]) {
+ return statusMap[status]
+ }
+ return status
+ },
+ },
+})
+</script>
+
+<style lang="scss" scoped>
+:deep(#header-menu-user-menu) {
+ padding: 0 !important;
+}
+
+.account-menu {
+ &__avatar {
+ --account-menu-outline: var(--border-width-input) solid color-mix(in srgb, var(--color-background-plain-text), transparent 75%);
+ outline: var(--account-menu-outline);
+ position: fixed;
+ // do not apply the alpha mask on the avatar div
+ mask: none !important;
+
+ &:hover {
+ --account-menu-outline: none;
+ // Add hover styles similar to the focus-visible style
+ border: var(--border-width-input-focused) solid var(--color-background-plain-text);
+ }
+ }
+
+ &__list {
+ display: inline-flex;
+ flex-direction: column;
+ padding-block: var(--default-grid-baseline) 0;
+ padding-inline: 0 var(--default-grid-baseline);
+
+ > :deep(li) {
+ box-sizing: border-box;
+ // basically "fit-content"
+ flex: 0 1;
+ }
+ }
+
+ // Ensure we do not waste space, as the header menu sets a default width of 350px
+ :deep(.header-menu__content) {
+ width: fit-content !important;
+ }
+
+ :deep(button) {
+ // Normally header menus are slightly translucent when not active
+ // this is generally ok but for the avatar this is weird so fix the opacity
+ opacity: 1 !important;
+
+ // The avatar is just the "icon" of the button
+ // So we add the focus-visible manually
+ &:focus-visible {
+ .account-menu__avatar {
+ --account-menu-outline: none;
+ border: var(--border-width-input-focused) solid var(--color-background-plain-text);
+ }
+ }
+ }
+}
+</style>
diff --git a/core/src/views/ContactsMenu.vue b/core/src/views/ContactsMenu.vue
index 443d0c4eb17..924ddcea56b 100644
--- a/core/src/views/ContactsMenu.vue
+++ b/core/src/views/ContactsMenu.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -26,25 +9,25 @@
:aria-label="t('core', 'Search contacts')"
@open="handleOpen">
<template #trigger>
- <Contacts :size="20" />
+ <NcIconSvgWrapper class="contactsmenu__trigger-icon" :path="mdiContacts" />
</template>
<div class="contactsmenu__menu">
<div class="contactsmenu__menu__input-wrapper">
- <NcTextField :value.sync="searchTerm"
- trailing-button-icon="close"
+ <NcTextField id="contactsmenu__menu__search"
ref="contactsMenuInput"
+ :value.sync="searchTerm"
+ trailing-button-icon="close"
:label="t('core', 'Search contacts')"
:trailing-button-label="t('core','Reset search')"
:show-trailing-button="searchTerm !== ''"
:placeholder="t('core', 'Search contacts …')"
- id="contactsmenu__menu__search"
class="contactsmenu__menu__search"
@input="onInputDebounced"
@trailing-button-click="onReset" />
</div>
<NcEmptyContent v-if="error" :name="t('core', 'Could not load your contacts')">
<template #icon>
- <Magnify />
+ <NcIconSvgWrapper :path="mdiMagnify" />
</template>
</NcEmptyContent>
<NcEmptyContent v-else-if="loadingText" :name="loadingText">
@@ -54,7 +37,7 @@
</NcEmptyContent>
<NcEmptyContent v-else-if="contacts.length === 0" :name="t('core', 'No contacts found')">
<template #icon>
- <Magnify />
+ <NcIconSvgWrapper :path="mdiMagnify" />
</template>
</NcEmptyContent>
<div v-else class="contactsmenu__menu__content">
@@ -79,39 +62,46 @@
</template>
<script>
+import { mdiContacts, mdiMagnify } from '@mdi/js'
+import { generateUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import { t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
-import Contacts from 'vue-material-design-icons/Contacts.vue'
import debounce from 'debounce'
-import { getCurrentUser } from '@nextcloud/auth'
-import { generateUrl } from '@nextcloud/router'
-import Magnify from 'vue-material-design-icons/Magnify.vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import { translate as t } from '@nextcloud/l10n'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import Contact from '../components/ContactsMenu/Contact.vue'
import logger from '../logger.js'
import Nextcloud from '../mixins/Nextcloud.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
export default {
name: 'ContactsMenu',
components: {
Contact,
- Contacts,
- Magnify,
NcButton,
NcEmptyContent,
NcHeaderMenu,
+ NcIconSvgWrapper,
NcLoadingIcon,
NcTextField,
},
mixins: [Nextcloud],
+ setup() {
+ return {
+ mdiContacts,
+ mdiMagnify,
+ }
+ },
+
data() {
const user = getCurrentUser()
return {
@@ -188,6 +178,10 @@ export default {
.contactsmenu {
overflow-y: hidden;
+ &__trigger-icon {
+ color: var(--color-background-plain-text) !important;
+ }
+
&__menu {
display: flex;
flex-direction: column;
@@ -198,7 +192,7 @@ export default {
label[for="contactsmenu__menu__search"] {
font-weight: bold;
font-size: 19px;
- margin-left: 13px;
+ margin-inline-start: 13px;
}
&__input-wrapper {
@@ -231,5 +225,9 @@ export default {
}
}
}
+
+ :deep(.empty-content) {
+ margin: 0 !important;
+ }
}
</style>
diff --git a/core/src/views/LegacyUnifiedSearch.vue b/core/src/views/LegacyUnifiedSearch.vue
index 04e4c77fe39..1277970ba0e 100644
--- a/core/src/views/LegacyUnifiedSearch.vue
+++ b/core/src/views/LegacyUnifiedSearch.vue
@@ -1,24 +1,7 @@
- <!--
- - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+<!--
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcHeaderMenu id="unified-search"
class="unified-search"
@@ -29,8 +12,7 @@
@close="onClose">
<!-- Header icon -->
<template #trigger>
- <Magnify class="unified-search__trigger"
- :size="22/* fit better next to other 20px icons */" />
+ <Magnify class="unified-search__trigger-icon" :size="20" />
</template>
<!-- Search form & filters wrapper -->
@@ -126,11 +108,11 @@ import debounce from 'debounce'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import { showError } from '@nextcloud/dialogs'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import Magnify from 'vue-material-design-icons/Magnify.vue'
@@ -288,7 +270,7 @@ export default {
return n('core',
'Please enter {minSearchLength} character or more to search',
- 'Please enter {minSearchLength} characters or more to search',
+ 'Please enter {minSearchLength} characters or more to search',
this.minSearchLength,
{ minSearchLength: this.minSearchLength })
},
@@ -723,6 +705,10 @@ $input-height: 34px;
$input-padding: 10px;
.unified-search {
+ &__trigger-icon {
+ color: var(--color-background-plain-text) !important;
+ }
+
&__input-wrapper {
position: sticky;
// above search results
@@ -738,17 +724,7 @@ $input-padding: 10px;
align-self: flex-start;
font-weight: bold;
font-size: 19px;
- margin-left: 13px;
- }
- }
-
- &__form-input {
- margin: 0 !important;
- &:focus,
- &:focus-visible,
- &:active {
- border-color: 2px solid var(--color-main-text) !important;
- box-shadow: 0 0 0 2px var(--color-main-background) !important;
+ margin-inline-start: 13px;
}
}
@@ -759,7 +735,8 @@ $input-padding: 10px;
}
&__filters {
- margin: $margin 0 $margin math.div($margin, 2);
+ margin-block: $margin;
+ margin-inline: math.div($margin, 2) 0;
padding-top: 5px;
ul {
display: inline-flex;
@@ -774,8 +751,7 @@ $input-padding: 10px;
// Loading spinner
&::after {
- right: $input-padding;
- left: auto;
+ inset-inline-start: auto $input-padding;
}
&-input,
@@ -788,6 +764,13 @@ $input-padding: 10px;
height: $input-height;
padding: $input-padding;
+ &:focus,
+ &:focus-visible,
+ &:active {
+ border-color: 2px solid var(--color-main-text) !important;
+ box-shadow: 0 0 0 2px var(--color-main-background) !important;
+ }
+
&,
&[placeholder],
&::placeholder {
@@ -805,10 +788,11 @@ $input-padding: 10px;
}
}
- &-reset, &-submit {
+ &-reset,
+ &-submit {
position: absolute;
top: 0;
- right: 4px;
+ inset-inline-end: 4px;
width: $input-height - $input-padding;
height: $input-height - $input-padding;
min-height: 30px;
@@ -816,7 +800,7 @@ $input-padding: 10px;
opacity: .5;
border: none;
background-color: transparent;
- margin-right: 0;
+ margin-inline-end: 0;
&:hover,
&:focus,
@@ -826,35 +810,36 @@ $input-padding: 10px;
}
&-submit {
- right: 28px;
+ inset-inline-end: 28px;
}
}
&__results {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
&-header {
display: block;
margin: $margin;
margin-bottom: $margin - 4px;
- margin-left: 13px;
+ margin-inline-start: 13px;
color: var(--color-primary-element);
font-size: 19px;
font-weight: bold;
}
- display: flex;
- flex-direction: column;
- gap: 4px;
}
- .unified-search__result-more::v-deep {
+ :deep(.unified-search__result-more) {
color: var(--color-text-maxcontrast);
}
.empty-content {
margin: 10vh 0;
- ::v-deep .empty-content__title {
+ :deep(.empty-content__title) {
font-weight: normal;
- font-size: var(--default-font-size);
+ font-size: var(--default-font-size);
text-align: center;
}
}
diff --git a/core/src/views/Login.vue b/core/src/views/Login.vue
index 643cf66c07b..a6fe8442779 100644
--- a/core/src/views/Login.vue
+++ b/core/src/views/Login.vue
@@ -1,30 +1,13 @@
<!--
- - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- - @author Richard Steinmetz <richard@steinmetz.cloud>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div class="guest-box login-box">
<template v-if="!hideLoginForm || directLogin">
<transition name="fade" mode="out-in">
- <div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''">
+ <div v-if="!passwordlessLogin && !resetPassword && resetPasswordTarget === ''" class="login-box__wrapper">
<LoginForm :username.sync="user"
:redirect-url="redirectUrl"
:direct-login="directLogin"
@@ -32,47 +15,37 @@
:errors="errors"
:throttle-delay="throttleDelay"
:auto-complete-allowed="autoCompleteAllowed"
+ :email-states="emailStates"
@submit="loading = true" />
- <a v-if="canResetPassword && resetPasswordLink !== ''"
+ <NcButton v-if="hasPasswordless"
+ type="tertiary"
+ wide
+ @click.prevent="passwordlessLogin = true">
+ {{ t('core', 'Log in with a device') }}
+ </NcButton>
+ <NcButton v-if="canResetPassword && resetPasswordLink !== ''"
id="lost-password"
- class="login-box__link"
- :href="resetPasswordLink">
+ :href="resetPasswordLink"
+ type="tertiary-no-background"
+ wide>
{{ t('core', 'Forgot password?') }}
- </a>
- <a v-else-if="canResetPassword && !resetPassword"
+ </NcButton>
+ <NcButton v-else-if="canResetPassword && !resetPassword"
id="lost-password"
- class="login-box__link"
- :href="resetPasswordLink"
+ type="tertiary"
+ wide
@click.prevent="resetPassword = true">
{{ t('core', 'Forgot password?') }}
- </a>
- <template v-if="hasPasswordless">
- <div v-if="countAlternativeLogins"
- class="alternative-logins">
- <a v-if="hasPasswordless"
- class="button"
- :class="{ 'single-alt-login-option': countAlternativeLogins }"
- href="#"
- @click.prevent="passwordlessLogin = true">
- {{ t('core', 'Log in with a device') }}
- </a>
- </div>
- <a v-else
- href="#"
- @click.prevent="passwordlessLogin = true">
- {{ t('core', 'Log in with a device') }}
- </a>
- </template>
+ </NcButton>
</div>
<div v-else-if="!loading && passwordlessLogin"
- key="reset"
- class="login-additional login-passwordless">
+ key="reset-pw-less"
+ class="login-additional login-box__wrapper">
<PasswordLessLoginForm :username.sync="user"
:redirect-url="redirectUrl"
:auto-complete-allowed="autoCompleteAllowed"
:is-https="isHttps"
:is-localhost="isLocalhost"
- :has-public-key-credential="hasPublicKeyCredential"
@submit="loading = true" />
<NcButton type="tertiary"
:aria-label="t('core', 'Back to login form')"
@@ -82,7 +55,7 @@
</NcButton>
</div>
<div v-else-if="!loading && canResetPassword"
- key="reset"
+ key="reset-can-reset"
class="login-additional">
<div class="lost-password-container">
<ResetPassword v-if="resetPassword"
@@ -100,13 +73,13 @@
</template>
<template v-else>
<transition name="fade" mode="out-in">
- <NcNoteCard type="warning" :title="t('core', 'Login form is disabled.')">
- {{ t('core', 'Please contact your administrator.') }}
+ <NcNoteCard type="info" :title="t('core', 'Login form is disabled.')">
+ {{ t('core', 'The Nextcloud login form is disabled. Use another login option if available or contact your administration.') }}
</NcNoteCard>
</transition>
</template>
- <div id="alternative-logins" class="alternative-logins">
+ <div id="alternative-logins" class="login-box__alternative-logins">
<NcButton v-for="(alternativeLogin, index) in alternativeLogins"
:key="index"
type="secondary"
@@ -122,24 +95,21 @@
<script>
import { loadState } from '@nextcloud/initial-state'
+import { generateUrl } from '@nextcloud/router'
+
import queryString from 'query-string'
import LoginForm from '../components/login/LoginForm.vue'
import PasswordLessLoginForm from '../components/login/PasswordLessLoginForm.vue'
import ResetPassword from '../components/login/ResetPassword.vue'
import UpdatePassword from '../components/login/UpdatePassword.vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import { wipeBrowserStorages } from '../utils/xhr-request.js'
const query = queryString.parse(location.search)
if (query.clear === '1') {
- try {
- window.localStorage.clear()
- window.sessionStorage.clear()
- console.debug('Browser storage cleared')
- } catch (e) {
- console.error('Could not clear browser storage', e)
- }
+ wipeBrowserStorages()
}
export default {
@@ -177,59 +147,43 @@ export default {
alternativeLogins: loadState('core', 'alternativeLogins', []),
isHttps: window.location.protocol === 'https:',
isLocalhost: window.location.hostname === 'localhost',
- hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined',
hideLoginForm: loadState('core', 'hideLoginForm', false),
+ emailStates: loadState('core', 'emailStates', []),
}
},
methods: {
passwordResetFinished() {
- this.resetPasswordTarget = ''
- this.directLogin = true
+ window.location.href = generateUrl('login')
},
},
}
</script>
-<style lang="scss">
-body {
- font-size: var(--default-font-size);
-}
-
+<style scoped lang="scss">
.login-box {
// Same size as dashboard panels
width: 320px;
box-sizing: border-box;
- &__link {
- display: block;
- padding: 1rem;
- font-size: var(--default-font-size);
- text-align: center;
- font-weight: normal !important;
+ &__wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: calc(2 * var(--default-grid-baseline));
+ }
+
+ &__alternative-logins {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
}
}
.fade-enter-active, .fade-leave-active {
transition: opacity .3s;
}
+
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
-
-.alternative-logins {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-
- .button-vue {
- box-sizing: border-box;
- }
-}
-
-.login-passwordless {
- .button-vue {
- margin-top: 0.5rem;
- }
-}
</style>
diff --git a/core/src/views/Profile.vue b/core/src/views/Profile.vue
deleted file mode 100644
index b7f593df8d2..00000000000
--- a/core/src/views/Profile.vue
+++ /dev/null
@@ -1,491 +0,0 @@
-<!--
- - @copyright Copyright (c) 2021 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
-
-<template>
- <NcContent app-name="profile">
- <NcAppContent>
- <div class="profile__header">
- <div class="profile__header__container">
- <div class="profile__header__container__placeholder" />
- <div class="profile__header__container__displayname">
- <h2>{{ displayname || userId }}</h2>
- <NcButton v-if="isCurrentUser"
- type="primary"
- :href="settingsUrl">
- <template #icon>
- <PencilIcon :size="20" />
- </template>
- {{ t('core', 'Edit Profile') }}
- </NcButton>
- </div>
- <NcButton v-if="status.icon || status.message"
- :disabled="!isCurrentUser"
- :type="isCurrentUser ? 'tertiary' : 'tertiary-no-background'"
- @click="openStatusModal">
- {{ status.icon }} {{ status.message }}
- </NcButton>
- </div>
- </div>
-
- <div class="profile__wrapper">
- <div class="profile__content">
- <div class="profile__sidebar">
- <NcAvatar class="avatar"
- :class="{ interactive: isCurrentUser }"
- :user="userId"
- :size="180"
- :show-user-status="true"
- :show-user-status-compact="false"
- :disable-menu="true"
- :disable-tooltip="true"
- :is-no-user="!isUserAvatarVisible"
- @click.native.prevent.stop="openStatusModal" />
-
- <div class="user-actions">
- <!-- When a tel: URL is opened with target="_blank", a blank new tab is opened which is inconsistent with the handling of other URLs so we set target="_self" for the phone action -->
- <NcButton v-if="primaryAction"
- type="primary"
- class="user-actions__primary"
- :href="primaryAction.target"
- :icon="primaryAction.icon"
- :target="primaryAction.id === 'phone' ? '_self' :'_blank'">
- <template #icon>
- <!-- Fix for https://github.com/nextcloud-libraries/nextcloud-vue/issues/2315 -->
- <img :src="primaryAction.icon" alt="" class="user-actions__primary__icon">
- </template>
- {{ primaryAction.title }}
- </NcButton>
- <NcActions class="user-actions__other" :inline="4">
- <NcActionLink v-for="action in otherActions"
- :key="action.id"
- :close-after-click="true"
- :href="action.target"
- :target="action.id === 'phone' ? '_self' :'_blank'">
- <template #icon>
- <!-- Fix for https://github.com/nextcloud-libraries/nextcloud-vue/issues/2315 -->
- <img :src="action.icon" alt="" class="user-actions__other__icon">
- </template>
- {{ action.title }}
- </NcActionLink>
- </NcActions>
- </div>
- </div>
-
- <div class="profile__blocks">
- <div v-if="organisation || role || address" class="profile__blocks-details">
- <div v-if="organisation || role" class="detail">
- <p>{{ organisation }} <span v-if="organisation && role">•</span> {{ role }}</p>
- </div>
- <div v-if="address" class="detail">
- <p>
- <MapMarkerIcon class="map-icon"
- :size="16" />
- {{ address }}
- </p>
- </div>
- </div>
- <template v-if="headline || biography || sections.length > 0">
- <h3 v-if="headline" class="profile__blocks-headline">
- {{ headline }}
- </h3>
- <p v-if="biography" class="profile__blocks-biography">
- {{ biography }}
- </p>
-
- <!-- additional entries, use it with cautious -->
- <div v-for="(section, index) in sections"
- :ref="'section-' + index"
- :key="index"
- class="profile__additionalContent">
- <component :is="section($refs['section-'+index], userId)" :user-id="userId" />
- </div>
- </template>
- <NcEmptyContent v-else
- class="profile__blocks-empty-info"
- :name="emptyProfileMessage"
- :description="t('core', 'The headline and about sections will show up here')">
- <template #icon>
- <AccountIcon :size="60" />
- </template>
- </NcEmptyContent>
- </div>
- </div>
- </div>
- </NcAppContent>
- </NcContent>
-</template>
-
-<script lang="ts">
-import { getCurrentUser } from '@nextcloud/auth'
-import { showError } from '@nextcloud/dialogs'
-import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import { loadState } from '@nextcloud/initial-state'
-import { translate as t } from '@nextcloud/l10n'
-import { generateUrl } from '@nextcloud/router'
-import { defineComponent } from 'vue'
-
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js'
-import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import AccountIcon from 'vue-material-design-icons/Account.vue'
-import MapMarkerIcon from 'vue-material-design-icons/MapMarker.vue'
-import PencilIcon from 'vue-material-design-icons/Pencil.vue'
-
-interface IProfileAction {
- target: string
- icon: string
- id: string
- title: string
-}
-
-interface IStatus {
- icon: string,
- message: string,
- userId: string,
-}
-
-export default defineComponent({
- name: 'Profile',
-
- components: {
- AccountIcon,
- MapMarkerIcon,
- NcActionLink,
- NcActions,
- NcAppContent,
- NcAvatar,
- NcButton,
- NcContent,
- NcEmptyContent,
- PencilIcon,
- },
-
- data() {
- const profileParameters = loadState('core', 'profileParameters', {
- userId: null as string|null,
- displayname: null as string|null,
- address: null as string|null,
- organisation: null as string|null,
- role: null as string|null,
- headline: null as string|null,
- biography: null as string|null,
- actions: [] as IProfileAction[],
- isUserAvatarVisible: false,
- })
-
- return {
- ...profileParameters,
- status: loadState<Partial<IStatus>>('core', 'status', {}),
- sections: window.OCA.Core.ProfileSections.getSections(),
- }
- },
-
- computed: {
- isCurrentUser() {
- return getCurrentUser()?.uid === this.userId
- },
-
- allActions() {
- return this.actions
- },
-
- primaryAction() {
- if (this.allActions.length) {
- return this.allActions[0]
- }
- return null
- },
-
- otherActions() {
- console.warn(this.allActions)
- if (this.allActions.length > 1) {
- return this.allActions.slice(1)
- }
- return []
- },
-
- settingsUrl() {
- return generateUrl('/settings/user')
- },
-
- emptyProfileMessage() {
- return this.isCurrentUser
- ? t('core', 'You have not added any info yet')
- : t('core', '{user} has not added any info yet', { user: (this.displayname || this.userId!) })
- },
- },
-
- mounted() {
- // Set the user's displayname or userId in the page title and preserve the default title of "Nextcloud" at the end
- document.title = `${this.displayname || this.userId} - ${document.title}`
- subscribe('user_status:status.updated', this.handleStatusUpdate)
- },
-
- beforeDestroy() {
- unsubscribe('user_status:status.updated', this.handleStatusUpdate)
- },
-
- methods: {
- t,
-
- handleStatusUpdate(status: IStatus) {
- if (this.isCurrentUser && status.userId === this.userId) {
- this.status = status
- }
- },
-
- openStatusModal() {
- const statusMenuItem = document.querySelector<HTMLButtonElement>('.user-status-menu-item')
- // Changing the user status is only enabled if you are the current user
- if (this.isCurrentUser) {
- if (statusMenuItem) {
- statusMenuItem.click()
- } else {
- showError(t('core', 'Error opening the user status modal, try hard refreshing the page'))
- }
- }
- },
- },
-})
-</script>
-
-<style lang="scss" scoped>
-$profile-max-width: 1024px;
-$content-max-width: 640px;
-
-:deep(#app-content-vue) {
- background-color: unset;
-}
-
-.profile {
- width: 100%;
- overflow-y: auto;
-
- &__header {
- position: sticky;
- height: 190px;
- top: -40px;
- background-color: var(--color-main-background-blur);
- backdrop-filter: var(--filter-background-blur);
- -webkit-backdrop-filter: var(--filter-background-blur);
-
- &__container {
- align-self: flex-end;
- width: 100%;
- max-width: $profile-max-width;
- margin: 0 auto;
- display: grid;
- grid-template-rows: max-content max-content;
- grid-template-columns: 240px 1fr;
- justify-content: center;
-
- &__placeholder {
- grid-row: 1 / 3;
- }
-
- &__displayname {
- padding-inline: 16px; // same as the status text button, see NcButton
- width: $content-max-width;
- height: 45px;
- margin-block: 100px 0;
- display: flex;
- align-items: center;
- gap: 18px;
-
- h2 {
- font-size: 30px;
- }
- }
- }
- }
-
- &__sidebar {
- position: sticky;
- top: 0;
- align-self: flex-start;
- padding-top: 20px;
- min-width: 220px;
- margin: -150px 20px 0 0;
-
- // Specificity hack is needed to override Avatar component styles
- :deep(.avatar.avatardiv) {
- text-align: center;
- margin: auto;
- display: block;
- padding: 8px;
-
- &.interactive {
- .avatardiv__user-status {
- // Show that the status is interactive
- cursor: pointer;
- }
- }
-
- .avatardiv__user-status {
- right: 14px;
- bottom: 14px;
- width: 34px;
- height: 34px;
- background-size: 28px;
- border: none;
- // Styles when custom status icon and status text are set
- background-color: var(--color-main-background);
- line-height: 34px;
- font-size: 20px;
- }
- }
- }
-
- &__wrapper {
- background-color: var(--color-main-background);
- min-height: 100%;
- }
-
- &__content {
- max-width: $profile-max-width;
- margin: 0 auto;
- display: flex;
- width: 100%;
- }
-
- &__blocks {
- margin: 18px 0 80px 0;
- display: grid;
- gap: 16px 0;
- width: $content-max-width;
-
- p, h3 {
- cursor: text;
- overflow-wrap: anywhere;
- }
-
- &-details {
- display: flex;
- flex-direction: column;
- gap: 2px 0;
-
- .detail {
- display: inline-block;
- color: var(--color-text-maxcontrast);
-
- p .map-icon {
- display: inline-block;
- vertical-align: middle;
- }
- }
- }
-
- &-headline {
- margin-inline: 0;
- margin-block: 10px 0;
- font-weight: bold;
- font-size: 20px;
- }
-
- &-biography {
- white-space: pre-line;
- }
- }
-}
-
-@media only screen and (max-width: 1024px) {
- .profile {
- &__header {
- height: 250px;
- position: unset;
-
- &__container {
- grid-template-columns: unset;
-
- &__displayname {
- margin: 80px 20px 0px!important;
- height: 1em;
- width: unset;
- display: unset;
- text-align: center;
- }
-
- &__edit-button {
- width: fit-content;
- display: block;
- margin: 60px auto;
- }
-
- &__status-text {
- margin: 4px auto;
- }
- }
- }
-
- &__content {
- display: block;
- }
-
- &__blocks {
- width: unset;
- max-width: 600px;
- margin: 0 auto;
- padding: 20px 50px 50px 50px;
- }
-
- &__sidebar {
- margin: unset;
- position: unset;
- }
- }
-}
-
-.user-actions {
- display: flex;
- flex-direction: column;
- gap: 8px 0;
- margin-top: 20px;
-
- &__primary {
- margin: 0 auto;
-
- &__icon {
- filter: var(--primary-invert-if-dark);
- }
- }
-
- &__other {
- display: flex;
- justify-content: center;
- gap: 0 4px;
-
- &__icon {
- height: 20px;
- width: 20px;
- object-fit: contain;
- filter: var(--background-invert-if-dark);
- align-self: center;
- margin: 12px; // so we get 44px x 44px
- }
- }
-}
-</style>
diff --git a/core/src/views/PublicPageMenu.vue b/core/src/views/PublicPageMenu.vue
new file mode 100644
index 00000000000..a05f3a6b889
--- /dev/null
+++ b/core/src/views/PublicPageMenu.vue
@@ -0,0 +1,131 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<template>
+ <div class="public-page-menu__wrapper">
+ <NcButton v-if="primaryAction"
+ id="public-page-menu--primary"
+ class="public-page-menu__primary"
+ :href="primaryAction.href"
+ type="primary"
+ @click="openDialogIfNeeded">
+ <template v-if="primaryAction.icon" #icon>
+ <div :class="['icon', primaryAction.icon, 'public-page-menu__primary-icon']" />
+ </template>
+ {{ primaryAction.label }}
+ </NcButton>
+
+ <NcHeaderMenu v-if="secondaryActions.length > 0"
+ id="public-page-menu"
+ :aria-label="t('core', 'More actions')"
+ :open.sync="showMenu">
+ <template #trigger>
+ <IconMore :size="20" />
+ </template>
+ <ul :aria-label="t('core', 'More actions')"
+ class="public-page-menu"
+ role="menu">
+ <component :is="getComponent(entry)"
+ v-for="entry, index in secondaryActions"
+ :key="index"
+ v-bind="entry"
+ @click="showMenu = false" />
+ </ul>
+ </NcHeaderMenu>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import { useIsSmallMobile } from '@nextcloud/vue/composables/useIsMobile'
+import { spawnDialog } from '@nextcloud/vue/functions/dialog'
+import { computed, ref, type Ref } from 'vue'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
+import IconMore from 'vue-material-design-icons/DotsHorizontal.vue'
+import PublicPageMenuEntry from '../components/PublicPageMenu/PublicPageMenuEntry.vue'
+import PublicPageMenuCustomEntry from '../components/PublicPageMenu/PublicPageMenuCustomEntry.vue'
+import PublicPageMenuExternalEntry from '../components/PublicPageMenu/PublicPageMenuExternalEntry.vue'
+import PublicPageMenuExternalDialog from '../components/PublicPageMenu/PublicPageMenuExternalDialog.vue'
+import PublicPageMenuLinkEntry from '../components/PublicPageMenu/PublicPageMenuLinkEntry.vue'
+
+interface IPublicPageMenu {
+ id: string
+ label: string
+ href: string
+ icon?: string
+ html?: string
+ details?: string
+}
+
+const menuEntries = loadState<Array<IPublicPageMenu>>('core', 'public-page-menu')
+
+/** used to conditionally close the menu when clicking entry */
+const showMenu = ref(false)
+
+const isMobile = useIsSmallMobile() as Readonly<Ref<boolean>>
+/** The primary menu action - only showed when not on mobile */
+const primaryAction = computed(() => isMobile.value ? undefined : menuEntries[0])
+/** All other secondary actions (including primary action on mobile) */
+const secondaryActions = computed(() => isMobile.value ? menuEntries : menuEntries.slice(1))
+
+/**
+ * Get the render component for an entry
+ * @param entry The entry to get the component for
+ */
+function getComponent(entry: IPublicPageMenu) {
+ if ('html' in entry) {
+ return PublicPageMenuCustomEntry
+ }
+ switch (entry.id) {
+ case 'save':
+ return PublicPageMenuExternalEntry
+ case 'directLink':
+ return PublicPageMenuLinkEntry
+ default:
+ return PublicPageMenuEntry
+ }
+}
+
+/**
+ * Open the "federated share" dialog if needed
+ */
+function openDialogIfNeeded() {
+ if (primaryAction.value?.id !== 'save') {
+ return
+ }
+ spawnDialog(PublicPageMenuExternalDialog, { label: primaryAction.value.label })
+}
+</script>
+
+<style scoped lang="scss">
+.public-page-menu {
+ box-sizing: border-box;
+
+ > :deep(*) {
+ box-sizing: border-box;
+ }
+
+ &__wrapper {
+ display: flex;
+ flex-direction: row;
+ gap: var(--default-grid-baseline);
+ }
+
+ &__primary {
+ height: var(--default-clickable-area);
+ margin-block: calc((var(--header-height) - var(--default-clickable-area)) / 2);
+
+ // Ensure the correct focus-visible color is used (as this is rendered directly on the background(-image))
+ &:focus-visible {
+ border-color: var(--color-background-plain-text) !important;
+ }
+ }
+
+ &__primary-icon {
+ filter: var(--primary-invert-if-bright);
+ }
+}
+</style>
diff --git a/core/src/views/PublicPageUserMenu.vue b/core/src/views/PublicPageUserMenu.vue
new file mode 100644
index 00000000000..7bd6521e7aa
--- /dev/null
+++ b/core/src/views/PublicPageUserMenu.vue
@@ -0,0 +1,138 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+<template>
+ <NcHeaderMenu id="public-page-user-menu"
+ class="public-page-user-menu"
+ is-nav
+ :aria-label="t('core', 'User menu')"
+ :description="avatarDescription">
+ <template #trigger>
+ <NcAvatar class="public-page-user-menu__avatar"
+ disable-menu
+ disable-tooltip
+ is-guest
+ :user="displayName || '?'" />
+ </template>
+
+ <!-- Privacy notice -->
+ <NcNoteCard class="public-page-user-menu__list-note"
+ :text="privacyNotice"
+ type="info" />
+
+ <ul class="public-page-user-menu__list">
+ <!-- Nickname dialog -->
+ <AccountMenuEntry id="set-nickname"
+ :name="!displayName ? t('core', 'Set public name') : t('core', 'Change public name')"
+ href="#"
+ @click.prevent.stop="setNickname">
+ <template #icon>
+ <IconAccount />
+ </template>
+ </AccountMenuEntry>
+ </ul>
+ </NcHeaderMenu>
+</template>
+
+<script lang="ts">
+import type { NextcloudUser } from '@nextcloud/auth'
+
+import '@nextcloud/dialogs/style.css'
+import { defineComponent } from 'vue'
+import { getGuestUser } from '@nextcloud/auth'
+import { showGuestUserPrompt } from '@nextcloud/dialogs'
+import { subscribe } from '@nextcloud/event-bus'
+import { t } from '@nextcloud/l10n'
+
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import IconAccount from 'vue-material-design-icons/AccountOutline.vue'
+
+import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue'
+
+export default defineComponent({
+ name: 'PublicPageUserMenu',
+ components: {
+ AccountMenuEntry,
+ IconAccount,
+ NcAvatar,
+ NcHeaderMenu,
+ NcNoteCard,
+ },
+
+ setup() {
+ return {
+ t,
+ }
+ },
+
+ data() {
+ return {
+ displayName: getGuestUser().displayName,
+ }
+ },
+
+ computed: {
+ avatarDescription(): string {
+ return t('core', 'User menu')
+ },
+
+ privacyNotice(): string {
+ return this.displayName
+ ? t('core', 'You will be identified as {user} by the account owner.', { user: this.displayName })
+ : t('core', 'You are currently not identified.')
+ },
+ },
+
+ mounted() {
+ subscribe('user:info:changed', (user: NextcloudUser) => {
+ this.displayName = user.displayName || ''
+ })
+ },
+
+ methods: {
+ setNickname() {
+ showGuestUserPrompt({
+ nickname: this.displayName,
+ cancellable: true,
+ })
+ },
+ },
+})
+</script>
+
+<style scoped lang="scss">
+.public-page-user-menu {
+ &, * {
+ box-sizing: border-box;
+ }
+
+ // Ensure we do not waste space, as the header menu sets a default width of 350px
+ :deep(.header-menu__content) {
+ width: fit-content !important;
+ }
+
+ &__list-note {
+ padding-block: 5px !important;
+ padding-inline: 5px !important;
+ max-width: 300px;
+ margin: 5px !important;
+ margin-bottom: 0 !important;
+ }
+
+ &__list {
+ display: inline-flex;
+ flex-direction: column;
+ padding-block: var(--default-grid-baseline) 0;
+ width: 100%;
+
+ > :deep(li) {
+ box-sizing: border-box;
+ // basically "fit-content"
+ flex: 0 1;
+ }
+ }
+}
+</style>
diff --git a/core/src/views/Setup.cy.ts b/core/src/views/Setup.cy.ts
new file mode 100644
index 00000000000..f252801c4d8
--- /dev/null
+++ b/core/src/views/Setup.cy.ts
@@ -0,0 +1,369 @@
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { SetupConfig, SetupLinks } from '../install'
+import SetupView from './Setup.vue'
+
+import '../../css/guest.css'
+
+const defaultConfig = Object.freeze({
+ adminlogin: '',
+ adminpass: '',
+ dbuser: '',
+ dbpass: '',
+ dbname: '',
+ dbtablespace: '',
+ dbhost: '',
+ dbtype: '',
+ databases: {
+ sqlite: 'SQLite',
+ mysql: 'MySQL/MariaDB',
+ pgsql: 'PostgreSQL',
+ },
+ directory: '',
+ hasAutoconfig: false,
+ htaccessWorking: true,
+ serverRoot: '/var/www/html',
+ errors: [],
+}) as SetupConfig
+
+const links = {
+ adminInstall: 'https://docs.nextcloud.com/server/32/go.php?to=admin-install',
+ adminSourceInstall: 'https://docs.nextcloud.com/server/32/go.php?to=admin-source_install',
+ adminDBConfiguration: 'https://docs.nextcloud.com/server/32/go.php?to=admin-db-configuration',
+} as SetupLinks
+
+describe('Default setup page', () => {
+ beforeEach(() => {
+ cy.mockInitialState('core', 'links', links)
+ })
+
+ afterEach(() => cy.unmockInitialState())
+
+ it('Renders default config', () => {
+ cy.mockInitialState('core', 'config', defaultConfig)
+ cy.mount(SetupView)
+
+ cy.get('[data-cy-setup-form]').scrollIntoView()
+ cy.get('[data-cy-setup-form]').should('be.visible')
+
+ // Single note is the footer help
+ cy.get('[data-cy-setup-form-note]')
+ .should('have.length', 1)
+ .should('be.visible')
+ cy.get('[data-cy-setup-form-note]').should('contain', 'See the documentation')
+
+ // DB radio selectors
+ cy.get('[data-cy-setup-form-field^="dbtype"]')
+ .should('exist')
+ .find('input')
+ .should('be.checked')
+
+ cy.get('[data-cy-setup-form-field="dbtype-mysql"]').should('exist')
+ cy.get('[data-cy-setup-form-field="dbtype-pgsql"]').should('exist')
+ cy.get('[data-cy-setup-form-field="dbtype-oci"]').should('not.exist')
+
+ // Sqlite warning
+ cy.get('[data-cy-setup-form-db-note="sqlite"]')
+ .should('be.visible')
+
+ // admin login, password, data directory and 3 DB radio selectors
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 6)
+ })
+
+ it('Renders single DB sqlite', () => {
+ const config = {
+ ...defaultConfig,
+ databases: {
+ sqlite: 'SQLite',
+ },
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // No DB radio selectors if only sqlite
+ cy.get('[data-cy-setup-form-field^="dbtype"]')
+ .should('not.exist')
+
+ // Two warnings: sqlite and single db support
+ cy.get('[data-cy-setup-form-db-note="sqlite"]')
+ .should('be.visible')
+ cy.get('[data-cy-setup-form-db-note="single-db"]')
+ .should('be.visible')
+
+ // Admin login, password and data directory
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 3)
+ })
+
+ it('Renders single DB mysql', () => {
+ const config = {
+ ...defaultConfig,
+ databases: {
+ mysql: 'MySQL/MariaDB',
+ },
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // No DB radio selectors if only mysql
+ cy.get('[data-cy-setup-form-field^="dbtype"]')
+ .should('not.exist')
+
+ // Single db support warning
+ cy.get('[data-cy-setup-form-db-note="single-db"]')
+ .should('be.visible')
+ .invoke('html')
+ .should('contains', links.adminSourceInstall)
+
+ // No SQLite warning
+ cy.get('[data-cy-setup-form-db-note="sqlite"]')
+ .should('not.exist')
+
+ // Admin login, password, data directory, db user,
+ // db password, db name and db host
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 7)
+ })
+
+ it('Changes fields from sqlite to mysql then oci', () => {
+ const config = {
+ ...defaultConfig,
+ databases: {
+ sqlite: 'SQLite',
+ mysql: 'MySQL/MariaDB',
+ pgsql: 'PostgreSQL',
+ oci: 'Oracle',
+ },
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // SQLite selected
+ cy.get('[data-cy-setup-form-field="dbtype-sqlite"]')
+ .should('be.visible')
+ .find('input')
+ .should('be.checked')
+
+ // Admin login, password, data directory and 4 DB radio selectors
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 7)
+
+ // Change to MySQL
+ cy.get('[data-cy-setup-form-field="dbtype-mysql"]').click()
+ cy.get('[data-cy-setup-form-field="dbtype-mysql"] input').should('be.checked')
+
+ // Admin login, password, data directory, db user, db password,
+ // db name, db host and 4 DB radio selectors
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 11)
+
+ // Change to Oracle
+ cy.get('[data-cy-setup-form-field="dbtype-oci"]').click()
+ cy.get('[data-cy-setup-form-field="dbtype-oci"] input').should('be.checked')
+
+ // Admin login, password, data directory, db user, db password,
+ // db name, db table space, db host and 4 DB radio selectors
+ cy.get('[data-cy-setup-form-field]')
+ .should('be.visible')
+ .should('have.length', 12)
+ cy.get('[data-cy-setup-form-field="dbtablespace"]')
+ .should('be.visible')
+ })
+})
+
+describe('Setup page with errors and warning', () => {
+ beforeEach(() => {
+ cy.mockInitialState('core', 'links', links)
+ })
+
+ afterEach(() => cy.unmockInitialState())
+
+ it('Renders error from backend', () => {
+ const config = {
+ ...defaultConfig,
+ errors: [
+ {
+ error: 'Error message',
+ hint: 'Error hint',
+ },
+ ],
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // Error message and hint
+ cy.get('[data-cy-setup-form-note="error"]')
+ .should('be.visible')
+ .should('have.length', 1)
+ .should('contain', 'Error message')
+ .should('contain', 'Error hint')
+ })
+
+ it('Renders errors from backend', () => {
+ const config = {
+ ...defaultConfig,
+ errors: [
+ 'Error message 1',
+ {
+ error: 'Error message',
+ hint: 'Error hint',
+ },
+ ],
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // Error message and hint
+ cy.get('[data-cy-setup-form-note="error"]')
+ .should('be.visible')
+ .should('have.length', 2)
+ cy.get('[data-cy-setup-form-note="error"]').eq(0)
+ .should('contain', 'Error message 1')
+ cy.get('[data-cy-setup-form-note="error"]').eq(1)
+ .should('contain', 'Error message')
+ .should('contain', 'Error hint')
+ })
+
+ it('Renders all the submitted fields on error', () => {
+ const config = {
+ ...defaultConfig,
+ adminlogin: 'admin',
+ adminpass: 'password',
+ dbname: 'nextcloud',
+ dbtype: 'mysql',
+ dbuser: 'nextcloud',
+ dbpass: 'password',
+ dbhost: 'localhost',
+ directory: '/var/www/html/nextcloud',
+ } as SetupConfig
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ cy.get('input[data-cy-setup-form-field="adminlogin"]')
+ .should('have.value', 'admin')
+ cy.get('input[data-cy-setup-form-field="adminpass"]')
+ .should('have.value', 'password')
+ cy.get('[data-cy-setup-form-field="dbtype-mysql"] input')
+ .should('be.checked')
+ cy.get('input[data-cy-setup-form-field="dbname"]')
+ .should('have.value', 'nextcloud')
+ cy.get('input[data-cy-setup-form-field="dbuser"]')
+ .should('have.value', 'nextcloud')
+ cy.get('input[data-cy-setup-form-field="dbpass"]')
+ .should('have.value', 'password')
+ cy.get('input[data-cy-setup-form-field="dbhost"]')
+ .should('have.value', 'localhost')
+ cy.get('input[data-cy-setup-form-field="directory"]')
+ .should('have.value', '/var/www/html/nextcloud')
+ })
+
+ it('Renders the htaccess warning', () => {
+ const config = {
+ ...defaultConfig,
+ htaccessWorking: false,
+ }
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ cy.get('[data-cy-setup-form-note="htaccess"]')
+ .should('be.visible')
+ .should('contain', 'Security warning')
+ .invoke('html')
+ .should('contains', links.adminInstall)
+ })
+})
+
+describe('Setup page with autoconfig', () => {
+ beforeEach(() => {
+ cy.mockInitialState('core', 'links', links)
+ })
+
+ afterEach(() => cy.unmockInitialState())
+
+ it('Renders autoconfig', () => {
+ const config = {
+ ...defaultConfig,
+ hasAutoconfig: true,
+ dbname: 'nextcloud',
+ dbtype: 'mysql',
+ dbuser: 'nextcloud',
+ dbpass: 'password',
+ dbhost: 'localhost',
+ directory: '/var/www/html/nextcloud',
+ } as SetupConfig
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // Autoconfig info note
+ cy.get('[data-cy-setup-form-note="autoconfig"]')
+ .should('be.visible')
+ .should('contain', 'Autoconfig file detected')
+
+ // Database and storage section is hidden as already set in autoconfig
+ cy.get('[data-cy-setup-form-advanced-config]').should('be.visible')
+ .invoke('attr', 'open')
+ .should('equal', undefined)
+
+ // Oracle tablespace is hidden
+ cy.get('[data-cy-setup-form-field="dbtablespace"]')
+ .should('not.exist')
+ })
+})
+
+describe('Submit a full form sends the data', () => {
+ beforeEach(() => {
+ cy.mockInitialState('core', 'links', links)
+ })
+
+ afterEach(() => cy.unmockInitialState())
+
+ it('Submits a full form', () => {
+ const config = {
+ ...defaultConfig,
+ adminlogin: 'admin',
+ adminpass: 'password',
+ dbname: 'nextcloud',
+ dbtype: 'mysql',
+ dbuser: 'nextcloud',
+ dbpass: 'password',
+ dbhost: 'localhost',
+ dbtablespace: 'tablespace',
+ directory: '/var/www/html/nextcloud',
+ } as SetupConfig
+
+ cy.intercept('POST', '**', {
+ delay: 2000,
+ }).as('setup')
+
+ cy.mockInitialState('core', 'config', config)
+ cy.mount(SetupView)
+
+ // Not chaining breaks the test as the POST prevents the element from being retrieved twice
+ // eslint-disable-next-line cypress/unsafe-to-chain-command
+ cy.get('[data-cy-setup-form-submit]')
+ .click()
+ .invoke('attr', 'disabled')
+ .should('equal', 'disabled', { timeout: 500 })
+
+ cy.wait('@setup')
+ .its('request.body')
+ .should('deep.equal', new URLSearchParams({
+ adminlogin: 'admin',
+ adminpass: 'password',
+ directory: '/var/www/html/nextcloud',
+ dbtype: 'mysql',
+ dbuser: 'nextcloud',
+ dbpass: 'password',
+ dbname: 'nextcloud',
+ dbhost: 'localhost',
+ }).toString())
+ })
+})
diff --git a/core/src/views/Setup.vue b/core/src/views/Setup.vue
new file mode 100644
index 00000000000..50ec0da9035
--- /dev/null
+++ b/core/src/views/Setup.vue
@@ -0,0 +1,460 @@
+<!--
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <form ref="form"
+ class="setup-form"
+ :class="{ 'setup-form--loading': loading }"
+ action=""
+ data-cy-setup-form
+ method="POST"
+ @submit="onSubmit">
+ <!-- Autoconfig info -->
+ <NcNoteCard v-if="config.hasAutoconfig"
+ :heading="t('core', 'Autoconfig file detected')"
+ data-cy-setup-form-note="autoconfig"
+ type="success">
+ {{ t('core', 'The setup form below is pre-filled with the values from the config file.') }}
+ </NcNoteCard>
+
+ <!-- Htaccess warning -->
+ <NcNoteCard v-if="config.htaccessWorking === false"
+ :heading="t('core', 'Security warning')"
+ data-cy-setup-form-note="htaccess"
+ type="warning">
+ <p v-html="htaccessWarning" />
+ </NcNoteCard>
+
+ <!-- Various errors -->
+ <NcNoteCard v-for="(error, index) in errors"
+ :key="index"
+ :heading="error.heading"
+ data-cy-setup-form-note="error"
+ type="error">
+ {{ error.message }}
+ </NcNoteCard>
+
+ <!-- Admin creation -->
+ <fieldset class="setup-form__administration">
+ <legend>{{ t('core', 'Create administration account') }}</legend>
+
+ <!-- Username -->
+ <NcTextField v-model="config.adminlogin"
+ :label="t('core', 'Administration account name')"
+ data-cy-setup-form-field="adminlogin"
+ name="adminlogin"
+ required />
+
+ <!-- Password -->
+ <NcPasswordField v-model="config.adminpass"
+ :label="t('core', 'Administration account password')"
+ data-cy-setup-form-field="adminpass"
+ name="adminpass"
+ required />
+
+ <!-- Password entropy -->
+ <NcNoteCard v-show="config.adminpass !== ''" :type="passwordHelperType">
+ {{ passwordHelperText }}
+ </NcNoteCard>
+ </fieldset>
+
+ <!-- Autoconfig toggle -->
+ <details :open="!isValidAutoconfig" data-cy-setup-form-advanced-config>
+ <summary>{{ t('core', 'Storage & database') }}</summary>
+
+ <!-- Data folder -->
+ <fieldset class="setup-form__data-folder">
+ <NcTextField v-model="config.directory"
+ :label="t('core', 'Data folder')"
+ :placeholder="config.serverRoot + '/data'"
+ required
+ autocomplete="off"
+ autocapitalize="none"
+ data-cy-setup-form-field="directory"
+ name="directory"
+ spellcheck="false" />
+ </fieldset>
+
+ <!-- Database -->
+ <fieldset class="setup-form__database">
+ <legend>{{ t('core', 'Database configuration') }}</legend>
+
+ <!-- Database type select -->
+ <fieldset class="setup-form__database-type">
+ <p v-if="!firstAndOnlyDatabase" :class="`setup-form__database-type-select--${DBTypeGroupDirection}`" class="setup-form__database-type-select">
+ <NcCheckboxRadioSwitch v-for="(name, db) in config.databases"
+ :key="db"
+ v-model="config.dbtype"
+ :button-variant="true"
+ :data-cy-setup-form-field="`dbtype-${db}`"
+ :value="db"
+ :button-variant-grouped="DBTypeGroupDirection"
+ name="dbtype"
+ type="radio">
+ {{ name }}
+ </NcCheckboxRadioSwitch>
+ </p>
+
+ <NcNoteCard v-else data-cy-setup-form-db-note="single-db" type="warning">
+ {{ t('core', 'Only {firstAndOnlyDatabase} is available.', { firstAndOnlyDatabase }) }}<br>
+ {{ t('core', 'Install and activate additional PHP modules to choose other database types.') }}<br>
+ <a :href="links.adminSourceInstall" target="_blank" rel="noreferrer noopener">
+ {{ t('core', 'For more details check out the documentation.') }} ↗
+ </a>
+ </NcNoteCard>
+
+ <NcNoteCard v-if="config.dbtype === 'sqlite'"
+ :heading="t('core', 'Performance warning')"
+ data-cy-setup-form-db-note="sqlite"
+ type="warning">
+ {{ t('core', 'You chose SQLite as database.') }}<br>
+ {{ t('core', 'SQLite should only be used for minimal and development instances. For production we recommend a different database backend.') }}<br>
+ {{ t('core', 'If you use clients for file syncing, the use of SQLite is highly discouraged.') }}
+ </NcNoteCard>
+ </fieldset>
+
+ <!-- Database configuration -->
+ <fieldset v-if="config.dbtype !== 'sqlite'">
+ <NcTextField v-model="config.dbuser"
+ :label="t('core', 'Database user')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbuser"
+ name="dbuser"
+ spellcheck="false"
+ required />
+
+ <NcPasswordField v-model="config.dbpass"
+ :label="t('core', 'Database password')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbpass"
+ name="dbpass"
+ spellcheck="false"
+ required />
+
+ <NcTextField v-model="config.dbname"
+ :label="t('core', 'Database name')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbname"
+ name="dbname"
+ pattern="[0-9a-zA-Z\$_\-]+"
+ spellcheck="false"
+ required />
+
+ <NcTextField v-if="config.dbtype === 'oci'"
+ v-model="config.dbtablespace"
+ :label="t('core', 'Database tablespace')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbtablespace"
+ name="dbtablespace"
+ spellcheck="false" />
+
+ <NcTextField v-model="config.dbhost"
+ :helper-text="t('core', 'Please specify the port number along with the host name (e.g., localhost:5432).')"
+ :label="t('core', 'Database host')"
+ :placeholder="t('core', 'localhost')"
+ autocapitalize="none"
+ autocomplete="off"
+ data-cy-setup-form-field="dbhost"
+ name="dbhost"
+ spellcheck="false" />
+ </fieldset>
+ </fieldset>
+ </details>
+
+ <!-- Submit -->
+ <NcButton class="setup-form__button"
+ :class="{ 'setup-form__button--loading': loading }"
+ :disabled="loading"
+ :loading="loading"
+ :wide="true"
+ alignment="center-reverse"
+ data-cy-setup-form-submit
+ native-type="submit"
+ type="primary">
+ <template #icon>
+ <NcLoadingIcon v-if="loading" />
+ <IconArrowRight v-else />
+ </template>
+ {{ loading ? t('core', 'Installing …') : t('core', 'Install') }}
+ </NcButton>
+
+ <!-- Help note -->
+ <NcNoteCard data-cy-setup-form-note="help" type="info">
+ {{ t('core', 'Need help?') }}
+ <a target="_blank" rel="noreferrer noopener" :href="links.adminInstall">{{ t('core', 'See the documentation') }} ↗</a>
+ </NcNoteCard>
+ </form>
+</template>
+<script lang="ts">
+import type { DbType, SetupConfig, SetupLinks } from '../install'
+
+import { defineComponent } from 'vue'
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import DomPurify from 'dompurify'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+
+import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
+
+enum PasswordStrength {
+ VeryWeak,
+ Weak,
+ Moderate,
+ Strong,
+ VeryStrong,
+ ExtremelyStrong,
+}
+
+const checkPasswordEntropy = (password: string = ''): PasswordStrength => {
+ const uniqueCharacters = new Set(password)
+ const entropy = parseInt(Math.log2(Math.pow(parseInt(uniqueCharacters.size.toString()), password.length)).toFixed(2))
+ if (entropy < 16) {
+ return PasswordStrength.VeryWeak
+ } else if (entropy < 31) {
+ return PasswordStrength.Weak
+ } else if (entropy < 46) {
+ return PasswordStrength.Moderate
+ } else if (entropy < 61) {
+ return PasswordStrength.Strong
+ } else if (entropy < 76) {
+ return PasswordStrength.VeryStrong
+ }
+
+ return PasswordStrength.ExtremelyStrong
+}
+
+export default defineComponent({
+ name: 'Setup',
+
+ components: {
+ IconArrowRight,
+ NcButton,
+ NcCheckboxRadioSwitch,
+ NcLoadingIcon,
+ NcNoteCard,
+ NcPasswordField,
+ NcTextField,
+ },
+
+ setup() {
+ return {
+ t,
+ }
+ },
+
+ data() {
+ return {
+ config: {} as SetupConfig,
+ links: {} as SetupLinks,
+ isValidAutoconfig: false,
+ loading: false,
+ }
+ },
+
+ computed: {
+ passwordHelperText(): string {
+ if (this.config?.adminpass === '') {
+ return ''
+ }
+
+ const passwordStrength = checkPasswordEntropy(this.config?.adminpass)
+ switch (passwordStrength) {
+ case PasswordStrength.VeryWeak:
+ return t('core', 'Password is too weak')
+ case PasswordStrength.Weak:
+ return t('core', 'Password is weak')
+ case PasswordStrength.Moderate:
+ return t('core', 'Password is average')
+ case PasswordStrength.Strong:
+ return t('core', 'Password is strong')
+ case PasswordStrength.VeryStrong:
+ return t('core', 'Password is very strong')
+ case PasswordStrength.ExtremelyStrong:
+ return t('core', 'Password is extremely strong')
+ }
+
+ return t('core', 'Unknown password strength')
+ },
+ passwordHelperType() {
+ if (checkPasswordEntropy(this.config?.adminpass) < PasswordStrength.Moderate) {
+ return 'error'
+ }
+ if (checkPasswordEntropy(this.config?.adminpass) < PasswordStrength.Strong) {
+ return 'warning'
+ }
+ return 'success'
+ },
+
+ firstAndOnlyDatabase(): string|null {
+ const dbNames = Object.values(this.config?.databases || {})
+ if (dbNames.length === 1) {
+ return dbNames[0]
+ }
+
+ return null
+ },
+
+ DBTypeGroupDirection() {
+ const databases = Object.keys(this.config?.databases || {})
+ // If we have more than 3 databases, we want to display them vertically
+ if (databases.length > 3) {
+ return 'vertical'
+ }
+ return 'horizontal'
+ },
+
+ htaccessWarning(): string {
+ // We use v-html, let's make sure we're safe
+ const message = [
+ t('core', 'Your data directory and files are probably accessible from the internet because the <code>.htaccess</code> file does not work.'),
+ t('core', 'For information how to properly configure your server, please {linkStart}see the documentation{linkEnd}', {
+ linkStart: '<a href="' + this.links.adminInstall + '" target="_blank" rel="noreferrer noopener">',
+ linkEnd: '</a>',
+ }, { escape: false }),
+ ].join('<br>')
+ return DomPurify.sanitize(message)
+ },
+
+ errors() {
+ return (this.config?.errors || []).map(error => {
+ if (typeof error === 'string') {
+ return {
+ heading: '',
+ message: error,
+ }
+ }
+
+ // f no hint is set, we don't want to show a heading
+ if (error.hint === '') {
+ return {
+ heading: '',
+ message: error.error,
+ }
+ }
+
+ return {
+ heading: error.error,
+ message: error.hint,
+ }
+ })
+ },
+ },
+
+ beforeMount() {
+ // Needs to only read the state once we're mounted
+ // for Cypress to be properly initialized.
+ this.config = loadState<SetupConfig>('core', 'config')
+ this.links = loadState<SetupLinks>('core', 'links')
+
+ },
+
+ mounted() {
+ // Set the first database type as default if none is set
+ if (this.config.dbtype === '') {
+ this.config.dbtype = Object.keys(this.config.databases).at(0) as DbType
+ }
+
+ // Validate the legitimacy of the autoconfig
+ if (this.config.hasAutoconfig) {
+ const form = this.$refs.form as HTMLFormElement
+
+ // Check the form without the administration account fields
+ form.querySelectorAll('input[name="adminlogin"], input[name="adminpass"]').forEach(input => {
+ input.removeAttribute('required')
+ })
+
+ if (form.checkValidity() && this.config.errors.length === 0) {
+ this.isValidAutoconfig = true
+ } else {
+ this.isValidAutoconfig = false
+ }
+
+ // Restore the required attribute
+ // Check the form without the administration account fields
+ form.querySelectorAll('input[name="adminlogin"], input[name="adminpass"]').forEach(input => {
+ input.setAttribute('required', 'true')
+ })
+ }
+ },
+
+ methods: {
+ async onSubmit() {
+ this.loading = true
+ },
+ },
+})
+</script>
+<style lang="scss">
+form {
+ padding: calc(3 * var(--default-grid-baseline));
+ color: var(--color-main-text);
+ border-radius: var(--border-radius-container);
+ background-color: var(--color-main-background-blur);
+ box-shadow: 0 0 10px var(--color-box-shadow);
+ -webkit-backdrop-filter: var(--filter-background-blur);
+ backdrop-filter: var(--filter-background-blur);
+
+ max-width: 300px;
+ margin-bottom: 30px;
+
+ > fieldset:first-child,
+ > .notecard:first-child {
+ margin-top: 0;
+ }
+
+ > .notecard:last-child {
+ margin-bottom: 0;
+ }
+
+ fieldset,
+ details {
+ margin-block: 1rem;
+ }
+
+ .setup-form__button:not(.setup-form__button--loading) {
+ .material-design-icon {
+ transition: all linear var(--animation-quick);
+ }
+
+ &:hover .material-design-icon {
+ transform: translateX(0.2em);
+ }
+ }
+
+ // Db select required styling
+ .setup-form__database-type-select {
+ display: flex;
+ &--vertical {
+ flex-direction: column;
+ }
+ }
+
+}
+
+code {
+ background-color: var(--color-background-dark);
+ margin-top: 1rem;
+ padding: 0 0.3em;
+ border-radius: var(--border-radius);
+}
+
+// Various overrides
+.input-field {
+ margin-block-start: 1rem !important;
+}
+
+.notecard__heading {
+ font-size: inherit !important;
+}
+</style>
diff --git a/core/src/views/UnifiedSearch.vue b/core/src/views/UnifiedSearch.vue
index 419c0b47c41..103e47b0425 100644
--- a/core/src/views/UnifiedSearch.vue
+++ b/core/src/views/UnifiedSearch.vue
@@ -1,96 +1,182 @@
- <!--
- - @copyright Copyright (c) 2020 Fon E. Noel NFEBE <fenn25.fn@gmail.com>
- -
- - @author Fon E. Noel NFEBE <fenn25.fn@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+<!--
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <div class="header-menu">
- <NcButton class="unified-search__button" :aria-label="t('core', 'Unified search')" @click="toggleUnifiedSearch">
+ <div class="unified-search-menu">
+ <NcHeaderButton v-show="!showLocalSearch"
+ :aria-label="t('core', 'Unified search')"
+ @click="toggleUnifiedSearch">
<template #icon>
- <Magnify class="unified-search__trigger" :size="22" />
+ <NcIconSvgWrapper :path="mdiMagnify" />
</template>
- </NcButton>
- <UnifiedSearchModal :class="'unified-search-modal'" :is-visible="showUnifiedSearch" @update:isVisible="handleModalVisibilityChange" />
+ </NcHeaderButton>
+ <UnifiedSearchLocalSearchBar v-if="supportsLocalSearch"
+ :open.sync="showLocalSearch"
+ :query.sync="queryText"
+ @global-search="openModal" />
+ <UnifiedSearchModal :local-search="supportsLocalSearch"
+ :query.sync="queryText"
+ :open.sync="showUnifiedSearch" />
</div>
</template>
-<script>
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import Magnify from 'vue-material-design-icons/Magnify.vue'
-import UnifiedSearchModal from './UnifiedSearchModal.vue'
+<script lang="ts">
+import { mdiMagnify } from '@mdi/js'
+import { emit, subscribe } from '@nextcloud/event-bus'
+import { t } from '@nextcloud/l10n'
+import { useBrowserLocation } from '@vueuse/core'
+import debounce from 'debounce'
+import { defineComponent } from 'vue'
+import NcHeaderButton from '@nextcloud/vue/components/NcHeaderButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import UnifiedSearchModal from '../components/UnifiedSearch/UnifiedSearchModal.vue'
+import UnifiedSearchLocalSearchBar from '../components/UnifiedSearch/UnifiedSearchLocalSearchBar.vue'
+import logger from '../logger.js'
-export default {
+export default defineComponent({
name: 'UnifiedSearch',
+
components: {
- NcButton,
- Magnify,
+ NcHeaderButton,
+ NcIconSvgWrapper,
UnifiedSearchModal,
+ UnifiedSearchLocalSearchBar,
},
+
+ setup() {
+ const currentLocation = useBrowserLocation()
+
+ return {
+ currentLocation,
+
+ mdiMagnify,
+ t,
+ }
+ },
+
data() {
return {
+ /** The current search query */
+ queryText: '',
+ /** Open state of the modal */
showUnifiedSearch: false,
+ /** Open state of the local search bar */
+ showLocalSearch: false,
}
},
+
+ computed: {
+ /**
+ * Debounce emitting the search query by 250ms
+ */
+ debouncedQueryUpdate() {
+ return debounce(this.emitUpdatedQuery, 250)
+ },
+
+ /**
+ * Current page (app) supports local in-app search
+ */
+ supportsLocalSearch() {
+ // TODO: Make this an API
+ const providerPaths = ['/settings/users', '/apps/deck', '/settings/apps']
+ return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
+ },
+ },
+
+ watch: {
+ /**
+ * Emit the updated query as eventbus events
+ * (This is debounced)
+ */
+ queryText() {
+ this.debouncedQueryUpdate()
+ },
+ },
+
mounted() {
- console.debug('Unified search initialized!')
+ // register keyboard listener for search shortcut
+ if (window.OCP.Accessibility.disableKeyboardShortcuts() === false) {
+ window.addEventListener('keydown', this.onKeyDown)
+ }
+
+ // Allow external reset of the search / close local search
+ subscribe('nextcloud:unified-search:reset', () => {
+ this.showLocalSearch = false
+ this.queryText = ''
+ })
+
+ // Deprecated events to be removed
+ subscribe('nextcloud:unified-search:reset', () => {
+ emit('nextcloud:unified-search.reset', { query: '' })
+ })
+ subscribe('nextcloud:unified-search:search', ({ query }) => {
+ emit('nextcloud:unified-search.search', { query })
+ })
+
+ // all done
+ logger.debug('Unified search initialized!')
},
+
+ beforeDestroy() {
+ // keep in mind to remove the event listener
+ window.removeEventListener('keydown', this.onKeyDown)
+ },
+
methods: {
+ /**
+ * Handle the key down event to open search on `ctrl + F`
+ * @param event The keyboard event
+ */
+ onKeyDown(event: KeyboardEvent) {
+ if (event.ctrlKey && event.key === 'f') {
+ // only handle search if not already open - in this case the browser native search should be used
+ if (!this.showLocalSearch && !this.showUnifiedSearch) {
+ event.preventDefault()
+ }
+ this.toggleUnifiedSearch()
+ }
+ },
+
+ /**
+ * Toggle the local search if available - otherwise open the unified search modal
+ */
toggleUnifiedSearch() {
- this.showUnifiedSearch = !this.showUnifiedSearch
+ if (this.supportsLocalSearch) {
+ this.showLocalSearch = !this.showLocalSearch
+ } else {
+ this.showUnifiedSearch = !this.showUnifiedSearch
+ this.showLocalSearch = false
+ }
},
- handleModalVisibilityChange(newVisibilityVal) {
- this.showUnifiedSearch = newVisibilityVal
+
+ /**
+ * Open the unified search modal
+ */
+ openModal() {
+ this.showUnifiedSearch = true
+ this.showLocalSearch = false
+ },
+
+ /**
+ * Emit the updated search query as eventbus events
+ */
+ emitUpdatedQuery() {
+ if (this.queryText === '') {
+ emit('nextcloud:unified-search:reset')
+ } else {
+ emit('nextcloud:unified-search:search', { query: this.queryText })
+ }
},
},
-}
+})
</script>
<style lang="scss" scoped>
-.header-menu {
+// this is needed to allow us overriding component styles (focus-visible)
+.unified-search-menu {
display: flex;
align-items: center;
justify-content: center;
-
- .unified-search__button {
- display: flex;
- align-items: center;
- justify-content: center;
- width: var(--header-height);
- // height: var(--header-height);
- margin: 0;
- padding: 0;
- cursor: pointer;
- opacity: .85;
- background-color: transparent;
- border: none;
- filter: none !important;
- color: var(--color-primary-text) !important;
-
- &:hover {
- background-color: transparent !important;
- }
- }
-}
-
-.unified-search-modal {
- ::v-deep .modal-container {
- height: 80%;
- }
}
</style>
diff --git a/core/src/views/UnifiedSearchModal.vue b/core/src/views/UnifiedSearchModal.vue
deleted file mode 100644
index 004005b57d9..00000000000
--- a/core/src/views/UnifiedSearchModal.vue
+++ /dev/null
@@ -1,623 +0,0 @@
-<template>
- <NcModal id="unified-search"
- ref="unifiedSearchModal"
- :show.sync="internalIsVisible"
- :clear-view-delay="0"
- @close="closeModal">
- <CustomDateRangeModal :is-open="showDateRangeModal"
- class="unified-search__date-range"
- @set:custom-date-range="setCustomDateRange"
- @update:is-open="showDateRangeModal = $event" />
- <!-- Unified search form -->
- <div ref="unifiedSearch" class="unified-search-modal">
- <div class="unified-search-modal__header">
- <h2>{{ t('core', 'Unified search') }}</h2>
- <NcInputField ref="searchInput"
- :value.sync="searchQuery"
- type="text"
- :label="t('core', 'Search apps, files, tags, messages') + '...'"
- @update:value="debouncedFind" />
- <div class="unified-search-modal__filters">
- <NcActions :menu-name="t('core', 'Apps and Settings')" :open.sync="providerActionMenuIsOpen">
- <template #icon>
- <ListBox :size="20" />
- </template>
- <NcActionButton v-for="provider in providers"
- :key="provider.id"
- @click="addProviderFilter(provider)">
- <template #icon>
- <img :src="provider.icon" class="filter-button__icon" alt="">
- </template>
- {{ provider.name }}
- </NcActionButton>
- </NcActions>
- <NcActions :menu-name="t('core', 'Date')" :open.sync="dateActionMenuIsOpen">
- <template #icon>
- <CalendarRangeIcon :size="20" />
- </template>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('today')">
- {{ t('core', 'Today') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('7days')">
- {{ t('core', 'Last 7 days') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('30days')">
- {{ t('core', 'Last 30 days') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('thisyear')">
- {{ t('core', 'This year') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('lastyear')">
- {{ t('core', 'Last year') }}
- </NcActionButton>
- <NcActionButton :close-after-click="true" @click="applyQuickDateRange('custom')">
- {{ t('core', 'Custom date range') }}
- </NcActionButton>
- </NcActions>
- <SearchableList :label-text="t('core', 'Search people')"
- :search-list="userContacts"
- :empty-content-text="t('core', 'Not found')"
- @search-term-change="debouncedFilterContacts"
- @item-selected="applyPersonFilter">
- <template #trigger>
- <NcButton>
- <template #icon>
- <AccountGroup :size="20" />
- </template>
- {{ t('core', 'People') }}
- </NcButton>
- </template>
- </SearchableList>
- <NcButton v-if="supportFiltering" @click="closeModal">
- {{ t('core', 'Filter in current view') }}
- <template #icon>
- <FilterIcon :size="20" />
- </template>
- </NcButton>
- </div>
- <div class="unified-search-modal__filters-applied">
- <FilterChip v-for="filter in filters"
- :key="filter.id"
- :text="filter.name ?? filter.text"
- :pretext="''"
- @delete="removeFilter(filter)">
- <template #icon>
- <NcAvatar v-if="filter.type === 'person'"
- :user="filter.user"
- :size="24"
- :disable-menu="true"
- :show-user-status="false"
- :hide-favorite="false" />
- <CalendarRangeIcon v-else-if="filter.type === 'date'" />
- <img v-else :src="filter.icon" alt="">
- </template>
- </FilterChip>
- </div>
- </div>
- <div v-if="noContentInfo.show" class="unified-search-modal__no-content">
- <NcEmptyContent :name="noContentInfo.text">
- <template #icon>
- <component :is="noContentInfo.icon" />
- </template>
- </NcEmptyContent>
- </div>
- <div v-else class="unified-search-modal__results">
- <div v-for="providerResult in results" :key="providerResult.id" class="result">
- <div class="result-title">
- <span>{{ providerResult.provider }}</span>
- </div>
- <ul class="result-items">
- <SearchResult v-for="(result, index) in providerResult.results" :key="index" v-bind="result" />
- </ul>
- <div class="result-footer">
- <NcButton type="tertiary-no-background" @click="loadMoreResultsForProvider(providerResult.id)">
- {{ t('core', 'Load more results') }}
- <template #icon>
- <DotsHorizontalIcon :size="20" />
- </template>
- </NcButton>
- <NcButton v-if="providerResult.inAppSearch" alignment="end-reverse" type="tertiary-no-background">
- {{ t('core', 'Search in') }} {{ providerResult.provider }}
- <template #icon>
- <ArrowRight :size="20" />
- </template>
- </NcButton>
- </div>
- </div>
- </div>
- </div>
- </NcModal>
-</template>
-
-<script>
-import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
-import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
-import CalendarRangeIcon from 'vue-material-design-icons/CalendarRange.vue'
-import CustomDateRangeModal from '../components/UnifiedSearch/CustomDateRangeModal.vue'
-import DotsHorizontalIcon from 'vue-material-design-icons/DotsHorizontal.vue'
-import FilterIcon from 'vue-material-design-icons/Filter.vue'
-import FilterChip from '../components/UnifiedSearch/SearchFilterChip.vue'
-import ListBox from 'vue-material-design-icons/ListBox.vue'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
-import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
-import MagnifyIcon from 'vue-material-design-icons/Magnify.vue'
-import SearchableList from '../components/UnifiedSearch/SearchableList.vue'
-import SearchResult from '../components/UnifiedSearch/SearchResult.vue'
-
-import debounce from 'debounce'
-import { emit } from '@nextcloud/event-bus'
-import { useBrowserLocation } from '@vueuse/core'
-import { getProviders, search as unifiedSearch, getContacts } from '../services/UnifiedSearchService.js'
-
-export default {
- name: 'UnifiedSearchModal',
- components: {
- ArrowRight,
- AccountGroup,
- CalendarRangeIcon,
- CustomDateRangeModal,
- DotsHorizontalIcon,
- FilterIcon,
- FilterChip,
- ListBox,
- NcActions,
- NcActionButton,
- NcAvatar,
- NcButton,
- NcEmptyContent,
- NcModal,
- NcInputField,
- MagnifyIcon,
- SearchableList,
- SearchResult,
- },
- props: {
- isVisible: {
- type: Boolean,
- required: true,
- },
- },
- setup() {
- /**
- * Reactive version of window.location
- */
- const currentLocation = useBrowserLocation()
- return {
- currentLocation,
- }
- },
- data() {
- return {
- providers: [],
- providerActionMenuIsOpen: false,
- dateActionMenuIsOpen: false,
- providerResultLimit: 5,
- dateFilter: { id: 'date', type: 'date', text: '', startFrom: null, endAt: null },
- personFilter: { id: 'person', type: 'person', name: '' },
- dateFilterIsApplied: false,
- personFilterIsApplied: false,
- filteredProviders: [],
- searching: false,
- searchQuery: '',
- placessearchTerm: '',
- dateTimeFilter: null,
- filters: [],
- results: [],
- contacts: [],
- debouncedFind: debounce(this.find, 300),
- debouncedFilterContacts: debounce(this.filterContacts, 300),
- showDateRangeModal: false,
- internalIsVisible: false,
- }
- },
-
- computed: {
- userContacts() {
- return this.contacts
- },
- noContentInfo() {
- const isEmptySearch = this.searchQuery.length === 0
- const hasNoResults = this.searchQuery.length > 0 && this.results.length === 0
- return {
- show: isEmptySearch || hasNoResults,
- text: this.searching && hasNoResults ? t('core', 'Searching …') : (isEmptySearch ? t('core', 'Start typing to search') : t('core', 'No matching results')),
- icon: MagnifyIcon,
- }
- },
- supportFiltering() {
- /* Hard coded apps for the moment this would be improved in coming updates. */
- const providerPaths = ['/settings/users', '/apps/files', '/apps/deck']
- return providerPaths.some((path) => this.currentLocation.pathname?.includes?.(path))
- },
- },
- watch: {
- isVisible(value) {
- this.internalIsVisible = value
- },
- internalIsVisible(value) {
- this.$emit('update:isVisible', value)
- this.$nextTick(() => {
- if (value) {
- this.focusInput()
- }
- })
- },
-
- },
- mounted() {
- getProviders().then((providers) => {
- this.providers = providers
- console.debug('Search providers', this.providers)
- })
- getContacts({ searchTerm: '' }).then((contacts) => {
- this.contacts = this.mapContacts(contacts)
- console.debug('Contacts', this.contacts)
- })
- },
- methods: {
- find(query) {
- this.searching = true
- if (query.length === 0) {
- this.results = []
- this.searching = false
- return
- }
- // Event should probably be refactored at some point to used nextcloud:unified-search.search
- emit('nextcloud:unified-search.search', { query })
- const newResults = []
- const providersToSearch = this.filteredProviders.length > 0 ? this.filteredProviders : this.providers
- const searchProvider = (provider, filters) => {
- const params = {
- type: provider.id,
- query,
- cursor: null,
- }
-
- if (filters.dateFilterIsApplied) {
- if (provider.filters.since && provider.filters.until) {
- params.since = this.dateFilter.startFrom
- params.until = this.dateFilter.endAt
- } else {
- // Date filter is applied but provider does not support it, no need to search provider
- return
- }
- }
-
- if (filters.personFilterIsApplied) {
- if (provider.filters.person) {
- params.person = this.personFilter.user
- } else {
- // Person filter is applied but provider does not support it, no need to search provider
- return
- }
- }
-
- if (this.providerResultLimit > 5) {
- params.limit = this.providerResultLimit
- }
-
- const request = unifiedSearch(params).request
-
- request().then((response) => {
- newResults.push({
- id: provider.id,
- provider: provider.name,
- inAppSearch: provider.inAppSearch,
- results: response.data.ocs.data.entries,
- })
-
- console.debug('New results', newResults)
- console.debug('Unified search results:', this.results)
-
- this.updateResults(newResults)
- this.searching = false
- })
- }
- providersToSearch.forEach(provider => {
- const dateFilterIsApplied = this.dateFilterIsApplied
- const personFilterIsApplied = this.personFilterIsApplied
- searchProvider(provider, { dateFilterIsApplied, personFilterIsApplied })
- })
-
- },
- updateResults(newResults) {
- let updatedResults = [...this.results]
- // If filters are applied, remove any previous results for providers that are not in current filters
- if (this.filters.length > 0) {
- updatedResults = updatedResults.filter(result => {
- return this.filters.some(filter => filter.id === result.id)
- })
- }
- // Process the new results
- newResults.forEach(newResult => {
- const existingResultIndex = updatedResults.findIndex(result => result.id === newResult.id)
- if (existingResultIndex !== -1) {
- if (newResult.results.length === 0) {
- // If the new results data has no matches for and existing result, remove the existing result
- updatedResults.splice(existingResultIndex, 1)
- } else {
- // If input triggered a change in existing results, update existing result
- updatedResults.splice(existingResultIndex, 1, newResult)
- }
- } else if (newResult.results.length > 0) {
- // Push the new result to the array only if its results array is not empty
- updatedResults.push(newResult)
- }
- })
- const sortedResults = updatedResults.slice(0)
- // Order results according to provider preference
- sortedResults.sort((a, b) => {
- const aProvider = this.providers.find(provider => provider.id === a.id)
- const bProvider = this.providers.find(provider => provider.id === b.id)
- const aOrder = aProvider ? aProvider.order : 0
- const bOrder = bProvider ? bProvider.order : 0
- return aOrder - bOrder
- })
- this.results = sortedResults
- },
- mapContacts(contacts) {
- return contacts.map(contact => {
- return {
- // id: contact.id,
- // name: '',
- displayName: contact.fullName,
- isNoUser: false,
- subname: contact.emailAddresses[0] ? contact.emailAddresses[0] : '',
- icon: '',
- user: contact.id,
- }
- })
- },
- filterContacts(query) {
- getContacts({ searchTerm: query }).then((contacts) => {
- this.contacts = this.mapContacts(contacts)
- console.debug(`Contacts filtered by ${query}`, this.contacts)
- })
- },
- applyPersonFilter(person) {
- this.personFilterIsApplied = true
- const existingPersonFilter = this.filters.findIndex(filter => filter.id === person.id)
- if (existingPersonFilter === -1) {
- this.personFilter.id = person.id
- this.personFilter.user = person.user
- this.personFilter.name = person.displayName
- this.filters.push(this.personFilter)
- } else {
- this.filters[existingPersonFilter].id = person.id
- this.filters[existingPersonFilter].user = person.user
- this.filters[existingPersonFilter].name = person.displayName
- }
-
- this.debouncedFind(this.searchQuery)
- console.debug('Person filter applied', person)
- },
- loadMoreResultsForProvider(providerId) {
- this.providerResultLimit += 5
- this.filters = this.filters.filter(filter => filter.type !== 'provider')
- const provider = this.providers.find(provider => provider.id === providerId)
- this.addProviderFilter(provider, true)
- },
- addProviderFilter(providerFilter, loadMoreResultsForProvider = false) {
- if (!providerFilter.id) return
- this.providerResultLimit = loadMoreResultsForProvider ? this.providerResultLimit : 5
- this.providerActionMenuIsOpen = false
- const existingFilter = this.filteredProviders.find(existing => existing.id === providerFilter.id)
- if (!existingFilter) {
- this.filteredProviders.push({ id: providerFilter.id, name: providerFilter.name, icon: providerFilter.icon, type: 'provider', filters: providerFilter.filters })
- }
- this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
- console.debug('Search filters (newly added)', this.filters)
- this.debouncedFind(this.searchQuery)
- },
- removeFilter(filter) {
- if (filter.type === 'provider') {
- for (let i = 0; i < this.filteredProviders.length; i++) {
- if (this.filteredProviders[i].id === filter.id) {
- this.filteredProviders.splice(i, 1)
- break
- }
- }
- this.filters = this.syncProviderFilters(this.filters, this.filteredProviders)
- console.debug('Search filters (recently removed)', this.filters)
-
- } else {
- for (let i = 0; i < this.filters.length; i++) {
- // Remove date and person filter
- if (this.filters[i].id === 'date' || this.filters[i].id === filter.id) {
- this.dateFilterIsApplied = false
- this.filters.splice(i, 1)
- if (filter.type === 'person') {
- this.personFilterIsApplied = false
- }
- break
- }
- }
- }
- this.debouncedFind(this.searchQuery)
- },
- syncProviderFilters(firstArray, secondArray) {
- // Create a copy of the first array to avoid modifying it directly.
- const synchronizedArray = firstArray.slice()
- // Remove items from the synchronizedArray that are not in the secondArray.
- synchronizedArray.forEach((item, index) => {
- const itemId = item.id
- if (item.type === 'provider') {
- if (!secondArray.some(secondItem => secondItem.id === itemId)) {
- synchronizedArray.splice(index, 1)
- }
- }
- })
- // Add items to the synchronizedArray that are in the secondArray but not in the firstArray.
- secondArray.forEach(secondItem => {
- const itemId = secondItem.id
- if (secondItem.type === 'provider') {
- if (!synchronizedArray.some(item => item.id === itemId)) {
- synchronizedArray.push(secondItem)
- }
- }
- })
-
- return synchronizedArray
- },
- updateDateFilter() {
- const currFilterIndex = this.filters.findIndex(filter => filter.id === 'date')
- if (currFilterIndex !== -1) {
- this.filters[currFilterIndex] = this.dateFilter
- } else {
- this.filters.push(this.dateFilter)
- }
- this.dateFilterIsApplied = true
- this.debouncedFind(this.searchQuery)
- },
- applyQuickDateRange(range) {
- this.dateActionMenuIsOpen = false
- const today = new Date()
- let startDate
- let endDate
-
- switch (range) {
- case 'today':
- // For 'Today', both start and end are set to today
- startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0)
- endDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59, 999)
- this.dateFilter.text = t('core', 'Today')
- break
- case '7days':
- // For 'Last 7 days', start date is 7 days ago, end is today
- startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6, 0, 0, 0, 0)
- this.dateFilter.text = t('core', 'Last 7 days')
- break
- case '30days':
- // For 'Last 30 days', start date is 30 days ago, end is today
- startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29, 0, 0, 0, 0)
- this.dateFilter.text = t('core', 'Last 30 days')
- break
- case 'thisyear':
- // For 'This year', start date is the first day of the year, end is the last day of the year
- startDate = new Date(today.getFullYear(), 0, 1, 0, 0, 0, 0)
- endDate = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999)
- this.dateFilter.text = t('core', 'This year')
- break
- case 'lastyear':
- // For 'Last year', start date is the first day of the previous year, end is the last day of the previous year
- startDate = new Date(today.getFullYear() - 1, 0, 1, 0, 0, 0, 0)
- endDate = new Date(today.getFullYear() - 1, 11, 31, 23, 59, 59, 999)
- this.dateFilter.text = t('core', 'Last year')
- break
- case 'custom':
- this.showDateRangeModal = true
- return
- default:
- return
- }
- this.dateFilter.startFrom = startDate
- this.dateFilter.endAt = endDate
- this.updateDateFilter()
-
- },
- setCustomDateRange(event) {
- console.debug('Custom date range', event)
- this.dateFilter.startFrom = event.startFrom
- this.dateFilter.endAt = event.endAt
- this.dateFilter.text = t('core', `Between ${this.dateFilter.startFrom.toLocaleDateString()} and ${this.dateFilter.endAt.toLocaleDateString()}`)
- this.updateDateFilter()
- },
- focusInput() {
- this.$refs.searchInput.$el.children[0].children[0].focus()
- },
- closeModal() {
- this.internalIsVisible = false
- this.searchQuery = ''
- },
- },
-}
-</script>
-
-<style lang="scss" scoped>
-.unified-search-modal {
- box-sizing: border-box;
- height: 100%;
-
- display: flex;
- flex-direction: column;
- padding-block: 10px 0;
-
- // inline padding on direct children to make sure the scrollbar is on the modal container
- > * {
- padding-inline: 20px;
- }
-
- &__header {
- padding-block-end: 8px;
- }
-
- &__heading {
- font-size: 16px;
- font-weight: bolder;
- line-height: 2em;
- margin-bottom: 0;
- }
-
- &__filters {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- justify-content: start;
- padding-top: 4px;
- }
-
- &__filters-applied {
- padding-top: 4px;
- display: flex;
- flex-wrap: wrap;
- }
-
- &__no-content {
- display: flex;
- align-items: center;
- height: 100%;
- }
-
- &__results {
- overflow: hidden scroll;
- padding-block: 0 10px;
-
- .result {
- &-title {
- span {
- color: var(--color-primary-element);
- font-weight: bolder;
- font-size: 16px;
- }
- }
-
- &-footer {
- justify-content: space-between;
- align-items: center;
- display: flex;
- }
- }
-
- }
-}
-
-.filter-button__icon {
- height: 20px;
- width: 20px;
- object-fit: contain;
- filter: var(--background-invert-if-bright);
- padding: 11px; // align with text to fit at least 44px
-}
-
-// Ensure modal is accessible on small devices
-@media only screen and (max-height: 400px) {
- .unified-search-modal__results {
- overflow: unset;
- }
-}
-</style>
diff --git a/core/src/views/UnsupportedBrowser.vue b/core/src/views/UnsupportedBrowser.vue
index f9125fa9958..408cccf61e9 100644
--- a/core/src/views/UnsupportedBrowser.vue
+++ b/core/src/views/UnsupportedBrowser.vue
@@ -1,24 +1,7 @@
- <!--
- - @copyright 2022 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+<!--
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div class="content-unsupported-browser guest-box">
<NcEmptyContent>
@@ -48,10 +31,13 @@
</template>
<script>
-import { generateUrl } from '@nextcloud/router'
+// eslint-disable-next-line n/no-extraneous-import
+import { agents } from 'caniuse-lite/dist/unpacker/agents.js'
+import { generateUrl, getRootUrl } from '@nextcloud/router'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
+
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import Web from 'vue-material-design-icons/Web.vue'
import { browserStorageKey } from '../utils/RedirectUnsupportedBrowsers.js'
@@ -69,12 +55,6 @@ export default {
NcEmptyContent,
},
- data() {
- return {
- agents: {},
- }
- },
-
computed: {
isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
@@ -109,12 +89,12 @@ export default {
})
return Object.keys(list).map(id => {
- if (!this.agents[id]?.browser) {
+ if (!agents[id]?.browser) {
return null
}
const version = list[id]
- const name = this.agents[id]?.browser
+ const name = agents[id]?.browser
return this.t('core', '{name} version {version} and above', {
name, version,
})
@@ -122,13 +102,6 @@ export default {
},
},
- async beforeMount() {
- // Dynamic load big list of user agents
- // eslint-disable-next-line n/no-extraneous-import
- const { agents } = await import('caniuse-lite')
- this.agents = agents
- },
-
methods: {
t,
n,
@@ -140,12 +113,22 @@ export default {
// Redirect if there is the data
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has('redirect_url')) {
- const redirectPath = Buffer.from(urlParams.get('redirect_url'), 'base64').toString() || '/'
+ let redirectPath = Buffer.from(urlParams.get('redirect_url'), 'base64').toString() || '/'
+
+ // remove index.php and double slashes
+ redirectPath = redirectPath
+ .replace('index.php', '')
+ .replace(getRootUrl(), '')
+ .replace(/\/\//g, '/')
+
+ // if we have a valid redirect url, use it
if (redirectPath.startsWith('/')) {
window.location = generateUrl(redirectPath)
return
}
}
+
+ // else redirect to root
window.location = generateUrl('/')
},
@@ -181,7 +164,8 @@ $spacing: 30px;
.empty-content {
margin: 0;
- &::v-deep .empty-content__icon {
+
+ :deep(.empty-content__icon) {
opacity: 1;
}
}
@@ -195,7 +179,7 @@ $spacing: 30px;
margin-top: 2 * $spacing;
margin-bottom: $spacing;
li {
- text-align: left;
+ text-align: start;
}
}
}
diff --git a/core/src/views/UserMenu.vue b/core/src/views/UserMenu.vue
deleted file mode 100644
index 0c5084842a1..00000000000
--- a/core/src/views/UserMenu.vue
+++ /dev/null
@@ -1,281 +0,0 @@
-<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
--->
-
-<template>
- <NcHeaderMenu id="user-menu"
- class="user-menu"
- is-nav
- :aria-label="t('core', 'Settings menu')"
- :description="avatarDescription">
- <template #trigger>
- <NcAvatar v-if="!isLoadingUserStatus"
- class="user-menu__avatar"
- :disable-menu="true"
- :disable-tooltip="true"
- :user="userId"
- :preloaded-user-status="userStatus" />
- </template>
- <ul>
- <ProfileUserMenuEntry :id="profileEntry.id"
- :name="profileEntry.name"
- :href="profileEntry.href"
- :active="profileEntry.active" />
- <UserMenuEntry v-for="entry in otherEntries"
- :id="entry.id"
- :key="entry.id"
- :name="entry.name"
- :href="entry.href"
- :active="entry.active"
- :icon="entry.icon" />
- </ul>
- </NcHeaderMenu>
-</template>
-
-<script>
-import axios from '@nextcloud/axios'
-import { emit, subscribe } from '@nextcloud/event-bus'
-import { loadState } from '@nextcloud/initial-state'
-import { generateOcsUrl } from '@nextcloud/router'
-import { getCurrentUser } from '@nextcloud/auth'
-import { getCapabilities } from '@nextcloud/capabilities'
-
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js'
-
-import { getAllStatusOptions } from '../../../apps/user_status/src/services/statusOptionsService.js'
-import ProfileUserMenuEntry from '../components/UserMenu/ProfileUserMenuEntry.vue'
-import UserMenuEntry from '../components/UserMenu/UserMenuEntry.vue'
-
-import logger from '../logger.js'
-
-/**
- * @typedef SettingNavEntry
- * @property {string} id - id of the entry, used as HTML ID, for example, "settings"
- * @property {string} name - Label of the entry, for example, "Personal Settings"
- * @property {string} icon - Icon of the entry, for example, "/apps/settings/img/personal.svg"
- * @property {'settings'|'link'|'guest'} type - Type of the entry
- * @property {string} href - Link of the entry, for example, "/settings/user"
- * @property {boolean} active - Whether the entry is active
- * @property {number} order - Order of the entry
- * @property {number} unread - Number of unread pf this items
- * @property {string} classes - Classes for custom styling
- */
-
-/** @type {Record<string, SettingNavEntry>} */
-const settingsNavEntries = loadState('core', 'settingsNavEntries', [])
-const { profile: profileEntry, ...otherEntries } = settingsNavEntries
-
-const translateStatus = (status) => {
- const statusMap = Object.fromEntries(
- getAllStatusOptions()
- .map(({ type, label }) => [type, label]),
- )
- if (statusMap[status]) {
- return statusMap[status]
- }
- return status
-}
-
-export default {
- name: 'UserMenu',
-
- components: {
- NcAvatar,
- NcHeaderMenu,
- ProfileUserMenuEntry,
- UserMenuEntry,
- },
-
- data() {
- return {
- profileEntry,
- otherEntries,
- displayName: getCurrentUser()?.displayName,
- userId: getCurrentUser()?.uid,
- isLoadingUserStatus: true,
- userStatus: {
- status: null,
- icon: null,
- message: null,
- },
- }
- },
-
- computed: {
- translatedUserStatus() {
- return {
- ...this.userStatus,
- status: translateStatus(this.userStatus.status),
- }
- },
-
- avatarDescription() {
- const description = [
- t('core', 'Avatar of {displayName}', { displayName: this.displayName }),
- ...Object.values(this.translatedUserStatus).filter(Boolean),
- ].join(' — ')
- return description
- },
- },
-
- async created() {
- if (!getCapabilities()?.user_status?.enabled) {
- this.isLoadingUserStatus = false
- return
- }
-
- const url = generateOcsUrl('/apps/user_status/api/v1/user_status')
- try {
- const response = await axios.get(url)
- const { status, icon, message } = response.data.ocs.data
- this.userStatus = { status, icon, message }
- } catch (e) {
- logger.error('Failed to load user status')
- }
- this.isLoadingUserStatus = false
- },
-
- mounted() {
- subscribe('user_status:status.updated', this.handleUserStatusUpdated)
- emit('core:user-menu:mounted')
- },
-
- methods: {
- handleUserStatusUpdated(state) {
- if (this.userId === state.userId) {
- this.userStatus = {
- status: state.status,
- icon: state.icon,
- message: state.message,
- }
- }
- },
- },
-}
-</script>
-
-<style lang="scss" scoped>
-.user-menu {
- margin-right: 12px;
-
- &:deep {
- .header-menu {
- &__trigger {
- opacity: 1 !important;
- &:focus-visible {
- .user-menu__avatar {
- border: 2px solid var(--color-primary-element);
- }
- }
- }
-
- &__carret {
- display: none !important;
- }
-
- &__content {
- width: fit-content !important;
- }
- }
- }
-
- &__avatar {
- &:active,
- &:focus,
- &:hover {
- border: 2px solid var(--color-primary-element-text);
- }
- }
-
- ul {
- display: flex;
- flex-direction: column;
- gap: 2px;
-
- &:deep {
- li {
- a,
- button {
- border-radius: 6px;
- display: inline-flex;
- align-items: center;
- height: var(--header-menu-item-height);
- color: var(--color-main-text);
- padding: 10px 8px;
- box-sizing: border-box;
- white-space: nowrap;
- position: relative;
- width: 100%;
-
- &:hover {
- background-color: var(--color-background-hover);
- }
-
- &:focus-visible {
- background-color: var(--color-background-hover) !important;
- box-shadow: inset 0 0 0 2px var(--color-primary-element) !important;
- outline: none !important;
- }
-
- &:active:not(:focus-visible),
- &.active:not(:focus-visible) {
- background-color: var(--color-primary-element);
- color: var(--color-primary-element-text);
-
- img,
- svg {
- filter: var(--primary-invert-if-dark);
- }
- }
-
- span {
- padding-bottom: 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 210px;
- }
-
- img {
- width: 16px;
- height: 16px;
- margin-right: 10px;
- }
-
- img,
- svg {
- filter: var(--background-invert-if-dark);
- }
- }
-
- // Override global button styles
- button {
- background-color: transparent;
- border: none;
- font-weight: normal;
- margin: 0;
- }
- }
- }
- }
-}
-</style>